User config
This commit is contained in:
parent
6f100cac97
commit
371f84571a
|
|
@ -182,6 +182,24 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,7 @@ export const ChatPanel = () => {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={styles.content}
|
class={styles.content}
|
||||||
dangerouslySetInnerHTML={{ __html: highlight(message.content, false).trim() }}
|
dangerouslySetInnerHTML={{ __html: highlight(Prompt.substituteVars(appState, message.content), false).trim() }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{message.role === 'assistant' && message.tool_calls && (
|
{message.role === 'assistant' && message.tool_calls && (
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ import styles from "../assets/settings-modal.module.css";
|
||||||
import { BannedTokensSettings } from "./settings/banned-tokens";
|
import { BannedTokensSettings } from "./settings/banned-tokens";
|
||||||
import { ConnectionSettings } from "./settings/connection";
|
import { ConnectionSettings } from "./settings/connection";
|
||||||
import { SystemInstructionSettings } from "./settings/system-instruction";
|
import { SystemInstructionSettings } from "./settings/system-instruction";
|
||||||
|
import { UserSettings } from "./settings/user";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = "banned-tokens" | "system-instruction" | "connection";
|
type Tab = "banned-tokens" | "system-instruction" | "connection" | "user";
|
||||||
|
|
||||||
export const SettingsModal = ({ onClose }: Props) => {
|
export const SettingsModal = ({ onClose }: Props) => {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("connection");
|
const [activeTab, setActiveTab] = useState<Tab>("connection");
|
||||||
|
|
@ -32,6 +33,12 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
>
|
>
|
||||||
Connection
|
Connection
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class={clsx(styles.menuItem, activeTab === "user" && styles.active)}
|
||||||
|
onClick={() => setActiveTab("user")}
|
||||||
|
>
|
||||||
|
User
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.menuItem, activeTab === "banned-tokens" && styles.active)}
|
class={clsx(styles.menuItem, activeTab === "banned-tokens" && styles.active)}
|
||||||
onClick={() => setActiveTab("banned-tokens")}
|
onClick={() => setActiveTab("banned-tokens")}
|
||||||
|
|
@ -46,7 +53,8 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class={styles.content}>
|
<div class={styles.content}>
|
||||||
{activeTab === "banned-tokens" && <BannedTokensSettings onClose={onClose} />}
|
{activeTab === "user" && <UserSettings />}
|
||||||
|
{activeTab === "banned-tokens" && <BannedTokensSettings />}
|
||||||
{activeTab === "system-instruction" && <SystemInstructionSettings />}
|
{activeTab === "system-instruction" && <SystemInstructionSettings />}
|
||||||
{activeTab === "connection" && <ConnectionSettings />}
|
{activeTab === "connection" && <ConnectionSettings />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import { X } from "lucide-preact";
|
||||||
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";
|
||||||
|
|
||||||
interface Props {
|
export const BannedTokensSettings = () => {
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BannedTokensSettings = ({ onClose }: Props) => {
|
|
||||||
const { bannedTokens, dispatch } = useAppState();
|
const { bannedTokens, dispatch } = useAppState();
|
||||||
const [inputValue, setInputValue] = useInputState();
|
const [inputValue, setInputValue] = useInputState();
|
||||||
|
|
||||||
|
|
@ -30,7 +26,6 @@ export const BannedTokensSettings = ({ onClose }: Props) => {
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Enter") handleAdd();
|
if (e.key === "Enter") handleAdd();
|
||||||
else if (e.key === "Escape") onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useBool } from "@common/hooks/useBool";
|
||||||
import { useQuery } from "@common/hooks/useAsyncState";
|
import { useQuery } from "@common/hooks/useAsyncState";
|
||||||
import { useInputState } from "@common/hooks/useInputState";
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
import { useUpdate } from "@common/hooks/useUpdate";
|
import { useUpdate } from "@common/hooks/useUpdate";
|
||||||
|
|
@ -12,6 +13,7 @@ export const ConnectionSettings = () => {
|
||||||
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
|
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
|
||||||
const [selectedModel, setSelectedModel] = useInputState(model?.id ?? "");
|
const [selectedModel, setSelectedModel] = useInputState(model?.id ?? "");
|
||||||
const [update, triggerFetch] = useUpdate();
|
const [update, triggerFetch] = useUpdate();
|
||||||
|
const showPassword = useBool(false);
|
||||||
|
|
||||||
const urlRef = useRef(url);
|
const urlRef = useRef(url);
|
||||||
const apiKeyRef = useRef(apiKey);
|
const apiKeyRef = useRef(apiKey);
|
||||||
|
|
@ -91,17 +93,27 @@ export const ConnectionSettings = () => {
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.formGroup}>
|
<div class={styles.formGroup}>
|
||||||
<label class={styles.label}>API Key</label>
|
<label class={styles.label}>API Key</label>
|
||||||
<input
|
<div class={styles.inputRow}>
|
||||||
type="password"
|
<input
|
||||||
value={apiKey}
|
type={showPassword.value ? "text" : "password"}
|
||||||
onInput={setApiKey}
|
value={apiKey}
|
||||||
onBlur={handleBlur}
|
onInput={setApiKey}
|
||||||
onKeyDown={handleKeyDown}
|
onBlur={handleBlur}
|
||||||
placeholder="your-api-key"
|
onKeyDown={handleKeyDown}
|
||||||
class={styles.input}
|
placeholder="your-api-key"
|
||||||
autocomplete="new-password"
|
class={styles.input}
|
||||||
name="api-key-random"
|
autocomplete="new-password"
|
||||||
/>
|
name="api-key-random"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={showPassword.toggle}
|
||||||
|
class={styles.iconButton}
|
||||||
|
title={showPassword.value ? "Hide API key" : "Show API key"}
|
||||||
|
>
|
||||||
|
{showPassword.value ? "🙈" : "👁️"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.formGroup}>
|
<div class={styles.formGroup}>
|
||||||
<label class={styles.label}>Model</label>
|
<label class={styles.label}>Model</label>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ContentEditable } from "@common/components/ContentEditable";
|
||||||
|
import { highlight } from "@common/highlight";
|
||||||
|
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||||
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import styles from "../../assets/settings-modal.module.css";
|
||||||
|
import { useAppState } from "../../contexts/state";
|
||||||
|
|
||||||
|
export const UserSettings = () => {
|
||||||
|
const { userName, userDescription, dispatch } = useAppState();
|
||||||
|
|
||||||
|
const [nameValue, setNameValue] = useInputState(userName);
|
||||||
|
|
||||||
|
const handleNameBlur = () => {
|
||||||
|
const trimmed = nameValue.trim();
|
||||||
|
if (trimmed !== userName) {
|
||||||
|
dispatch({ type: "SET_USER_NAME", userName: trimmed || "User" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDescription = useInputCallback((value) => {
|
||||||
|
dispatch({ type: "SET_USER_DESCRIPTION", userDescription: value });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.form}>
|
||||||
|
<div class={styles.formGroup}>
|
||||||
|
<label class={styles.label}>Your Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={nameValue}
|
||||||
|
onInput={setNameValue}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
placeholder="User"
|
||||||
|
class={styles.input}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class={clsx(styles.formGroup, styles.formGroupFill)}>
|
||||||
|
<label class={styles.label}>Your Description</label>
|
||||||
|
<ContentEditable
|
||||||
|
value={highlight(userDescription)}
|
||||||
|
onInput={setDescription}
|
||||||
|
placeholder="Describe yourself for the AI..."
|
||||||
|
class={clsx(styles.input, styles.textarea)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -116,6 +116,8 @@ interface IState {
|
||||||
enableThinking: boolean;
|
enableThinking: boolean;
|
||||||
bannedTokens: string[];
|
bannedTokens: string[];
|
||||||
systemInstruction: string;
|
systemInstruction: string;
|
||||||
|
userName: string;
|
||||||
|
userDescription: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Actions ─────────────────────────────────────────────────────────────────
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -140,6 +142,8 @@ type Action =
|
||||||
| { type: 'DELETE_LORE_ENTRY'; worldId: string; storyId: string | null; entryId: string }
|
| { type: 'DELETE_LORE_ENTRY'; worldId: string; storyId: string | null; entryId: string }
|
||||||
| { type: 'REORDER_LORE_ENTRIES'; worldId: string; storyId: string | null; entryIds: string[] }
|
| { type: 'REORDER_LORE_ENTRIES'; worldId: string; storyId: string | null; entryIds: string[] }
|
||||||
// Settings
|
// Settings
|
||||||
|
| { type: 'SET_USER_NAME'; userName: string }
|
||||||
|
| { type: 'SET_USER_DESCRIPTION'; userDescription: string }
|
||||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||||
| { type: 'SET_WORLD_SYSTEM_INSTRUCTION_OVERRIDE'; worldId: string; systemInstructionOverride: string | undefined }
|
| { type: 'SET_WORLD_SYSTEM_INSTRUCTION_OVERRIDE'; worldId: string; systemInstructionOverride: string | undefined }
|
||||||
| { type: 'SET_CURRENT_TAB'; tab: Tab }
|
| { type: 'SET_CURRENT_TAB'; tab: Tab }
|
||||||
|
|
@ -212,6 +216,8 @@ const DEFAULT_STATE: IState = {
|
||||||
model: null,
|
model: null,
|
||||||
enableThinking: false,
|
enableThinking: false,
|
||||||
bannedTokens: [],
|
bannedTokens: [],
|
||||||
|
userName: 'User',
|
||||||
|
userDescription: '',
|
||||||
systemInstruction: `You are a creative writing assistant. Help the user develop their story by writing engaging content, maintaining consistency with the established characters, settings, and plot. Follow the user's instructions while staying true to the story's tone and style.
|
systemInstruction: `You are a creative writing assistant. Help the user develop their story by writing engaging content, maintaining consistency with the established characters, settings, and plot. Follow the user's instructions while staying true to the story's tone and style.
|
||||||
|
|
||||||
Write using markdown to highlight special parts.
|
Write using markdown to highlight special parts.
|
||||||
|
|
@ -382,6 +388,12 @@ function reducer(state: IState, action: Action): IState {
|
||||||
return { lore: reordered };
|
return { lore: reordered };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case 'SET_USER_NAME': {
|
||||||
|
return { ...state, userName: action.userName };
|
||||||
|
}
|
||||||
|
case 'SET_USER_DESCRIPTION': {
|
||||||
|
return { ...state, userDescription: action.userDescription };
|
||||||
|
}
|
||||||
case 'SET_SYSTEM_INSTRUCTION': {
|
case 'SET_SYSTEM_INSTRUCTION': {
|
||||||
return { ...state, systemInstruction: action.systemInstruction };
|
return { ...state, systemInstruction: action.systemInstruction };
|
||||||
}
|
}
|
||||||
|
|
@ -570,6 +582,8 @@ export interface AppState {
|
||||||
enableThinking: boolean;
|
enableThinking: boolean;
|
||||||
bannedTokens: string[];
|
bannedTokens: string[];
|
||||||
systemInstruction: string;
|
systemInstruction: string;
|
||||||
|
userName: string;
|
||||||
|
userDescription: string;
|
||||||
/** Effective system instruction: world override if set, otherwise global */
|
/** Effective system instruction: world override if set, otherwise global */
|
||||||
effectiveSystemInstruction: string;
|
effectiveSystemInstruction: string;
|
||||||
dispatch: (action: Action) => void;
|
dispatch: (action: Action) => void;
|
||||||
|
|
@ -616,6 +630,8 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
enableThinking: state.enableThinking,
|
enableThinking: state.enableThinking,
|
||||||
bannedTokens: state.bannedTokens ?? [],
|
bannedTokens: state.bannedTokens ?? [],
|
||||||
systemInstruction: state.systemInstruction ?? '',
|
systemInstruction: state.systemInstruction ?? '',
|
||||||
|
userName: state.userName ?? 'User',
|
||||||
|
userDescription: state.userDescription ?? '',
|
||||||
effectiveSystemInstruction: currentWorld?.systemInstructionOverride ?? state.systemInstruction ?? '',
|
effectiveSystemInstruction: currentWorld?.systemInstructionOverride ?? state.systemInstruction ?? '',
|
||||||
dispatch,
|
dispatch,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ Key Guidelines:
|
||||||
parts.push(`## {{char}}'s Example Response:\n${data.mes_example.trim()}`);
|
parts.push(`## {{char}}'s Example Response:\n${data.mes_example.trim()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `# **Roleplay Context**\n${substituteVars(parts.join('\n\n'), data.name)}\n### **End of Roleplay Context**`;
|
return `# **Roleplay Context**\n${parts.join('\n\n')}\n### **End of Roleplay Context**`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── World Builder ────────────────────────────────────────────────────────
|
// ─── World Builder ────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,16 @@ namespace Prompt {
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatUserSection(state: AppState): string {
|
||||||
|
if (!state.userName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.userDescription
|
||||||
|
? `## ${state.userName}:\n${state.userDescription}`
|
||||||
|
: `## User name: ${state.userName}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatLocationsMarkdown(state: AppState): string {
|
export function formatLocationsMarkdown(state: AppState): string {
|
||||||
const { mergedLocations } = state;
|
const { mergedLocations } = state;
|
||||||
if (!mergedLocations?.length) {
|
if (!mergedLocations?.length) {
|
||||||
|
|
@ -274,44 +284,55 @@ namespace Prompt {
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function substituteVars(state: AppState, text: string): string {
|
||||||
|
const charName = state.currentWorld?.title || 'Assistant';
|
||||||
|
const userName = state.userName || 'User';
|
||||||
|
return text
|
||||||
|
.replaceAll('{{char}}', charName)
|
||||||
|
.replaceAll('{{user}}', userName);
|
||||||
|
}
|
||||||
|
|
||||||
export function formatSystemPrompt(state: AppState, storyTokenBudget: number = 0): string {
|
export function formatSystemPrompt(state: AppState, storyTokenBudget: number = 0): string {
|
||||||
const { currentStory, currentWorld } = state;
|
const { currentStory, currentWorld } = state;
|
||||||
if (!currentStory) {
|
if (!currentStory) {
|
||||||
return state.effectiveSystemInstruction;
|
return state.effectiveSystemInstruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat-only worlds: just the system instruction, no story scaffolding
|
|
||||||
if (currentWorld?.chatOnly) {
|
|
||||||
return state.effectiveSystemInstruction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: string[] = [state.effectiveSystemInstruction];
|
const parts: string[] = [state.effectiveSystemInstruction];
|
||||||
|
|
||||||
parts.push(`# Story Title: ${currentStory.title}`);
|
// Chat-only worlds: system instruction + user info, no story scaffolding
|
||||||
|
if (currentWorld?.chatOnly) {
|
||||||
|
const userSection = formatUserSection(state);
|
||||||
|
if (userSection) {
|
||||||
|
parts.push(userSection);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts.push(`# Story Title: ${currentStory.title}`);
|
||||||
|
|
||||||
const loreSection = formatLoreMarkdown(state);
|
const loreSection = formatLoreMarkdown(state);
|
||||||
if (loreSection) {
|
if (loreSection) {
|
||||||
parts.push(loreSection);
|
parts.push(loreSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
const charactersSection = formatCharactersMarkdown(state);
|
const charactersSection = formatCharactersMarkdown(state);
|
||||||
if (charactersSection) {
|
if (charactersSection) {
|
||||||
parts.push(charactersSection);
|
parts.push(charactersSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationsSection = formatLocationsMarkdown(state);
|
const locationsSection = formatLocationsMarkdown(state);
|
||||||
if (locationsSection) {
|
if (locationsSection) {
|
||||||
parts.push(locationsSection);
|
parts.push(locationsSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStory.scratchpad) {
|
if (currentStory.scratchpad) {
|
||||||
parts.push(`## Scratchpad`, currentStory.scratchpad);
|
parts.push(`## Scratchpad`, currentStory.scratchpad);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStory.text && storyTokenBudget > 0) {
|
if (currentStory.text && storyTokenBudget > 0) {
|
||||||
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
||||||
if (storyText) {
|
if (storyText) {
|
||||||
parts.push(`## Story Text:`, storyText);
|
parts.push(`## Story Text:`, storyText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,15 +366,16 @@ namespace Prompt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const charName = currentWorld?.title || 'Assistant';
|
||||||
|
const userName = state.userName || 'User';
|
||||||
|
const applyVars = (msgs: ChatMessage[]) =>
|
||||||
|
msgs.map(m => ({ ...m, content: substituteVars(state, m.content) }));
|
||||||
|
|
||||||
// Chat-only world: format messages with name prefixes
|
// Chat-only world: format messages with name prefixes
|
||||||
if (currentWorld?.chatOnly) {
|
if (currentWorld?.chatOnly) {
|
||||||
const charName = currentWorld.title ?? 'Assistant';
|
|
||||||
const formattedMessages: ChatMessage[] = messages.map(msg => {
|
const formattedMessages: ChatMessage[] = messages.map(msg => {
|
||||||
const prefix = msg.role === 'user' ? 'User' : charName;
|
const prefix = msg.role === 'user' ? userName : charName;
|
||||||
return {
|
return { ...msg, content: `${prefix}: ${msg.content}` };
|
||||||
...msg,
|
|
||||||
content: `${prefix}: ${msg.content}`,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepend system message
|
// Prepend system message
|
||||||
|
|
@ -365,7 +387,7 @@ namespace Prompt {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: formattedMessages,
|
messages: applyVars(formattedMessages),
|
||||||
enable_thinking: false,
|
enable_thinking: false,
|
||||||
max_tokens: model.max_length ? model.max_length : 2048,
|
max_tokens: model.max_length ? model.max_length : 2048,
|
||||||
add_generation_prompt: true,
|
add_generation_prompt: true,
|
||||||
|
|
@ -391,7 +413,7 @@ namespace Prompt {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages,
|
messages: applyVars(messages),
|
||||||
tools: Tools.getTools(),
|
tools: Tools.getTools(),
|
||||||
banned_tokens: state.bannedTokens,
|
banned_tokens: state.bannedTokens,
|
||||||
enable_thinking: enableThinking,
|
enable_thinking: enableThinking,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue