User config
This commit is contained in:
parent
6f100cac97
commit
371f84571a
|
|
@ -182,6 +182,24 @@
|
|||
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 {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ export const ChatPanel = () => {
|
|||
|
||||
<div
|
||||
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 && (
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import styles from "../assets/settings-modal.module.css";
|
|||
import { BannedTokensSettings } from "./settings/banned-tokens";
|
||||
import { ConnectionSettings } from "./settings/connection";
|
||||
import { SystemInstructionSettings } from "./settings/system-instruction";
|
||||
import { UserSettings } from "./settings/user";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type Tab = "banned-tokens" | "system-instruction" | "connection";
|
||||
type Tab = "banned-tokens" | "system-instruction" | "connection" | "user";
|
||||
|
||||
export const SettingsModal = ({ onClose }: Props) => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("connection");
|
||||
|
|
@ -32,6 +33,12 @@ export const SettingsModal = ({ onClose }: Props) => {
|
|||
>
|
||||
Connection
|
||||
</button>
|
||||
<button
|
||||
class={clsx(styles.menuItem, activeTab === "user" && styles.active)}
|
||||
onClick={() => setActiveTab("user")}
|
||||
>
|
||||
User
|
||||
</button>
|
||||
<button
|
||||
class={clsx(styles.menuItem, activeTab === "banned-tokens" && styles.active)}
|
||||
onClick={() => setActiveTab("banned-tokens")}
|
||||
|
|
@ -46,7 +53,8 @@ export const SettingsModal = ({ onClose }: Props) => {
|
|||
</button>
|
||||
</nav>
|
||||
<div class={styles.content}>
|
||||
{activeTab === "banned-tokens" && <BannedTokensSettings onClose={onClose} />}
|
||||
{activeTab === "user" && <UserSettings />}
|
||||
{activeTab === "banned-tokens" && <BannedTokensSettings />}
|
||||
{activeTab === "system-instruction" && <SystemInstructionSettings />}
|
||||
{activeTab === "connection" && <ConnectionSettings />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ import { X } from "lucide-preact";
|
|||
import styles from "../../assets/settings-modal.module.css";
|
||||
import { useAppState } from "../../contexts/state";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const BannedTokensSettings = ({ onClose }: Props) => {
|
||||
export const BannedTokensSettings = () => {
|
||||
const { bannedTokens, dispatch } = useAppState();
|
||||
const [inputValue, setInputValue] = useInputState();
|
||||
|
||||
|
|
@ -30,7 +26,6 @@ export const BannedTokensSettings = ({ onClose }: Props) => {
|
|||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
else if (e.key === "Escape") onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useBool } from "@common/hooks/useBool";
|
||||
import { useQuery } from "@common/hooks/useAsyncState";
|
||||
import { useInputState } from "@common/hooks/useInputState";
|
||||
import { useUpdate } from "@common/hooks/useUpdate";
|
||||
|
|
@ -12,6 +13,7 @@ export const ConnectionSettings = () => {
|
|||
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
|
||||
const [selectedModel, setSelectedModel] = useInputState(model?.id ?? "");
|
||||
const [update, triggerFetch] = useUpdate();
|
||||
const showPassword = useBool(false);
|
||||
|
||||
const urlRef = useRef(url);
|
||||
const apiKeyRef = useRef(apiKey);
|
||||
|
|
@ -91,17 +93,27 @@ export const ConnectionSettings = () => {
|
|||
</div>
|
||||
<div class={styles.formGroup}>
|
||||
<label class={styles.label}>API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onInput={setApiKey}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="your-api-key"
|
||||
class={styles.input}
|
||||
autocomplete="new-password"
|
||||
name="api-key-random"
|
||||
/>
|
||||
<div class={styles.inputRow}>
|
||||
<input
|
||||
type={showPassword.value ? "text" : "password"}
|
||||
value={apiKey}
|
||||
onInput={setApiKey}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="your-api-key"
|
||||
class={styles.input}
|
||||
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 class={styles.formGroup}>
|
||||
<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;
|
||||
bannedTokens: string[];
|
||||
systemInstruction: string;
|
||||
userName: string;
|
||||
userDescription: string;
|
||||
}
|
||||
|
||||
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -140,6 +142,8 @@ type Action =
|
|||
| { type: 'DELETE_LORE_ENTRY'; worldId: string; storyId: string | null; entryId: string }
|
||||
| { type: 'REORDER_LORE_ENTRIES'; worldId: string; storyId: string | null; entryIds: string[] }
|
||||
// Settings
|
||||
| { type: 'SET_USER_NAME'; userName: string }
|
||||
| { type: 'SET_USER_DESCRIPTION'; userDescription: string }
|
||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||
| { type: 'SET_WORLD_SYSTEM_INSTRUCTION_OVERRIDE'; worldId: string; systemInstructionOverride: string | undefined }
|
||||
| { type: 'SET_CURRENT_TAB'; tab: Tab }
|
||||
|
|
@ -212,6 +216,8 @@ const DEFAULT_STATE: IState = {
|
|||
model: null,
|
||||
enableThinking: false,
|
||||
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.
|
||||
|
||||
Write using markdown to highlight special parts.
|
||||
|
|
@ -382,6 +388,12 @@ function reducer(state: IState, action: Action): IState {
|
|||
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': {
|
||||
return { ...state, systemInstruction: action.systemInstruction };
|
||||
}
|
||||
|
|
@ -570,6 +582,8 @@ export interface AppState {
|
|||
enableThinking: boolean;
|
||||
bannedTokens: string[];
|
||||
systemInstruction: string;
|
||||
userName: string;
|
||||
userDescription: string;
|
||||
/** Effective system instruction: world override if set, otherwise global */
|
||||
effectiveSystemInstruction: string;
|
||||
dispatch: (action: Action) => void;
|
||||
|
|
@ -616,6 +630,8 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
|||
enableThinking: state.enableThinking,
|
||||
bannedTokens: state.bannedTokens ?? [],
|
||||
systemInstruction: state.systemInstruction ?? '',
|
||||
userName: state.userName ?? 'User',
|
||||
userDescription: state.userDescription ?? '',
|
||||
effectiveSystemInstruction: currentWorld?.systemInstructionOverride ?? state.systemInstruction ?? '',
|
||||
dispatch,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ Key Guidelines:
|
|||
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 ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -250,6 +250,16 @@ namespace Prompt {
|
|||
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 {
|
||||
const { mergedLocations } = state;
|
||||
if (!mergedLocations?.length) {
|
||||
|
|
@ -274,44 +284,55 @@ namespace Prompt {
|
|||
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 {
|
||||
const { currentStory, currentWorld } = state;
|
||||
if (!currentStory) {
|
||||
return state.effectiveSystemInstruction;
|
||||
}
|
||||
|
||||
// Chat-only worlds: just the system instruction, no story scaffolding
|
||||
if (currentWorld?.chatOnly) {
|
||||
return 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);
|
||||
if (loreSection) {
|
||||
parts.push(loreSection);
|
||||
}
|
||||
const loreSection = formatLoreMarkdown(state);
|
||||
if (loreSection) {
|
||||
parts.push(loreSection);
|
||||
}
|
||||
|
||||
const charactersSection = formatCharactersMarkdown(state);
|
||||
if (charactersSection) {
|
||||
parts.push(charactersSection);
|
||||
}
|
||||
const charactersSection = formatCharactersMarkdown(state);
|
||||
if (charactersSection) {
|
||||
parts.push(charactersSection);
|
||||
}
|
||||
|
||||
const locationsSection = formatLocationsMarkdown(state);
|
||||
if (locationsSection) {
|
||||
parts.push(locationsSection);
|
||||
}
|
||||
const locationsSection = formatLocationsMarkdown(state);
|
||||
if (locationsSection) {
|
||||
parts.push(locationsSection);
|
||||
}
|
||||
|
||||
if (currentStory.scratchpad) {
|
||||
parts.push(`## Scratchpad`, currentStory.scratchpad);
|
||||
}
|
||||
if (currentStory.scratchpad) {
|
||||
parts.push(`## Scratchpad`, currentStory.scratchpad);
|
||||
}
|
||||
|
||||
if (currentStory.text && storyTokenBudget > 0) {
|
||||
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
||||
if (storyText) {
|
||||
parts.push(`## Story Text:`, storyText);
|
||||
if (currentStory.text && storyTokenBudget > 0) {
|
||||
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
||||
if (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
|
||||
if (currentWorld?.chatOnly) {
|
||||
const charName = currentWorld.title ?? 'Assistant';
|
||||
const formattedMessages: ChatMessage[] = messages.map(msg => {
|
||||
const prefix = msg.role === 'user' ? 'User' : charName;
|
||||
return {
|
||||
...msg,
|
||||
content: `${prefix}: ${msg.content}`,
|
||||
};
|
||||
const prefix = msg.role === 'user' ? userName : charName;
|
||||
return { ...msg, content: `${prefix}: ${msg.content}` };
|
||||
});
|
||||
|
||||
// Prepend system message
|
||||
|
|
@ -365,7 +387,7 @@ namespace Prompt {
|
|||
|
||||
return {
|
||||
model: model.id,
|
||||
messages: formattedMessages,
|
||||
messages: applyVars(formattedMessages),
|
||||
enable_thinking: false,
|
||||
max_tokens: model.max_length ? model.max_length : 2048,
|
||||
add_generation_prompt: true,
|
||||
|
|
@ -391,7 +413,7 @@ namespace Prompt {
|
|||
|
||||
return {
|
||||
model: model.id,
|
||||
messages,
|
||||
messages: applyVars(messages),
|
||||
tools: Tools.getTools(),
|
||||
banned_tokens: state.bannedTokens,
|
||||
enable_thinking: enableThinking,
|
||||
|
|
|
|||
Loading…
Reference in New Issue