1
0
Fork 0

Move menu to the main tabs area

This commit is contained in:
Pabloader 2026-03-27 16:08:55 +00:00
parent 9803512c0b
commit d8b1893739
5 changed files with 54 additions and 72 deletions

View File

@ -1,4 +1,3 @@
import { MenuSidebar } from "./menu-sidebar";
import { Editor } from "./editor"; import { Editor } from "./editor";
import { ChatSidebar } from "./chat-sidebar"; import { ChatSidebar } from "./chat-sidebar";
import { Title } from "@common/components/Title"; import { Title } from "@common/components/Title";
@ -13,7 +12,6 @@ export const App = () => {
{currentStory {currentStory
? <Title>{currentStory.title} - Storywriter</Title> ? <Title>{currentStory.title} - Storywriter</Title>
: <Title>Storywriter</Title>} : <Title>Storywriter</Title>}
<MenuSidebar />
<Editor /> <Editor />
<ChatSidebar /> <ChatSidebar />
</div> </div>

View File

@ -8,10 +8,12 @@ import { CharacterEditor } from "./character-editor";
import { LocationEditor } from "./location-editor"; import { LocationEditor } from "./location-editor";
import { ChaptersEditor } from "./chapters-editor"; import { ChaptersEditor } from "./chapters-editor";
import { LoreEditor } from "./lore-editor"; import { LoreEditor } from "./lore-editor";
import { Menu } from "./menu";
import { useInputCallback } from "@common/hooks/useInputCallback"; import { useInputCallback } from "@common/hooks/useInputCallback";
import Prompt from "../utils/prompt"; import Prompt from "../utils/prompt";
const TABS: { id: Tab; label: string; right?: boolean }[] = [ const TABS: { id: Tab; label: string; right?: boolean }[] = [
{ id: "menu", label: "Menu" },
{ id: "story", label: "Story" }, { id: "story", label: "Story" },
{ id: "chapters", label: "Chapters" }, { id: "chapters", label: "Chapters" },
{ id: "lore", label: "Lore" }, { id: "lore", label: "Lore" },
@ -23,7 +25,7 @@ const TABS: { id: Tab; label: string; right?: boolean }[] = [
export const Editor = () => { export const Editor = () => {
const appState = useAppState(); const appState = useAppState();
const { currentStory, dispatch } = appState; const { currentStory, currentTab, dispatch } = appState;
const handleInput = useInputCallback((text: string) => { const handleInput = useInputCallback((text: string) => {
if (!currentStory) return; if (!currentStory) return;
@ -36,12 +38,7 @@ export const Editor = () => {
}, [currentStory?.id]); }, [currentStory?.id]);
const handleTabChange = (tab: Tab) => { const handleTabChange = (tab: Tab) => {
if (!currentStory) return; dispatch({ type: 'SET_CURRENT_TAB', tab });
dispatch({
type: 'SET_CURRENT_TAB',
id: currentStory.id,
tab,
});
}; };
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
@ -65,10 +62,10 @@ export const Editor = () => {
}, [currentStory?.scratchpad]); }, [currentStory?.scratchpad]);
const promptPreview = useMemo(() => { const promptPreview = useMemo(() => {
if (currentStory?.currentTab !== 'prompt') return ''; if (currentTab !== 'prompt') return '';
const text = Prompt.formatSystemPrompt(appState); const text = Prompt.formatSystemPrompt(appState);
return highlight(text, false); return highlight(text, false);
}, [currentStory?.currentTab, appState]); }, [currentTab, appState]);
useEffect(() => { useEffect(() => {
if (currentStory?.lastEditedText) { if (currentStory?.lastEditedText) {
@ -91,19 +88,16 @@ export const Editor = () => {
} }
}); });
return () => cancelAnimationFrame(raf); return () => cancelAnimationFrame(raf);
}, [currentStory?.id, currentStory?.currentTab]); }, [currentStory?.id, currentTab]);
if (!currentStory) {
return <div class={styles.editor} />;
}
return ( return (
<div class={styles.editor}> <div class={styles.editor}>
<div class={styles.title}> {currentStory && <div class={styles.title}>{currentStory.title}</div>}
{currentStory.title}
</div>
<div class={styles.content} ref={contentRef}> <div class={styles.content} ref={contentRef}>
{currentStory.currentTab === "story" && ( {currentTab === "menu" && (
<Menu />
)}
{currentTab === "story" && currentStory && (
<ContentEditable <ContentEditable
class={styles.editable} class={styles.editable}
value={storyValue} value={storyValue}
@ -111,19 +105,19 @@ export const Editor = () => {
placeholder="Start writing your story..." placeholder="Start writing your story..."
/> />
)} )}
{currentStory.currentTab === "lore" && ( {currentTab === "lore" && currentStory && (
<LoreEditor /> <LoreEditor />
)} )}
{currentStory.currentTab === "characters" && ( {currentTab === "characters" && currentStory && (
<CharacterEditor /> <CharacterEditor />
)} )}
{currentStory.currentTab === "locations" && ( {currentTab === "locations" && currentStory && (
<LocationEditor /> <LocationEditor />
)} )}
{currentStory.currentTab === "chapters" && ( {currentTab === "chapters" && currentStory && (
<ChaptersEditor /> <ChaptersEditor />
)} )}
{currentStory.currentTab === "scratchpad" && ( {currentTab === "scratchpad" && currentStory && (
<ContentEditable <ContentEditable
class={styles.editable} class={styles.editable}
value={scratchpadValue} value={scratchpadValue}
@ -131,15 +125,15 @@ export const Editor = () => {
placeholder="Notes, ideas, outlines — anything you don't want in the story..." placeholder="Notes, ideas, outlines — anything you don't want in the story..."
/> />
)} )}
{currentStory.currentTab === "prompt" && ( {currentTab === "prompt" && currentStory && (
<div class={styles.promptPreview} dangerouslySetInnerHTML={{ __html: promptPreview }} /> <div class={styles.promptPreview} dangerouslySetInnerHTML={{ __html: promptPreview }} />
)} )}
</div> </div>
<div class={styles.tabs}> <div class={styles.tabs}>
{TABS.map((tab) => ( {TABS.filter(tab => currentStory || tab.id === 'menu').map((tab) => (
<button <button
key={tab.id} key={tab.id}
class={clsx(styles.tab, currentStory.currentTab === tab.id && styles.active, tab.right && styles.tabRight)} class={clsx(styles.tab, currentTab === tab.id && styles.active, tab.right && styles.tabRight)}
onClick={() => handleTabChange(tab.id)} onClick={() => handleTabChange(tab.id)}
> >
{tab.label} {tab.label}

View File

@ -1,12 +1,11 @@
import clsx from "clsx"; import clsx from "clsx";
import { Sidebar } from "./sidebar";
import { ConnectionSettingsModal } from "./connection-settings-modal"; import { ConnectionSettingsModal } from "./connection-settings-modal";
import { SettingsModal } from "./settings-modal"; import { SettingsModal } from "./settings-modal";
import { useAppState } from "../contexts/state"; import { useAppState } from "../contexts/state";
import { useBool } from "@common/hooks/useBool"; import { useBool } from "@common/hooks/useBool";
import { useInputState } from "@common/hooks/useInputState"; import { useInputState } from "@common/hooks/useInputState";
import type { Story } from "../contexts/state"; import type { Story } from "../contexts/state";
import styles from '../assets/menu-sidebar.module.css'; import styles from '../assets/menu.module.css';
import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact"; import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact";
// ─── Story Item ─────────────────────────────────────────────────────────────── // ─── Story Item ───────────────────────────────────────────────────────────────
@ -85,7 +84,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
// ─── Menu Sidebar ───────────────────────────────────────────────────────────── // ─── Menu Sidebar ─────────────────────────────────────────────────────────────
export const MenuSidebar = () => { export const Menu = () => {
const { stories, currentStory, dispatch } = useAppState(); const { stories, currentStory, dispatch } = useAppState();
const isConnectionSettingsOpen = useBool(false); const isConnectionSettingsOpen = useBool(false);
const isSettingsOpen = useBool(false); const isSettingsOpen = useBool(false);
@ -115,32 +114,30 @@ export const MenuSidebar = () => {
}; };
return ( return (
<Sidebar side="left"> <div class={styles.menu}>
<div class={styles.menu}> <button class={styles.newButton} onClick={handleCreate}>
<button class={styles.newButton} onClick={handleCreate}> <Plus size={16} /> New Story
<Plus size={16} /> New Story </button>
<div class={styles.list}>
{stories.map(story => (
<StoryItem
key={story.id}
story={story}
active={story.id === currentStory?.id}
onSelect={() => handleSelect(story.id)}
onRename={(newTitle) => handleRename(story.id, newTitle)}
onDelete={() => handleDelete(story.id)}
onDuplicate={() => handleDuplicate(story.id)}
/>
))}
</div>
<div class={styles.bottomButtons}>
<button class={styles.settingsButton} onClick={isSettingsOpen.toggle}>
<Settings size={16} /> Settings
</button>
<button class={styles.settingsButton} onClick={isConnectionSettingsOpen.toggle}>
<Plug size={16} /> Connection Settings
</button> </button>
<div class={styles.list}>
{stories.map(story => (
<StoryItem
key={story.id}
story={story}
active={story.id === currentStory?.id}
onSelect={() => handleSelect(story.id)}
onRename={(newTitle) => handleRename(story.id, newTitle)}
onDelete={() => handleDelete(story.id)}
onDuplicate={() => handleDuplicate(story.id)}
/>
))}
</div>
<div class={styles.bottomButtons}>
<button class={styles.settingsButton} onClick={isSettingsOpen.toggle}>
<Settings size={16} /> Settings
</button>
<button class={styles.settingsButton} onClick={isConnectionSettingsOpen.toggle}>
<Plug size={16} /> Connection Settings
</button>
</div>
</div> </div>
{isSettingsOpen.value && ( {isSettingsOpen.value && (
<SettingsModal onClose={isSettingsOpen.toggle} /> <SettingsModal onClose={isSettingsOpen.toggle} />
@ -148,6 +145,6 @@ export const MenuSidebar = () => {
{isConnectionSettingsOpen.value && ( {isConnectionSettingsOpen.value && (
<ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} /> <ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} />
)} )}
</Sidebar> </div>
); );
}; };

View File

@ -11,7 +11,7 @@ export type ChatMessage = LLM.ChatMessage & {
id: string; id: string;
} }
export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt"; export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt" | "menu";
export enum CharacterRole { export enum CharacterRole {
Protagonist = 'protagonist', Protagonist = 'protagonist',
@ -72,7 +72,6 @@ export interface Story {
lore: LoreEntry[]; lore: LoreEntry[];
characters: Character[]; characters: Character[];
locations: Location[]; locations: Location[];
currentTab: Tab;
chatMessages: ChatMessage[]; chatMessages: ChatMessage[];
chapters: Chapters.Chapter[]; chapters: Chapters.Chapter[];
lastEditedText?: string; lastEditedText?: string;
@ -83,6 +82,7 @@ export interface Story {
interface IState { interface IState {
stories: Story[]; stories: Story[];
currentStoryId: string | null; currentStoryId: string | null;
currentTab: Tab;
connection: LLM.Connection | null; connection: LLM.Connection | null;
model: LLM.ModelInfo | null; model: LLM.ModelInfo | null;
enableThinking: boolean; enableThinking: boolean;
@ -102,7 +102,7 @@ type Action =
| { type: 'DELETE_LORE_ENTRY'; storyId: string; entryId: string } | { type: 'DELETE_LORE_ENTRY'; storyId: string; entryId: string }
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] } | { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] }
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string } | { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab } | { type: 'SET_CURRENT_TAB'; tab: Tab }
| { type: 'DELETE_STORY'; id: string } | { type: 'DELETE_STORY'; id: string }
| { type: 'SELECT_STORY'; id: string } | { type: 'SELECT_STORY'; id: string }
| { type: 'DUPLICATE_STORY'; id: string } | { type: 'DUPLICATE_STORY'; id: string }
@ -129,6 +129,7 @@ type Action =
const DEFAULT_STATE: IState = { const DEFAULT_STATE: IState = {
stories: [], stories: [],
currentStoryId: null, currentStoryId: null,
currentTab: 'menu',
connection: null, connection: null,
model: null, model: null,
enableThinking: false, enableThinking: false,
@ -170,7 +171,6 @@ function reducer(state: IState, action: Action): IState {
lore: [], lore: [],
characters: [], characters: [],
locations: [], locations: [],
currentTab: 'story',
chatMessages: [], chatMessages: [],
chapters: [], chapters: [],
}; };
@ -261,12 +261,7 @@ function reducer(state: IState, action: Action): IState {
}; };
} }
case 'SET_CURRENT_TAB': { case 'SET_CURRENT_TAB': {
return { return { ...state, currentTab: action.tab };
...state,
stories: state.stories.map(s =>
s.id === action.id ? { ...s, currentTab: action.tab } : s
),
};
} }
case 'DELETE_STORY': { case 'DELETE_STORY': {
const remaining = state.stories.filter(s => s.id !== action.id); const remaining = state.stories.filter(s => s.id !== action.id);
@ -288,7 +283,6 @@ function reducer(state: IState, action: Action): IState {
lore: [...original.lore], lore: [...original.lore],
characters: original.characters, characters: original.characters,
locations: original.locations, locations: original.locations,
currentTab: 'story',
chatMessages: [], chatMessages: [],
chapters: [], chapters: [],
}; };
@ -299,10 +293,7 @@ function reducer(state: IState, action: Action): IState {
}; };
} }
case 'SELECT_STORY': { case 'SELECT_STORY': {
return { return { ...state, currentStoryId: action.id, currentTab: 'story' };
...state,
currentStoryId: action.id,
};
} }
case 'ADD_CHAT_MESSAGE': { case 'ADD_CHAT_MESSAGE': {
return { return {
@ -547,6 +538,7 @@ function reducer(state: IState, action: Action): IState {
export interface AppState { export interface AppState {
stories: Story[]; stories: Story[];
currentStory: Story | null; currentStory: Story | null;
currentTab: Tab;
connection: LLM.Connection | null; connection: LLM.Connection | null;
model: LLM.ModelInfo | null; model: LLM.ModelInfo | null;
enableThinking: boolean; enableThinking: boolean;
@ -567,6 +559,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
const value = useMemo<AppState>(() => ({ const value = useMemo<AppState>(() => ({
stories: state.stories, stories: state.stories,
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null, currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
currentTab: state.currentTab,
connection: state.connection, connection: state.connection,
model: state.model, model: state.model,
enableThinking: state.enableThinking, enableThinking: state.enableThinking,