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 ( +
+ {message.role} + {toolName && ( + + {toolName} + + )} +
+ ); +}; + export const ChatSidebar = () => { const appState = useAppState(); const { currentStory, dispatch, connection, model, enableThinking } = appState; @@ -208,7 +237,7 @@ export const ChatSidebar = () => {
{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'], + }, } };