Move menu to the main tabs area
This commit is contained in:
parent
9803512c0b
commit
d8b1893739
|
|
@ -1,4 +1,3 @@
|
|||
import { MenuSidebar } from "./menu-sidebar";
|
||||
import { Editor } from "./editor";
|
||||
import { ChatSidebar } from "./chat-sidebar";
|
||||
import { Title } from "@common/components/Title";
|
||||
|
|
@ -13,7 +12,6 @@ export const App = () => {
|
|||
{currentStory
|
||||
? <Title>{currentStory.title} - Storywriter</Title>
|
||||
: <Title>Storywriter</Title>}
|
||||
<MenuSidebar />
|
||||
<Editor />
|
||||
<ChatSidebar />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ 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 { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import Prompt from "../utils/prompt";
|
||||
|
||||
const TABS: { id: Tab; label: string; right?: boolean }[] = [
|
||||
{ id: "menu", label: "Menu" },
|
||||
{ id: "story", label: "Story" },
|
||||
{ id: "chapters", label: "Chapters" },
|
||||
{ id: "lore", label: "Lore" },
|
||||
|
|
@ -23,7 +25,7 @@ const TABS: { id: Tab; label: string; right?: boolean }[] = [
|
|||
|
||||
export const Editor = () => {
|
||||
const appState = useAppState();
|
||||
const { currentStory, dispatch } = appState;
|
||||
const { currentStory, currentTab, dispatch } = appState;
|
||||
|
||||
const handleInput = useInputCallback((text: string) => {
|
||||
if (!currentStory) return;
|
||||
|
|
@ -36,12 +38,7 @@ export const Editor = () => {
|
|||
}, [currentStory?.id]);
|
||||
|
||||
const handleTabChange = (tab: Tab) => {
|
||||
if (!currentStory) return;
|
||||
dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: currentStory.id,
|
||||
tab,
|
||||
});
|
||||
dispatch({ type: 'SET_CURRENT_TAB', tab });
|
||||
};
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -65,10 +62,10 @@ export const Editor = () => {
|
|||
}, [currentStory?.scratchpad]);
|
||||
|
||||
const promptPreview = useMemo(() => {
|
||||
if (currentStory?.currentTab !== 'prompt') return '';
|
||||
if (currentTab !== 'prompt') return '';
|
||||
const text = Prompt.formatSystemPrompt(appState);
|
||||
return highlight(text, false);
|
||||
}, [currentStory?.currentTab, appState]);
|
||||
}, [currentTab, appState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStory?.lastEditedText) {
|
||||
|
|
@ -91,19 +88,16 @@ export const Editor = () => {
|
|||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [currentStory?.id, currentStory?.currentTab]);
|
||||
|
||||
if (!currentStory) {
|
||||
return <div class={styles.editor} />;
|
||||
}
|
||||
}, [currentStory?.id, currentTab]);
|
||||
|
||||
return (
|
||||
<div class={styles.editor}>
|
||||
<div class={styles.title}>
|
||||
{currentStory.title}
|
||||
</div>
|
||||
{currentStory && <div class={styles.title}>{currentStory.title}</div>}
|
||||
<div class={styles.content} ref={contentRef}>
|
||||
{currentStory.currentTab === "story" && (
|
||||
{currentTab === "menu" && (
|
||||
<Menu />
|
||||
)}
|
||||
{currentTab === "story" && currentStory && (
|
||||
<ContentEditable
|
||||
class={styles.editable}
|
||||
value={storyValue}
|
||||
|
|
@ -111,19 +105,19 @@ export const Editor = () => {
|
|||
placeholder="Start writing your story..."
|
||||
/>
|
||||
)}
|
||||
{currentStory.currentTab === "lore" && (
|
||||
{currentTab === "lore" && currentStory && (
|
||||
<LoreEditor />
|
||||
)}
|
||||
{currentStory.currentTab === "characters" && (
|
||||
{currentTab === "characters" && currentStory && (
|
||||
<CharacterEditor />
|
||||
)}
|
||||
{currentStory.currentTab === "locations" && (
|
||||
{currentTab === "locations" && currentStory && (
|
||||
<LocationEditor />
|
||||
)}
|
||||
{currentStory.currentTab === "chapters" && (
|
||||
{currentTab === "chapters" && currentStory && (
|
||||
<ChaptersEditor />
|
||||
)}
|
||||
{currentStory.currentTab === "scratchpad" && (
|
||||
{currentTab === "scratchpad" && currentStory && (
|
||||
<ContentEditable
|
||||
class={styles.editable}
|
||||
value={scratchpadValue}
|
||||
|
|
@ -131,15 +125,15 @@ export const Editor = () => {
|
|||
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>
|
||||
<div class={styles.tabs}>
|
||||
{TABS.map((tab) => (
|
||||
{TABS.filter(tab => currentStory || tab.id === 'menu').map((tab) => (
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
{tab.label}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import clsx from "clsx";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { ConnectionSettingsModal } from "./connection-settings-modal";
|
||||
import { SettingsModal } from "./settings-modal";
|
||||
import { useAppState } from "../contexts/state";
|
||||
import { useBool } from "@common/hooks/useBool";
|
||||
import { useInputState } from "@common/hooks/useInputState";
|
||||
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";
|
||||
|
||||
// ─── Story Item ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -85,7 +84,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
|
|||
|
||||
// ─── Menu Sidebar ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const MenuSidebar = () => {
|
||||
export const Menu = () => {
|
||||
const { stories, currentStory, dispatch } = useAppState();
|
||||
const isConnectionSettingsOpen = useBool(false);
|
||||
const isSettingsOpen = useBool(false);
|
||||
|
|
@ -115,32 +114,30 @@ export const MenuSidebar = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Sidebar side="left">
|
||||
<div class={styles.menu}>
|
||||
<button class={styles.newButton} onClick={handleCreate}>
|
||||
<Plus size={16} /> New Story
|
||||
<div class={styles.menu}>
|
||||
<button class={styles.newButton} onClick={handleCreate}>
|
||||
<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>
|
||||
<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>
|
||||
{isSettingsOpen.value && (
|
||||
<SettingsModal onClose={isSettingsOpen.toggle} />
|
||||
|
|
@ -148,6 +145,6 @@ export const MenuSidebar = () => {
|
|||
{isConnectionSettingsOpen.value && (
|
||||
<ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} />
|
||||
)}
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ export type ChatMessage = LLM.ChatMessage & {
|
|||
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 {
|
||||
Protagonist = 'protagonist',
|
||||
|
|
@ -72,7 +72,6 @@ export interface Story {
|
|||
lore: LoreEntry[];
|
||||
characters: Character[];
|
||||
locations: Location[];
|
||||
currentTab: Tab;
|
||||
chatMessages: ChatMessage[];
|
||||
chapters: Chapters.Chapter[];
|
||||
lastEditedText?: string;
|
||||
|
|
@ -83,6 +82,7 @@ export interface Story {
|
|||
interface IState {
|
||||
stories: Story[];
|
||||
currentStoryId: string | null;
|
||||
currentTab: Tab;
|
||||
connection: LLM.Connection | null;
|
||||
model: LLM.ModelInfo | null;
|
||||
enableThinking: boolean;
|
||||
|
|
@ -102,7 +102,7 @@ type Action =
|
|||
| { type: 'DELETE_LORE_ENTRY'; storyId: string; entryId: string }
|
||||
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: 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: 'SELECT_STORY'; id: string }
|
||||
| { type: 'DUPLICATE_STORY'; id: string }
|
||||
|
|
@ -129,6 +129,7 @@ type Action =
|
|||
const DEFAULT_STATE: IState = {
|
||||
stories: [],
|
||||
currentStoryId: null,
|
||||
currentTab: 'menu',
|
||||
connection: null,
|
||||
model: null,
|
||||
enableThinking: false,
|
||||
|
|
@ -170,7 +171,6 @@ function reducer(state: IState, action: Action): IState {
|
|||
lore: [],
|
||||
characters: [],
|
||||
locations: [],
|
||||
currentTab: 'story',
|
||||
chatMessages: [],
|
||||
chapters: [],
|
||||
};
|
||||
|
|
@ -261,12 +261,7 @@ function reducer(state: IState, action: Action): IState {
|
|||
};
|
||||
}
|
||||
case 'SET_CURRENT_TAB': {
|
||||
return {
|
||||
...state,
|
||||
stories: state.stories.map(s =>
|
||||
s.id === action.id ? { ...s, currentTab: action.tab } : s
|
||||
),
|
||||
};
|
||||
return { ...state, currentTab: action.tab };
|
||||
}
|
||||
case 'DELETE_STORY': {
|
||||
const remaining = state.stories.filter(s => s.id !== action.id);
|
||||
|
|
@ -288,7 +283,6 @@ function reducer(state: IState, action: Action): IState {
|
|||
lore: [...original.lore],
|
||||
characters: original.characters,
|
||||
locations: original.locations,
|
||||
currentTab: 'story',
|
||||
chatMessages: [],
|
||||
chapters: [],
|
||||
};
|
||||
|
|
@ -299,10 +293,7 @@ function reducer(state: IState, action: Action): IState {
|
|||
};
|
||||
}
|
||||
case 'SELECT_STORY': {
|
||||
return {
|
||||
...state,
|
||||
currentStoryId: action.id,
|
||||
};
|
||||
return { ...state, currentStoryId: action.id, currentTab: 'story' };
|
||||
}
|
||||
case 'ADD_CHAT_MESSAGE': {
|
||||
return {
|
||||
|
|
@ -547,6 +538,7 @@ function reducer(state: IState, action: Action): IState {
|
|||
export interface AppState {
|
||||
stories: Story[];
|
||||
currentStory: Story | null;
|
||||
currentTab: Tab;
|
||||
connection: LLM.Connection | null;
|
||||
model: LLM.ModelInfo | null;
|
||||
enableThinking: boolean;
|
||||
|
|
@ -567,6 +559,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
|||
const value = useMemo<AppState>(() => ({
|
||||
stories: state.stories,
|
||||
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
|
||||
currentTab: state.currentTab,
|
||||
connection: state.connection,
|
||||
model: state.model,
|
||||
enableThinking: state.enableThinking,
|
||||
|
|
|
|||
Loading…
Reference in New Issue