Keep all required editors mounted
This commit is contained in:
parent
e28835846b
commit
67f70f236d
|
|
@ -108,6 +108,10 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabHidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.promptPreview {
|
.promptPreview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatPanel = () => {
|
export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const { currentWorld, currentStory, dispatch, connection, model, enableThinking, chatOpen, continuePrompt } = appState;
|
const { currentWorld, currentStory, dispatch, connection, model, enableThinking, chatOpen, continuePrompt } = appState;
|
||||||
const { summarizeAll, isSummarizing } = useChapterSummarization();
|
const { summarizeAll, isSummarizing } = useChapterSummarization();
|
||||||
|
|
@ -415,6 +415,8 @@ export const ChatPanel = () => {
|
||||||
setEditingContent('');
|
setEditingContent('');
|
||||||
}, [setEditingContent]);
|
}, [setEditingContent]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
const isDisabled = !currentStory || !connection || !model || isLoading;
|
const isDisabled = !currentStory || !connection || !model || isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -647,7 +649,7 @@ export const ChatSidebar = () => {
|
||||||
})}
|
})}
|
||||||
class={sidebarStyles.mobileOverlay}
|
class={sidebarStyles.mobileOverlay}
|
||||||
>
|
>
|
||||||
<ChatPanel />
|
<ChatPanel visible />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { ContentEditable } from "@common/components/ContentEditable";
|
|
||||||
import { highlight } from "@common/highlight";
|
import { highlight } from "@common/highlight";
|
||||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact";
|
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";
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
|
|
@ -12,6 +10,9 @@ import { ChaptersEditor } from "./editors/chapters";
|
||||||
import { CharacterEditor } from "./editors/character";
|
import { CharacterEditor } from "./editors/character";
|
||||||
import { LocationEditor } from "./editors/location";
|
import { LocationEditor } from "./editors/location";
|
||||||
import { LoreEditor } from "./editors/lore";
|
import { LoreEditor } from "./editors/lore";
|
||||||
|
import { ScratchpadEditor } from "./editors/scratchpad";
|
||||||
|
import { StoryEditor } from "./editors/story";
|
||||||
|
import { SystemEditor } from "./editors/system";
|
||||||
import { Menu } from "./menu";
|
import { Menu } from "./menu";
|
||||||
|
|
||||||
// Tabs available when a story is selected (regular world)
|
// Tabs available when a story is selected (regular world)
|
||||||
|
|
@ -53,54 +54,16 @@ export const Editor = () => {
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const { currentWorld, currentStory, currentTab, chatOpen, dispatch } = appState;
|
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) => {
|
const handleTabChange = (tab: Tab) => {
|
||||||
dispatch({ type: 'SET_CURRENT_TAB', tab });
|
dispatch({ type: 'SET_CURRENT_TAB', tab });
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
const promptPreview = useMemo(() => {
|
||||||
if (currentTab !== 'prompt') return '';
|
|
||||||
const text = Prompt.formatSystemPrompt(appState);
|
const text = Prompt.formatSystemPrompt(appState);
|
||||||
return highlight(text, false);
|
return highlight(text, false);
|
||||||
}, [currentTab, appState]);
|
}, [appState]);
|
||||||
|
|
||||||
const overrideValue = useMemo(() => {
|
|
||||||
return highlight(currentWorld?.systemInstructionOverride || '');
|
|
||||||
}, [currentWorld?.systemInstructionOverride]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentStory?.lastEditedText) {
|
if (currentStory?.lastEditedText) {
|
||||||
|
|
@ -144,57 +107,31 @@ export const Editor = () => {
|
||||||
? <div class={styles.title}><WorldIcon size={24} />{currentWorld.title}</div>
|
? <div class={styles.title}><WorldIcon size={24} />{currentWorld.title}</div>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const isChatTab = currentTab === 'chat';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.editor}>
|
<div class={styles.editor}>
|
||||||
{titleBar}
|
{titleBar}
|
||||||
<div class={clsx(styles.content, currentTab === 'menu' && styles.menuContent, isChatTab && styles.chatContent)} ref={contentRef}>
|
<div class={clsx(
|
||||||
{currentTab === "menu" && (
|
styles.content,
|
||||||
<Menu />
|
currentTab === 'menu' && styles.menuContent,
|
||||||
)}
|
currentTab === 'chat' && styles.chatContent,
|
||||||
{currentTab === "story" && currentStory && (
|
)} ref={contentRef}>
|
||||||
<ContentEditable
|
<Menu visible={currentTab === "menu"} />
|
||||||
class={styles.editable}
|
{currentStory && (<>
|
||||||
value={storyValue}
|
<StoryEditor visible={currentTab === "story"} />
|
||||||
onInput={handleInput}
|
<ChaptersEditor visible={currentTab === "chapters"} />
|
||||||
placeholder="Start writing your story..."
|
<ScratchpadEditor visible={currentTab === "scratchpad"} />
|
||||||
|
<div
|
||||||
|
class={clsx(styles.promptPreview, currentTab !== "prompt" && styles.tabHidden)}
|
||||||
|
dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }}
|
||||||
/>
|
/>
|
||||||
)}
|
{isChatOnly && <ChatPanel visible={currentTab === "chat"} />}
|
||||||
{currentTab === "lore" && (currentStory || currentWorld) && (
|
</>)}
|
||||||
<LoreEditor />
|
{(currentStory || currentWorld) && (<>
|
||||||
)}
|
<LoreEditor visible={currentTab === "lore"} />
|
||||||
{currentTab === "characters" && (currentStory || currentWorld) && (
|
<CharacterEditor visible={currentTab === "characters"} />
|
||||||
<CharacterEditor />
|
<LocationEditor visible={currentTab === "locations"} />
|
||||||
)}
|
</>)}
|
||||||
{currentTab === "locations" && (currentStory || currentWorld) && (
|
{currentWorld && <SystemEditor visible={currentTab === "system"} />}
|
||||||
<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 />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.tabs}>
|
<div class={styles.tabs}>
|
||||||
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => (
|
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => (
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,18 @@ import styles from "../../assets/chapters-editor.module.css";
|
||||||
import { useAppState } from "../../contexts/state";
|
import { useAppState } from "../../contexts/state";
|
||||||
import Chapters from "../../utils/chapters";
|
import Chapters from "../../utils/chapters";
|
||||||
|
|
||||||
export const ChaptersEditor = () => {
|
export const ChaptersEditor = ({ visible }: { visible: boolean }) => {
|
||||||
const { currentWorld, currentStory, dispatch } = useAppState();
|
const { currentWorld, currentStory, dispatch } = useAppState();
|
||||||
|
|
||||||
if (!currentStory) return null;
|
|
||||||
|
|
||||||
const parsed = useMemo(
|
const parsed = useMemo(
|
||||||
() => Chapters.parseText(currentStory.text),
|
() => Chapters.parseText(currentStory?.text ?? ''),
|
||||||
[currentStory.text]
|
[currentStory?.text]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!currentWorld || !currentStory || !visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div class={styles.chaptersEditor}>
|
<div class={styles.chaptersEditor}>
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import styles from '../../assets/character-editor.module.css';
|
||||||
import { CharacterRole, useAppState, type Character } from "../../contexts/state";
|
import { CharacterRole, useAppState, type Character } from "../../contexts/state";
|
||||||
import LLM from "../../utils/llm";
|
import LLM from "../../utils/llm";
|
||||||
|
|
||||||
export const CharacterEditor = () => {
|
export const CharacterEditor = ({ visible }: { visible: boolean }) => {
|
||||||
const { currentWorld, currentStory, mergedCharacters, dispatch, connection, model } = useAppState();
|
const { currentWorld, currentStory, mergedCharacters, dispatch, connection, model } = useAppState();
|
||||||
const [newNickname, setNewNickname] = useState<Record<string, string>>({});
|
const [newNickname, setNewNickname] = useState<Record<string, string>>({});
|
||||||
const [newRelation, setNewRelation] = useState<Record<string, { name: string; relation: string }>>({});
|
const [newRelation, setNewRelation] = useState<Record<string, { name: string; relation: string }>>({});
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||||
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
||||||
|
|
||||||
if (!currentWorld) {
|
if (!currentWorld || !visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import styles from '../../assets/location-editor.module.css';
|
||||||
import { LocationScale, useAppState, type Location } from "../../contexts/state";
|
import { LocationScale, useAppState, type Location } from "../../contexts/state";
|
||||||
import LLM from "../../utils/llm";
|
import LLM from "../../utils/llm";
|
||||||
|
|
||||||
export const LocationEditor = () => {
|
export const LocationEditor = ({ visible }: { visible: boolean }) => {
|
||||||
const { currentWorld, currentStory, dispatch, connection, model } = useAppState();
|
const { currentWorld, currentStory, dispatch, connection, model } = useAppState();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||||
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
||||||
|
|
||||||
if (!currentWorld) {
|
if (!currentWorld || !visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { useState } from "preact/hooks";
|
||||||
import styles from '../../assets/lore-editor.module.css';
|
import styles from '../../assets/lore-editor.module.css';
|
||||||
import { useAppState, type LoreEntry } from "../../contexts/state";
|
import { useAppState, type LoreEntry } from "../../contexts/state";
|
||||||
|
|
||||||
export const LoreEditor = () => {
|
export const LoreEditor = ({ visible }: { visible: boolean }) => {
|
||||||
const { currentWorld, currentStory, dispatch } = useAppState();
|
const { currentWorld, currentStory, dispatch } = useAppState();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [newTitle, setNewTitle] = useState('');
|
const [newTitle, setNewTitle] = useState('');
|
||||||
|
|
||||||
if (!currentWorld) {
|
if (!currentWorld || !visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -187,9 +187,10 @@ const WorldItem = ({
|
||||||
|
|
||||||
// ─── Menu Sidebar ─────────────────────────────────────────────────────────────
|
// ─── Menu Sidebar ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const Menu = () => {
|
export const Menu = ({ visible }: { visible: boolean }) => {
|
||||||
const { worlds, currentWorld, currentStory, dispatch } = useAppState();
|
const { worlds, currentWorld, currentStory, dispatch } = useAppState();
|
||||||
const isSettingsOpen = useBool(false);
|
const isSettingsOpen = useBool(false);
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
const handleCreateWorld = () => {
|
const handleCreateWorld = () => {
|
||||||
dispatch({ type: 'CREATE_WORLD', title: 'New World' });
|
dispatch({ type: 'CREATE_WORLD', title: 'New World' });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue