import { ContentEditable } from "@common/components/ContentEditable"; import { highlight } from "@common/highlight"; import { useAppState, type Tab } from "../contexts/state"; import styles from '../assets/editor.module.css'; import { useMemo, useRef, useEffect } from "preact/hooks"; import clsx from "clsx"; import { CharacterEditor } from "./character-editor"; import { LocationEditor } from "./location-editor"; import { ChaptersEditor } from "./chapters-editor"; import { LoreEditor } from "./lore-editor"; import { Menu } from "./menu"; import { ChatPanel } from "./chat-sidebar"; import { useInputCallback } from "@common/hooks/useInputCallback"; import Prompt from "../utils/prompt"; import { BookOpen, List, Users, MapPin, BookMarked, FileText, Code, Layers, MessageSquare, Globe, BrainCircuit, MessagesSquare, type LucideIcon } from "lucide-preact"; // Tabs available when a story is selected (regular world) const STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ { id: "menu", label: "Menu", icon: List }, { id: "story", label: "Story", icon: BookOpen }, { id: "chapters", label: "Chapters", icon: Layers }, { id: "lore", label: "Lore", icon: BookMarked }, { id: "characters", label: "Characters", icon: Users }, { id: "locations", label: "Locations", icon: MapPin }, { id: "scratchpad", label: "Scratchpad", icon: FileText, right: true }, { id: "prompt", label: "Prompt", icon: Code }, ]; // Tabs available when only a world is selected (no story, regular world) const WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ { id: "menu", label: "Menu", icon: List }, { id: "lore", label: "Lore", icon: BookMarked }, { id: "characters", label: "Characters", icon: Users }, { id: "locations", label: "Locations", icon: MapPin }, { id: "system", label: "System", icon: BrainCircuit }, ]; // Tabs for a chat session within a chat-only world const CHAT_STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ { id: "menu", label: "Menu", icon: List }, { id: "chat", label: "Chat", icon: MessageSquare }, { id: "scratchpad", label: "Scratchpad", icon: FileText, right: true }, { id: "prompt", label: "Prompt", icon: Code }, ]; // Tabs for a chat-only world with no session selected const CHAT_WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ { id: "menu", label: "Menu", icon: List }, { id: "system", label: "System", icon: BrainCircuit }, ]; 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]); useEffect(() => { if (currentStory?.lastEditedText) { const raf = requestAnimationFrame(() => { if (contentRef.current) { contentRef.current.scrollTo({ top: contentRef.current.scrollHeight, behavior: 'smooth', }); } }); return () => cancelAnimationFrame(raf); } }, [currentStory?.lastEditedText]); useEffect(() => { const raf = requestAnimationFrame(() => { if (contentRef.current) { contentRef.current.scrollTop = contentRef.current.scrollHeight; } }); return () => cancelAnimationFrame(raf); }, [currentStory?.id, currentWorld?.id, currentTab]); const hasSelection = currentWorld !== null; const isChatOnly = currentWorld?.chatOnly ?? false; const tabs = currentStory ? (isChatOnly ? CHAT_STORY_TABS : STORY_TABS) : currentWorld ? (isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS) : [{ id: "menu" as Tab, label: "Menu", icon: List }]; // Title bar: use MessagesSquare icon for chat-only worlds const WorldIcon = isChatOnly ? MessagesSquare : Globe; const titleBar = currentStory ?
{currentWorld?.title} /{currentStory.title}
: currentWorld ?
{currentWorld.title}
: null; const isChatTab = currentTab === 'chat'; return (
{titleBar}
{currentTab === "menu" && ( )} {currentTab === "story" && 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 && ( )}
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => ( ))} {currentStory && !isChatOnly && ( )}
); };