1
0
Fork 0

Chat-mode

This commit is contained in:
Pabloader 2026-04-07 08:57:30 +00:00
parent 62c71a02c3
commit 23ea3eb7db
6 changed files with 207 additions and 136 deletions

View File

@ -43,6 +43,11 @@
padding-bottom: 10px; padding-bottom: 10px;
} }
.content.chatContent {
padding: 0;
overflow: hidden;
}
.editable { .editable {
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;

View File

@ -42,7 +42,7 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
); );
}; };
export const ChatSidebar = () => { export const ChatPanel = () => {
const appState = useAppState(); const appState = useAppState();
const { currentWorld, currentStory, dispatch, connection, model, enableThinking, chatOpen } = appState; const { currentWorld, currentStory, dispatch, connection, model, enableThinking, chatOpen } = appState;
const { summarizeAll, isSummarizing } = useChapterSummarization(); const { summarizeAll, isSummarizing } = useChapterSummarization();
@ -307,115 +307,115 @@ export const ChatSidebar = () => {
const isDisabled = !currentStory || !connection || !model || isLoading; const isDisabled = !currentStory || !connection || !model || isLoading;
return ( return (
<Sidebar side="right" open={chatOpen} onToggle={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} class={sidebarStyles.mobileOverlay}> <div class={styles.chat}>
<div class={styles.chat}> {!currentStory ? (
{!currentStory ? ( <div class={styles.placeholder}>
<div class={styles.placeholder}> Select a chat to start
Select a story to start chatting </div>
</div> ) : !connection || !model ? (
) : !connection || !model ? ( <div class={styles.placeholder}>
<div class={styles.placeholder}> {!connection ? 'Connect to an LLM server' : 'Select a model'} to start chatting
{!connection ? 'Connect to an LLM server' : 'Select a model'} to start chatting </div>
</div> ) : currentStory.chatMessages.length === 0 ? (
) : currentStory.chatMessages.length === 0 ? ( <div class={styles.placeholder}>
<div class={styles.placeholder}> No messages yet
No messages yet </div>
</div> ) : (
) : ( <div class={styles.messages} ref={messagesRef}>
<div class={styles.messages} ref={messagesRef}> {currentStory.chatMessages.map((message) => (
{currentStory.chatMessages.map((message) => ( <div key={message.id} class={styles.message} data-role={message.role}>
<div key={message.id} class={styles.message} data-role={message.role}> <RoleHeader message={message} chatMessages={currentStory.chatMessages} />
<RoleHeader message={message} chatMessages={currentStory.chatMessages} />
{message.role === 'assistant' && message.reasoning_content && ( {message.role === 'assistant' && message.reasoning_content && (
<div class={styles.reasoningContent}> <div class={styles.reasoningContent}>
{message.reasoning_content} {message.reasoning_content}
</div> </div>
)} )}
<div <div
class={styles.content} class={styles.content}
dangerouslySetInnerHTML={{ __html: highlight(message.content, false).trim() }} dangerouslySetInnerHTML={{ __html: highlight(message.content, false).trim() }}
/> />
{message.role === 'assistant' && message.tool_calls && ( {message.role === 'assistant' && message.tool_calls && (
<div class={styles.toolCalls}> <div class={styles.toolCalls}>
{message.tool_calls.map((tool) => ( {message.tool_calls.map((tool) => (
<span key={tool.id} class={styles.toolBadge}> <span key={tool.id} class={styles.toolBadge}>
{tool.function.name} {tool.function.name}
</span> </span>
))} ))}
</div> </div>
)} )}
</div>
))}
{error && (
<div class={clsx(styles.message, styles.errorMessage)} data-role="assistant">
<div class={styles.role}>error</div>
<div class={styles.errorText}>{error}</div>
</div>
)}
<button class={styles.clearButton} onClick={handleClear}>
Clear chat
</button>
</div>
)}
{currentStory && (
<div class={styles.inputContainer}>
<div class={styles.optionsRow}>
<label class={styles.toggleContainer}>
<input
type="checkbox"
checked={enableThinking}
onChange={(e) => dispatch({
type: 'SET_ENABLE_THINKING',
enable: (e.target as HTMLInputElement).checked,
})}
disabled={isDisabled}
/>
<span>Enable thinking</span>
</label>
<div class={styles.tokenCounter}>
{tokenCount && <span>{tokenCount.taken} / {tokenCount.total} tokens</span>}
<button
class={styles.summarizeButton}
onClick={summarizeAll}
disabled={isSummarizing || !currentStory || !connection || !model}
title={isSummarizing ? 'Summarizing...' : 'Summarize'}>
<Sparkles size={14} />
</button>
</div>
</div> </div>
<ContentEditable ))}
autoLines {error && (
class={styles.input} <div class={clsx(styles.message, styles.errorMessage)} data-role="assistant">
value={input} <div class={styles.role}>error</div>
onInput={setInput} <div class={styles.errorText}>{error}</div>
onKeyDown={handleKeyDown} </div>
placeholder={ )}
isLoading <button class={styles.clearButton} onClick={handleClear}>
? 'Generating...' Clear chat
: isDisabled </button>
? 'Connect to an LLM server to chat' </div>
: 'Type a message...'} )}
disabled={isDisabled} {currentStory && (
/> <div class={styles.inputContainer}>
{isLoading ? ( <div class={styles.optionsRow}>
<label class={styles.toggleContainer}>
<input
type="checkbox"
checked={enableThinking}
onChange={(e) => dispatch({
type: 'SET_ENABLE_THINKING',
enable: (e.target as HTMLInputElement).checked,
})}
disabled={isDisabled}
/>
<span>Enable thinking</span>
</label>
<div class={styles.tokenCounter}>
{tokenCount && <span>{tokenCount.taken} / {tokenCount.total} tokens</span>}
<button <button
class={styles.stopButton} class={styles.summarizeButton}
onClick={handleStopGeneration} onClick={summarizeAll}
> disabled={isSummarizing || !currentStory || !connection || !model}
Stop title={isSummarizing ? 'Summarizing...' : 'Summarize'}>
<Sparkles size={14} />
</button> </button>
) : ( </div>
<div class={styles.buttonRow}> </div>
<button <ContentEditable
class={styles.sendButton} autoLines
onClick={handleSendMessage} class={styles.input}
disabled={isDisabled || !input.trim()} value={input}
> onInput={setInput}
Send onKeyDown={handleKeyDown}
</button> placeholder={
isLoading
? 'Generating...'
: isDisabled
? 'Connect to an LLM server to chat'
: 'Type a message...'}
disabled={isDisabled}
/>
{isLoading ? (
<button
class={styles.stopButton}
onClick={handleStopGeneration}
>
Stop
</button>
) : (
<div class={styles.buttonRow}>
<button
class={styles.sendButton}
onClick={handleSendMessage}
disabled={isDisabled || !input.trim()}
>
Send
</button>
{!currentWorld?.chatOnly && (
<button <button
class={styles.continueButton} class={styles.continueButton}
onClick={handleContinue} onClick={handleContinue}
@ -424,11 +424,32 @@ export const ChatSidebar = () => {
> >
<ChevronsRight size={14} /> <ChevronsRight size={14} />
</button> </button>
</div> )}
)} </div>
</div> )}
)} </div>
</div> )}
</div>
);
};
export const ChatSidebar = () => {
const { currentWorld, chatOpen, dispatch } = useAppState();
// In chat-only worlds, chat is a full editor tab — no sidebar needed
if (currentWorld?.chatOnly) return null;
return (
<Sidebar
side="right"
open={chatOpen}
onToggle={() => dispatch({
type: 'SET_CHAT_OPEN',
open: !chatOpen,
})}
class={sidebarStyles.mobileOverlay}
>
<ChatPanel />
</Sidebar> </Sidebar>
); );
}; };

View File

@ -9,11 +9,12 @@ 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 { Menu } from "./menu";
import { ChatPanel } from "./chat-sidebar";
import { useInputCallback } from "@common/hooks/useInputCallback"; import { useInputCallback } from "@common/hooks/useInputCallback";
import Prompt from "../utils/prompt"; import Prompt from "../utils/prompt";
import { BookOpen, List, Users, MapPin, BookMarked, FileText, Code, Layers, MessageSquare, Globe, BrainCircuit, type LucideIcon } from "lucide-preact"; import { BookOpen, List, Users, MapPin, BookMarked, FileText, Code, Layers, MessageSquare, Globe, BrainCircuit, MessagesSquare, type LucideIcon } from "lucide-preact";
// Tabs available when a story is selected // Tabs available when a story is selected (regular world)
const STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ const STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
{ id: "menu", label: "Menu", icon: List }, { id: "menu", label: "Menu", icon: List },
{ id: "story", label: "Story", icon: BookOpen }, { id: "story", label: "Story", icon: BookOpen },
@ -25,7 +26,7 @@ const STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[
{ id: "prompt", label: "Prompt", icon: Code }, { id: "prompt", label: "Prompt", icon: Code },
]; ];
// Tabs available when only a world is selected (no story) // Tabs available when only a world is selected (no story, regular world)
const WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ const WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
{ id: "menu", label: "Menu", icon: List }, { id: "menu", label: "Menu", icon: List },
{ id: "lore", label: "Lore", icon: BookMarked }, { id: "lore", label: "Lore", icon: BookMarked },
@ -34,6 +35,20 @@ const WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[
{ id: "system", label: "System", icon: BrainCircuit }, { id: "system", label: "System", icon: BrainCircuit },
]; ];
// Tabs for a chat session within a chat-only world
const CHAT_STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
{ id: "menu", label: "Menu", icon: List },
{ id: "chat", label: "Chat", icon: MessageSquare },
{ id: "scratchpad", label: "Scratchpad", icon: FileText, right: true },
{ id: "prompt", label: "Prompt", icon: Code },
];
// Tabs for a chat-only world with no session selected
const CHAT_WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
{ id: "menu", label: "Menu", icon: List },
{ id: "system", label: "System", icon: BrainCircuit },
];
export const Editor = () => { export const Editor = () => {
const appState = useAppState(); const appState = useAppState();
const { currentWorld, currentStory, currentTab, chatOpen, dispatch } = appState; const { currentWorld, currentStory, currentTab, chatOpen, dispatch } = appState;
@ -107,21 +122,30 @@ export const Editor = () => {
}, [currentStory?.id, currentWorld?.id, currentTab]); }, [currentStory?.id, currentWorld?.id, currentTab]);
const hasSelection = currentWorld !== null; const hasSelection = currentWorld !== null;
const tabs = currentStory ? STORY_TABS : currentWorld ? WORLD_TABS : [{ id: "menu" as Tab, label: "Menu", icon: List }]; const isChatOnly = currentWorld?.chatOnly ?? false;
// Title bar: show world > story or just world const tabs = currentStory
? (isChatOnly ? CHAT_STORY_TABS : STORY_TABS)
: currentWorld
? (isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS)
: [{ id: "menu" as Tab, label: "Menu", icon: List }];
// Title bar: use MessagesSquare icon for chat-only worlds
const WorldIcon = isChatOnly ? MessagesSquare : Globe;
const titleBar = currentStory const titleBar = currentStory
? <div class={styles.title}> ? <div class={styles.title}>
<span class={styles.titleWorld}>{currentWorld?.title}</span> <span class={styles.titleWorld}>{currentWorld?.title}</span>
<span class={styles.titleSep}>/</span>{currentStory.title}</div> <span class={styles.titleSep}>/</span>{currentStory.title}</div>
: currentWorld : currentWorld
? <div class={styles.title}><Globe size={24} />{currentWorld.title}</div> ? <div class={styles.title}><WorldIcon size={24} />{currentWorld.title}</div>
: null; : null;
const isChatTab = currentTab === 'chat';
return ( return (
<div class={styles.editor}> <div class={styles.editor}>
{titleBar} {titleBar}
<div class={clsx(styles.content, currentTab === 'menu' && styles.menuContent)} ref={contentRef}> <div class={clsx(styles.content, currentTab === 'menu' && styles.menuContent, isChatTab && styles.chatContent)} ref={contentRef}>
{currentTab === "menu" && ( {currentTab === "menu" && (
<Menu /> <Menu />
)} )}
@ -164,6 +188,9 @@ export const Editor = () => {
placeholder="Override the global system instruction for this world. Leave empty to use the global setting." placeholder="Override the global system instruction for this world. Leave empty to use the global setting."
/> />
)} )}
{currentTab === "chat" && currentStory && isChatOnly && (
<ChatPanel />
)}
</div> </div>
<div class={styles.tabs}> <div class={styles.tabs}>
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => ( {tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => (
@ -177,7 +204,7 @@ export const Editor = () => {
<span class={styles.tabLabel}>{tab.label}</span> <span class={styles.tabLabel}>{tab.label}</span>
</button> </button>
))} ))}
{currentStory && ( {currentStory && !isChatOnly && (
<button <button
class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)} class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)}
onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })}

