From d5481e1a13db993a386bfc51a125045ddd2b76a3 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Mon, 2 Nov 2026 13:58:33 +0000 Subject: [PATCH] Global edit tools --- src/common/assets/content-editable.module.css | 1 + src/common/components/ContentEditable.tsx | 10 ++-- src/games/storywriter/utils/tools.ts | 51 +++++-------------- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/src/common/assets/content-editable.module.css b/src/common/assets/content-editable.module.css index 70b1cb8..29ff8d3 100644 --- a/src/common/assets/content-editable.module.css +++ b/src/common/assets/content-editable.module.css @@ -3,6 +3,7 @@ } .root { + position: relative; white-space: pre-wrap; outline: none; word-wrap: break-word; diff --git a/src/common/components/ContentEditable.tsx b/src/common/components/ContentEditable.tsx index d2ea29d..b303d39 100644 --- a/src/common/components/ContentEditable.tsx +++ b/src/common/components/ContentEditable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useRef } from "preact/hooks"; import type { JSX } from "preact"; import clsx from "clsx"; import styles from "../assets/content-editable.module.css"; @@ -9,6 +9,7 @@ type Props = Omit, 'value' | 'onInput'> & { placeholder?: string; autoLines?: boolean; highlight?: boolean; + disabled?: boolean; onInput?: JSX.EventHandler>; }; @@ -83,6 +84,7 @@ export const ContentEditable = ({ placeholder, autoLines, highlight: enableHighlight, + disabled, onInput, class: externalClass, ...props @@ -118,10 +120,10 @@ export const ContentEditable = ({
); }; diff --git a/src/games/storywriter/utils/tools.ts b/src/games/storywriter/utils/tools.ts index 232274a..7e6805d 100644 --- a/src/games/storywriter/utils/tools.ts +++ b/src/games/storywriter/utils/tools.ts @@ -101,7 +101,7 @@ export namespace Tools { appState.dispatch({ type: 'ADD_CHARACTER', worldId: appState.currentWorld.id, - storyId: appState.currentStory.id, + storyId: args.world ? null : appState.currentStory.id, character: { id: crypto.randomUUID(), name: args.name.trim(), @@ -134,6 +134,7 @@ export namespace Tools { }), { description: 'Optional list of relationships with other characters (allowed only when adding a new character)' } )), + world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })), }), }), 'set_character_relation': tool({ @@ -150,7 +151,7 @@ export namespace Tools { return `Error: Target character "${args.target_name.trim()}" not found, please add it first.`; } const inStory = appState.currentStory.characters.some(c => c.id === character.id); - const storyId = inStory ? appState.currentStory.id : null; + const storyId = inStory && !args.world ? appState.currentStory.id : null; const existingRelationIndex = character.relations.findIndex(r => r.name === args.target_name.trim()); if (existingRelationIndex !== -1) { appState.dispatch({ @@ -183,6 +184,7 @@ export namespace Tools { character_name: Type.String({ description: "The character's full name to add/edit the relation for" }), target_name: Type.String({ description: "The target character's full name" }), relation: Type.String({ description: 'The relationship type (e.g., friend, enemy, sibling)' }), + world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })), }), }), 'get_location': tool({ @@ -244,7 +246,7 @@ export namespace Tools { appState.dispatch({ type: 'ADD_LOCATION', worldId: appState.currentWorld.id, - storyId: appState.currentStory.id, + storyId: args.world ? null : appState.currentStory.id, location: { id: crypto.randomUUID(), name: args.name.trim(), @@ -265,6 +267,7 @@ export namespace Tools { scale: Type.Optional(Type.Enum(VALID_SCALES, { description: `Location scale (enum): ${VALID_SCALES.join(', ')}`, })), + world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })), }), }), 'set_lore_entry': tool({ @@ -289,7 +292,7 @@ export namespace Tools { appState.dispatch({ type: 'ADD_LORE_ENTRY', worldId: appState.currentWorld.id, - storyId: appState.currentStory.id, + storyId: args.world ? null : appState.currentStory.id, entry: { id: crypto.randomUUID(), title: args.title.trim(), @@ -304,6 +307,7 @@ export namespace Tools { parameters: Type.Object({ title: Type.String({ description: "The lore entry's title" }), text: Type.String({ description: 'The lore entry content.' }), + world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })), }), }), 'edit_text': tool({ @@ -324,42 +328,11 @@ export namespace Tools { // Append mode: when old_text is not provided, append new_text if (args.old_text == null) { - if (!isScratchpad) { - const isAppendToStory = (tc: LLM.ToolCall) => { - const jsonArgs = JSON.stringify(tc.function.arguments); - return tc.function.name === 'edit_text' - && ( - !jsonArgs.match(/\\?"old_text\\?"/) - || jsonArgs.match(/\\?"old_text\\?":\\?"\\?"/) - ) - && ( - !jsonArgs.match(/\\?"target\\?"/) - || jsonArgs.match(/\\?"target\\?":\\?"story\\?"/) - ); - } - - - // Check that there was a user message since the last edit_text append - const messages = appState.currentStory.chatMessages; - let hasUserMessageSinceLastAppend = true; - for (let i = messages.length - 1; i >= 0; i--) { - const m = messages[i]; - if (m.role === 'user') break; - if (m.role === 'assistant' && m.tool_calls?.some(isAppendToStory)) { - hasUserMessageSinceLastAppend = false; - break; - } - } - if (!hasUserMessageSinceLastAppend) { - return `Error: You cannot add new text to the story until user approves your last edit. Stop right there.`; - } - } - - const numLines = args.new_text.split('\n').filter(l => l.trim()).length; + const lines = args.new_text.split(/\n+/ig).filter(l => l.trim()); + const numLines = lines.length; let cropped = false; if (!isScratchpad && numLines > LINES_LIMIT) { - const lines = args.new_text.split('\n'); let kept = 0; const croppedLines: string[] = []; for (const line of lines) { @@ -373,13 +346,13 @@ export namespace Tools { cropped = true; } - dispatchEdit(currentText + '\n' + args.new_text); + dispatchEdit(currentText.trim() + '\n' + args.new_text.trim()); appState.dispatch({ type: 'SET_CURRENT_TAB', tab }); let message = cropped ? `Added text:\n${args.new_text.split('\n').filter(l => l.trim()).map(l => '> ' + l).join('\n')}\n\nNote: The rest was cropped due to ${LINES_LIMIT} lines limit!` : `Text appended to ${target} successfully.`; - message += `\nNote: you can't continue nor add more text until user's approval. Stop any attempts and wait.` + message += `\nNote: you can't continue nor add more text until user's approval. Stop any attempts and wait, even if you think the work is not finished.` return message; }