diff --git a/src/games/storywriter/assets/chat-sidebar.module.css b/src/games/storywriter/assets/chat-sidebar.module.css
index 841e09e..468bf6f 100644
--- a/src/games/storywriter/assets/chat-sidebar.module.css
+++ b/src/games/storywriter/assets/chat-sidebar.module.css
@@ -54,6 +54,10 @@
font-weight: bold;
color: var(--accent);
text-transform: uppercase;
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+ align-items: center;
}
.reasoningContent {
@@ -83,11 +87,16 @@
.toolBadge {
font-size: 11px;
- padding: 2px 8px;
+ padding: 2px 6px;
background: var(--bg-active);
color: var(--text-dim);
border-radius: var(--radius);
font-weight: 500;
+ text-transform: none;
+
+ .role & {
+ padding: 0 3px;
+ }
}
.loading {
diff --git a/src/games/storywriter/assets/editor.module.css b/src/games/storywriter/assets/editor.module.css
index 4f799e1..48e530a 100644
--- a/src/games/storywriter/assets/editor.module.css
+++ b/src/games/storywriter/assets/editor.module.css
@@ -3,9 +3,9 @@
display: flex;
flex-direction: column;
height: 100%;
- overflow-y: auto;
+ overflow: hidden;
background: var(--bg);
- padding: 36px 0;
+ padding: 36px 0 0;
}
.title {
@@ -17,11 +17,16 @@
text-align: center;
}
-.editable {
+.content {
flex: 1;
- width: 100%;
- resize: none;
+ overflow-y: auto;
padding: 0 72px;
+}
+
+.editable {
+ width: 100%;
+ min-height: 100%;
+ resize: none;
font-family: 'Georgia', serif;
font-size: 17px;
line-height: 1.9;
@@ -44,3 +49,43 @@
color: var(--yellow);
}
}
+
+.placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 200px;
+ color: var(--text-muted);
+ font-style: italic;
+ border: 2px dashed var(--border);
+ border-radius: 8px;
+}
+
+.tabs {
+ display: flex;
+ border-top: 1px solid var(--border);
+ padding: 0 12px;
+ gap: 8px;
+}
+
+.tab {
+ padding: 12px 16px;
+ background: transparent;
+ border: none;
+ border-top: 2px solid transparent;
+ color: var(--text-muted);
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+ margin-top: -1px;
+
+ &:hover {
+ color: var(--text);
+ background: var(--bg-hover);
+ }
+
+ &.active {
+ color: var(--accent);
+ border-top-color: var(--accent);
+ }
+}
diff --git a/src/games/storywriter/assets/menu-sidebar.module.css b/src/games/storywriter/assets/menu-sidebar.module.css
index aa65007..5820f85 100644
--- a/src/games/storywriter/assets/menu-sidebar.module.css
+++ b/src/games/storywriter/assets/menu-sidebar.module.css
@@ -102,7 +102,6 @@
}
.settingsButton {
- margin-top: auto;
padding: 6px 8px;
font-size: 13px;
color: var(--text-muted);
@@ -118,3 +117,12 @@
background: var(--bg-hover);
}
}
+
+.bottomButtons {
+ margin-top: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border);
+}
diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx
index b853b0b..088c4be 100644
--- a/src/games/storywriter/components/chat-sidebar.tsx
+++ b/src/games/storywriter/components/chat-sidebar.tsx
@@ -9,6 +9,35 @@ import Prompt from "../utils/prompt";
import { Tools } from "../utils/tools";
import clsx from "clsx";
+// ─── Role Header ──────────────────────────────────────────────────────────────
+
+interface RoleHeaderProps {
+ message: ChatMessage;
+ chatMessages: ChatMessage[];
+}
+
+const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
+ const toolName = useMemo(() => {
+ if (message.role !== 'tool') return;
+ for (const m of chatMessages.toReversed()) {
+ if (m.role !== 'assistant') continue;
+ const toolCall = m.tool_calls?.find(tc => tc.id === message.tool_call_id);
+ if (toolCall) return toolCall.function.name;
+ }
+ }, [message, chatMessages]);
+
+ return (
+
{currentStory.chatMessages.map((message) => (
-
{message.role}
+
{message.role === 'assistant' && message.reasoning_content && (
diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx
index 3025347..7bef4f0 100644
--- a/src/games/storywriter/components/editor.tsx
+++ b/src/games/storywriter/components/editor.tsx
@@ -1,9 +1,16 @@
import { ContentEditable } from "@common/components/ContentEditable";
-import { useAppState } from "../contexts/state";
+import { useAppState, type Tab } from "../contexts/state";
import styles from '../assets/editor.module.css';
import { highlight } from "../utils/highlight";
import { useMemo } from "preact/hooks";
+const TABS: { id: Tab; label: string }[] = [
+ { id: "story", label: "Story" },
+ { id: "lore", label: "Lore" },
+ { id: "characters", label: "Characters" },
+ { id: "locations", label: "Locations" },
+];
+
export const Editor = () => {
const { currentStory, dispatch } = useAppState();
@@ -20,16 +27,68 @@ export const Editor = () => {
});
};
- const value = useMemo(() => highlight(currentStory.text), [currentStory.text]);
+ const handleLoreInput = (e: Event) => {
+ const lore = (e.target as HTMLElement).textContent || '';
+ dispatch({
+ type: 'EDIT_LORE',
+ id: currentStory.id,
+ lore,
+ });
+ };
+
+ const handleTabChange = (tab: Tab) => {
+ dispatch({
+ type: 'SET_CURRENT_TAB',
+ id: currentStory.id,
+ tab,
+ });
+ };
+
+ const storyValue = useMemo(() => highlight(currentStory.text), [currentStory.text]);
+ const loreValue = useMemo(() => highlight(currentStory.lore), [currentStory.lore]);
return (
{currentStory.title}
-
+
+ {currentStory.currentTab === "story" && (
+
+ )}
+ {currentStory.currentTab === "lore" && (
+
+ )}
+ {currentStory.currentTab === "characters" && (
+
+
Characters content placeholder
+
+ )}
+ {currentStory.currentTab === "locations" && (
+
+
Locations content placeholder
+
+ )}
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
);
};
diff --git a/src/games/storywriter/components/menu-sidebar.tsx b/src/games/storywriter/components/menu-sidebar.tsx
index 46e2b60..c0ef6f8 100644
--- a/src/games/storywriter/components/menu-sidebar.tsx
+++ b/src/games/storywriter/components/menu-sidebar.tsx
@@ -120,9 +120,11 @@ export const MenuSidebar = () => {
/>
))}
-
+
+
+
{isSettingsOpen.value && (
diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx
index 88384af..879c678 100644
--- a/src/games/storywriter/contexts/state.tsx
+++ b/src/games/storywriter/contexts/state.tsx
@@ -10,10 +10,14 @@ export type ChatMessage = LLM.ChatMessage & {
id: string;
}
+export type Tab = "story" | "lore" | "characters" | "locations"
+
export interface Story {
id: string;
title: string;
text: string;
+ lore: string;
+ currentTab: Tab;
chatMessages: ChatMessage[];
}
@@ -33,7 +37,8 @@ type Action =
| { type: 'CREATE_STORY'; title: string }
| { type: 'RENAME_STORY'; id: string; title: string }
| { type: 'EDIT_STORY'; id: string; text: string }
- | { type: 'APPEND_TO_STORY'; id: string; text: string }
+ | { type: 'EDIT_LORE'; id: string; lore: string }
+ | { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
| { type: 'DELETE_STORY'; id: string }
| { type: 'SELECT_STORY'; id: string }
| { type: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage }
@@ -61,6 +66,8 @@ function reducer(state: IState, action: Action): IState {
id: crypto.randomUUID(),
title: action.title,
text: '',
+ lore: '',
+ currentTab: 'story',
chatMessages: [],
};
return {
@@ -85,6 +92,22 @@ function reducer(state: IState, action: Action): IState {
),
};
}
+ case 'EDIT_LORE': {
+ return {
+ ...state,
+ stories: state.stories.map(s =>
+ s.id === action.id ? { ...s, lore: action.lore } : s
+ ),
+ };
+ }
+ case 'SET_CURRENT_TAB': {
+ return {
+ ...state,
+ stories: state.stories.map(s =>
+ s.id === action.id ? { ...s, currentTab: action.tab } : s
+ ),
+ };
+ }
case 'DELETE_STORY': {
const remaining = state.stories.filter(s => s.id !== action.id);
const deletingCurrent = state.currentStoryId === action.id;
@@ -125,14 +148,6 @@ function reducer(state: IState, action: Action): IState {
),
};
}
- case 'APPEND_TO_STORY': {
- return {
- ...state,
- stories: state.stories.map(s =>
- s.id === action.id ? { ...s, text: s.text + action.text } : s
- ),
- };
- }
case 'SET_CONNECTION': {
return {
...state,
diff --git a/src/games/storywriter/utils/tools.ts b/src/games/storywriter/utils/tools.ts
index 06c91e5..c611e68 100644
--- a/src/games/storywriter/utils/tools.ts
+++ b/src/games/storywriter/utils/tools.ts
@@ -23,9 +23,14 @@ export namespace Tools {
return 'Error: No story selected';
}
appState.dispatch({
- type: 'APPEND_TO_STORY',
+ type: 'EDIT_STORY',
id: appState.currentStory.id,
- text
+ text: appState.currentStory.text + text
+ });
+ appState.dispatch({
+ type: 'SET_CURRENT_TAB',
+ id: appState.currentStory.id,
+ tab: 'story'
});
return 'Text appended successfully';
},
@@ -40,6 +45,42 @@ export namespace Tools {
},
required: ['text'],
},
+ },
+ 'append_to_lore': {
+ handler: async (args, appState) => {
+ if (!args || typeof args !== 'object' || !('text' in args)) {
+ return 'Error: Missing required argument "text"';
+ }
+ const { text } = args as { text: string };
+ if (typeof text !== 'string') {
+ return 'Error: Argument "text" must be a string';
+ }
+ if (!appState.currentStory) {
+ return 'Error: No story selected';
+ }
+ appState.dispatch({
+ type: 'EDIT_LORE',
+ id: appState.currentStory.id,
+ lore: appState.currentStory.lore + text
+ });
+ appState.dispatch({
+ type: 'SET_CURRENT_TAB',
+ id: appState.currentStory.id,
+ tab: 'lore'
+ });
+ return 'Text appended to lore successfully';
+ },
+ description: 'Append text to the story lore',
+ parameters: {
+ type: 'object',
+ properties: {
+ text: {
+ type: 'string',
+ description: 'The text to append to the lore',
+ },
+ },
+ required: ['text'],
+ },
}
};