diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx
index 2fc848c..3066d35 100644
--- a/src/games/storywriter/components/chat-sidebar.tsx
+++ b/src/games/storywriter/components/chat-sidebar.tsx
@@ -2,7 +2,7 @@ import { ContentEditable } from "@common/components/ContentEditable";
import { highlight } from "@common/highlight";
import { useInputState } from "@common/hooks/useInputState";
import clsx from "clsx";
-import { Check, ChevronsRight, Edit2, RefreshCw, Sparkles, Trash2, X } from "lucide-preact";
+import { Check, ChevronsRight, Edit2, GitFork, RefreshCw, Sparkles, Trash2, X } from "lucide-preact";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import styles from '../assets/chat-sidebar.module.css';
import sidebarStyles from '../assets/sidebar.module.css';
@@ -20,6 +20,8 @@ interface RoleHeaderProps {
}
const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
+ const { currentWorld, userName } = useAppState();
+
const toolName = useMemo(() => {
if (message.role !== 'tool') return;
for (const m of chatMessages.toReversed()) {
@@ -29,12 +31,28 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
}
}, [message, chatMessages]);
+ let displayName: string = message.role;
+ let roleLabel = null;
+
+ if (message.role === 'tool' && toolName) {
+ displayName = toolName;
+ roleLabel = message.role;
+ } else if (currentWorld?.chatOnly) {
+ if (message.role === 'user' && userName) {
+ displayName = userName;
+ roleLabel = message.role;
+ } else if (message.role === 'assistant' && currentWorld.title) {
+ displayName = currentWorld.title;
+ roleLabel = message.role;
+ }
+ }
+
return (
- {message.role}
- {toolName && (
+ {displayName}
+ {roleLabel && (
- {toolName}
+ {message.role}
)}
@@ -362,6 +380,16 @@ export const ChatPanel = () => {
});
}, [currentStory, currentWorld, dispatch]);
+ const handleForkChat = useCallback((messageId: string) => {
+ if (!currentStory || !currentWorld) return;
+ dispatch({
+ type: 'DUPLICATE_STORY',
+ worldId: currentWorld.id,
+ id: currentStory.id,
+ upToMessageId: messageId,
+ });
+ }, [currentStory, currentWorld, dispatch]);
+
const handleStartEdit = useCallback((message: ChatMessage) => {
setEditingMessageId(message.id);
setEditingContent(message.content);
@@ -416,6 +444,15 @@ export const ChatPanel = () => {
{!isLoading && canEdit && (
+ {currentWorld?.chatOnly && (
+
+ )}
) : (
-
+
diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx
index 4c9c75e..7582651 100644
--- a/src/games/storywriter/contexts/state.tsx
+++ b/src/games/storywriter/contexts/state.tsx
@@ -137,7 +137,7 @@ type Action =
| { type: 'EDIT_SCRATCHPAD'; worldId: string; id: string; text: string }
| { type: 'DELETE_STORY'; worldId: string; id: string }
| { type: 'SELECT_STORY'; worldId: string; id: string }
- | { type: 'DUPLICATE_STORY'; worldId: string; id: string }
+ | { type: 'DUPLICATE_STORY'; worldId: string; id: string; upToMessageId?: string }
// Story lore
| { type: 'ADD_LORE_ENTRY'; worldId: string; storyId: string | null; entry: LoreEntry }
| { type: 'EDIT_LORE_ENTRY'; worldId: string; storyId: string | null; entryId: string; updates: Partial }
@@ -278,7 +278,6 @@ function reducer(state: IState, action: Action): IState {
worlds: [...state.worlds, world],
currentWorldId: world.id,
currentStoryId: null,
- currentTab: 'menu',
};
}
case 'RENAME_WORLD': {
@@ -299,7 +298,6 @@ function reducer(state: IState, action: Action): IState {
...state,
currentWorldId: action.worldId,
currentStoryId: null,
- currentTab: 'menu',
};
}
case 'CREATE_STORY': {
@@ -320,7 +318,6 @@ function reducer(state: IState, action: Action): IState {
...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, story] })),
currentWorldId: action.worldId,
currentStoryId: story.id,
- currentTab: 'menu',
};
}
case 'RENAME_STORY': {
@@ -347,20 +344,24 @@ function reducer(state: IState, action: Action): IState {
};
}
case 'SELECT_STORY': {
- const world = state.worlds.find(w => w.id === action.worldId);
return {
...state,
currentWorldId: action.worldId,
currentStoryId: action.id,
- currentTab: 'menu',
};
}
case 'DUPLICATE_STORY': {
const world = state.worlds.find(w => w.id === action.worldId);
const original = world?.stories.find(s => s.id === action.id);
if (!original) return state;
- const firstMessage = original.chatMessages[0];
- const chatMessages = world?.chatOnly && firstMessage && firstMessage.role === 'assistant' ? [firstMessage] : [];
+ let chatMessages: ChatMessage[];
+ if (action.upToMessageId) {
+ const idx = original.chatMessages.findIndex(m => m.id === action.upToMessageId);
+ chatMessages = idx !== -1 ? original.chatMessages.slice(0, idx + 1) : [...original.chatMessages];
+ } else {
+ const firstMessage = original.chatMessages[0];
+ chatMessages = world?.chatOnly && firstMessage && firstMessage.role === 'assistant' ? [firstMessage] : [];
+ }
const newStory: Story = {
id: crypto.randomUUID(),
title: `${original.title} (Copy)`,
@@ -375,7 +376,6 @@ function reducer(state: IState, action: Action): IState {
return {
...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, newStory] })),
currentStoryId: newStory.id,
- currentTab: 'menu',
};
}
case 'ADD_LORE_ENTRY': {
@@ -582,7 +582,6 @@ function reducer(state: IState, action: Action): IState {
worlds: [...state.worlds, world],
currentWorldId: world.id,
currentStoryId: null,
- currentTab: 'menu',
};
}
}