1
0
Fork 0

Add story duplication and fix switching

This commit is contained in:
Pabloader 2026-03-25 19:30:26 +00:00
parent 97f88a24b9
commit 951c13022c
3 changed files with 43 additions and 10 deletions

View File

@ -20,27 +20,26 @@ const TABS: { id: Tab; label: string }[] = [
export const Editor = () => { export const Editor = () => {
const { currentStory, dispatch } = useAppState(); const { currentStory, dispatch } = useAppState();
if (!currentStory) {
return <div class={styles.editor} />;
}
const handleInput = useInputCallback((text: string) => { const handleInput = useInputCallback((text: string) => {
if (!currentStory) return;
dispatch({ dispatch({
type: 'EDIT_STORY', type: 'EDIT_STORY',
id: currentStory.id, id: currentStory.id,
text, text,
}); });
}, []); }, [currentStory?.id]);
const handleLoreInput = useInputCallback((lore: string) => { const handleLoreInput = useInputCallback((lore: string) => {
if (!currentStory) return;
dispatch({ dispatch({
type: 'EDIT_LORE', type: 'EDIT_LORE',
id: currentStory.id, id: currentStory.id,
lore, lore,
}); });
}, []); }, [currentStory?.id]);
const handleTabChange = (tab: Tab) => { const handleTabChange = (tab: Tab) => {
if (!currentStory) return;
dispatch({ dispatch({
type: 'SET_CURRENT_TAB', type: 'SET_CURRENT_TAB',
id: currentStory.id, id: currentStory.id,
@ -48,8 +47,12 @@ export const Editor = () => {
}); });
}; };
const storyValue = useMemo(() => highlight(currentStory.text), [currentStory.text]); const storyValue = useMemo(() => currentStory ? highlight(currentStory.text) : '', [currentStory?.text]);
const loreValue = useMemo(() => highlight(currentStory.lore), [currentStory.lore]); const loreValue = useMemo(() => currentStory ? highlight(currentStory.lore) : '', [currentStory?.lore]);
if (!currentStory) {
return <div class={styles.editor} />;
}
return ( return (
<div class={styles.editor}> <div class={styles.editor}>

View File

@ -7,7 +7,7 @@ import { useBool } from "@common/hooks/useBool";
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-sidebar.module.css';
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Pencil, X, Plus, Plug, Settings } from "lucide-preact"; import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact";
// ─── Story Item ─────────────────────────────────────────────────────────────── // ─── Story Item ───────────────────────────────────────────────────────────────
@ -17,9 +17,10 @@ interface StoryItemProps {
onSelect: () => void; onSelect: () => void;
onRename: (newTitle: string) => void; onRename: (newTitle: string) => void;
onDelete: () => void; onDelete: () => void;
onDuplicate: () => void;
} }
const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemProps) => { const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }: StoryItemProps) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editTitle, setEditTitle] = useState(story.title); const [editTitle, setEditTitle] = useState(story.title);
@ -70,6 +71,9 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro
<button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename"> <button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename">
<Pencil size={14} /> <Pencil size={14} />
</button> </button>
<button class={styles.actionButton} onClick={onDuplicate} title="Duplicate">
<Copy size={14} />
</button>
<button class={styles.actionButton} onClick={onDelete} title="Delete"> <button class={styles.actionButton} onClick={onDelete} title="Delete">
<X size={14} /> <X size={14} />
</button> </button>
@ -105,6 +109,10 @@ export const MenuSidebar = () => {
} }
}; };
const handleDuplicate = (id: string) => {
dispatch({ type: 'DUPLICATE_STORY', id });
};
return ( return (
<Sidebar side="left"> <Sidebar side="left">
<div class={styles.menu}> <div class={styles.menu}>
@ -120,6 +128,7 @@ export const MenuSidebar = () => {
onSelect={() => handleSelect(story.id)} onSelect={() => handleSelect(story.id)}
onRename={(newTitle) => handleRename(story.id, newTitle)} onRename={(newTitle) => handleRename(story.id, newTitle)}
onDelete={() => handleDelete(story.id)} onDelete={() => handleDelete(story.id)}
onDuplicate={() => handleDuplicate(story.id)}
/> />
))} ))}
</div> </div>

View File

@ -81,6 +81,7 @@ type Action =
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab } | { type: 'SET_CURRENT_TAB'; id: string; 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: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage } | { type: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage }
| { type: 'CLEAR_CHAT'; storyId: string } | { type: 'CLEAR_CHAT'; storyId: string }
| { type: 'SET_CONNECTION'; connection: LLM.Connection | null } | { type: 'SET_CONNECTION'; connection: LLM.Connection | null }
@ -180,6 +181,26 @@ function reducer(state: IState, action: Action): IState {
currentStoryId: deletingCurrent ? null : state.currentStoryId, currentStoryId: deletingCurrent ? null : state.currentStoryId,
}; };
} }
case 'DUPLICATE_STORY': {
const original = state.stories.find(s => s.id === action.id);
if (!original) return state;
const newStory: Story = {
id: crypto.randomUUID(),
title: `${original.title} (Copy)`,
text: '',
lore: original.lore,
characters: original.characters,
locations: original.locations,
currentTab: 'story',
chatMessages: [],
chapters: [],
};
return {
...state,
stories: [...state.stories, newStory],
currentStoryId: newStory.id,
};
}
case 'SELECT_STORY': { case 'SELECT_STORY': {
return { return {
...state, ...state,