Refactor the tools
This commit is contained in:
parent
951c13022c
commit
e39de39e35
|
|
@ -65,10 +65,10 @@ export const highlight = (message: string, keepMarkup = true): string => {
|
||||||
} else if (token === '```') {
|
} else if (token === '```') {
|
||||||
stack.push(token);
|
stack.push(token);
|
||||||
inCodeBlock = true;
|
inCodeBlock = true;
|
||||||
resultHTML += `<span class="${styles.codeBlock}">`;
|
resultHTML += `<span class="${styles.codeBlock}">${keepToken ? token : ''}`;
|
||||||
} else if (token === '`') {
|
} else if (token === '`') {
|
||||||
stack.push(token);
|
stack.push(token);
|
||||||
resultHTML += `<span class="${styles.inlineCode}">`;
|
resultHTML += `<span class="${styles.inlineCode}">${keepToken ? token : ''}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import { ConnectionSettingsModal } from "./connection-settings-modal";
|
||||||
import { SettingsModal } from "./settings-modal";
|
import { SettingsModal } from "./settings-modal";
|
||||||
import { useAppState } from "../contexts/state";
|
import { useAppState } from "../contexts/state";
|
||||||
import { useBool } from "@common/hooks/useBool";
|
import { useBool } from "@common/hooks/useBool";
|
||||||
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
import type { Story } from "../contexts/state";
|
import type { Story } from "../contexts/state";
|
||||||
import styles from '../assets/menu-sidebar.module.css';
|
import styles from '../assets/menu-sidebar.module.css';
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact";
|
import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact";
|
||||||
|
|
||||||
// ─── Story Item ───────────────────────────────────────────────────────────────
|
// ─── Story Item ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -21,14 +21,14 @@ interface StoryItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }: StoryItemProps) => {
|
const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }: StoryItemProps) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const isEditing = useBool(false);
|
||||||
const [editTitle, setEditTitle] = useState(story.title);
|
const [editTitle, setEditTitle] = useInputState(story.title);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (editTitle.trim()) {
|
if (editTitle.trim()) {
|
||||||
onRename(editTitle.trim());
|
onRename(editTitle.trim());
|
||||||
}
|
}
|
||||||
setIsEditing(false);
|
isEditing.setFalse();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
@ -36,7 +36,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
setEditTitle(story.title);
|
setEditTitle(story.title);
|
||||||
setIsEditing(false);
|
isEditing.setFalse();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,13 +44,13 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing.value) {
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.itemWrapper, active && styles.active)}>
|
<div class={clsx(styles.itemWrapper, active && styles.active)}>
|
||||||
<input
|
<input
|
||||||
class={styles.input}
|
class={styles.input}
|
||||||
value={editTitle}
|
value={editTitle}
|
||||||
onInput={(e) => setEditTitle((e.target as HTMLInputElement).value)}
|
onInput={setEditTitle}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
@ -64,11 +64,12 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.item, active && styles.active)}
|
class={clsx(styles.item, active && styles.active)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
|
onDblClick={isEditing.setTrue}
|
||||||
>
|
>
|
||||||
{story.title}
|
{story.title}
|
||||||
</button>
|
</button>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename">
|
<button class={styles.actionButton} onClick={isEditing.setTrue} title="Rename">
|
||||||
<Pencil size={14} />
|
<Pencil size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button class={styles.actionButton} onClick={onDuplicate} title="Duplicate">
|
<button class={styles.actionButton} onClick={onDuplicate} title="Duplicate">
|
||||||
|
|
|
||||||
|
|
@ -286,11 +286,20 @@ function reducer(state: IState, action: Action): IState {
|
||||||
case 'DELETE_CHARACTER': {
|
case 'DELETE_CHARACTER': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
stories: state.stories.map(s =>
|
stories: state.stories.map(s => {
|
||||||
s.id === action.storyId
|
if (s.id !== action.storyId) return s;
|
||||||
? { ...s, characters: s.characters.filter(c => c.id !== action.characterId) }
|
const deletedChar = s.characters.find(c => c.id === action.characterId);
|
||||||
: s
|
if (!deletedChar) return s;
|
||||||
),
|
return {
|
||||||
|
...s,
|
||||||
|
characters: s.characters
|
||||||
|
.filter(c => c.id !== action.characterId)
|
||||||
|
.map(c => ({
|
||||||
|
...c,
|
||||||
|
relations: c.relations.filter(r => r.name !== deletedChar.name),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'ADD_CHARACTER_RELATION': {
|
case 'ADD_CHARACTER_RELATION': {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { formatError } from "@common/errors";
|
import { formatErrorMessage } from "@common/errors";
|
||||||
import { Type, type Static, type TObject } from '@common/typebox';
|
import { Type, type Static, type TObject } from '@common/typebox';
|
||||||
import { LocationScale, type AppState, type Character, type Location } from "../contexts/state";
|
import { LocationScale, type AppState, type Character, type Location } from "../contexts/state";
|
||||||
import type LLM from "./llm";
|
import type LLM from "./llm";
|
||||||
|
|
@ -10,6 +10,12 @@ const SCALE_DESCRIPTION = Object.entries(LocationScale)
|
||||||
|
|
||||||
const VALID_SCALES = Object.values(LocationScale).filter(v => typeof v === 'number');
|
const VALID_SCALES = Object.values(LocationScale).filter(v => typeof v === 'number');
|
||||||
|
|
||||||
|
const SCALE_NAMES = Object.fromEntries(
|
||||||
|
Object.entries(LocationScale)
|
||||||
|
.filter(([, value]) => typeof value === 'number')
|
||||||
|
.map(([key, value]) => [value, key])
|
||||||
|
);
|
||||||
|
|
||||||
export namespace Tools {
|
export namespace Tools {
|
||||||
interface Tool<T extends TObject = TObject> {
|
interface Tool<T extends TObject = TObject> {
|
||||||
description: string;
|
description: string;
|
||||||
|
|
@ -20,21 +26,82 @@ export namespace Tools {
|
||||||
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
||||||
|
|
||||||
const TOOLS: Record<string, Tool> = {
|
const TOOLS: Record<string, Tool> = {
|
||||||
'add_character': tool({
|
'get_character': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
// Filter out relations that reference non-existent characters
|
const character = appState.currentStory.characters.find(c => c.name === args.name.trim());
|
||||||
const existingCharacterNames = appState.currentStory.characters.map(c => c.name);
|
if (!character) {
|
||||||
const invalidRelations: string[] = [];
|
return `Error: Character "${args.name.trim()}" not found`;
|
||||||
const validRelations = (args.relations || []).filter(rel => {
|
|
||||||
if (!existingCharacterNames.includes(rel.name)) {
|
|
||||||
invalidRelations.push(`${rel.name} (${rel.relation})`);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
let result = `# Character: ${character.name}\n\n`;
|
||||||
|
if (character.nicknames && character.nicknames.length > 0) {
|
||||||
|
result += `**Nicknames:** ${character.nicknames.join(', ')}\n\n`;
|
||||||
|
}
|
||||||
|
result += `**Short Description:** ${character.shortDescription}\n\n`;
|
||||||
|
if (character.description) {
|
||||||
|
result += `**Description:** ${character.description}\n\n`;
|
||||||
|
}
|
||||||
|
if (character.relations && character.relations.length > 0) {
|
||||||
|
result += `**Relations:**\n`;
|
||||||
|
for (const rel of character.relations) {
|
||||||
|
result += `- ${rel.name}: ${rel.relation}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.trim();
|
||||||
|
},
|
||||||
|
description: 'Get full information about a character by name',
|
||||||
|
parameters: Type.Object({
|
||||||
|
name: Type.String({ description: "The character's full name" }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'set_character': tool({
|
||||||
|
handler: async (args, appState) => {
|
||||||
|
if (!appState.currentStory) {
|
||||||
|
return 'Error: No story selected';
|
||||||
|
}
|
||||||
|
const existingCharacter = appState.currentStory.characters.find(c => c.name === args.name.trim());
|
||||||
|
|
||||||
|
if (existingCharacter) {
|
||||||
|
// Edit existing character
|
||||||
|
const definedUpdates: Partial<Character> = {};
|
||||||
|
if (args.shortDescription !== undefined) {
|
||||||
|
definedUpdates.shortDescription = args.shortDescription;
|
||||||
|
}
|
||||||
|
if (args.description !== undefined) {
|
||||||
|
definedUpdates.description = args.description;
|
||||||
|
}
|
||||||
|
if (args.nicknames !== undefined) {
|
||||||
|
definedUpdates.nicknames = args.nicknames;
|
||||||
|
}
|
||||||
|
if (args.relations !== undefined) {
|
||||||
|
return 'Error: set_character does not support updating relations. Use set_character_relation instead.';
|
||||||
|
}
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_CHARACTER',
|
||||||
|
storyId: appState.currentStory.id,
|
||||||
|
characterId: existingCharacter.id,
|
||||||
|
updates: definedUpdates,
|
||||||
});
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'characters'
|
||||||
|
});
|
||||||
|
return `Character "${args.name.trim()}" updated successfully`;
|
||||||
|
} else {
|
||||||
|
// Add new character - validate required fields
|
||||||
|
if (!args.shortDescription) {
|
||||||
|
return 'Error: shortDescription is required when adding a new character';
|
||||||
|
}
|
||||||
|
const existingCharacterNames = new Set(appState.currentStory.characters.map(c => c.name));
|
||||||
|
const invalidRelations: string[] = [];
|
||||||
|
for (const rel of args.relations || []) {
|
||||||
|
if (!existingCharacterNames.has(rel.name)) {
|
||||||
|
invalidRelations.push(`${rel.name} (${rel.relation})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'ADD_CHARACTER',
|
type: 'ADD_CHARACTER',
|
||||||
storyId: appState.currentStory.id,
|
storyId: appState.currentStory.id,
|
||||||
|
|
@ -44,7 +111,7 @@ export namespace Tools {
|
||||||
nicknames: args.nicknames || [],
|
nicknames: args.nicknames || [],
|
||||||
shortDescription: args.shortDescription.trim(),
|
shortDescription: args.shortDescription.trim(),
|
||||||
description: args.description || '',
|
description: args.description || '',
|
||||||
relations: validRelations,
|
relations: args.relations || [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
|
|
@ -54,14 +121,15 @@ export namespace Tools {
|
||||||
});
|
});
|
||||||
let message = `Character "${args.name.trim()}" added successfully`;
|
let message = `Character "${args.name.trim()}" added successfully`;
|
||||||
if (invalidRelations.length > 0) {
|
if (invalidRelations.length > 0) {
|
||||||
message += `. Removed invalid relations to non-existent characters: ${invalidRelations.join(', ')}`;
|
message += `.\nNote: found invalid relations to non-existent characters: ${invalidRelations.join(', ')}`;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Add a new character to the story',
|
description: 'Add or edit a character. If character exists, updates it; otherwise creates new one.',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
name: Type.String({ description: "The character's full name" }),
|
name: Type.String({ description: "The character's full name" }),
|
||||||
shortDescription: Type.String({ description: 'A brief description of the character (one line)' }),
|
shortDescription: Type.Optional(Type.String({ description: 'A brief description of the character (one line). Required on a new character.' })),
|
||||||
nicknames: Type.Optional(Type.Array(Type.String(), { description: 'Optional list of nicknames' })),
|
nicknames: Type.Optional(Type.Array(Type.String(), { description: 'Optional list of nicknames' })),
|
||||||
description: Type.Optional(Type.String({ description: 'Optional full character description' })),
|
description: Type.Optional(Type.String({ description: 'Optional full character description' })),
|
||||||
relations: Type.Optional(Type.Array(
|
relations: Type.Optional(Type.Array(
|
||||||
|
|
@ -69,52 +137,125 @@ export namespace Tools {
|
||||||
name: Type.String({ description: 'Related character name' }),
|
name: Type.String({ description: 'Related character name' }),
|
||||||
relation: Type.String({ description: 'Relationship type' }),
|
relation: Type.String({ description: 'Relationship type' }),
|
||||||
}),
|
}),
|
||||||
{ description: 'Optional list of relationships with other characters' }
|
{ description: 'Optional list of relationships with other characters (allowed only when adding a new character)' }
|
||||||
)),
|
)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'edit_character': tool({
|
'set_character_relation': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
const character = appState.currentStory.characters.find(c => c.name === args.name.trim());
|
const character = appState.currentStory.characters.find(c => c.name === args.character_name.trim());
|
||||||
if (!character) {
|
if (!character) {
|
||||||
return `Error: Character "${args.name.trim()}" not found`;
|
return `Error: Character "${args.character_name.trim()}" not found`;
|
||||||
}
|
}
|
||||||
// Only include defined values to avoid setting fields to undefined
|
const targetCharacter = appState.currentStory.characters.find(c => c.name === args.target_name.trim());
|
||||||
const definedUpdates: Partial<Character> = {};
|
if (!targetCharacter) {
|
||||||
if (args.shortDescription !== undefined) {
|
return `Error: Target character "${args.target_name.trim()}" not found, please add it first.`;
|
||||||
definedUpdates.shortDescription = args.shortDescription;
|
|
||||||
}
|
|
||||||
if (args.description !== undefined) {
|
|
||||||
definedUpdates.description = args.description;
|
|
||||||
}
|
}
|
||||||
|
const existingRelationIndex = character.relations.findIndex(r => r.name === args.target_name.trim());
|
||||||
|
if (existingRelationIndex !== -1) {
|
||||||
|
// Edit existing relation
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'EDIT_CHARACTER',
|
type: 'EDIT_CHARACTER_RELATION',
|
||||||
storyId: appState.currentStory.id,
|
storyId: appState.currentStory.id,
|
||||||
characterId: character.id,
|
characterId: character.id,
|
||||||
updates: definedUpdates,
|
targetName: args.target_name.trim(),
|
||||||
|
updates: { relation: args.relation.trim() },
|
||||||
});
|
});
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'SET_CURRENT_TAB',
|
type: 'SET_CURRENT_TAB',
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
tab: 'characters'
|
tab: 'characters'
|
||||||
});
|
});
|
||||||
return `Character "${args.name.trim()}" updated successfully`;
|
return `Relation updated: "${args.character_name.trim()}" -> "${args.target_name.trim()}" (${args.relation.trim()})`;
|
||||||
|
} else {
|
||||||
|
// Add new relation
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'ADD_CHARACTER_RELATION',
|
||||||
|
storyId: appState.currentStory.id,
|
||||||
|
characterId: character.id,
|
||||||
|
relation: {
|
||||||
|
name: args.target_name.trim(),
|
||||||
|
relation: args.relation.trim(),
|
||||||
},
|
},
|
||||||
description: "Edit an existing character's description",
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'characters'
|
||||||
|
});
|
||||||
|
return `Relation added: "${args.character_name.trim()}" -> "${args.target_name.trim()}" (${args.relation.trim()})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: 'Add or edit a relationship between two characters. If relation exists, updates it; otherwise creates new one.',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
name: Type.String({ description: "The character's full name to identify which character to edit" }),
|
character_name: Type.String({ description: "The character's full name to add/edit the relation for" }),
|
||||||
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the character (one line)' })),
|
target_name: Type.String({ description: "The target character's full name" }),
|
||||||
description: Type.Optional(Type.String({ description: 'Full character description' })),
|
relation: Type.String({ description: 'The relationship type (e.g., friend, enemy, sibling)' }),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'add_location': tool({
|
'get_location': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
|
const location = appState.currentStory.locations.find(l => l.name === args.name.trim());
|
||||||
|
if (!location) {
|
||||||
|
return `Error: Location "${args.name.trim()}" not found`;
|
||||||
|
}
|
||||||
|
let result = `# Location: ${location.name}\n\n`;
|
||||||
|
result += `**Short Description:** ${location.shortDescription}\n\n`;
|
||||||
|
if (location.description) {
|
||||||
|
result += `**Description:** ${location.description}\n\n`;
|
||||||
|
}
|
||||||
|
result += `**Scale:** ${SCALE_NAMES[location.scale]}\n`;
|
||||||
|
return result.trim();
|
||||||
|
},
|
||||||
|
description: 'Get full information about a location by name',
|
||||||
|
parameters: Type.Object({
|
||||||
|
name: Type.String({ description: "The location's full name" }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'set_location': tool({
|
||||||
|
handler: async (args, appState) => {
|
||||||
|
if (!appState.currentStory) {
|
||||||
|
return 'Error: No story selected';
|
||||||
|
}
|
||||||
|
const existingLocation = appState.currentStory.locations.find(l => l.name === args.name.trim());
|
||||||
|
if (existingLocation) {
|
||||||
|
// Edit existing location
|
||||||
|
const definedUpdates: Partial<Location> = {};
|
||||||
|
if (args.shortDescription) {
|
||||||
|
definedUpdates.shortDescription = args.shortDescription;
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
definedUpdates.description = args.description;
|
||||||
|
}
|
||||||
|
if (args.scale != null) {
|
||||||
|
definedUpdates.scale = args.scale;
|
||||||
|
}
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_LOCATION',
|
||||||
|
storyId: appState.currentStory.id,
|
||||||
|
locationId: existingLocation.id,
|
||||||
|
updates: definedUpdates,
|
||||||
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'locations'
|
||||||
|
});
|
||||||
|
return `Location "${args.name.trim()}" updated successfully`;
|
||||||
|
} else {
|
||||||
|
// Add new location - validate required fields
|
||||||
|
if (!args.shortDescription) {
|
||||||
|
return 'Error: shortDescription is required when adding a new location';
|
||||||
|
}
|
||||||
|
if (args.scale == null) {
|
||||||
|
return 'Error: scale is required when adding a new location';
|
||||||
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'ADD_LOCATION',
|
type: 'ADD_LOCATION',
|
||||||
storyId: appState.currentStory.id,
|
storyId: appState.currentStory.id,
|
||||||
|
|
@ -132,54 +273,13 @@ export namespace Tools {
|
||||||
tab: 'locations'
|
tab: 'locations'
|
||||||
});
|
});
|
||||||
return `Location "${args.name.trim()}" added successfully`;
|
return `Location "${args.name.trim()}" added successfully`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Add a new location to the story',
|
description: 'Add or edit a location. If location exists, updates it; otherwise creates new one.',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
name: Type.String({ description: "The location's full name" }),
|
name: Type.String({ description: "The location's full name" }),
|
||||||
shortDescription: Type.String({ description: 'A brief description of the location (one line)' }),
|
shortDescription: Type.Optional(Type.String({ description: 'A brief description of the location (one line). Required when adding a new location.' })),
|
||||||
description: Type.Optional(Type.String({ description: 'Optional full location description' })),
|
description: Type.Optional(Type.String({ description: 'Optional full location description' })),
|
||||||
scale: Type.Enum(VALID_SCALES, {
|
|
||||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
'edit_location': tool({
|
|
||||||
handler: async (args, appState) => {
|
|
||||||
if (!appState.currentStory) {
|
|
||||||
return 'Error: No story selected';
|
|
||||||
}
|
|
||||||
const location = appState.currentStory.locations.find(l => l.name === args.name.trim());
|
|
||||||
if (!location) {
|
|
||||||
return `Error: Location "${args.name.trim()}" not found`;
|
|
||||||
}
|
|
||||||
const definedUpdates: Partial<Location> = {};
|
|
||||||
if (args.shortDescription !== undefined) {
|
|
||||||
definedUpdates.shortDescription = args.shortDescription;
|
|
||||||
}
|
|
||||||
if (args.description !== undefined) {
|
|
||||||
definedUpdates.description = args.description;
|
|
||||||
}
|
|
||||||
if (args.scale !== undefined) {
|
|
||||||
definedUpdates.scale = args.scale;
|
|
||||||
}
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'EDIT_LOCATION',
|
|
||||||
storyId: appState.currentStory.id,
|
|
||||||
locationId: location.id,
|
|
||||||
updates: definedUpdates,
|
|
||||||
});
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'SET_CURRENT_TAB',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
tab: 'locations'
|
|
||||||
});
|
|
||||||
return `Location "${args.name.trim()}" updated successfully`;
|
|
||||||
},
|
|
||||||
description: "Edit an existing location's description",
|
|
||||||
parameters: Type.Object({
|
|
||||||
name: Type.String({ description: "The location's full name to identify which location to edit" }),
|
|
||||||
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the location (one line)' })),
|
|
||||||
description: Type.Optional(Type.String({ description: 'Full location description' })),
|
|
||||||
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
||||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||||
})),
|
})),
|
||||||
|
|
@ -258,7 +358,7 @@ export namespace Tools {
|
||||||
}
|
}
|
||||||
return `${target === 'lore' ? 'Lore' : 'Story'} edited successfully`;
|
return `${target === 'lore' ? 'Lore' : 'Story'} edited successfully`;
|
||||||
},
|
},
|
||||||
description: 'Replace text in the story or lore. When old_text is omitted, appends new_text to the target.',
|
description: "Replace text in the story or lore. When old_text is omitted, appends new_text to the target's end. Case-sensitive.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
new_text: Type.String({ description: 'The new text to replace old_text with, or to append if old_text is omitted' }),
|
new_text: Type.String({ description: 'The new text to replace old_text with, or to append if old_text is omitted' }),
|
||||||
old_text: Type.Optional(Type.String({ description: 'The text to find and replace. If omitted, new_text will be appended' })),
|
old_text: Type.Optional(Type.String({ description: 'The text to find and replace. If omitted, new_text will be appended' })),
|
||||||
|
|
@ -277,14 +377,16 @@ export namespace Tools {
|
||||||
const sources: { name: string; content: string }[] = [
|
const sources: { name: string; content: string }[] = [
|
||||||
{ name: 'story', content: appState.currentStory.text },
|
{ name: 'story', content: appState.currentStory.text },
|
||||||
{ name: 'lore', content: appState.currentStory.lore },
|
{ name: 'lore', content: appState.currentStory.lore },
|
||||||
...appState.currentStory.characters.map(c => ({
|
...appState.currentStory.characters.flatMap(c => [
|
||||||
name: `character:${c.name}`,
|
{ name: `character:${c.name}`, content: c.name },
|
||||||
content: `${c.shortDescription}\n${c.description || ''}`.trim(),
|
{ name: `character:${c.name}`, content: c.shortDescription },
|
||||||
})),
|
{ name: `character:${c.name}`, content: c.description || '' },
|
||||||
...appState.currentStory.locations.map(l => ({
|
]),
|
||||||
name: `location:${l.name}`,
|
...appState.currentStory.locations.flatMap(l => [
|
||||||
content: `${l.shortDescription}\n${l.description || ''}`.trim(),
|
{ name: `location:${l.name}`, content: l.name },
|
||||||
})),
|
{ name: `location:${l.name}`, content: l.shortDescription },
|
||||||
|
{ name: `location:${l.name}`, content: l.description || '' },
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const allMatches: { source: string; line: number; content: string }[] = [];
|
const allMatches: { source: string; line: number; content: string }[] = [];
|
||||||
|
|
@ -320,7 +422,7 @@ export namespace Tools {
|
||||||
},
|
},
|
||||||
description: 'Search for a pattern in the story text, lore, characters, and locations',
|
description: 'Search for a pattern in the story text, lore, characters, and locations',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
pattern: Type.String({ description: 'The regex pattern to search for' }),
|
pattern: Type.String({ description: 'The JS regex pattern to search for' }),
|
||||||
case_sensitive: Type.Optional(Type.Boolean({ description: 'If true, search is case-sensitive (default: false)' })),
|
case_sensitive: Type.Optional(Type.Boolean({ description: 'If true, search is case-sensitive (default: false)' })),
|
||||||
limit: Type.Optional(Type.Integer({ description: 'Maximum number of matches to return (default: 20)' })),
|
limit: Type.Optional(Type.Integer({ description: 'Maximum number of matches to return (default: 20)' })),
|
||||||
}),
|
}),
|
||||||
|
|
@ -378,7 +480,7 @@ export namespace Tools {
|
||||||
}
|
}
|
||||||
return JSON.stringify(result);
|
return JSON.stringify(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return formatError(err, 'Error executing tool');
|
return formatErrorMessage(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue