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 { 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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,7 +114,6 @@ 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
|
||||||
|
|
@ -141,13 +139,12 @@ export const MenuSidebar = () => {
|
||||||
<Plug size={16} /> Connection Settings
|
<Plug size={16} /> Connection Settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{isSettingsOpen.value && (
|
{isSettingsOpen.value && (
|
||||||
<SettingsModal onClose={isSettingsOpen.toggle} />
|
<SettingsModal onClose={isSettingsOpen.toggle} />
|
||||||
)}
|
)}
|
||||||
{isConnectionSettingsOpen.value && (
|
{isConnectionSettingsOpen.value && (
|
||||||
<ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} />
|
<ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} />
|
||||||
)}
|
)}
|
||||||
</Sidebar>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue