Compare commits
4 Commits
d1325b33b7
...
bbe5716e40
| Author | SHA1 | Date |
|---|---|---|
|
|
bbe5716e40 | |
|
|
4b24ce85e0 | |
|
|
6e61ff7194 | |
|
|
af59e03212 |
12
bun.lock
12
bun.lock
|
|
@ -14,11 +14,11 @@
|
||||||
"ace-builds": "1.36.3",
|
"ace-builds": "1.36.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"delay": "6.0.0",
|
"delay": "6.0.0",
|
||||||
"lucide-preact": "^0.577.0",
|
"lucide-preact": "0.577.0",
|
||||||
"preact": "10.22.0",
|
"preact": "10.22.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.10",
|
"@types/bun": "latest",
|
||||||
"@types/html-minifier": "4.0.5",
|
"@types/html-minifier": "4.0.5",
|
||||||
"@types/inquirer": "9.0.7",
|
"@types/inquirer": "9.0.7",
|
||||||
"@types/web-bluetooth": "0.0.21",
|
"@types/web-bluetooth": "0.0.21",
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
"@inquirer/type": ["@inquirer/type@1.4.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-AjOqykVyjdJQvtfkNDGUyMYGF8xN50VUxftCQWsOyIo4DFRLr6VQhW0VItGI1JIyQGCGgIpKa7hMMwNhZb4OIw=="],
|
"@inquirer/type": ["@inquirer/type@1.4.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-AjOqykVyjdJQvtfkNDGUyMYGF8xN50VUxftCQWsOyIo4DFRLr6VQhW0VItGI1JIyQGCGgIpKa7hMMwNhZb4OIw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
|
|
||||||
"@types/clean-css": ["@types/clean-css@4.2.11", "", { "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw=="],
|
"@types/clean-css": ["@types/clean-css@4.2.11", "", { "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw=="],
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
"browser-detect": ["browser-detect@0.2.28", "", { "dependencies": { "core-js": "^2.5.7" } }, "sha512-KeWGHqYQmHDkCFG2dIiX/2wFUgqevbw/rd6wNi9N6rZbaSJFtG5kel0HtprRwCGp8sqpQP79LzDJXf/WCx4WAw=="],
|
"browser-detect": ["browser-detect@0.2.28", "", { "dependencies": { "core-js": "^2.5.7" } }, "sha512-KeWGHqYQmHDkCFG2dIiX/2wFUgqevbw/rd6wNi9N6rZbaSJFtG5kel0HtprRwCGp8sqpQP79LzDJXf/WCx4WAw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||||
|
|
||||||
"camel-case": ["camel-case@3.0.0", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.1.1" } }, "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w=="],
|
"camel-case": ["camel-case@3.0.0", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.1.1" } }, "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w=="],
|
||||||
|
|
||||||
|
|
@ -163,8 +163,6 @@
|
||||||
|
|
||||||
"@types/through/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
|
"@types/through/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@20.14.10", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ=="],
|
|
||||||
|
|
||||||
"html-minifier/uglify-js": ["uglify-js@3.18.0", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A=="],
|
"html-minifier/uglify-js": ["uglify-js@3.18.0", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A=="],
|
||||||
|
|
||||||
"@inquirer/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"@inquirer/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
@ -174,7 +172,5 @@
|
||||||
"@types/mute-stream/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"@types/mute-stream/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
"@types/through/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"@types/through/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--italicColor, #AFAFAF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
color: var(--quoteColor, #D4E5FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock {
|
||||||
|
font-family: monospace;
|
||||||
|
background: var(--codeBg, #49483e);
|
||||||
|
padding: 0.5em;
|
||||||
|
display: block;
|
||||||
|
border-radius: var(--radius, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineCode {
|
||||||
|
font-family: monospace;
|
||||||
|
background: var(--codeBg, #49483e);
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--yellow, #e6db74);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import styles from './assets/highlight.module.css';
|
||||||
|
|
||||||
|
export const highlight = (message: string, keepMarkup = true): string => {
|
||||||
|
let resultHTML = '';
|
||||||
|
const tokenRegex = /(\*\*?|"|```|`|(?:^|\n)# |\n)/g;
|
||||||
|
const stack: string[] = [];
|
||||||
|
let inCodeBlock = false;
|
||||||
|
let inHeader = false;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = tokenRegex.exec(message)) !== null) {
|
||||||
|
resultHTML += message.slice(lastIndex, match.index);
|
||||||
|
lastIndex = tokenRegex.lastIndex;
|
||||||
|
|
||||||
|
const token = match[0];
|
||||||
|
const isClose = stack.at(-1) === token;
|
||||||
|
const keepToken = keepMarkup || token === '"';
|
||||||
|
|
||||||
|
if (inCodeBlock) {
|
||||||
|
if (token === '```' && isClose) {
|
||||||
|
inCodeBlock = false;
|
||||||
|
stack.pop();
|
||||||
|
resultHTML += `${keepToken ? token : ''}</span>`;
|
||||||
|
} else {
|
||||||
|
resultHTML += token;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.endsWith('# ')) {
|
||||||
|
if (inHeader) resultHTML += '</span>';
|
||||||
|
inHeader = true;
|
||||||
|
resultHTML += `${token.slice(0, -2)}<span class="${styles.header}">${keepMarkup ? '# ' : ''}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token === '\n') {
|
||||||
|
if (inHeader) {
|
||||||
|
resultHTML += `${keepMarkup ? '\n' : ''}</span>`;
|
||||||
|
inHeader = false;
|
||||||
|
} else {
|
||||||
|
resultHTML += '\n';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClose) {
|
||||||
|
stack.pop();
|
||||||
|
resultHTML += `${keepToken ? token : ''}</span>`;
|
||||||
|
} else if (token === '*') {
|
||||||
|
stack.push(token);
|
||||||
|
resultHTML += `<span class="${styles.italic}">${keepToken ? token : ''}`;
|
||||||
|
} else if (token === '**') {
|
||||||
|
stack.push(token);
|
||||||
|
resultHTML += `<span class="${styles.bold}">${keepToken ? token : ''}`;
|
||||||
|
} else if (token === '"') {
|
||||||
|
stack.push(token);
|
||||||
|
resultHTML += `<span class="${styles.quote}">"`;
|
||||||
|
} else if (token === '```') {
|
||||||
|
stack.push(token);
|
||||||
|
inCodeBlock = true;
|
||||||
|
resultHTML += `<span class="${styles.codeBlock}">`;
|
||||||
|
} else if (token === '`') {
|
||||||
|
stack.push(token);
|
||||||
|
resultHTML += `<span class="${styles.inlineCode}">`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultHTML += message.slice(lastIndex);
|
||||||
|
|
||||||
|
if (inHeader) resultHTML += '</span>';
|
||||||
|
resultHTML += '</span>'.repeat(stack.length);
|
||||||
|
|
||||||
|
return resultHTML;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
const optional = Symbol();
|
||||||
|
const GlobalObject = Object;
|
||||||
|
const GlobalNumber = Number;
|
||||||
|
const GlobalArray = Array;
|
||||||
|
|
||||||
|
export namespace Type {
|
||||||
|
export function String(args: { description?: string, enum?: string[] } = {}) {
|
||||||
|
const result: TString = {
|
||||||
|
type: 'string',
|
||||||
|
};
|
||||||
|
if (args.enum) {
|
||||||
|
result.enum = args.enum;
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Number(args: { description?: string, enum?: number[] } = {}) {
|
||||||
|
const result: TNumber = {
|
||||||
|
type: 'number',
|
||||||
|
};
|
||||||
|
if (args.enum) {
|
||||||
|
result.enum = args.enum;
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Integer(args: { description?: string, enum?: number[] } = {}) {
|
||||||
|
const result: TNumber = {
|
||||||
|
type: 'integer',
|
||||||
|
};
|
||||||
|
if (args.enum) {
|
||||||
|
result.enum = args.enum;
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Boolean(args: { description?: string } = {}) {
|
||||||
|
const result: TBoolean = {
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Array<T extends TScheme = TScheme>(items: T, args: { description?: string } = {}) {
|
||||||
|
const result: TArray<T> = {
|
||||||
|
type: 'array',
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Optional<T extends TScheme = TScheme>(scheme: T): TOptional<T> {
|
||||||
|
const result = { ...scheme };
|
||||||
|
GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false });
|
||||||
|
return result as unknown as TOptional<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Object<T extends Record<string, TScheme> = Record<string, TScheme>>(properties: T, args: { description?: string } = {}) {
|
||||||
|
const result: TObject<T> = {
|
||||||
|
type: 'object',
|
||||||
|
properties,
|
||||||
|
required: GlobalObject.keys(properties)
|
||||||
|
.filter(key => !(properties[key] as any)[optional]),
|
||||||
|
}
|
||||||
|
if (args.description) {
|
||||||
|
result.description = args.description;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
return String({ enum: items.map(item => item!.toString()), ...args });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckError {
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check(scheme: TScheme, value: unknown, path: string): CheckError[] {
|
||||||
|
if (value == null) {
|
||||||
|
if ((scheme as any)[optional]) return [];
|
||||||
|
return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scheme.type) {
|
||||||
|
case 'string': {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return [{ path, message: `Expected string at ${path}, got ${typeof value}` }];
|
||||||
|
}
|
||||||
|
if (scheme.enum && !scheme.enum.includes(value)) {
|
||||||
|
return [{ path, message: `Expected one of [${scheme.enum.join(', ')}] at ${path}, got "${value}"` }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case 'number':
|
||||||
|
case 'integer': {
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
return [{ path, message: `Expected number at ${path}, got ${typeof value}` }];
|
||||||
|
}
|
||||||
|
if (scheme.type === 'integer' && !GlobalNumber.isInteger(value)) {
|
||||||
|
return [{ path, message: `Expected integer at ${path}, got ${value}` }];
|
||||||
|
}
|
||||||
|
const s = scheme as TNumber;
|
||||||
|
if (s.enum && !s.enum.includes(value)) {
|
||||||
|
return [{ path, message: `Expected one of [${s.enum.join(', ')}] at ${path}, got ${value}` }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
if (typeof value !== 'boolean') {
|
||||||
|
return [{ path, message: `Expected boolean at ${path}, got ${typeof value}` }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case 'array': {
|
||||||
|
if (!GlobalArray.isArray(value)) {
|
||||||
|
return [{ path, message: `Expected array at ${path}, got ${typeof value}` }];
|
||||||
|
}
|
||||||
|
const s = scheme as TArray;
|
||||||
|
return value.flatMap((item, i) => check(s.items, item, `${path}[${i}]`));
|
||||||
|
}
|
||||||
|
case 'object': {
|
||||||
|
if (typeof value !== 'object' || GlobalArray.isArray(value)) {
|
||||||
|
return [{ path, message: `Expected object at ${path}, got ${typeof value}` }];
|
||||||
|
}
|
||||||
|
const s = scheme as TObject;
|
||||||
|
const errors: CheckError[] = [];
|
||||||
|
for (const key of s.required || []) {
|
||||||
|
if (!(key in (value as Record<string, unknown>))) {
|
||||||
|
errors.push({ path: `${path}.${key}`, message: `Missing required property ${path}.${key}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, propScheme] of GlobalObject.entries(s.properties)) {
|
||||||
|
const propValue = (value as Record<string, unknown>)[key];
|
||||||
|
if (propValue === undefined) continue;
|
||||||
|
errors.push(...check(propScheme, propValue, `${path}.${key}`));
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Check(scheme: TScheme, value: unknown): CheckError[] {
|
||||||
|
return check(scheme, value, '$');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TString<T extends string = string> {
|
||||||
|
type: 'string';
|
||||||
|
enum?: T[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TNumber<T extends number = number> {
|
||||||
|
type: 'number' | 'integer';
|
||||||
|
enum?: T[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TBoolean {
|
||||||
|
type: 'boolean';
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TArray<T extends TScheme = TScheme> {
|
||||||
|
type: 'array';
|
||||||
|
description?: string;
|
||||||
|
items: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TObject<T extends Record<string, TScheme> = Record<string, TScheme>> {
|
||||||
|
type: 'object';
|
||||||
|
description?: string;
|
||||||
|
properties: T;
|
||||||
|
required?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOptionalString<T extends string = string> extends TString<T> {
|
||||||
|
[optional]: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOptionalNumber<T extends number = number> extends TNumber<T> {
|
||||||
|
[optional]: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOptionalBoolean extends TBoolean {
|
||||||
|
[optional]: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOptionalArray<T extends TScheme = TScheme> extends TArray<T> {
|
||||||
|
[optional]: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOptionalObject<T extends Record<string, TScheme> = Record<string, TScheme>> extends TObject<T> {
|
||||||
|
[optional]: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TOptional<T extends TScheme = TScheme> =
|
||||||
|
T extends TString<infer S> ? TOptionalString<S> :
|
||||||
|
T extends TNumber<infer N> ? TOptionalNumber<N> :
|
||||||
|
T extends TBoolean ? TOptionalBoolean :
|
||||||
|
T extends TArray<infer I> ? TOptionalArray<I> :
|
||||||
|
T extends TObject<infer P> ? TOptionalObject<P> :
|
||||||
|
never;
|
||||||
|
|
||||||
|
export type IsOptional<T> = T extends { [optional]: true } ? true : false;
|
||||||
|
|
||||||
|
export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject;
|
||||||
|
|
||||||
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
||||||
|
|
||||||
|
type RequiredKeys<T extends Record<string, TScheme>> = {
|
||||||
|
[K in keyof T]: IsOptional<T[K]> extends true ? never : K
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
type OptionalKeys<T extends Record<string, TScheme>> = {
|
||||||
|
[K in keyof T]: IsOptional<T[K]> extends true ? K : never
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
type StaticObject<T extends Record<string, TScheme>> = Prettify<
|
||||||
|
{ [K in RequiredKeys<T>]: Static<T[K]> } &
|
||||||
|
{ [K in OptionalKeys<T>]?: Static<T[K]> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type Static<T extends TScheme> =
|
||||||
|
T extends TString<infer S> ? S :
|
||||||
|
T extends TNumber<infer N> ? N :
|
||||||
|
T extends TBoolean ? boolean :
|
||||||
|
T extends TArray<infer I> ? Static<I>[] :
|
||||||
|
T extends TObject<infer P> ? StaticObject<P> :
|
||||||
|
never;
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 72px;
|
padding: 0 72px 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editable {
|
.editable {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useInputState } from "@common/hooks/useInputState";
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
|
import { highlight } from "@common/highlight";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { useAppState, type ChatMessage } from "../contexts/state";
|
import { useAppState, type ChatMessage } from "../contexts/state";
|
||||||
import styles from '../assets/chat-sidebar.module.css';
|
import styles from '../assets/chat-sidebar.module.css';
|
||||||
import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
|
import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
|
||||||
import LLM from "../utils/llm";
|
import LLM from "../utils/llm";
|
||||||
import { highlight } from "../utils/highlight";
|
|
||||||
import Prompt from "../utils/prompt";
|
import Prompt from "../utils/prompt";
|
||||||
import { Tools } from "../utils/tools";
|
import { Tools } from "../utils/tools";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ContentEditable } from "@common/components/ContentEditable";
|
import { ContentEditable } from "@common/components/ContentEditable";
|
||||||
|
import { highlight } from "@common/highlight";
|
||||||
import { useAppState, type Tab } from "../contexts/state";
|
import { useAppState, type Tab } from "../contexts/state";
|
||||||
import styles from '../assets/editor.module.css';
|
import styles from '../assets/editor.module.css';
|
||||||
import { highlight } from "../utils/highlight";
|
|
||||||
import { useMemo } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
import { CharacterEditor } from "./character-editor";
|
import { CharacterEditor } from "./character-editor";
|
||||||
import { LocationEditor } from "./location-editor";
|
import { LocationEditor } from "./location-editor";
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export interface Story {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
lastModifiedChunk: string;
|
||||||
lore: string;
|
lore: string;
|
||||||
characters: Character[];
|
characters: Character[];
|
||||||
locations: Location[];
|
locations: Location[];
|
||||||
|
|
@ -73,7 +74,7 @@ interface IState {
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'CREATE_STORY'; title: string }
|
| { type: 'CREATE_STORY'; title: string }
|
||||||
| { type: 'RENAME_STORY'; id: string; title: string }
|
| { type: 'RENAME_STORY'; id: string; title: string }
|
||||||
| { type: 'EDIT_STORY'; id: string; text: string }
|
| { type: 'EDIT_STORY'; id: string; text: string; lastModifiedChunk?: string }
|
||||||
| { type: 'EDIT_LORE'; id: string; lore: string }
|
| { type: 'EDIT_LORE'; id: string; lore: string }
|
||||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||||
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
|
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
|
||||||
|
|
@ -116,6 +117,7 @@ function reducer(state: IState, action: Action): IState {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
title: action.title,
|
title: action.title,
|
||||||
text: '',
|
text: '',
|
||||||
|
lastModifiedChunk: '',
|
||||||
lore: '',
|
lore: '',
|
||||||
characters: [],
|
characters: [],
|
||||||
locations: [],
|
locations: [],
|
||||||
|
|
@ -140,7 +142,11 @@ function reducer(state: IState, action: Action): IState {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
stories: state.stories.map(s =>
|
stories: state.stories.map(s =>
|
||||||
s.id === action.id ? { ...s, text: action.text } : s
|
s.id === action.id ? {
|
||||||
|
...s,
|
||||||
|
text: action.text,
|
||||||
|
lastModifiedChunk: action.lastModifiedChunk ?? s.lastModifiedChunk
|
||||||
|
} : s
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
export const highlight = (message: string, keepMarkup = true): string => {
|
|
||||||
let resultHTML = '';
|
|
||||||
const replaceRegex = /(\*\*?|"|```|`)/ig;
|
|
||||||
const splitToken = '___SPLIT_AWOORWA___';
|
|
||||||
|
|
||||||
const preparedMessage = message.replace(replaceRegex, `${splitToken}$1${splitToken}`);
|
|
||||||
const parts = preparedMessage.split(splitToken);
|
|
||||||
|
|
||||||
const stack: string[] = [];
|
|
||||||
let inCodeBlock = false;
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
const isClose = stack.at(-1) === part;
|
|
||||||
const keepPart = keepMarkup || part === '"';
|
|
||||||
|
|
||||||
if (inCodeBlock) {
|
|
||||||
if (part === '```' && isClose) {
|
|
||||||
inCodeBlock = false;
|
|
||||||
stack.pop();
|
|
||||||
resultHTML += `${keepPart ? part : ''}</span>`;
|
|
||||||
} else {
|
|
||||||
resultHTML += part;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isClose) {
|
|
||||||
stack.pop();
|
|
||||||
if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') {
|
|
||||||
resultHTML += `${keepPart ? part : ''}</span>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (part === '*') {
|
|
||||||
stack.push(part);
|
|
||||||
resultHTML += `<span style="font-style:italic;color:var(--italicColor)">${keepPart ? part : ''}`;
|
|
||||||
} else if (part === '**') {
|
|
||||||
stack.push(part);
|
|
||||||
resultHTML += `<span style="font-weight:bold">${keepPart ? part : ''}`;
|
|
||||||
} else if (part === '"') {
|
|
||||||
stack.push(part);
|
|
||||||
resultHTML += `<span style="color:var(--quoteColor)">"`;
|
|
||||||
} else if (part === '```') {
|
|
||||||
stack.push(part);
|
|
||||||
inCodeBlock = true;
|
|
||||||
resultHTML += `<span style="font-family:monospace;background:var(--codeBg);padding:0.5em;display:block;border-radius:var(--radius)">`;
|
|
||||||
} else if (part === '`') {
|
|
||||||
stack.push(part);
|
|
||||||
resultHTML += `<span style="font-family:monospace;background:var(--codeBg);padding:0.1em 0.3em;border-radius:0.2em">`;
|
|
||||||
} else {
|
|
||||||
resultHTML += part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (stack.length) {
|
|
||||||
const part = stack.pop();
|
|
||||||
if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') {
|
|
||||||
resultHTML += `</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultHTML;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { formatError } from '@common/errors';
|
|
||||||
import SSE from '@common/sse';
|
import SSE from '@common/sse';
|
||||||
|
import type { TObject } from '@common/typebox';
|
||||||
|
|
||||||
namespace LLM {
|
namespace LLM {
|
||||||
export interface Connection {
|
export interface Connection {
|
||||||
|
|
@ -41,45 +41,12 @@ namespace LLM {
|
||||||
|
|
||||||
export type ChatMessage = ChatMessageUser | ChatMessageAssistant | ChatMessageSystem | ChatMessageTool;
|
export type ChatMessage = ChatMessageUser | ChatMessageAssistant | ChatMessageSystem | ChatMessageTool;
|
||||||
|
|
||||||
export interface ToolStringParameter {
|
|
||||||
type: 'string';
|
|
||||||
enum?: string[];
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolNumberParameter {
|
|
||||||
type: 'number' | 'integer';
|
|
||||||
enum?: number[];
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolBooleanParameter {
|
|
||||||
type: 'boolean';
|
|
||||||
enum?: boolean[];
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolArrayParameter {
|
|
||||||
type: 'array';
|
|
||||||
description?: string;
|
|
||||||
items: ToolParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolObjectParameter {
|
|
||||||
type: 'object';
|
|
||||||
description?: string;
|
|
||||||
properties: Record<string, ToolParameter>;
|
|
||||||
required?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ToolParameter = ToolStringParameter | ToolNumberParameter | ToolBooleanParameter | ToolArrayParameter | ToolObjectParameter;
|
|
||||||
|
|
||||||
export interface Tool {
|
export interface Tool {
|
||||||
type: 'function';
|
type: 'function';
|
||||||
function: {
|
function: {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
parameters: ToolObjectParameter;
|
parameters: TObject;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,62 @@ import { type AppState, LocationScale } from "../contexts/state";
|
||||||
import { Tools } from "./tools";
|
import { Tools } from "./tools";
|
||||||
|
|
||||||
namespace Prompt {
|
namespace Prompt {
|
||||||
|
function approxTokens(text: string): number {
|
||||||
|
return text.length / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatStoryText(text: string, tokenBudget: number): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
if (approxTokens(text) <= tokenBudget / 2) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const separator = '[...]';
|
||||||
|
// Max chars for content = half-budget tokens * 3 chars/token, minus separator overhead
|
||||||
|
const targetChars = Math.floor(tokenBudget / 2 * 3) - separator.length - 2;
|
||||||
|
|
||||||
|
if (targetChars <= 0) {
|
||||||
|
return separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1/3 of budget for start, 2/3 for end
|
||||||
|
const startCharsMax = Math.floor(targetChars / 3);
|
||||||
|
const endCharsMax = targetChars - startCharsMax;
|
||||||
|
|
||||||
|
let startCharsUsed = 0;
|
||||||
|
let startEnd = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const lineLen = lines[i].length + 1; // +1 for '\n'
|
||||||
|
if (startCharsUsed + lineLen > startCharsMax) break;
|
||||||
|
startCharsUsed += lineLen;
|
||||||
|
startEnd = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let endCharsUsed = 0;
|
||||||
|
let endStart = lines.length;
|
||||||
|
for (let i = lines.length - 1; i >= startEnd; i--) {
|
||||||
|
const lineLen = lines[i].length + 1;
|
||||||
|
if (endCharsUsed + lineLen > endCharsMax) break;
|
||||||
|
endCharsUsed += lineLen;
|
||||||
|
endStart = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startEnd >= endStart) {
|
||||||
|
return text; // All lines fit after all
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPart = lines.slice(0, startEnd).join('\n');
|
||||||
|
const endPart = lines.slice(endStart).join('\n');
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (startPart) parts.push(startPart);
|
||||||
|
parts.push(separator);
|
||||||
|
if (endPart) parts.push(endPart);
|
||||||
|
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
export function formatCharactersMarkdown(state: AppState): string {
|
export function formatCharactersMarkdown(state: AppState): string {
|
||||||
const { currentStory } = state;
|
const { currentStory } = state;
|
||||||
if (!currentStory || !currentStory.characters?.length) {
|
if (!currentStory || !currentStory.characters?.length) {
|
||||||
|
|
@ -57,7 +113,7 @@ namespace Prompt {
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSystemPrompt(state: AppState): string {
|
export function formatSystemPrompt(state: AppState, storyTokenBudget: number = 0): string {
|
||||||
const { currentStory } = state;
|
const { currentStory } = state;
|
||||||
if (!currentStory) {
|
if (!currentStory) {
|
||||||
return state.systemInstruction;
|
return state.systemInstruction;
|
||||||
|
|
@ -68,7 +124,7 @@ namespace Prompt {
|
||||||
parts.push(`# ${currentStory.title}`);
|
parts.push(`# ${currentStory.title}`);
|
||||||
|
|
||||||
if (currentStory.lore) {
|
if (currentStory.lore) {
|
||||||
parts.push('## Lore\n' + currentStory.lore);
|
parts.push(`## Lore\n${currentStory.lore}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const charactersSection = formatCharactersMarkdown(state);
|
const charactersSection = formatCharactersMarkdown(state);
|
||||||
|
|
@ -81,6 +137,17 @@ namespace Prompt {
|
||||||
parts.push(locationsSection);
|
parts.push(locationsSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentStory.text && storyTokenBudget > 0) {
|
||||||
|
const storyText = formatStoryText(currentStory.text, storyTokenBudget);
|
||||||
|
if (storyText) {
|
||||||
|
parts.push(`## Story\n${storyText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStory.lastModifiedChunk) {
|
||||||
|
parts.push(`## Last Modified Chunk\n${currentStory.lastModifiedChunk}`);
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join('\n\n');
|
return parts.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,9 +158,20 @@ namespace Prompt {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Estimate token budget for story text
|
||||||
|
let storyTokenBudget = 0;
|
||||||
|
if (model.max_context) {
|
||||||
|
const nonStorySystem = formatSystemPrompt(state, 0);
|
||||||
|
const chatText = [...currentStory.chatMessages, ...newMessages]
|
||||||
|
.map(m => m.content)
|
||||||
|
.join('');
|
||||||
|
const maxOutput = model.max_length ?? 2048;
|
||||||
|
const otherTokens = approxTokens(nonStorySystem) + approxTokens(chatText) + maxOutput;
|
||||||
|
storyTokenBudget = model.max_context - otherTokens;
|
||||||
|
}
|
||||||
|
|
||||||
const messages: LLM.ChatMessage[] = [
|
const messages: LLM.ChatMessage[] = [
|
||||||
{ role: 'system', content: formatSystemPrompt(state) },
|
{ role: 'system', content: formatSystemPrompt(state, storyTokenBudget) },
|
||||||
// TODO part of story
|
|
||||||
...currentStory.chatMessages,
|
...currentStory.chatMessages,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -105,6 +183,7 @@ namespace Prompt {
|
||||||
tools: Tools.getTools(),
|
tools: 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { formatError } from "@common/errors";
|
import { formatError } from "@common/errors";
|
||||||
|
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,29 +11,25 @@ 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');
|
||||||
|
|
||||||
export namespace Tools {
|
export namespace Tools {
|
||||||
interface Tool {
|
interface Tool<T extends TObject = TObject> {
|
||||||
description: string;
|
description: string;
|
||||||
parameters: LLM.ToolObjectParameter;
|
parameters: T;
|
||||||
handler(args: string | Record<string, any>, appState: AppState): unknown;
|
handler(args: Static<T>, appState: AppState): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
||||||
|
|
||||||
const TOOLS: Record<string, Tool> = {
|
const TOOLS: Record<string, Tool> = {
|
||||||
'append_to_story': {
|
'append_to_story': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object' || !('text' in args)) {
|
|
||||||
return 'Error: Missing required argument "text"';
|
|
||||||
}
|
|
||||||
const { text } = args as { text: string };
|
|
||||||
if (typeof text !== 'string') {
|
|
||||||
return 'Error: Argument "text" must be a string';
|
|
||||||
}
|
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'EDIT_STORY',
|
type: 'EDIT_STORY',
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
text: appState.currentStory.text + text
|
text: appState.currentStory.text + args.text,
|
||||||
|
lastModifiedChunk: args.text
|
||||||
});
|
});
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'SET_CURRENT_TAB',
|
type: 'SET_CURRENT_TAB',
|
||||||
|
|
@ -42,33 +39,19 @@ export namespace Tools {
|
||||||
return 'Text appended successfully';
|
return 'Text appended successfully';
|
||||||
},
|
},
|
||||||
description: 'Append text to the current story',
|
description: 'Append text to the current story',
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
text: Type.String({ description: 'The text to append to the story' }),
|
||||||
properties: {
|
}),
|
||||||
text: {
|
}),
|
||||||
type: 'string',
|
'append_to_lore': tool({
|
||||||
description: 'The text to append to the story',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['text'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'append_to_lore': {
|
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object' || !('text' in args)) {
|
|
||||||
return 'Error: Missing required argument "text"';
|
|
||||||
}
|
|
||||||
const { text } = args as { text: string };
|
|
||||||
if (typeof text !== 'string') {
|
|
||||||
return 'Error: Argument "text" must be a string';
|
|
||||||
}
|
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'EDIT_LORE',
|
type: 'EDIT_LORE',
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
lore: appState.currentStory.lore + text
|
lore: appState.currentStory.lore + args.text
|
||||||
});
|
});
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'SET_CURRENT_TAB',
|
type: 'SET_CURRENT_TAB',
|
||||||
|
|
@ -78,42 +61,19 @@ export namespace Tools {
|
||||||
return 'Text appended to lore successfully';
|
return 'Text appended to lore successfully';
|
||||||
},
|
},
|
||||||
description: 'Append text to the story lore',
|
description: 'Append text to the story lore',
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
text: Type.String({ description: 'The text to append to the lore' }),
|
||||||
properties: {
|
}),
|
||||||
text: {
|
}),
|
||||||
type: 'string',
|
'add_character': tool({
|
||||||
description: 'The text to append to the lore',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['text'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'add_character': {
|
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object') {
|
|
||||||
return 'Error: Missing required arguments';
|
|
||||||
}
|
|
||||||
const { name, shortDescription, nicknames, description, relations } = args as {
|
|
||||||
name: string;
|
|
||||||
shortDescription: string;
|
|
||||||
nicknames?: string[];
|
|
||||||
description?: string;
|
|
||||||
relations?: { name: string; relation: string }[];
|
|
||||||
};
|
|
||||||
if (typeof name !== 'string' || !name.trim()) {
|
|
||||||
return 'Error: Argument "name" must be a non-empty string';
|
|
||||||
}
|
|
||||||
if (typeof shortDescription !== 'string' || !shortDescription.trim()) {
|
|
||||||
return 'Error: Argument "shortDescription" must be a non-empty string';
|
|
||||||
}
|
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
// Filter out relations that reference non-existent characters
|
// Filter out relations that reference non-existent characters
|
||||||
const existingCharacterNames = appState.currentStory.characters.map(c => c.name);
|
const existingCharacterNames = appState.currentStory.characters.map(c => c.name);
|
||||||
const invalidRelations: string[] = [];
|
const invalidRelations: string[] = [];
|
||||||
const validRelations = (relations || []).filter(rel => {
|
const validRelations = (args.relations || []).filter(rel => {
|
||||||
if (!existingCharacterNames.includes(rel.name)) {
|
if (!existingCharacterNames.includes(rel.name)) {
|
||||||
invalidRelations.push(`${rel.name} (${rel.relation})`);
|
invalidRelations.push(`${rel.name} (${rel.relation})`);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -125,10 +85,10 @@ export namespace Tools {
|
||||||
storyId: appState.currentStory.id,
|
storyId: appState.currentStory.id,
|
||||||
character: {
|
character: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: name.trim(),
|
name: args.name.trim(),
|
||||||
nicknames: nicknames || [],
|
nicknames: args.nicknames || [],
|
||||||
shortDescription: shortDescription.trim(),
|
shortDescription: args.shortDescription.trim(),
|
||||||
description: description || '',
|
description: args.description || '',
|
||||||
relations: validRelations,
|
relations: validRelations,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -137,75 +97,43 @@ export namespace Tools {
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
tab: 'characters'
|
tab: 'characters'
|
||||||
});
|
});
|
||||||
let message = `Character "${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 += `. Removed invalid relations to non-existent characters: ${invalidRelations.join(', ')}`;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
description: 'Add a new character to the story',
|
description: 'Add a new character to the story',
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
name: Type.String({ description: "The character's full name" }),
|
||||||
properties: {
|
shortDescription: Type.String({ description: 'A brief description of the character (one line)' }),
|
||||||
name: {
|
nicknames: Type.Optional(Type.Array(Type.String(), { description: 'Optional list of nicknames' })),
|
||||||
type: 'string',
|
description: Type.Optional(Type.String({ description: 'Optional full character description' })),
|
||||||
description: 'The character\'s full name',
|
relations: Type.Optional(Type.Array(
|
||||||
},
|
Type.Object({
|
||||||
shortDescription: {
|
name: Type.String({ description: 'Related character name' }),
|
||||||
type: 'string',
|
relation: Type.String({ description: 'Relationship type' }),
|
||||||
description: 'A brief description of the character (one line)',
|
}),
|
||||||
},
|
{ description: 'Optional list of relationships with other characters' }
|
||||||
nicknames: {
|
)),
|
||||||
type: 'array',
|
}),
|
||||||
items: { type: 'string' },
|
}),
|
||||||
description: 'Optional list of nicknames',
|
'edit_character': tool({
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional full character description',
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string', description: 'Related character name' },
|
|
||||||
relation: { type: 'string', description: 'Relationship type' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: 'Optional list of relationships with other characters',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['name', 'shortDescription'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'edit_character': {
|
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object') {
|
|
||||||
return 'Error: Missing required arguments';
|
|
||||||
}
|
|
||||||
const { name, shortDescription, description } = args as {
|
|
||||||
name: string;
|
|
||||||
shortDescription?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
if (typeof name !== 'string' || !name.trim()) {
|
|
||||||
return 'Error: Argument "name" must be a non-empty string';
|
|
||||||
}
|
|
||||||
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 === name.trim());
|
const character = appState.currentStory.characters.find(c => c.name === args.name.trim());
|
||||||
if (!character) {
|
if (!character) {
|
||||||
return `Error: Character "${name.trim()}" not found`;
|
return `Error: Character "${args.name.trim()}" not found`;
|
||||||
}
|
}
|
||||||
// Only include defined values to avoid setting fields to undefined
|
// Only include defined values to avoid setting fields to undefined
|
||||||
const definedUpdates: Partial<Character> = {};
|
const definedUpdates: Partial<Character> = {};
|
||||||
if (shortDescription !== undefined) {
|
if (args.shortDescription !== undefined) {
|
||||||
definedUpdates.shortDescription = shortDescription;
|
definedUpdates.shortDescription = args.shortDescription;
|
||||||
}
|
}
|
||||||
if (description !== undefined) {
|
if (args.description !== undefined) {
|
||||||
definedUpdates.description = description;
|
definedUpdates.description = args.description;
|
||||||
}
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'EDIT_CHARACTER',
|
type: 'EDIT_CHARACTER',
|
||||||
|
|
@ -218,51 +146,17 @@ export namespace Tools {
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
tab: 'characters'
|
tab: 'characters'
|
||||||
});
|
});
|
||||||
return `Character "${name.trim()}" updated successfully`;
|
return `Character "${args.name.trim()}" updated successfully`;
|
||||||
},
|
},
|
||||||
description: 'Edit an existing character\'s description',
|
description: "Edit an existing character's description",
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
name: Type.String({ description: "The character's full name to identify which character to edit" }),
|
||||||
properties: {
|
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the character (one line)' })),
|
||||||
name: {
|
description: Type.Optional(Type.String({ description: 'Full character description' })),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'The character\'s full name to identify which character to edit',
|
}),
|
||||||
},
|
'add_location': tool({
|
||||||
shortDescription: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Brief description of the character (one line)',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Full character description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['name'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'add_location': {
|
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object') {
|
|
||||||
return 'Error: Missing required arguments';
|
|
||||||
}
|
|
||||||
const { name, shortDescription, description, scale } = args as {
|
|
||||||
name: string;
|
|
||||||
shortDescription: string;
|
|
||||||
description?: string;
|
|
||||||
scale: number;
|
|
||||||
};
|
|
||||||
if (typeof name !== 'string' || !name.trim()) {
|
|
||||||
return 'Error: Argument "name" must be a non-empty string';
|
|
||||||
}
|
|
||||||
if (typeof shortDescription !== 'string' || !shortDescription.trim()) {
|
|
||||||
return 'Error: Argument "shortDescription" must be a non-empty string';
|
|
||||||
}
|
|
||||||
if (typeof scale !== 'number') {
|
|
||||||
return 'Error: Argument "scale" must be a number';
|
|
||||||
}
|
|
||||||
if (!VALID_SCALES.includes(scale)) {
|
|
||||||
return `Error: Argument "scale" must be a valid LocationScale value (${VALID_SCALES.join(', ')})`;
|
|
||||||
}
|
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
|
|
@ -271,10 +165,10 @@ export namespace Tools {
|
||||||
storyId: appState.currentStory.id,
|
storyId: appState.currentStory.id,
|
||||||
location: {
|
location: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: name.trim(),
|
name: args.name.trim(),
|
||||||
shortDescription: shortDescription.trim(),
|
shortDescription: args.shortDescription.trim(),
|
||||||
description: description || '',
|
description: args.description || '',
|
||||||
scale: scale as LocationScale,
|
scale: args.scale,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
|
|
@ -282,66 +176,36 @@ export namespace Tools {
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
tab: 'locations'
|
tab: 'locations'
|
||||||
});
|
});
|
||||||
return `Location "${name.trim()}" added successfully`;
|
return `Location "${args.name.trim()}" added successfully`;
|
||||||
},
|
},
|
||||||
description: 'Add a new location to the story',
|
description: 'Add a new location to the story',
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
name: Type.String({ description: "The location's full name" }),
|
||||||
properties: {
|
shortDescription: Type.String({ description: 'A brief description of the location (one line)' }),
|
||||||
name: {
|
description: Type.Optional(Type.String({ description: 'Optional full location description' })),
|
||||||
type: 'string',
|
scale: Type.Enum(VALID_SCALES, {
|
||||||
description: 'The location\'s full name',
|
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||||
},
|
}),
|
||||||
shortDescription: {
|
}),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'A brief description of the location (one line)',
|
'edit_location': tool({
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional full location description',
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: 'number',
|
|
||||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
|
||||||
enum: VALID_SCALES
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['name', 'shortDescription', 'scale'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'edit_location': {
|
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!args || typeof args !== 'object') {
|
|
||||||
return 'Error: Missing required arguments';
|
|
||||||
}
|
|
||||||
const { name, shortDescription, description, scale } = args as {
|
|
||||||
name: string;
|
|
||||||
shortDescription?: string;
|
|
||||||
description?: string;
|
|
||||||
scale?: number;
|
|
||||||
};
|
|
||||||
if (typeof name !== 'string' || !name.trim()) {
|
|
||||||
return 'Error: Argument "name" must be a non-empty string';
|
|
||||||
}
|
|
||||||
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 === name.trim());
|
const location = appState.currentStory.locations.find(l => l.name === args.name.trim());
|
||||||
if (!location) {
|
if (!location) {
|
||||||
return `Error: Location "${name.trim()}" not found`;
|
return `Error: Location "${args.name.trim()}" not found`;
|
||||||
}
|
}
|
||||||
const definedUpdates: Partial<Location> = {};
|
const definedUpdates: Partial<Location> = {};
|
||||||
if (shortDescription !== undefined) {
|
if (args.shortDescription !== undefined) {
|
||||||
definedUpdates.shortDescription = shortDescription;
|
definedUpdates.shortDescription = args.shortDescription;
|
||||||
}
|
}
|
||||||
if (description !== undefined) {
|
if (args.description !== undefined) {
|
||||||
definedUpdates.description = description;
|
definedUpdates.description = args.description;
|
||||||
}
|
}
|
||||||
if (scale !== undefined) {
|
if (args.scale !== undefined) {
|
||||||
if (!VALID_SCALES.includes(scale)) {
|
definedUpdates.scale = args.scale;
|
||||||
return `Error: Argument "scale" must be a valid LocationScale value (${VALID_SCALES.join(', ')})`;
|
|
||||||
}
|
|
||||||
definedUpdates.scale = scale as LocationScale;
|
|
||||||
}
|
}
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'EDIT_LOCATION',
|
type: 'EDIT_LOCATION',
|
||||||
|
|
@ -354,45 +218,125 @@ export namespace Tools {
|
||||||
id: appState.currentStory.id,
|
id: appState.currentStory.id,
|
||||||
tab: 'locations'
|
tab: 'locations'
|
||||||
});
|
});
|
||||||
return `Location "${name.trim()}" updated successfully`;
|
return `Location "${args.name.trim()}" updated successfully`;
|
||||||
},
|
},
|
||||||
description: 'Edit an existing location\'s description',
|
description: "Edit an existing location's description",
|
||||||
parameters: {
|
parameters: Type.Object({
|
||||||
type: 'object',
|
name: Type.String({ description: "The location's full name to identify which location to edit" }),
|
||||||
properties: {
|
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the location (one line)' })),
|
||||||
name: {
|
description: Type.Optional(Type.String({ description: 'Full location description' })),
|
||||||
type: 'string',
|
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
||||||
description: 'The location\'s full name to identify which location to edit',
|
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||||
},
|
})),
|
||||||
shortDescription: {
|
}),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'Brief description of the location (one line)',
|
'edit_story': tool({
|
||||||
},
|
handler: async (args, appState) => {
|
||||||
description: {
|
if (!appState.currentStory) {
|
||||||
type: 'string',
|
return 'Error: No story selected';
|
||||||
description: 'Full location description',
|
}
|
||||||
},
|
const occurrences = appState.currentStory.text.split(args.old_text).length - 1;
|
||||||
scale: {
|
if (occurrences === 0) {
|
||||||
type: 'number',
|
return 'Error: old_text not found in story';
|
||||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
}
|
||||||
},
|
if (occurrences > 1 && !args.replace_all) {
|
||||||
},
|
return 'Error: old_text appears multiple times in story';
|
||||||
required: ['name'],
|
}
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_STORY',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
text: appState.currentStory.text.replaceAll(args.old_text, args.new_text),
|
||||||
|
lastModifiedChunk: args.replace_all ? undefined : args.new_text,
|
||||||
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'story'
|
||||||
|
});
|
||||||
|
return 'Story edited successfully';
|
||||||
},
|
},
|
||||||
}
|
description: 'Replace text in the current story',
|
||||||
|
parameters: Type.Object({
|
||||||
|
old_text: Type.String({ description: 'The text to find and replace in the story' }),
|
||||||
|
new_text: Type.String({ description: 'The new text to replace old_text with' }),
|
||||||
|
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'edit_lore': tool({
|
||||||
|
handler: async (args, appState) => {
|
||||||
|
if (!appState.currentStory) {
|
||||||
|
return 'Error: No story selected';
|
||||||
|
}
|
||||||
|
const occurrences = appState.currentStory.lore.split(args.old_text).length - 1;
|
||||||
|
if (occurrences === 0) {
|
||||||
|
return 'Error: old_text not found in lore';
|
||||||
|
}
|
||||||
|
if (occurrences > 1 && !args.replace_all) {
|
||||||
|
return 'Error: old_text appears multiple times in 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'
|
||||||
|
});
|
||||||
|
return 'Lore edited successfully';
|
||||||
|
},
|
||||||
|
description: 'Replace text in the story lore',
|
||||||
|
parameters: Type.Object({
|
||||||
|
old_text: Type.String({ description: 'The text to find and replace in the lore' }),
|
||||||
|
new_text: Type.String({ description: 'The new text to replace old_text with' }),
|
||||||
|
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'grep': tool({
|
||||||
|
handler: async (args, appState) => {
|
||||||
|
if (!appState.currentStory) {
|
||||||
|
return 'Error: No story selected';
|
||||||
|
}
|
||||||
|
const lines = appState.currentStory.text.split('\n');
|
||||||
|
const matches: { line: number; content: string }[] = [];
|
||||||
|
const pattern = new RegExp(args.pattern, args.case_sensitive ? 'g' : 'gi');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (pattern.test(lines[i])) {
|
||||||
|
matches.push({ line: i + 1, content: lines[i].trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return `No matches found for pattern: ${args.pattern}`;
|
||||||
|
}
|
||||||
|
const limit = args.limit ?? 20;
|
||||||
|
const truncated = matches.length > limit;
|
||||||
|
const displayed = truncated ? matches.slice(0, limit) : matches;
|
||||||
|
let result = `Found ${matches.length} match(es) for pattern "${args.pattern}":\n`;
|
||||||
|
result += displayed.map(m => `Line ${m.line}: ${m.content}`).join('\n');
|
||||||
|
if (truncated) {
|
||||||
|
result += `\n... and ${matches.length - limit} more match(es)`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
description: 'Search for a pattern in the story text',
|
||||||
|
parameters: Type.Object({
|
||||||
|
pattern: Type.String({ description: 'The regex pattern to search for' }),
|
||||||
|
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)' })),
|
||||||
|
}),
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getTools(): LLM.Tool[] {
|
export function getTools(): LLM.Tool[] {
|
||||||
return Object.entries(TOOLS).map(([key, tool]) => {
|
return Object.entries(TOOLS).map(([key, tool]) => ({
|
||||||
return {
|
type: 'function',
|
||||||
type: 'function',
|
function: {
|
||||||
function: {
|
name: key,
|
||||||
name: key,
|
description: tool.description,
|
||||||
description: tool.description,
|
parameters: tool.parameters,
|
||||||
parameters: tool.parameters,
|
},
|
||||||
},
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArg(arg: unknown): unknown {
|
function parseArg(arg: unknown): unknown {
|
||||||
|
|
@ -418,17 +362,18 @@ export namespace Tools {
|
||||||
const { function: fn } = toolCall;
|
const { function: fn } = toolCall;
|
||||||
const args = parseArg(fn.arguments);
|
const args = parseArg(fn.arguments);
|
||||||
|
|
||||||
if (!args || typeof args !== 'object') {
|
const tool = TOOLS[fn.name];
|
||||||
return 'Error: Arguments must be an object';
|
if (!tool) {
|
||||||
}
|
|
||||||
|
|
||||||
const handler = TOOLS[fn.name]?.handler;
|
|
||||||
if (!handler) {
|
|
||||||
return `Unknown tool: ${fn.name}`;
|
return `Unknown tool: ${fn.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = Type.Check(tool.parameters, args);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return errors.map(e => e.message).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await handler(args, appState);
|
const result = await tool.handler(args as any, appState);
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue