Compare commits
No commits in common. "f3d916982f296428d1e36ead0e28cb0f58551a17" and "e39de39e35b00b368e37298ca016c79443e0672d" have entirely different histories.
f3d916982f
...
e39de39e35
|
|
@ -7,7 +7,6 @@ export const highlight = (message: string, keepMarkup = true): string => {
|
|||
const headerRegex = /#{1,3} $/;
|
||||
const stack: string[] = [];
|
||||
let inCodeBlock = false;
|
||||
let inMonospaced = false;
|
||||
let inHeader = false;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
|
@ -31,17 +30,6 @@ export const highlight = (message: string, keepMarkup = true): string => {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (inMonospaced) {
|
||||
if (token === '`' && isClose) {
|
||||
inMonospaced = false;
|
||||
stack.pop();
|
||||
resultHTML += `${keepToken ? token : ''}</span>`;
|
||||
} else {
|
||||
resultHTML += token;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const headerMatch = token.match(headerRegex);
|
||||
if (headerMatch) {
|
||||
if (inHeader) resultHTML += '</span>';
|
||||
|
|
@ -80,7 +68,6 @@ export const highlight = (message: string, keepMarkup = true): string => {
|
|||
resultHTML += `<span class="${styles.codeBlock}">${keepToken ? token : ''}`;
|
||||
} else if (token === '`') {
|
||||
stack.push(token);
|
||||
inMonospaced = true;
|
||||
resultHTML += `<span class="${styles.inlineCode}">${keepToken ? token : ''}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const GlobalNumber = Number;
|
|||
const GlobalArray = Array;
|
||||
|
||||
export namespace Type {
|
||||
export function String<S extends string = string>(args: { description?: string, enum?: S[] } = {}) {
|
||||
const result: TString<S> = {
|
||||
export function String(args: { description?: string, enum?: string[] } = {}) {
|
||||
const result: TString = {
|
||||
type: 'string',
|
||||
};
|
||||
if (args.enum) {
|
||||
|
|
@ -17,8 +17,8 @@ export namespace Type {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function Number<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
|
||||
const result: TNumber<N> = {
|
||||
export function Number(args: { description?: string, enum?: number[] } = {}) {
|
||||
const result: TNumber = {
|
||||
type: 'number',
|
||||
};
|
||||
if (args.enum) {
|
||||
|
|
@ -30,8 +30,8 @@ export namespace Type {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function Integer<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
|
||||
const result: TNumber<N> = {
|
||||
export function Integer(args: { description?: string, enum?: number[] } = {}) {
|
||||
const result: TNumber = {
|
||||
type: 'integer',
|
||||
};
|
||||
if (args.enum) {
|
||||
|
|
@ -70,7 +70,7 @@ export namespace Type {
|
|||
return result as unknown as TOptional<T>;
|
||||
}
|
||||
|
||||
export function Object<T extends TProperties = TProperties>(properties: T, args: { description?: string } = {}) {
|
||||
export function Object<T extends Record<string, TScheme> = Record<string, TScheme>>(properties: T, args: { description?: string } = {}) {
|
||||
const result: TObject<T> = {
|
||||
type: 'object',
|
||||
properties,
|
||||
|
|
@ -83,9 +83,13 @@ export namespace Type {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function Enum<S extends string, T extends TString<S>>(items: S[], args?: { description?: string }): T;
|
||||
export function Enum<N extends number, T extends TNumber<N>>(items: N[], args?: { description?: string }): T;
|
||||
export function Enum(items: (number | string)[], args: { description?: string } = {}) {
|
||||
type TEnumType<T extends (string | number) = (string | number)> =
|
||||
T extends string ? TString<T> :
|
||||
T extends number ? TNumber<T> : never;
|
||||
|
||||
export function Enum<T extends TString>(items: Static<T>[], args?: { description?: string }): T;
|
||||
export function Enum<T extends TNumber>(items: Static<T>[], args?: { description?: string }): T;
|
||||
export function Enum<T extends TEnumType = TEnumType>(items: Static<T>[], args: { description?: string } = {}) {
|
||||
if (typeof items?.[0] === 'number') {
|
||||
return Number({ enum: items.map(item => +item!), ...args });
|
||||
}
|
||||
|
|
@ -191,9 +195,7 @@ export interface TArray<T extends TScheme = TScheme> {
|
|||
items: T;
|
||||
}
|
||||
|
||||
export type TProperties = Record<string, TScheme>;
|
||||
|
||||
export interface TObject<T extends TProperties = TProperties> {
|
||||
export interface TObject<T extends Record<string, TScheme> = Record<string, TScheme>> {
|
||||
type: 'object';
|
||||
description?: string;
|
||||
properties: T;
|
||||
|
|
@ -216,7 +218,7 @@ export interface TOptionalArray<T extends TScheme = TScheme> extends TArray<T> {
|
|||
[optional]: true;
|
||||
}
|
||||
|
||||
export interface TOptionalObject<T extends TProperties = TProperties> extends TObject<T> {
|
||||
export interface TOptionalObject<T extends Record<string, TScheme> = Record<string, TScheme>> extends TObject<T> {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
|
|
@ -234,15 +236,15 @@ export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptiona
|
|||
|
||||
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
||||
|
||||
type RequiredKeys<T extends TProperties> = {
|
||||
type RequiredKeys<T extends Record<string, TScheme>> = {
|
||||
[K in keyof T]: IsOptional<T[K]> extends true ? never : K
|
||||
}[keyof T];
|
||||
|
||||
type OptionalKeys<T extends TProperties> = {
|
||||
type OptionalKeys<T extends Record<string, TScheme>> = {
|
||||
[K in keyof T]: IsOptional<T[K]> extends true ? K : never
|
||||
}[keyof T];
|
||||
|
||||
type StaticObject<T extends TProperties> = Prettify<
|
||||
type StaticObject<T extends Record<string, TScheme>> = Prettify<
|
||||
{ [K in RequiredKeys<T>]: Static<T[K]> } &
|
||||
{ [K in OptionalKeys<T>]?: Static<T[K]> }
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -257,23 +257,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.smallButton {
|
||||
padding: 4px 10px;
|
||||
background: var(--bg);
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
.loreEditor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.addEntry {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.titleInput {
|
||||
padding: 8px 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
width: 250px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.addButton {
|
||||
padding: 8px 16px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent-alt);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.entryCard {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.titleEditInput {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.moveButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
color: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { CharacterRole, useAppState, type Character } from "../contexts/state";
|
||||
import { useAppState, type Character } from "../contexts/state";
|
||||
import { useState } from "preact/hooks";
|
||||
import styles from '../assets/character-editor.module.css';
|
||||
import LLM from "../utils/llm";
|
||||
|
|
@ -22,7 +22,6 @@ export const CharacterEditor = () => {
|
|||
character: {
|
||||
id: crypto.randomUUID(),
|
||||
name: 'New Character',
|
||||
role: CharacterRole.Main,
|
||||
nicknames: [],
|
||||
shortDescription: '',
|
||||
description: '',
|
||||
|
|
@ -174,23 +173,6 @@ export const CharacterEditor = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div class={styles.field}>
|
||||
<div class={styles.label}>Role</div>
|
||||
<select
|
||||
class={styles.select}
|
||||
value={character.role}
|
||||
onInput={(e) => handleEditCharacter(character.id, 'role', e.currentTarget.value as CharacterRole)}
|
||||
>
|
||||
{Object.entries(CharacterRole)
|
||||
.filter(([, value]) => typeof value === 'string')
|
||||
.map(([key, value]) => (
|
||||
<option key={key} value={value}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class={styles.field}>
|
||||
<div class={styles.label}>Description</div>
|
||||
<ContentEditable
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import clsx from "clsx";
|
|||
import { CharacterEditor } from "./character-editor";
|
||||
import { LocationEditor } from "./location-editor";
|
||||
import { ChaptersEditor } from "./chapters-editor";
|
||||
import { LoreEditor } from "./lore-editor";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
|
|
@ -30,6 +29,15 @@ export const Editor = () => {
|
|||
});
|
||||
}, [currentStory?.id]);
|
||||
|
||||
const handleLoreInput = useInputCallback((lore: string) => {
|
||||
if (!currentStory) return;
|
||||
dispatch({
|
||||
type: 'EDIT_LORE',
|
||||
id: currentStory.id,
|
||||
lore,
|
||||
});
|
||||
}, [currentStory?.id]);
|
||||
|
||||
const handleTabChange = (tab: Tab) => {
|
||||
if (!currentStory) return;
|
||||
dispatch({
|
||||
|
|
@ -40,6 +48,7 @@ export const Editor = () => {
|
|||
};
|
||||
|
||||
const storyValue = useMemo(() => currentStory ? highlight(currentStory.text) : '', [currentStory?.text]);
|
||||
const loreValue = useMemo(() => currentStory ? highlight(currentStory.lore) : '', [currentStory?.lore]);
|
||||
|
||||
if (!currentStory) {
|
||||
return <div class={styles.editor} />;
|
||||
|
|
@ -60,7 +69,12 @@ export const Editor = () => {
|
|||
/>
|
||||
)}
|
||||
{currentStory.currentTab === "lore" && (
|
||||
<LoreEditor />
|
||||
<ContentEditable
|
||||
class={styles.editable}
|
||||
value={loreValue}
|
||||
onInput={handleLoreInput}
|
||||
placeholder="Add lore, world-building details, and background information..."
|
||||
/>
|
||||
)}
|
||||
{currentStory.currentTab === "characters" && (
|
||||
<CharacterEditor />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ import styles from '../assets/location-editor.module.css';
|
|||
import LLM from "../utils/llm";
|
||||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
|
||||
const SCALE_OPTIONS = Object.entries(LocationScale)
|
||||
.filter(([, value]) => typeof value === 'number')
|
||||
.map(([label, value]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
export const LocationEditor = () => {
|
||||
const { currentStory, dispatch, connection, model } = useAppState();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
|
|
@ -120,11 +127,11 @@ export const LocationEditor = () => {
|
|||
<select
|
||||
class={styles.select}
|
||||
value={location.scale}
|
||||
onInput={(e) => handleEditLocation(location.id, 'scale', e.currentTarget.value as LocationScale)}
|
||||
onInput={(e) => handleEditLocation(location.id, 'scale', Number(e.currentTarget.value) as LocationScale)}
|
||||
>
|
||||
{Object.values(LocationScale).map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option.charAt(0).toUpperCase() + option.slice(1).toLowerCase()}
|
||||
{SCALE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
import { useAppState, type LoreEntry } from "../contexts/state";
|
||||
import styles from '../assets/lore-editor.module.css';
|
||||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
export const LoreEditor = () => {
|
||||
const { currentStory, dispatch } = useAppState();
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
|
||||
if (!currentStory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleAddEntry = () => {
|
||||
if (!newTitle.trim()) return;
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_LORE_ENTRY',
|
||||
storyId: currentStory.id,
|
||||
entry: {
|
||||
id: crypto.randomUUID(),
|
||||
title: newTitle.trim(),
|
||||
text: '',
|
||||
},
|
||||
});
|
||||
setNewTitle('');
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleEditEntry = (entryId: string, field: keyof LoreEntry, value: string) => {
|
||||
dispatch({
|
||||
type: 'EDIT_LORE_ENTRY',
|
||||
storyId: currentStory.id,
|
||||
entryId,
|
||||
updates: { [field]: value },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteEntry = (entryId: string) => {
|
||||
dispatch({
|
||||
type: 'DELETE_LORE_ENTRY',
|
||||
storyId: currentStory.id,
|
||||
entryId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMoveUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
const entryIds = currentStory.lore.map(e => e.id);
|
||||
[entryIds[index - 1], entryIds[index]] = [entryIds[index], entryIds[index - 1]];
|
||||
dispatch({
|
||||
type: 'REORDER_LORE_ENTRIES',
|
||||
storyId: currentStory.id,
|
||||
entryIds,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMoveDown = (index: number) => {
|
||||
if (index === currentStory.lore.length - 1) return;
|
||||
const entryIds = currentStory.lore.map(e => e.id);
|
||||
[entryIds[index], entryIds[index + 1]] = [entryIds[index + 1], entryIds[index]];
|
||||
dispatch({
|
||||
type: 'REORDER_LORE_ENTRIES',
|
||||
storyId: currentStory.id,
|
||||
entryIds,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={styles.loreEditor}>
|
||||
<div class={styles.header}>
|
||||
<h2>Lore</h2>
|
||||
<div class={styles.addEntry}>
|
||||
<input
|
||||
type="text"
|
||||
class={styles.titleInput}
|
||||
value={newTitle}
|
||||
onInput={(e) => setNewTitle(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddEntry();
|
||||
}
|
||||
}}
|
||||
placeholder="New lore entry title..."
|
||||
/>
|
||||
<button class={styles.addButton} onClick={handleAddEntry}>
|
||||
+ Add Entry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.list}>
|
||||
{currentStory.lore.length === 0 && (
|
||||
<p class={styles.empty}>No lore entries yet. Add your first entry!</p>
|
||||
)}
|
||||
|
||||
{currentStory.lore.map((entry, index) => (
|
||||
<div key={entry.id} class={styles.entryCard}>
|
||||
<div class={styles.cardHeader}>
|
||||
<div class={styles.titleRow}>
|
||||
{editingId === entry.id ? (
|
||||
<input
|
||||
type="text"
|
||||
class={styles.titleEditInput}
|
||||
value={entry.title}
|
||||
onInput={(e) => handleEditEntry(entry.id, 'title', e.currentTarget.value)}
|
||||
onBlur={() => setEditingId(null)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setEditingId(null);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingId(null);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h3
|
||||
class={styles.entryTitle}
|
||||
onClick={() => setEditingId(entry.id)}
|
||||
title="Click to edit"
|
||||
>
|
||||
{entry.title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
<button
|
||||
class={styles.moveButton}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
disabled={index === 0}
|
||||
title="Move up"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class={styles.moveButton}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
disabled={index === currentStory.lore.length - 1}
|
||||
title="Move down"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
class={styles.deleteButton}
|
||||
onClick={() => handleDeleteEntry(entry.id)}
|
||||
title="Delete"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.content}>
|
||||
<ContentEditable
|
||||
autoLines
|
||||
class={styles.textarea}
|
||||
value={entry.text}
|
||||
onInput={(e) => handleEditEntry(entry.id, 'text', e.currentTarget.textContent || '')}
|
||||
placeholder="Enter lore content..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,20 +13,9 @@ export type ChatMessage = LLM.ChatMessage & {
|
|||
|
||||
export type Tab = "story" | "lore" | "characters" | "locations" | "chapters";
|
||||
|
||||
export enum CharacterRole {
|
||||
Protagonist = 'protagonist',
|
||||
Antagonist = 'antagonist',
|
||||
Main = 'main',
|
||||
Secondary = 'secondary',
|
||||
Supporting = 'supporting',
|
||||
Minor = 'minor',
|
||||
Cameo = 'cameo',
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
role: CharacterRole;
|
||||
nicknames: string[];
|
||||
shortDescription: string;
|
||||
description: string;
|
||||
|
|
@ -38,15 +27,15 @@ export interface Character {
|
|||
}
|
||||
|
||||
export enum LocationScale {
|
||||
Room = 'room',
|
||||
House = 'house',
|
||||
Street = 'street',
|
||||
City = 'city',
|
||||
Region = 'region',
|
||||
Country = 'country',
|
||||
Continent = 'continent',
|
||||
World = 'world',
|
||||
Universe = 'universe',
|
||||
Room,
|
||||
House,
|
||||
Street,
|
||||
City,
|
||||
Region,
|
||||
Country,
|
||||
Continent,
|
||||
World,
|
||||
Universe,
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
|
|
@ -57,17 +46,11 @@ export interface Location {
|
|||
scale: LocationScale;
|
||||
}
|
||||
|
||||
export interface LoreEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Story {
|
||||
id: string;
|
||||
title: string;
|
||||
text: string;
|
||||
lore: LoreEntry[];
|
||||
lore: string;
|
||||
characters: Character[];
|
||||
locations: Location[];
|
||||
currentTab: Tab;
|
||||
|
|
@ -93,10 +76,7 @@ type Action =
|
|||
| { type: 'CREATE_STORY'; title: string }
|
||||
| { type: 'RENAME_STORY'; id: string; title: string }
|
||||
| { type: 'EDIT_STORY'; id: string; text: string }
|
||||
| { type: 'ADD_LORE_ENTRY'; storyId: string; entry: LoreEntry }
|
||||
| { type: 'EDIT_LORE_ENTRY'; storyId: string; entryId: string; updates: Partial<LoreEntry> }
|
||||
| { type: 'DELETE_LORE_ENTRY'; storyId: string; entryId: string }
|
||||
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] }
|
||||
| { type: 'EDIT_LORE'; id: string; lore: string }
|
||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
|
||||
| { type: 'DELETE_STORY'; id: string }
|
||||
|
|
@ -109,7 +89,7 @@ type Action =
|
|||
| { type: 'SET_ENABLE_THINKING'; enable: boolean }
|
||||
| { type: 'SET_BANNED_TOKENS'; tokens: string[] }
|
||||
| { type: 'ADD_CHARACTER'; storyId: string; character: Character }
|
||||
| { type: 'EDIT_CHARACTER'; storyId: string; characterId: string; updates: Partial<Omit<Character, 'id' | 'name' | 'relations'>> }
|
||||
| { type: 'EDIT_CHARACTER'; storyId: string; characterId: string; updates: Partial<Omit<Character, 'relations'>> }
|
||||
| { type: 'DELETE_CHARACTER'; storyId: string; characterId: string }
|
||||
| { type: 'ADD_CHARACTER_RELATION'; storyId: string; characterId: string; relation: Character['relations'][number] }
|
||||
| { type: 'EDIT_CHARACTER_RELATION'; storyId: string; characterId: string; targetName: string; updates: Partial<Character['relations'][number]> }
|
||||
|
|
@ -141,7 +121,7 @@ function reducer(state: IState, action: Action): IState {
|
|||
id: crypto.randomUUID(),
|
||||
title: action.title,
|
||||
text: '',
|
||||
lore: [],
|
||||
lore: '',
|
||||
characters: [],
|
||||
locations: [],
|
||||
currentTab: 'story',
|
||||
|
|
@ -170,62 +150,14 @@ function reducer(state: IState, action: Action): IState {
|
|||
),
|
||||
};
|
||||
}
|
||||
case 'ADD_LORE_ENTRY': {
|
||||
case 'EDIT_LORE': {
|
||||
return {
|
||||
...state,
|
||||
stories: state.stories.map(s =>
|
||||
s.id === action.storyId
|
||||
? { ...s, lore: [...s.lore, action.entry] }
|
||||
: s
|
||||
s.id === action.id ? { ...s, lore: action.lore } : s
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'EDIT_LORE_ENTRY': {
|
||||
return {
|
||||
...state,
|
||||
stories: state.stories.map(s =>
|
||||
s.id === action.storyId
|
||||
? {
|
||||
...s,
|
||||
lore: s.lore.map(e =>
|
||||
e.id === action.entryId
|
||||
? { ...e, ...action.updates }
|
||||
: e
|
||||
),
|
||||
}
|
||||
: s
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'DELETE_LORE_ENTRY': {
|
||||
return {
|
||||
...state,
|
||||
stories: state.stories.map(s =>
|
||||
s.id === action.storyId
|
||||
? { ...s, lore: s.lore.filter(e => e.id !== action.entryId) }
|
||||
: s
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'REORDER_LORE_ENTRIES': {
|
||||
return {
|
||||
...state,
|
||||
stories: state.stories.map(s => {
|
||||
if (s.id !== action.storyId) return s;
|
||||
const entryMap = new Map(s.lore.map(e => [e.id, e]));
|
||||
const reordered = action.entryIds
|
||||
.map(id => entryMap.get(id))
|
||||
.filter((e): e is LoreEntry => e !== undefined);
|
||||
// Add any entries that weren't in the new order (safety)
|
||||
for (const entry of s.lore) {
|
||||
if (!action.entryIds.includes(entry.id)) {
|
||||
reordered.push(entry);
|
||||
}
|
||||
}
|
||||
return { ...s, lore: reordered };
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'SET_SYSTEM_INSTRUCTION': {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -256,7 +188,7 @@ function reducer(state: IState, action: Action): IState {
|
|||
id: crypto.randomUUID(),
|
||||
title: `${original.title} (Copy)`,
|
||||
text: '',
|
||||
lore: [...original.lore],
|
||||
lore: original.lore,
|
||||
characters: original.characters,
|
||||
locations: original.locations,
|
||||
currentTab: 'story',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import LLM from "./llm";
|
||||
import Chapters from "./chapters";
|
||||
import { type AppState, CharacterRole } from "../contexts/state";
|
||||
import { type AppState, LocationScale } from "../contexts/state";
|
||||
import { Tools } from "./tools";
|
||||
|
||||
namespace Prompt {
|
||||
|
|
@ -110,71 +110,6 @@ namespace Prompt {
|
|||
return renderSlots(slots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Character detail configuration based on role.
|
||||
* Determines how much information to include in the system prompt for each character.
|
||||
* AI agents can retrieve full character details on-demand via the get_character tool.
|
||||
*/
|
||||
interface CharacterDetailConfig {
|
||||
includeRole: boolean;
|
||||
includeNicknames: boolean;
|
||||
includeShortDescription: boolean;
|
||||
includeFullDescription: boolean;
|
||||
includeRelations: boolean;
|
||||
}
|
||||
|
||||
const CHARACTER_ROLE_DETAIL: Record<CharacterRole, CharacterDetailConfig> = {
|
||||
[CharacterRole.Protagonist]: {
|
||||
includeRole: true,
|
||||
includeNicknames: true,
|
||||
includeShortDescription: false, // omitted if full description present
|
||||
includeFullDescription: true,
|
||||
includeRelations: true,
|
||||
},
|
||||
[CharacterRole.Main]: {
|
||||
includeRole: true,
|
||||
includeNicknames: true,
|
||||
includeShortDescription: false,
|
||||
includeFullDescription: true,
|
||||
includeRelations: true,
|
||||
},
|
||||
[CharacterRole.Antagonist]: {
|
||||
includeRole: true,
|
||||
includeNicknames: true,
|
||||
includeShortDescription: false,
|
||||
includeFullDescription: true,
|
||||
includeRelations: true,
|
||||
},
|
||||
[CharacterRole.Secondary]: {
|
||||
includeRole: true,
|
||||
includeNicknames: false,
|
||||
includeShortDescription: true,
|
||||
includeFullDescription: false,
|
||||
includeRelations: true,
|
||||
},
|
||||
[CharacterRole.Supporting]: {
|
||||
includeRole: true,
|
||||
includeNicknames: false,
|
||||
includeShortDescription: false,
|
||||
includeFullDescription: false,
|
||||
includeRelations: false,
|
||||
},
|
||||
[CharacterRole.Minor]: {
|
||||
includeRole: true,
|
||||
includeNicknames: false,
|
||||
includeShortDescription: false,
|
||||
includeFullDescription: false,
|
||||
includeRelations: false,
|
||||
},
|
||||
[CharacterRole.Cameo]: {
|
||||
includeRole: true,
|
||||
includeNicknames: false,
|
||||
includeShortDescription: false,
|
||||
includeFullDescription: false,
|
||||
includeRelations: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function formatCharactersMarkdown(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory || !currentStory.characters?.length) {
|
||||
|
|
@ -184,40 +119,15 @@ namespace Prompt {
|
|||
const lines: string[] = [];
|
||||
lines.push('## Characters\n');
|
||||
|
||||
// Sort characters by importance (protagonist first, cameo last)
|
||||
const sortedCharacters = [...currentStory.characters].sort((a, b) => {
|
||||
const importanceOrder = [
|
||||
CharacterRole.Protagonist,
|
||||
CharacterRole.Main,
|
||||
CharacterRole.Antagonist,
|
||||
CharacterRole.Secondary,
|
||||
CharacterRole.Supporting,
|
||||
CharacterRole.Minor,
|
||||
CharacterRole.Cameo,
|
||||
];
|
||||
return importanceOrder.indexOf(a.role) - importanceOrder.indexOf(b.role);
|
||||
});
|
||||
|
||||
for (const character of sortedCharacters) {
|
||||
const config = CHARACTER_ROLE_DETAIL[character.role];
|
||||
for (const character of currentStory.characters) {
|
||||
lines.push(`### ${character.name}`);
|
||||
|
||||
if (config.includeRole) {
|
||||
lines.push(`**Role:** ${character.role}`);
|
||||
}
|
||||
|
||||
if (config.includeNicknames && character.nicknames?.length) {
|
||||
lines.push(`**Nicknames:** ${character.nicknames.join(', ')}`);
|
||||
}
|
||||
|
||||
const description = character.description || character.shortDescription;
|
||||
if (config.includeFullDescription && description) {
|
||||
const description = character.shortDescription || character.description;
|
||||
if (description) {
|
||||
lines.push(description);
|
||||
} else if (config.includeShortDescription && character.shortDescription) {
|
||||
lines.push(character.shortDescription);
|
||||
}
|
||||
|
||||
if (config.includeRelations && character.relations?.length) {
|
||||
if (character.relations?.length) {
|
||||
lines.push('**Relations:**');
|
||||
for (const relation of character.relations) {
|
||||
lines.push(`- ${relation.name}: ${relation.relation}`);
|
||||
|
|
@ -230,26 +140,6 @@ namespace Prompt {
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatLoreMarkdown(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory?.lore?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('## Lore\n');
|
||||
|
||||
for (const entry of currentStory.lore) {
|
||||
lines.push(`### ${entry.title}`);
|
||||
if (entry.text) {
|
||||
lines.push(entry.text);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatLocationsMarkdown(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory || !currentStory.locations?.length) {
|
||||
|
|
@ -267,7 +157,7 @@ namespace Prompt {
|
|||
lines.push(description);
|
||||
}
|
||||
|
||||
lines.push(`**Scale:** ${location.scale}`);
|
||||
lines.push(`**Scale:** ${LocationScale[location.scale]}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
|
|
@ -284,9 +174,8 @@ namespace Prompt {
|
|||
|
||||
parts.push(`# Story Title: ${currentStory.title}`);
|
||||
|
||||
const loreSection = formatLoreMarkdown(state);
|
||||
if (loreSection) {
|
||||
parts.push(loreSection);
|
||||
if (currentStory.lore) {
|
||||
parts.push(`## Lore`, currentStory.lore);
|
||||
}
|
||||
|
||||
const charactersSection = formatCharactersMarkdown(state);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
import { formatErrorMessage } from "@common/errors";
|
||||
import { Type, type Static, type TObject } from '@common/typebox';
|
||||
import { CharacterRole, 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";
|
||||
|
||||
const VALID_SCALES = Object.values(LocationScale);
|
||||
const VALID_ROLES = Object.values(CharacterRole);
|
||||
const SCALE_DESCRIPTION = Object.entries(LocationScale)
|
||||
.filter(([, value]) => typeof value === 'number')
|
||||
.map(([key, value]) => `${value}=${key}`)
|
||||
.join(', ');
|
||||
|
||||
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 {
|
||||
interface Tool<T extends TObject = TObject> {
|
||||
|
|
@ -26,7 +36,6 @@ export namespace Tools {
|
|||
return `Error: Character "${args.name.trim()}" not found`;
|
||||
}
|
||||
let result = `# Character: ${character.name}\n\n`;
|
||||
result += `**Role:** ${character.role}\n\n`;
|
||||
if (character.nicknames && character.nicknames.length > 0) {
|
||||
result += `**Nicknames:** ${character.nicknames.join(', ')}\n\n`;
|
||||
}
|
||||
|
|
@ -66,9 +75,6 @@ export namespace Tools {
|
|||
if (args.nicknames !== undefined) {
|
||||
definedUpdates.nicknames = args.nicknames;
|
||||
}
|
||||
if (args.role !== undefined) {
|
||||
definedUpdates.role = args.role;
|
||||
}
|
||||
if (args.relations !== undefined) {
|
||||
return 'Error: set_character does not support updating relations. Use set_character_relation instead.';
|
||||
}
|
||||
|
|
@ -89,9 +95,6 @@ export namespace Tools {
|
|||
if (!args.shortDescription) {
|
||||
return 'Error: shortDescription is required when adding a new character';
|
||||
}
|
||||
if (args.role === undefined) {
|
||||
return 'Error: role 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 || []) {
|
||||
|
|
@ -105,7 +108,6 @@ export namespace Tools {
|
|||
character: {
|
||||
id: crypto.randomUUID(),
|
||||
name: args.name.trim(),
|
||||
role: args.role,
|
||||
nicknames: args.nicknames || [],
|
||||
shortDescription: args.shortDescription.trim(),
|
||||
description: args.description || '',
|
||||
|
|
@ -128,7 +130,6 @@ export namespace Tools {
|
|||
parameters: Type.Object({
|
||||
name: Type.String({ description: "The character's full name" }),
|
||||
shortDescription: Type.Optional(Type.String({ description: 'A brief description of the character (one line). Required on a new character.' })),
|
||||
role: Type.Optional(Type.Enum(VALID_ROLES, { description: `The character role in the story. Required when adding a new character.` })),
|
||||
nicknames: Type.Optional(Type.Array(Type.String(), { description: 'Optional list of nicknames' })),
|
||||
description: Type.Optional(Type.String({ description: 'Optional full character description' })),
|
||||
relations: Type.Optional(Type.Array(
|
||||
|
|
@ -209,7 +210,7 @@ export namespace Tools {
|
|||
if (location.description) {
|
||||
result += `**Description:** ${location.description}\n\n`;
|
||||
}
|
||||
result += `**Scale:** ${location.scale}\n`;
|
||||
result += `**Scale:** ${SCALE_NAMES[location.scale]}\n`;
|
||||
return result.trim();
|
||||
},
|
||||
description: 'Get full information about a location by name',
|
||||
|
|
@ -280,102 +281,91 @@ export namespace Tools {
|
|||
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' })),
|
||||
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
||||
description: `Location scale (enum): ${VALID_SCALES.join(', ')}`,
|
||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
'set_lore_entry': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
const existingEntry = appState.currentStory.lore.find(e => e.title.toLowerCase() === args.title.trim().toLowerCase());
|
||||
|
||||
if (existingEntry) {
|
||||
appState.dispatch({
|
||||
type: 'EDIT_LORE_ENTRY',
|
||||
storyId: appState.currentStory.id,
|
||||
entryId: existingEntry.id,
|
||||
updates: { text: args.text },
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'lore'
|
||||
});
|
||||
return `Lore entry "${existingEntry.title}" updated successfully`;
|
||||
} else {
|
||||
appState.dispatch({
|
||||
type: 'ADD_LORE_ENTRY',
|
||||
storyId: appState.currentStory.id,
|
||||
entry: {
|
||||
id: crypto.randomUUID(),
|
||||
title: args.title.trim(),
|
||||
text: args.text,
|
||||
},
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'lore'
|
||||
});
|
||||
return `Lore entry "${args.title.trim()}" added successfully`;
|
||||
}
|
||||
},
|
||||
description: 'Add or edit a lore entry. If entry with matching title exists, updates it; otherwise creates new one.',
|
||||
parameters: Type.Object({
|
||||
title: Type.String({ description: "The lore entry's title" }),
|
||||
text: Type.String({ description: 'The lore entry content.' }),
|
||||
}),
|
||||
}),
|
||||
'edit_text': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
const target = args.target ?? 'story';
|
||||
|
||||
// Append mode: when old_text is not provided, append new_text to the story
|
||||
// Append mode: when old_text is not provided, append new_text to the target
|
||||
if (args.old_text == null) {
|
||||
if (target === 'lore') {
|
||||
appState.dispatch({
|
||||
type: 'EDIT_LORE',
|
||||
id: appState.currentStory.id,
|
||||
lore: appState.currentStory.lore + '\n' + args.new_text,
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'lore'
|
||||
});
|
||||
} else {
|
||||
appState.dispatch({
|
||||
type: 'EDIT_STORY',
|
||||
id: appState.currentStory.id,
|
||||
text: appState.currentStory.text + '\n' + args.new_text,
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'story'
|
||||
});
|
||||
}
|
||||
return `Text appended to ${target} successfully`;
|
||||
}
|
||||
|
||||
// Replace mode: find and replace old_text with new_text
|
||||
const source = target === 'lore'
|
||||
? appState.currentStory.lore
|
||||
: appState.currentStory.text;
|
||||
|
||||
const occurrences = source.split(args.old_text).length - 1;
|
||||
if (occurrences === 0) {
|
||||
return `Error: old_text not found in ${target}`;
|
||||
}
|
||||
if (occurrences > 1 && !args.replace_all) {
|
||||
return `Error: old_text appears multiple times in ${target}`;
|
||||
}
|
||||
|
||||
if (target === 'lore') {
|
||||
appState.dispatch({
|
||||
type: 'EDIT_LORE',
|
||||
id: appState.currentStory.id,
|
||||
lore: appState.currentStory.lore.replaceAll(args.old_text, args.new_text),
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'lore'
|
||||
});
|
||||
} else {
|
||||
appState.dispatch({
|
||||
type: 'EDIT_STORY',
|
||||
id: appState.currentStory.id,
|
||||
text: appState.currentStory.text + '\n' + args.new_text,
|
||||
text: appState.currentStory.text.replaceAll(args.old_text, args.new_text),
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'story'
|
||||
});
|
||||
return 'Text appended to story successfully';
|
||||
}
|
||||
|
||||
// Replace mode: find and replace old_text with new_text in story
|
||||
const source = appState.currentStory.text;
|
||||
const occurrences = source.split(args.old_text).length - 1;
|
||||
if (occurrences === 0) {
|
||||
return 'Error: old_text not found in story';
|
||||
}
|
||||
if (occurrences > 1 && !args.replace_all) {
|
||||
return 'Error: old_text appears multiple times in story';
|
||||
}
|
||||
|
||||
appState.dispatch({
|
||||
type: 'EDIT_STORY',
|
||||
id: appState.currentStory.id,
|
||||
text: appState.currentStory.text.replaceAll(args.old_text, args.new_text),
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
id: appState.currentStory.id,
|
||||
tab: 'story'
|
||||
});
|
||||
return 'Story edited successfully';
|
||||
return `${target === 'lore' ? 'Lore' : 'Story'} edited successfully`;
|
||||
},
|
||||
description: "Replace text in the story. When old_text is omitted, appends new_text to the story's end. Case-sensitive.",
|
||||
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({
|
||||
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' })),
|
||||
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
||||
target: Type.Optional(Type.Enum(['lore', 'story'],
|
||||
{ description: 'Target to edit (story or lore, default: story)' },
|
||||
)),
|
||||
}),
|
||||
}),
|
||||
'grep': tool({
|
||||
|
|
@ -386,10 +376,7 @@ export namespace Tools {
|
|||
|
||||
const sources: { name: string; content: string }[] = [
|
||||
{ name: 'story', content: appState.currentStory.text },
|
||||
...appState.currentStory.lore.flatMap(e => [
|
||||
{ name: `lore:${e.title}`, content: e.title },
|
||||
{ name: `lore:${e.title}`, content: e.text },
|
||||
]),
|
||||
{ name: 'lore', content: appState.currentStory.lore },
|
||||
...appState.currentStory.characters.flatMap(c => [
|
||||
{ name: `character:${c.name}`, content: c.name },
|
||||
{ name: `character:${c.name}`, content: c.shortDescription },
|
||||
|
|
|
|||
Loading…
Reference in New Issue