View File

@ -6,7 +6,7 @@ import { useBool } from "@common/hooks/useBool";
import { useInputState } from "@common/hooks/useInputState"; import { useInputState } from "@common/hooks/useInputState";
import type { World, Story } from "../contexts/state"; import type { World, Story } from "../contexts/state";
import styles from '../assets/menu.module.css'; import styles from '../assets/menu.module.css';
import { Pencil, X, Plus, Plug, Settings, Copy, ChevronRight, ChevronDown, Globe, Download, Upload } from "lucide-preact"; import { Pencil, X, Plus, Plug, Settings, Copy, ChevronRight, ChevronDown, Globe, Download, Upload, MessagesSquare, MessageSquarePlus } from "lucide-preact";
// ─── Inline Rename Input ────────────────────────────────────────────────────── // ─── Inline Rename Input ──────────────────────────────────────────────────────
@ -144,12 +144,12 @@ const WorldItem = ({
onClick={onSelectWorld} onClick={onSelectWorld}
onDblClick={isRenaming.setTrue} onDblClick={isRenaming.setTrue}
> >
<Globe size={13} /> {world.chatOnly ? <MessagesSquare size={13} /> : <Globe size={13} />}
{world.title} {world.title}
</button> </button>
<div class={styles.actions}> <div class={styles.actions}>
<button class={styles.actionButton} onClick={onCreateStory} title="New Story"> <button class={styles.actionButton} onClick={onCreateStory} title={world.chatOnly ? "New Chat" : "New Story"}>
<Plus size={14} /> {world.chatOnly ? <MessageSquarePlus size={14} /> : <Plus size={14} />}
</button> </button>
<button class={styles.actionButton} onClick={onExportWorld} title="Export World"> <button class={styles.actionButton} onClick={onExportWorld} title="Export World">
<Download size={14} /> <Download size={14} />
@ -196,6 +196,10 @@ export const Menu = () => {
dispatch({ type: 'CREATE_WORLD', title: 'New World' }); dispatch({ type: 'CREATE_WORLD', title: 'New World' });
}; };
const handleCreateChatWorld = () => {
dispatch({ type: 'CREATE_WORLD', title: 'New Chat', chatOnly: true });
};
const handleSelectWorld = (worldId: string) => { const handleSelectWorld = (worldId: string) => {
dispatch({ type: 'SELECT_WORLD', worldId }); dispatch({ type: 'SELECT_WORLD', worldId });
}; };
@ -213,7 +217,7 @@ export const Menu = () => {
}; };
const handleCreateStory = (worldId: string) => { const handleCreateStory = (worldId: string) => {
dispatch({ type: 'CREATE_STORY', worldId, title: 'New Story' }); dispatch({ type: 'CREATE_STORY', worldId });
}; };
const handleSelectStory = (worldId: string, storyId: string) => { const handleSelectStory = (worldId: string, storyId: string) => {
@ -276,6 +280,9 @@ export const Menu = () => {
<button class={styles.newButton} onClick={handleCreateWorld}> <button class={styles.newButton} onClick={handleCreateWorld}>
<Plus size={16} /> New World <Plus size={16} /> New World
</button> </button>
<button class={styles.newButton} onClick={handleCreateChatWorld}>
<MessagesSquare size={16} /> New Chat World
</button>
<button class={styles.newButton} onClick={handleImportWorld}> <button class={styles.newButton} onClick={handleImportWorld}>
<Upload size={16} /> Import World <Upload size={16} /> Import World
</button> </button>

View File

@ -1,6 +1,6 @@
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useMemo } from "preact/hooks"; import { useContext, useMemo } from "preact/hooks";
import { useRemoteReducer } from "@common/hooks/useRemote"; import { useStoredReducer } from "@common/hooks/useStored";
import LLM from "../utils/llm"; import LLM from "../utils/llm";
import Chapters from "../utils/chapters"; import Chapters from "../utils/chapters";
@ -11,7 +11,7 @@ export type ChatMessage = LLM.ChatMessage & {
id: string; id: string;
} }
export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt" | "menu" | "system"; export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt" | "menu" | "system" | "chat";
export enum CharacterRole { export enum CharacterRole {
Protagonist = 'protagonist', Protagonist = 'protagonist',
@ -80,6 +80,7 @@ export interface Story {
export interface World { export interface World {
id: string; id: string;
title: string; title: string;
chatOnly: boolean;
lore: LoreEntry[]; lore: LoreEntry[];
characters: Character[]; characters: Character[];
locations: Location[]; locations: Location[];
@ -106,12 +107,12 @@ interface IState {
type Action = type Action =
// World actions // World actions
| { type: 'CREATE_WORLD'; title: string } | { type: 'CREATE_WORLD'; title: string; chatOnly?: boolean }
| { type: 'RENAME_WORLD'; worldId: string; title: string } | { type: 'RENAME_WORLD'; worldId: string; title: string }
| { type: 'DELETE_WORLD'; worldId: string } | { type: 'DELETE_WORLD'; worldId: string }
| { type: 'SELECT_WORLD'; worldId: string } | { type: 'SELECT_WORLD'; worldId: string }
// Story actions // Story actions
| { type: 'CREATE_STORY'; worldId: string; title: string } | { type: 'CREATE_STORY'; worldId: string }
| { type: 'RENAME_STORY'; worldId: string; id: string; title: string } | { type: 'RENAME_STORY'; worldId: string; id: string; title: string }
| { type: 'EDIT_STORY'; worldId: string; id: string; text: string; highlightText?: string } | { type: 'EDIT_STORY'; worldId: string; id: string; text: string; highlightText?: string }
| { type: 'EDIT_SCRATCHPAD'; worldId: string; id: string; text: string } | { type: 'EDIT_SCRATCHPAD'; worldId: string; id: string; text: string }
@ -226,6 +227,7 @@ function reducer(state: IState, action: Action): IState {
const world: World = { const world: World = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
title: action.title, title: action.title,
chatOnly: action.chatOnly ?? false,
lore: [], lore: [],
characters: [], characters: [],
locations: [], locations: [],
@ -236,7 +238,7 @@ function reducer(state: IState, action: Action): IState {
worlds: [...state.worlds, world], worlds: [...state.worlds, world],
currentWorldId: world.id, currentWorldId: world.id,
currentStoryId: null, currentStoryId: null,
currentTab: 'lore', currentTab: action.chatOnly ? 'system' : 'lore',
}; };
} }
case 'RENAME_WORLD': { case 'RENAME_WORLD': {
@ -253,17 +255,18 @@ function reducer(state: IState, action: Action): IState {
}; };
} }
case 'SELECT_WORLD': { case 'SELECT_WORLD': {
const world = state.worlds.find(w => w.id === action.worldId);
return { return {
...state, ...state,
currentWorldId: action.worldId, currentWorldId: action.worldId,
currentStoryId: null, currentStoryId: null,
currentTab: 'lore', currentTab: world?.chatOnly ? 'system' : 'lore',
}; };
} }
case 'CREATE_STORY': { case 'CREATE_STORY': {
const story: Story = { const story: Story = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
title: action.title, title: '',
text: '', text: '',
scratchpad: '', scratchpad: '',
lore: [], lore: [],
@ -272,11 +275,13 @@ function reducer(state: IState, action: Action): IState {
chatMessages: [], chatMessages: [],
chapters: [], chapters: [],
}; };
const world = state.worlds.find(w => w.id === action.worldId);
story.title = world?.chatOnly ? 'New chat' : 'New Story';
return { return {
...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, story] })), ...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, story] })),
currentWorldId: action.worldId, currentWorldId: action.worldId,
currentStoryId: story.id, currentStoryId: story.id,
currentTab: 'story', currentTab: world?.chatOnly ? 'chat' : 'story',
}; };
} }
case 'RENAME_STORY': { case 'RENAME_STORY': {
@ -303,11 +308,12 @@ function reducer(state: IState, action: Action): IState {
}; };
} }
case 'SELECT_STORY': { case 'SELECT_STORY': {
const world = state.worlds.find(w => w.id === action.worldId);
return { return {
...state, ...state,
currentWorldId: action.worldId, currentWorldId: action.worldId,
currentStoryId: action.id, currentStoryId: action.id,
currentTab: 'story', currentTab: world?.chatOnly ? 'chat' : 'story',
}; };
} }
case 'DUPLICATE_STORY': { case 'DUPLICATE_STORY': {
@ -328,7 +334,7 @@ function reducer(state: IState, action: Action): IState {
return { return {
...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, newStory] })), ...updateWorld(state, action.worldId, w => ({ ...w, stories: [...w.stories, newStory] })),
currentStoryId: newStory.id, currentStoryId: newStory.id,
currentTab: 'story', currentTab: world?.chatOnly ? 'chat' : 'story',
}; };
} }
case 'ADD_LORE_ENTRY': { case 'ADD_LORE_ENTRY': {
@ -508,7 +514,7 @@ function reducer(state: IState, action: Action): IState {
worlds: [...state.worlds, world], worlds: [...state.worlds, world],
currentWorldId: world.id, currentWorldId: world.id,
currentStoryId: null, currentStoryId: null,
currentTab: 'lore', currentTab: world.chatOnly ? 'system' : 'lore',
}; };
} }
} }
@ -543,7 +549,7 @@ export const useAppState = () => useContext(StateContext);
// ─── Provider ──────────────────────────────────────────────────────────────── // ─── Provider ────────────────────────────────────────────────────────────────
export const StateContextProvider = ({ children }: { children?: any }) => { export const StateContextProvider = ({ children }: { children?: any }) => {
const [state, dispatch] = useRemoteReducer('storywriter.state', reducer, DEFAULT_STATE); const [state, dispatch] = useStoredReducer('storywriter.state', reducer, DEFAULT_STATE);
const value = useMemo<AppState>(() => { const value = useMemo<AppState>(() => {
const currentWorld = state.worlds.find(w => w.id === state.currentWorldId) ?? null; const currentWorld = state.worlds.find(w => w.id === state.currentWorldId) ?? null;

View File

@ -275,11 +275,16 @@ namespace Prompt {
} }
export function formatSystemPrompt(state: AppState, storyTokenBudget: number = 0): string { export function formatSystemPrompt(state: AppState, storyTokenBudget: number = 0): string {
const { currentStory } = state; const { currentStory, currentWorld } = state;
if (!currentStory) { if (!currentStory) {
return state.effectiveSystemInstruction; return state.effectiveSystemInstruction;
} }
// Chat-only worlds: just the system instruction, no story scaffolding
if (currentWorld?.chatOnly) {
return state.effectiveSystemInstruction;
}
const parts: string[] = [state.effectiveSystemInstruction]; const parts: string[] = [state.effectiveSystemInstruction];
parts.push(`# Story Title: ${currentStory.title}`); parts.push(`# Story Title: ${currentStory.title}`);
@ -351,7 +356,7 @@ namespace Prompt {
return { return {
model: model.id, model: model.id,
messages, messages,
tools: Tools.getTools(), tools: state.currentWorld?.chatOnly ? undefined : Tools.getTools(),
banned_tokens: state.bannedTokens, banned_tokens: state.bannedTokens,
enable_thinking: enableThinking, enable_thinking: enableThinking,
max_tokens: model.max_length ? model.max_length : 2048, max_tokens: model.max_length ? model.max_length : 2048,