fix more stuff and improve styling immensely

This commit is contained in:
2026-06-09 12:01:42 +02:00
parent f8d1c5370d
commit 58df321d8d
15 changed files with 1594 additions and 70 deletions
+11 -2
View File
@@ -314,7 +314,9 @@ export function createPersonasRouter({repositories, chatService, modelService, c
scenarioPrompt,
sceneSummary
});
if (!opener) return res.status(500).json({error: "Failed to generate the character opener"});
if (!opener || chatService.containsUserVoiceInRoleplayOpener(opener, userPersona)) {
return res.json({chat, existing: false, generatedInitialMessage: false, degraded: true});
}
chatService.insertChatMessage(chat.id, "bot", opener, {
retryVariants: [opener],
retryActiveIndex: 0,
@@ -325,7 +327,14 @@ export function createPersonasRouter({repositories, chatService, modelService, c
return res.json({chat: repositories.getChat(chat.id, user), existing: false, generatedInitialMessage: true, opener});
} catch (error) {
const mapped = modelService.mapError(error);
return res.status(mapped.status).json(mapped.body);
return res.json({
chat,
existing: false,
generatedInitialMessage: false,
degraded: true,
modelError: mapped.body.error,
modelErrorCode: mapped.body.code
});
}
});
+3 -3
View File
@@ -278,6 +278,9 @@ export function createChatService(repositories, modelService, config) {
: await modelService.streamReply(model, messagesPayload, onChunk);
if (!fullReply) return {error: "Failed to generate a valid character reply", status: 500};
if (activePersona && containsUserVoiceInRoleplayOpener(fullReply, activeUserPersona)) {
return {error: "Failed to generate a valid character reply", status: 500};
}
insertChatMessage(chatId, "bot", fullReply, {
retryVariants: [fullReply],
retryActiveIndex: 0,
@@ -285,9 +288,6 @@ export function createChatService(repositories, modelService, config) {
retryPromptMessageId: null
}, model);
repositories.touchChat(chatId, user);
if (activePersona && containsUserVoiceInRoleplayOpener(fullReply, activeUserPersona)) {
return {error: "Failed to generate a valid character reply", status: 500};
}
return {reply: fullReply};
},
async retryMessage({user, chatId, selectedModel, targetMessage, retryStyle = null}) {
+31 -25
View File
@@ -92,18 +92,20 @@ export function createModelService(config, overrides = {}) {
state.activeRequests += 1;
try {
const response = await withTimeout(config.modelRequestTimeoutMs, (signal) => fetchImpl(`${config.modelApiBaseUrl}/chat`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({model, messages: messagesPayload, stream: true}),
signal
}));
const fullReply = await withTimeout(config.modelRequestTimeoutMs, async (signal) => {
const response = await fetchImpl(`${config.modelApiBaseUrl}/chat`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({model, messages: messagesPayload, stream: true}),
signal
});
if (!response.ok) {
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
}
if (!response.ok) {
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
}
const fullReply = await readStreamingReply(response);
return readStreamingReply(response);
});
lastHealth = {ok: true, checkedAt: new Date().toISOString(), error: null};
return fullReply;
} catch (error) {
@@ -124,18 +126,20 @@ export function createModelService(config, overrides = {}) {
state.activeRequests += 1;
try {
const response = await withTimeout(config.modelRequestTimeoutMs, (signal) => fetchImpl(`${config.modelApiBaseUrl}/chat`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({model, messages: messagesPayload, stream: true}),
signal
}));
const fullReply = await withTimeout(config.modelRequestTimeoutMs, async (signal) => {
const response = await fetchImpl(`${config.modelApiBaseUrl}/chat`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({model, messages: messagesPayload, stream: true}),
signal
});
if (!response.ok) {
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
}
if (!response.ok) {
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
}
const fullReply = await readStreamingReply(response, onChunk);
return readStreamingReply(response, onChunk);
});
lastHealth = {ok: true, checkedAt: new Date().toISOString(), error: null};
return fullReply;
} catch (error) {
@@ -157,11 +161,13 @@ export function createModelService(config, overrides = {}) {
}
try {
const response = await withTimeout(config.modelListTimeoutMs, (signal) => fetchImpl(`${config.modelApiBaseUrl}/tags`, {signal}));
if (!response.ok) {
throw new ModelServiceError("MODEL_LIST_FAILED", "Failed to load models.", 502);
}
modelsCache = await response.json();
modelsCache = await withTimeout(config.modelListTimeoutMs, async (signal) => {
const response = await fetchImpl(`${config.modelApiBaseUrl}/tags`, {signal});
if (!response.ok) {
throw new ModelServiceError("MODEL_LIST_FAILED", "Failed to load models.", 502);
}
return response.json();
});
modelsCacheAt = Date.now();
lastHealth = {ok: true, checkedAt: new Date().toISOString(), error: null};
return modelsCache;
+28 -11
View File
@@ -7,6 +7,15 @@ const PERSONA_FIELD_LIMITS = {
exampleDialogues: 12000
};
const PERSONA_PROMPT_FIELD_LIMITS = {
appearance: 1200,
background: 1400,
details: 1400,
exampleDialogues: 1800,
summaryField: 360,
scenarioPrompt: 700
};
const blockedPersonaPatterns = [
/\b(child|children|kid|kids|minor|underage|under-aged|teenager|young girl|young boy|loli|shota)\b/i,
/\b(nazi|hitler|third reich|kkk|ku klux klan|white power|nigger|nigga)\b/i,
@@ -59,13 +68,19 @@ export function clampText(value, limit = 260) {
return `${normalized.slice(0, limit - 3).trimEnd()}...`;
}
function clampPromptText(value, limit) {
const normalized = String(value || "").trim();
if (normalized.length <= limit) return normalized;
return `${normalized.slice(0, limit - 3).trimEnd()}...`;
}
export function summarizePersona(persona) {
if (!persona) return "";
return [
persona.name ? `Name: ${persona.name}` : null,
persona.pronouns ? `Pronouns: ${persona.pronouns}` : null,
persona.background ? `Background: ${persona.background}` : null,
persona.details ? `Traits: ${persona.details}` : null
persona.background ? `Background: ${clampText(persona.background, PERSONA_PROMPT_FIELD_LIMITS.summaryField)}` : null,
persona.details ? `Traits: ${clampText(persona.details, PERSONA_PROMPT_FIELD_LIMITS.summaryField)}` : null
].filter(Boolean).join(" | ");
}
@@ -85,13 +100,13 @@ export function buildPersonaPrompt(persona) {
];
if (persona.name) lines.push(`Name: ${persona.name}`);
if (persona.pronouns) lines.push(`Pronouns: ${persona.pronouns}`);
if (persona.appearance) lines.push(`Appearance: ${persona.appearance}`);
if (persona.background) lines.push(`Background: ${persona.background}`);
if (persona.details) lines.push(`Additional Traits: ${persona.details}`);
if (persona.appearance) lines.push(`Appearance: ${clampPromptText(persona.appearance, PERSONA_PROMPT_FIELD_LIMITS.appearance)}`);
if (persona.background) lines.push(`Background: ${clampPromptText(persona.background, PERSONA_PROMPT_FIELD_LIMITS.background)}`);
if (persona.details) lines.push(`Additional Traits: ${clampPromptText(persona.details, PERSONA_PROMPT_FIELD_LIMITS.details)}`);
if (persona.example_dialogues) {
lines.push("", "EXAMPLE DIALOGUES:");
lines.push("Use these examples to mirror tone, cadence, and phrasing without rigidly repeating them.");
lines.push(persona.example_dialogues);
lines.push(clampPromptText(persona.example_dialogues, PERSONA_PROMPT_FIELD_LIMITS.exampleDialogues));
}
return lines.join("\n");
}
@@ -110,9 +125,9 @@ export function buildUserPersonaPrompt(persona) {
];
if (persona.name) lines.push(`Name: ${persona.name}`);
if (persona.pronouns) lines.push(`Pronouns: ${persona.pronouns}`);
if (persona.appearance) lines.push(`Appearance: ${persona.appearance}`);
if (persona.background) lines.push(`Background: ${persona.background}`);
if (persona.details) lines.push(`Additional Traits: ${persona.details}`);
if (persona.appearance) lines.push(`Appearance: ${clampPromptText(persona.appearance, PERSONA_PROMPT_FIELD_LIMITS.appearance)}`);
if (persona.background) lines.push(`Background: ${clampPromptText(persona.background, PERSONA_PROMPT_FIELD_LIMITS.background)}`);
if (persona.details) lines.push(`Additional Traits: ${clampPromptText(persona.details, PERSONA_PROMPT_FIELD_LIMITS.details)}`);
return lines.join("\n");
}
@@ -128,6 +143,7 @@ export function buildRoleplaySceneSummary(assistantPersona, userPersona, scenari
}
export function buildRoleplayDirectionPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary}) {
const scenarioSeed = clampPromptText(scenarioPrompt || "", PERSONA_PROMPT_FIELD_LIMITS.scenarioPrompt);
const lines = [
"ROLEPLAY DIRECTION:",
"Create an immersive one-on-one roleplay between the assistant persona and the user.",
@@ -149,7 +165,7 @@ export function buildRoleplayDirectionPrompt({assistantPersona, userPersona, sce
"",
`SCENE SUMMARY: ${sceneSummary}`
];
if (scenarioPrompt) lines.push(`USER SCENARIO SEED: ${scenarioPrompt}`);
if (scenarioSeed) lines.push(`USER SCENARIO SEED: ${scenarioSeed}`);
if (assistantPersona) lines.push("", `ASSISTANT SNAPSHOT: ${summarizePersona(assistantPersona)}`);
if (userPersona) lines.push(`USER SNAPSHOT: ${summarizePersona(userPersona)}`);
return lines.join("\n");
@@ -158,8 +174,9 @@ export function buildRoleplayDirectionPrompt({assistantPersona, userPersona, sce
export function buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary}) {
const assistantName = assistantPersona?.name || "the assistant";
const userName = userPersona?.name || "the user";
const scenarioSeedText = clampPromptText(scenarioPrompt || "", PERSONA_PROMPT_FIELD_LIMITS.scenarioPrompt);
const sceneSeed = scenarioPrompt
? `Use this scenario seed: ${scenarioPrompt}`
? `Use this scenario seed: ${scenarioSeedText}`
: `Invent a fresh scenario that suits ${assistantName} and how they would realistically meet or confront ${userName}.`;
return [