Compare commits
2 Commits
8acac07c04
...
97533daa91
| Author | SHA1 | Date |
|---|---|---|
|
|
97533daa91 | |
|
|
d5481e1a13 |
9
bun.lock
9
bun.lock
|
|
@ -10,6 +10,7 @@
|
||||||
"@huggingface/jinja": "0.5.5",
|
"@huggingface/jinja": "0.5.5",
|
||||||
"@huggingface/tokenizers": "0.1.1",
|
"@huggingface/tokenizers": "0.1.1",
|
||||||
"@inquirer/select": "2.3.10",
|
"@inquirer/select": "2.3.10",
|
||||||
|
"@ironarachne/made-up-names": "^3.0.0",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
"ace-builds": "1.36.3",
|
"ace-builds": "1.36.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
|
|
@ -21,7 +22,7 @@
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@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",
|
||||||
"browser-detect": "0.2.28",
|
"browser-detect": "0.2.28",
|
||||||
"eruda": "3.2.3",
|
"eruda": "3.2.3",
|
||||||
"html-minifier": "4.0.0",
|
"html-minifier": "4.0.0",
|
||||||
|
|
@ -52,6 +53,12 @@
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
|
"@ironarachne/made-up-names": ["@ironarachne/made-up-names@3.0.0", "", { "dependencies": { "@ironarachne/rng": "^3.0.1", "@ironarachne/word-generator": "^3.0.1" } }, "sha512-EYwGC1bi9r8/VVbeDgm6jzp8/igC0vs1C2KGK0LIrBcSWg10UNbd9ue6+pXrKQeuCPVdpph2I7SRAnFQ80vIFA=="],
|
||||||
|
|
||||||
|
"@ironarachne/rng": ["@ironarachne/rng@3.0.1", "", {}, "sha512-256DtrY7UDMnSp9jTz2aM0JvOuXExcGd/SZlZwSutKR0Aehcq42Q/rGj4SpxxrOu6INb/jeiB6w2eIxHNQEA8Q=="],
|
||||||
|
|
||||||
|
"@ironarachne/word-generator": ["@ironarachne/word-generator@3.0.1", "", { "dependencies": { "@ironarachne/rng": "^3.0.1" } }, "sha512-A5NurF35phpZENXDy+DWUSjnRG/o8nhs1p2JCd5tBy01Ka0t2ANpYJUnDwMNXuZT80F7sLtx2wr9Ekmgxpre+g=="],
|
||||||
|
|
||||||
"@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="],
|
"@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="],
|
||||||
|
|
||||||
"@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="],
|
"@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="],
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@huggingface/jinja": "0.5.5",
|
"@huggingface/jinja": "0.5.5",
|
||||||
"@huggingface/tokenizers": "0.1.1",
|
"@huggingface/tokenizers": "0.1.1",
|
||||||
"@inquirer/select": "2.3.10",
|
"@inquirer/select": "2.3.10",
|
||||||
|
"@ironarachne/made-up-names": "^3.0.0",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
"ace-builds": "1.36.3",
|
"ace-builds": "1.36.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
position: relative;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
outline: none;
|
outline: none;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
import type { JSX } from "preact";
|
import type { JSX } from "preact";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import styles from "../assets/content-editable.module.css";
|
import styles from "../assets/content-editable.module.css";
|
||||||
|
|
@ -9,6 +9,7 @@ type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'value' | 'onInput'> & {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
autoLines?: boolean;
|
autoLines?: boolean;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
onInput?: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>>;
|
onInput?: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ export const ContentEditable = ({
|
||||||
placeholder,
|
placeholder,
|
||||||
autoLines,
|
autoLines,
|
||||||
highlight: enableHighlight,
|
highlight: enableHighlight,
|
||||||
|
disabled,
|
||||||
onInput,
|
onInput,
|
||||||
class: externalClass,
|
class: externalClass,
|
||||||
...props
|
...props
|
||||||
|
|
@ -118,10 +120,10 @@ export const ContentEditable = ({
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
contentEditable
|
contentEditable={!disabled}
|
||||||
data-placeholder={value?.replaceAll('\n', '').length ? undefined : placeholder}
|
data-placeholder={value?.replaceAll('\n', '').length ? undefined : placeholder}
|
||||||
class={clsx(styles.root, autoLines && styles.autoLines, externalClass)}
|
class={clsx(styles.root, autoLines && styles.autoLines, externalClass, disabled && styles.disabled)}
|
||||||
onInput={handleInput}
|
onInput={disabled ? undefined : handleInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
const charName = currentWorld.title ?? 'Assistant';
|
const charName = currentWorld.title ?? 'Assistant';
|
||||||
const prefix = `${charName}: `;
|
const prefix = `${charName}: `;
|
||||||
let accumulatedContent = continuedMessage?.content ?? '';
|
let accumulatedContent = continuedMessage?.content ?? '';
|
||||||
let accumulatedReasoning = continuedMessage?.role === 'assistant' ? continuedMessage.reasoning_content ?? '' : '';
|
let accumulatedReasoning = continuedMessage?.role === 'assistant'
|
||||||
|
? continuedMessage.reasoning_content ?? continuedMessage.reasoning ?? '' : '';
|
||||||
let tool_calls: LLM.ToolCall[] | undefined;
|
let tool_calls: LLM.ToolCall[] | undefined;
|
||||||
|
|
||||||
for await (const chunk of LLM.generateStream(connection, request)) {
|
for await (const chunk of LLM.generateStream(connection, request)) {
|
||||||
|
|
@ -207,7 +208,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
const delta = chunk.choices[0]?.delta;
|
const delta = chunk.choices[0]?.delta;
|
||||||
|
|
||||||
if (delta?.tool_calls) {
|
if (delta?.tool_calls) {
|
||||||
tool_calls = delta.tool_calls;
|
tool_calls = Prompt.applyDelta(tool_calls, delta.tool_calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = delta?.content;
|
const content = delta?.content;
|
||||||
|
|
@ -217,7 +218,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
accumulatedContent = accumulatedContent.trimStart().slice(prefix.length).trimStart();
|
accumulatedContent = accumulatedContent.trimStart().slice(prefix.length).trimStart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const reasoningContent = delta?.reasoning_content;
|
const reasoningContent = delta?.reasoning_content ?? delta?.reasoning;
|
||||||
if (reasoningContent) {
|
if (reasoningContent) {
|
||||||
accumulatedReasoning += reasoningContent;
|
accumulatedReasoning += reasoningContent;
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +232,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
role: targetRole,
|
role: targetRole,
|
||||||
content: accumulatedContent,
|
content: accumulatedContent,
|
||||||
reasoning_content: accumulatedReasoning,
|
reasoning_content: accumulatedReasoning,
|
||||||
|
reasoning: accumulatedReasoning,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -241,6 +243,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
role: targetRole,
|
role: targetRole,
|
||||||
content: accumulatedContent,
|
content: accumulatedContent,
|
||||||
reasoning_content: accumulatedReasoning,
|
reasoning_content: accumulatedReasoning,
|
||||||
|
reasoning: accumulatedReasoning,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
};
|
};
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
@ -251,6 +254,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tool_calls) {
|
if (tool_calls) {
|
||||||
|
console.log(tool_calls);
|
||||||
const toolMessages: ChatMessage[] = [];
|
const toolMessages: ChatMessage[] = [];
|
||||||
for (const tool of tool_calls) {
|
for (const tool of tool_calls) {
|
||||||
if (abortControllerRef.current?.signal.aborted) {
|
if (abortControllerRef.current?.signal.aborted) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useInputState } from "@common/hooks/useInputState";
|
||||||
import { useUpdate } from "@common/hooks/useUpdate";
|
import { useUpdate } from "@common/hooks/useUpdate";
|
||||||
import { fuzzyMatch } from "@common/utils";
|
import { fuzzyMatch } from "@common/utils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
import { useCallback, useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
import styles from "../../assets/settings-modal.module.css";
|
import styles from "../../assets/settings-modal.module.css";
|
||||||
import { useAppState } from "../../contexts/state";
|
import { useAppState } from "../../contexts/state";
|
||||||
import LLM from "../../utils/llm";
|
import LLM from "../../utils/llm";
|
||||||
|
|
@ -40,13 +40,15 @@ export const ConnectionSettings = () => {
|
||||||
const textModelsData = useMemo(() => modelsData?.filter(LLM.isTextModel), [modelsData]);
|
const textModelsData = useMemo(() => modelsData?.filter(LLM.isTextModel), [modelsData]);
|
||||||
const imageModelsData = useMemo(() => modelsData?.filter(LLM.isImageModel), [modelsData]);
|
const imageModelsData = useMemo(() => modelsData?.filter(LLM.isImageModel), [modelsData]);
|
||||||
const isLoadingModels = connectionToFetch != null && textModelsData == undefined;
|
const isLoadingModels = connectionToFetch != null && textModelsData == undefined;
|
||||||
|
const param = useCallback((m: LLM.ModelInfoText, param: string) =>
|
||||||
|
(m.supported_parameters ?? m.top_provider?.supported_parameters)?.includes(param), []);
|
||||||
|
|
||||||
const [modelFilter, setModelFilter] = useInputState("");
|
const [modelFilter, setModelFilter] = useInputState("");
|
||||||
|
|
||||||
const groupedModels = useMemo(() => {
|
const groupedModels = useMemo(() => {
|
||||||
const sorted = (textModelsData ?? []).sort((a, b) => {
|
const sorted = (textModelsData ?? []).sort((a, b) => {
|
||||||
const aWeight = Number(a.supported_parameters.includes('tools')) * 2 + Number(a.supported_parameters.includes('reasoning'));
|
const aWeight = Number(param(a, 'tools')) * 2 + Number(param(a, 'reasoning'));
|
||||||
const bWeight = Number(b.supported_parameters.includes('tools')) * 2 + Number(b.supported_parameters.includes('reasoning'));
|
const bWeight = Number(param(b, 'tools')) * 2 + Number(param(b, 'reasoning'));
|
||||||
if (aWeight !== bWeight) return bWeight - aWeight;
|
if (aWeight !== bWeight) return bWeight - aWeight;
|
||||||
if (a.context_length !== b.context_length) return b.context_length - a.context_length;
|
if (a.context_length !== b.context_length) return b.context_length - a.context_length;
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
|
|
@ -55,7 +57,7 @@ export const ConnectionSettings = () => {
|
||||||
return Array.from(groups.entries())
|
return Array.from(groups.entries())
|
||||||
.sort((a, b) => b[0] - a[0])
|
.sort((a, b) => b[0] - a[0])
|
||||||
.map(([context, models]) => ({ context, models }));
|
.map(([context, models]) => ({ context, models }));
|
||||||
}, [textModelsData]);
|
}, [textModelsData, param]);
|
||||||
|
|
||||||
const filteredGroupedModels = useMemo(() => {
|
const filteredGroupedModels = useMemo(() => {
|
||||||
if (!modelFilter) return groupedModels;
|
if (!modelFilter) return groupedModels;
|
||||||
|
|
@ -167,7 +169,7 @@ export const ConnectionSettings = () => {
|
||||||
<optgroup key={context} label={`${context} context`}>
|
<optgroup key={context} label={`${context} context`}>
|
||||||
{models.map(m => (
|
{models.map(m => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.supported_parameters.includes('tools') ? '🔨' : ''}{m.supported_parameters.includes('reasoning') ? '🧠' : ''}{m.id} {m.top_provider.max_completion_tokens ? `(len: ${m.top_provider.max_completion_tokens})` : ''}
|
{param(m, 'tools') ? '🔨' : ''}{param(m, 'reasoning') ? '🧠' : ''}{m.id} {m.top_provider.max_completion_tokens ? `(len: ${m.top_provider.max_completion_tokens})` : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace LLM {
|
||||||
role: 'assistant';
|
role: 'assistant';
|
||||||
content: string;
|
content: string;
|
||||||
reasoning_content?: string;
|
reasoning_content?: string;
|
||||||
|
reasoning?: string;
|
||||||
tool_calls?: ToolCall[];
|
tool_calls?: ToolCall[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +102,7 @@ namespace LLM {
|
||||||
role?: string;
|
role?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
reasoning_content?: string;
|
reasoning_content?: string;
|
||||||
|
reasoning?: string;
|
||||||
tool_calls?: ToolCall[];
|
tool_calls?: ToolCall[];
|
||||||
};
|
};
|
||||||
finish_reason: 'stop' | 'tool_calls' | null;
|
finish_reason: 'stop' | 'tool_calls' | null;
|
||||||
|
|
@ -168,7 +170,7 @@ namespace LLM {
|
||||||
object: 'model';
|
object: 'model';
|
||||||
created: number;
|
created: number;
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
supported_parameters: string[];
|
supported_parameters?: string[];
|
||||||
architecture?: {
|
architecture?: {
|
||||||
input_modalities: Modality[];
|
input_modalities: Modality[];
|
||||||
output_modalities: Modality[];
|
output_modalities: Modality[];
|
||||||
|
|
@ -181,6 +183,7 @@ namespace LLM {
|
||||||
context_length: number;
|
context_length: number;
|
||||||
max_completion_tokens: number;
|
max_completion_tokens: number;
|
||||||
is_moderated: boolean;
|
is_moderated: boolean;
|
||||||
|
supported_parameters?: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +235,7 @@ namespace LLM {
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(connection.url);
|
const url = new URL(connection.url);
|
||||||
url.pathname = path;
|
url.pathname = url.pathname.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
|
|
@ -256,7 +259,7 @@ namespace LLM {
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(connection.url);
|
const url = new URL(connection.url);
|
||||||
url.pathname = path;
|
url.pathname = url.pathname.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||||
|
|
||||||
using sse = new SSE(url.toString(), {
|
using sse = new SSE(url.toString(), {
|
||||||
headers,
|
headers,
|
||||||
|
|
@ -270,7 +273,7 @@ namespace LLM {
|
||||||
if (isMessageEvent(e)) {
|
if (isMessageEvent(e)) {
|
||||||
if (e.data === '[DONE]') {
|
if (e.data === '[DONE]') {
|
||||||
controller.close();
|
controller.close();
|
||||||
} else {
|
} else if (e.data) {
|
||||||
controller.enqueue(e.data);
|
controller.enqueue(e.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +313,7 @@ namespace LLM {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMessageEvent(e: unknown): e is { data: string } {
|
function isMessageEvent(e: unknown): e is { data: string } {
|
||||||
return e != null && typeof e === 'object' && 'data' in e && typeof e.data === 'string';
|
return e != null && typeof e === 'object' && 'data' in e && typeof e.data === 'string' && Boolean(e.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getModels(connection: Connection): Promise<ModelsResponse<ModelInfo>> {
|
export async function getModels(connection: Connection): Promise<ModelsResponse<ModelInfo>> {
|
||||||
|
|
|
||||||
|
|
@ -365,11 +365,12 @@ namespace Prompt {
|
||||||
newMessages?: Iterable<ChatMessage>;
|
newMessages?: Iterable<ChatMessage>;
|
||||||
excludedMessageIds?: Iterable<string>;
|
excludedMessageIds?: Iterable<string>;
|
||||||
continueLast?: boolean;
|
continueLast?: boolean;
|
||||||
|
keepThinking?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compilePrompt(
|
export function compilePrompt(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
config: CompileConfig = {},
|
config: CompileConfig = { keepThinking: true },
|
||||||
): LLM.ChatCompletionRequest | null {
|
): LLM.ChatCompletionRequest | null {
|
||||||
const { currentStory, model, enableThinking, currentWorld } = state;
|
const { currentStory, model, enableThinking, currentWorld } = state;
|
||||||
const {
|
const {
|
||||||
|
|
@ -401,7 +402,13 @@ namespace Prompt {
|
||||||
const charName = currentWorld?.title || 'Assistant';
|
const charName = currentWorld?.title || 'Assistant';
|
||||||
const userName = state.userName || 'User';
|
const userName = state.userName || 'User';
|
||||||
const applyVars = (msgs: ChatMessage[]) =>
|
const applyVars = (msgs: ChatMessage[]) =>
|
||||||
msgs.map(m => ({ ...m, content: stripDataUrlImages(substituteVars(state, m.content)) }));
|
msgs.map(m => ({
|
||||||
|
...m,
|
||||||
|
content: stripDataUrlImages(substituteVars(state, m.content)),
|
||||||
|
reasoning_content: config.keepThinking && ('reasoning_content' in m)
|
||||||
|
? m.reasoning_content
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
// Chat-only world: format messages with name prefixes
|
// Chat-only world: format messages with name prefixes
|
||||||
if (currentWorld?.chatOnly) {
|
if (currentWorld?.chatOnly) {
|
||||||
|
|
@ -455,6 +462,27 @@ namespace Prompt {
|
||||||
...state.generationSettings,
|
...state.generationSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyDelta(input: any, delta: any) {
|
||||||
|
if (Array.isArray(delta)) {
|
||||||
|
const result = [...(input ?? [])];
|
||||||
|
for (const item of delta) {
|
||||||
|
result[item.index] = applyDelta(result[item.index], item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (typeof delta === 'object') {
|
||||||
|
const result = { ...input };
|
||||||
|
for (const key of Object.keys(delta)) {
|
||||||
|
const value = delta[key];
|
||||||
|
result[key] = applyDelta(result[key], value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (typeof delta === 'string') {
|
||||||
|
return (input ?? '') + delta;
|
||||||
|
} else {
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Prompt;
|
export default Prompt;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { formatErrorMessage } from "@common/errors";
|
||||||
import { Type, type Static, type TObject } from '@common/typebox';
|
import { Type, type Static, type TObject } from '@common/typebox';
|
||||||
import { CharacterRole, LocationScale, type AppState, type Character, type Location } from "../contexts/state";
|
import { CharacterRole, LocationScale, type AppState, type Character, type Location } from "../contexts/state";
|
||||||
import LLM from "./llm";
|
import LLM from "./llm";
|
||||||
|
import { getClassicRaceNamePatternSet, getNameGeneratorForPatternSet, getSupportedClassicRaceNamePatternSets, type NameGeneratorPatternSet } from "@ironarachne/made-up-names";
|
||||||
|
|
||||||
const VALID_SCALES = Object.values(LocationScale);
|
const VALID_SCALES = Object.values(LocationScale);
|
||||||
const VALID_ROLES = Object.values(CharacterRole);
|
const VALID_ROLES = Object.values(CharacterRole);
|
||||||
|
|
@ -101,7 +102,7 @@ export namespace Tools {
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'ADD_CHARACTER',
|
type: 'ADD_CHARACTER',
|
||||||
worldId: appState.currentWorld.id,
|
worldId: appState.currentWorld.id,
|
||||||
storyId: appState.currentStory.id,
|
storyId: args.world ? null : appState.currentStory.id,
|
||||||
character: {
|
character: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: args.name.trim(),
|
name: args.name.trim(),
|
||||||
|
|
@ -134,6 +135,7 @@ export namespace Tools {
|
||||||
}),
|
}),
|
||||||
{ description: 'Optional list of relationships with other characters (allowed only when adding a new character)' }
|
{ description: 'Optional list of relationships with other characters (allowed only when adding a new character)' }
|
||||||
)),
|
)),
|
||||||
|
world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'set_character_relation': tool({
|
'set_character_relation': tool({
|
||||||
|
|
@ -150,7 +152,7 @@ export namespace Tools {
|
||||||
return `Error: Target character "${args.target_name.trim()}" not found, please add it first.`;
|
return `Error: Target character "${args.target_name.trim()}" not found, please add it first.`;
|
||||||
}
|
}
|
||||||
const inStory = appState.currentStory.characters.some(c => c.id === character.id);
|
const inStory = appState.currentStory.characters.some(c => c.id === character.id);
|
||||||
const storyId = inStory ? appState.currentStory.id : null;
|
const storyId = inStory && !args.world ? appState.currentStory.id : null;
|
||||||
const existingRelationIndex = character.relations.findIndex(r => r.name === args.target_name.trim());
|
const existingRelationIndex = character.relations.findIndex(r => r.name === args.target_name.trim());
|
||||||
if (existingRelationIndex !== -1) {
|
if (existingRelationIndex !== -1) {
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
|
|
@ -183,6 +185,7 @@ export namespace Tools {
|
||||||
character_name: Type.String({ description: "The character's full name to add/edit the relation for" }),
|
character_name: Type.String({ description: "The character's full name to add/edit the relation for" }),
|
||||||
target_name: Type.String({ description: "The target character's full name" }),
|
target_name: Type.String({ description: "The target character's full name" }),
|
||||||
relation: Type.String({ description: 'The relationship type (e.g., friend, enemy, sibling)' }),
|
relation: Type.String({ description: 'The relationship type (e.g., friend, enemy, sibling)' }),
|
||||||
|
world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'get_location': tool({
|
'get_location': tool({
|
||||||
|
|
@ -244,7 +247,7 @@ export namespace Tools {
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'ADD_LOCATION',
|
type: 'ADD_LOCATION',
|
||||||
worldId: appState.currentWorld.id,
|
worldId: appState.currentWorld.id,
|
||||||
storyId: appState.currentStory.id,
|
storyId: args.world ? null : appState.currentStory.id,
|
||||||
location: {
|
location: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: args.name.trim(),
|
name: args.name.trim(),
|
||||||
|
|
@ -265,6 +268,7 @@ export namespace Tools {
|
||||||
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
||||||
description: `Location scale (enum): ${VALID_SCALES.join(', ')}`,
|
description: `Location scale (enum): ${VALID_SCALES.join(', ')}`,
|
||||||
})),
|
})),
|
||||||
|
world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'set_lore_entry': tool({
|
'set_lore_entry': tool({
|
||||||
|
|
@ -289,7 +293,7 @@ export namespace Tools {
|
||||||
appState.dispatch({
|
appState.dispatch({
|
||||||
type: 'ADD_LORE_ENTRY',
|
type: 'ADD_LORE_ENTRY',
|
||||||
worldId: appState.currentWorld.id,
|
worldId: appState.currentWorld.id,
|
||||||
storyId: appState.currentStory.id,
|
storyId: args.world ? null : appState.currentStory.id,
|
||||||
entry: {
|
entry: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
title: args.title.trim(),
|
title: args.title.trim(),
|
||||||
|
|
@ -304,6 +308,7 @@ export namespace Tools {
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
title: Type.String({ description: "The lore entry's title" }),
|
title: Type.String({ description: "The lore entry's title" }),
|
||||||
text: Type.String({ description: 'The lore entry content.' }),
|
text: Type.String({ description: 'The lore entry content.' }),
|
||||||
|
world: Type.Optional(Type.Boolean({ description: "If `true`, entry would be visible to all stories under that world" })),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'edit_text': tool({
|
'edit_text': tool({
|
||||||
|
|
@ -351,15 +356,14 @@ export namespace Tools {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasUserMessageSinceLastAppend) {
|
if (!hasUserMessageSinceLastAppend) {
|
||||||
return `Error: You cannot add new text to the story until user approves your last edit. Stop right there.`;
|
return `Error: You're not allowed to add more text now. Do not retry nor continue.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const lines = args.new_text.split(/\n+/ig).filter(l => l.trim());
|
||||||
const numLines = args.new_text.split('\n').filter(l => l.trim()).length;
|
const numLines = lines.length;
|
||||||
|
|
||||||
let cropped = false;
|
let cropped = false;
|
||||||
if (!isScratchpad && numLines > LINES_LIMIT) {
|
if (!isScratchpad && numLines > LINES_LIMIT) {
|
||||||
const lines = args.new_text.split('\n');
|
|
||||||
let kept = 0;
|
let kept = 0;
|
||||||
const croppedLines: string[] = [];
|
const croppedLines: string[] = [];
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
|
@ -373,13 +377,13 @@ export namespace Tools {
|
||||||
cropped = true;
|
cropped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchEdit(currentText + '\n' + args.new_text);
|
dispatchEdit(currentText.trim() + '\n' + args.new_text.trim());
|
||||||
appState.dispatch({ type: 'SET_CURRENT_TAB', tab });
|
appState.dispatch({ type: 'SET_CURRENT_TAB', tab });
|
||||||
let message = cropped
|
let message = cropped
|
||||||
? `Added text:\n${args.new_text.split('\n').filter(l => l.trim()).map(l => '> ' + l).join('\n')}\n\nNote: The rest was cropped due to ${LINES_LIMIT} lines limit!`
|
? `Added text:\n${args.new_text.split('\n').filter(l => l.trim()).map(l => '> ' + l).join('\n')}\n\nNote: The rest was cropped due to ${LINES_LIMIT} lines limit!`
|
||||||
: `Text appended to ${target} successfully.`;
|
: `Text appended to ${target} successfully.`;
|
||||||
|
|
||||||
message += `\nNote: you can't continue nor add more text until user's approval. Stop any attempts and wait.`
|
message += `\nTool note: you're not allowed to add more text now. Do not retry nor continue.`
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
@ -504,6 +508,31 @@ export namespace Tools {
|
||||||
prompt: Type.String({ description: 'The image generation prompt' }),
|
prompt: Type.String({ description: 'The image generation prompt' }),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
'generate_name': tool({
|
||||||
|
handler: async (args) => {
|
||||||
|
const gen = getClassicRaceNamePatternSet(args.family);
|
||||||
|
const kind = gen[args.kind];
|
||||||
|
if (!kind) {
|
||||||
|
return `Error: unknown kind: '${args.kind}'`;
|
||||||
|
}
|
||||||
|
const subGen = getNameGeneratorForPatternSet(`${args.family}_${args.kind}`, gen[args.kind]);
|
||||||
|
const names = subGen.generate(args.amount ?? 1);
|
||||||
|
|
||||||
|
return names;
|
||||||
|
},
|
||||||
|
description: 'Generate a name. Use this tool when you need a new name, instead of inventing the name yourself.',
|
||||||
|
parameters: Type.Object({
|
||||||
|
family: Type.Enum(getSupportedClassicRaceNamePatternSets(), { description: 'Theme or race to which name belongs to' }),
|
||||||
|
kind: Type.Enum((() => {
|
||||||
|
type NameGenField = keyof Omit<NameGeneratorPatternSet, 'name'>;
|
||||||
|
const gen = getClassicRaceNamePatternSet('elf');
|
||||||
|
const keys = Object.keys(gen);
|
||||||
|
|
||||||
|
return keys.filter(k => k != 'name') as NameGenField[];
|
||||||
|
})(), { description: 'What kind of name to generate' }),
|
||||||
|
amount: Type.Optional(Type.Number({ description: "Specify the amount of names to generate. You can generate multiple names and select the most fitting. Default: 1" })),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolName = keyof typeof TOOLS;
|
export type ToolName = keyof typeof TOOLS;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue