moved js files in public into js and css into css folder

This commit is contained in:
2026-09-22 21:55:02 +02:00
parent 99a3277e64
commit 08f5f3f8d9
40 changed files with 1772 additions and 1555 deletions
+96 -96
View File
@@ -18,8 +18,8 @@ import {
retryStylePrompts
} from "./chatMessageHelpers.js";
import {SearXNGSearchProvider, WebSearchError, buildWebSearchContext, retrieveWebSearch} from "./webSearchService.js";
import {supportsReasoning} from "../../public/app/modelCapabilities.js";
import { SearXNGSearchProvider, WebSearchError, buildWebSearchContext, retrieveWebSearch } from "./webSearchService.js";
import { supportsReasoning } from "../../public/js/modelCapabilities.js";
export function createChatService(repositories, modelService, config, webSearchProvider = new SearXNGSearchProvider(config), attachmentService = null) {
const formatChatMessage = row => {
@@ -34,7 +34,7 @@ export function createChatService(repositories, modelService, config, webSearchP
}));
}
const attachments = row && attachmentService?.list(row.id);
return message && attachments?.length ? {...message, attachments} : message;
return message && attachments?.length ? { ...message, attachments } : message;
};
const chatGenerationQueues = new Map();
const summaryQueues = new Map();
@@ -74,7 +74,7 @@ export function createChatService(repositories, modelService, config, webSearchP
const controller = new AbortController();
const abortFromCaller = () => controller.abort(externalSignal?.reason);
if (externalSignal?.aborted) abortFromCaller();
else externalSignal?.addEventListener("abort", abortFromCaller, {once: true});
else externalSignal?.addEventListener("abort", abortFromCaller, { once: true });
activeGenerations.set(chatId, controller);
try {
return await operation(controller.signal);
@@ -93,7 +93,7 @@ export function createChatService(repositories, modelService, config, webSearchP
}
});
const persistChatMessageRetryState = ({chatId, messageId, content, retryVariants, retryActiveIndex, retryRetriesUsed, retryPromptMessageId}) => {
const persistChatMessageRetryState = ({ chatId, messageId, content, retryVariants, retryActiveIndex, retryRetriesUsed, retryPromptMessageId }) => {
const versioned = repositories.listMessageVersions(messageId).length > 0;
if (versioned) repositories.saveMessageVersion(chatId, messageId, null, retryActiveIndex);
repositories.updateChatMessageRetryState(chatId, messageId, {
@@ -118,7 +118,7 @@ export function createChatService(repositories, modelService, config, webSearchP
return `${normalized.slice(0, Math.max(0, config.chatSummaryMaxChars - 3)).trimEnd()}...`;
};
const maybeUpdateContextSummary = async ({user, chatId, selectedModel}) => {
const maybeUpdateContextSummary = async ({ user, chatId, selectedModel }) => {
if (config.chatSummaryUpdateEveryMessages <= 0) return;
const session = repositories.getChat(chatId, user);
if (!session) return;
@@ -159,19 +159,19 @@ export function createChatService(repositories, modelService, config, webSearchP
repositories.updateChatContextSummary(chatId, user, clampSummary(summary), latestSummarized.id);
};
const updateContextSummaryWithoutBreakingReply = async ({user, chatId, selectedModel}) => {
const updateContextSummaryWithoutBreakingReply = async ({ user, chatId, selectedModel }) => {
try {
await maybeUpdateContextSummary({user, chatId, selectedModel});
await maybeUpdateContextSummary({ user, chatId, selectedModel });
} catch (error) {
console.warn("CHAT SUMMARY UPDATE FAILED", chatId, error?.message || error);
}
};
const scheduleContextSummaryUpdate = ({user, chatId, selectedModel}) => {
const scheduleContextSummaryUpdate = ({ user, chatId, selectedModel }) => {
const previous = summaryQueues.get(chatId) || Promise.resolve();
const current = previous
.catch(() => undefined)
.then(() => updateContextSummaryWithoutBreakingReply({user, chatId, selectedModel}));
.then(() => updateContextSummaryWithoutBreakingReply({ user, chatId, selectedModel }));
summaryQueues.set(chatId, current);
void current.finally(() => {
if (summaryQueues.get(chatId) === current) summaryQueues.delete(chatId);
@@ -187,17 +187,17 @@ export function createChatService(repositories, modelService, config, webSearchP
return repositories.createChat(user, persona.name, persona.id, userPersonaId, scenarioPrompt, scenarioSummary);
};
const generateRoleplayOpener = async ({selectedModel = "qwen3:14b", assistantPersona, userPersona, scenarioPrompt, sceneSummary, onThinking = null, onChunk = null, signal = null, generation = {}, reasoning = false}) => {
const generateRoleplayOpener = async ({ selectedModel = "qwen3:14b", assistantPersona, userPersona, scenarioPrompt, sceneSummary, onThinking = null, onChunk = null, signal = null, generation = {}, reasoning = false }) => {
const baseMessages = [
{role: "system", content: buildPersonaPrompt(assistantPersona)},
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
{role: "system", content: buildRoleplayDirectionPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})},
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})}
{ role: "system", content: buildPersonaPrompt(assistantPersona) },
...(userPersona ? [{ role: "system", content: buildUserPersonaPrompt(userPersona) }] : []),
{ role: "system", content: buildRoleplayDirectionPrompt({ assistantPersona, userPersona, scenarioPrompt, sceneSummary }) },
{ role: "user", content: buildRoleplayOpenerPrompt({ assistantPersona, userPersona, scenarioPrompt, sceneSummary }) }
];
const generate = messages => onChunk
? modelService.streamReply(selectedModel, messages, onChunk, {onThinking, signal, generation, reasoning})
: modelService.generateReply(selectedModel, messages, {onThinking, signal, generation, reasoning});
? modelService.streamReply(selectedModel, messages, onChunk, { onThinking, signal, generation, reasoning })
: modelService.generateReply(selectedModel, messages, { onThinking, signal, generation, reasoning });
let opener = await generate(baseMessages);
if (!containsUserVoiceInRoleplayOpener(opener, userPersona)) {
return opener;
@@ -207,7 +207,7 @@ export function createChatService(repositories, modelService, config, webSearchP
onChunk?.("", "");
return generate([
...baseMessages,
{role: "assistant", content: opener || ""},
{ role: "assistant", content: opener || "" },
{
role: "user",
content: "Rewrite this opening. Hard rule: only the assistant character may speak or act. Never write or imply user speech, thoughts, feelings, reactions, decisions, actions, or labels like 'User:'/'You:'. Stop before the user's reply."
@@ -215,10 +215,10 @@ export function createChatService(repositories, modelService, config, webSearchP
]);
};
const generateValidatedRoleplayReply = async ({selectedModel, messagesPayload, userPersona, signal = null, generation = {}, onThinking = null, onChunk = null, reasoning = false}) => {
const generateValidatedRoleplayReply = async ({ selectedModel, messagesPayload, userPersona, signal = null, generation = {}, onThinking = null, onChunk = null, reasoning = false }) => {
const generate = (messages) => onChunk
? modelService.streamReply(selectedModel, messages, onChunk, {signal, generation, onThinking, reasoning})
: modelService.generateReply(selectedModel, messages, {signal, generation, onThinking, reasoning});
? modelService.streamReply(selectedModel, messages, onChunk, { signal, generation, onThinking, reasoning })
: modelService.generateReply(selectedModel, messages, { signal, generation, onThinking, reasoning });
let reply = await generate(messagesPayload);
if (reply && !containsUserVoiceInRoleplayOpener(reply, userPersona)) {
return reply;
@@ -228,7 +228,7 @@ export function createChatService(repositories, modelService, config, webSearchP
onChunk?.("", "");
reply = await generate([
...messagesPayload,
{role: "assistant", content: reply || ""},
{ role: "assistant", content: reply || "" },
{
role: "user",
content: "Rewrite your last reply. Hard rule: only the assistant character may speak or act. Never write or imply user speech, thoughts, feelings, reactions, decisions, or actions. Stop before the user's reply."
@@ -243,7 +243,7 @@ export function createChatService(repositories, modelService, config, webSearchP
const appendRetryStyleInstruction = (messagesPayload, retryStyle) => {
if (!retryStyle || !retryStylePrompts[retryStyle]) return messagesPayload;
return [...messagesPayload, {role: "user", content: retryStylePrompts[retryStyle]}];
return [...messagesPayload, { role: "user", content: retryStylePrompts[retryStyle] }];
};
const getUserPersonaForSession = (chatId, user, session, activePersona) => {
@@ -252,7 +252,7 @@ export function createChatService(repositories, modelService, config, webSearchP
return repositories.getActiveUserPersona(user);
};
const buildConversationContext = ({user, chatId, session = null, message, upToMessageId = null, sources = null}) => {
const buildConversationContext = ({ user, chatId, session = null, message, upToMessageId = null, sources = null }) => {
const activeSession = session || repositories.getChat(chatId, user);
if (!activeSession) return null;
const activePersona = repositories.getAssistantPersonaForChat(chatId, user);
@@ -260,17 +260,17 @@ export function createChatService(repositories, modelService, config, webSearchP
const history = upToMessageId == null
? repositories.getRecentChatMessages(chatId).reverse()
: repositories.getRecentChatMessagesUpToId(chatId, upToMessageId).reverse();
const conversation = message == null ? history : [...history, {role: "user", content: message}];
const conversation = message == null ? history : [...history, { role: "user", content: message }];
const messagesPayload = [];
// Result material remains a separate untrusted user-context message. The
// grounding rule is added after mutable persona/chat prompts so it remains
// the final trusted instruction governing the supplied references.
const webSearchContext = sources === null ? null : buildWebSearchContext(sources, now());
if (activeSession.system_instruction) {
messagesPayload.push({role: "system", content: `CHAT-SPECIFIC INSTRUCTIONS:\n${activeSession.system_instruction}`});
messagesPayload.push({ role: "system", content: `CHAT-SPECIFIC INSTRUCTIONS:\n${activeSession.system_instruction}` });
}
if (activePersona) messagesPayload.push({role: "system", content: buildPersonaPrompt(activePersona)});
if (activeUserPersona) messagesPayload.push({role: "system", content: buildUserPersonaPrompt(activeUserPersona)});
if (activePersona) messagesPayload.push({ role: "system", content: buildPersonaPrompt(activePersona) });
if (activeUserPersona) messagesPayload.push({ role: "system", content: buildUserPersonaPrompt(activeUserPersona) });
if (activePersona && activeSession.scenario_summary) {
messagesPayload.push({
role: "system",
@@ -283,12 +283,12 @@ export function createChatService(repositories, modelService, config, webSearchP
});
}
const contextSummaryPrompt = buildContextSummaryPrompt(upToMessageId == null ? activeSession.context_summary : null);
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
if (contextSummaryPrompt) messagesPayload.push({ role: "system", content: contextSummaryPrompt });
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
if (memoryPrompt) messagesPayload.push({ role: "system", content: memoryPrompt });
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
if (activePersona) messagesPayload.push({role: "system", content: buildRoleplayReplyGuardPrompt()});
if (personaStatePrompt) messagesPayload.push({ role: "system", content: personaStatePrompt });
if (activePersona) messagesPayload.push({ role: "system", content: buildRoleplayReplyGuardPrompt() });
const conversationMessages = conversation.map((entry) => ({
role: entry.role === "bot" ? "assistant" : "user",
content: entry.content
@@ -299,17 +299,17 @@ export function createChatService(repositories, modelService, config, webSearchP
if (webSearchContext) messagesPayload.push(webSearchContext[0]);
if (webSearchContext?.[1]) messagesPayload.push(webSearchContext[1]);
if (conversationMessages.length) messagesPayload.push(conversationMessages.at(-1));
return {session: activeSession, activePersona, activeUserPersona, messagesPayload};
return { session: activeSession, activePersona, activeUserPersona, messagesPayload };
};
const buildRetryPayload = async ({chatId, user, session, selectedModel, targetMessage, retryStyle = null, signal = null, generation = {}, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null}) => {
const buildRetryPayload = async ({ chatId, user, session, selectedModel, targetMessage, retryStyle = null, signal = null, generation = {}, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null }) => {
let thinking = "";
const captureThinking = (chunk, fullThinking) => {
if (!reasoning) return;
thinking = fullThinking;
onThinking?.(chunk, fullThinking);
};
onStatus?.({status: "accepted"});
onStatus?.({ status: "accepted" });
const parentMessage = targetMessage.parent_message_id
? repositories.getChatMessage(chatId, targetMessage.parent_message_id)
: null;
@@ -321,7 +321,7 @@ export function createChatService(repositories, modelService, config, webSearchP
if (!promptMessage) {
if (!activePersona || !session.scenario_summary) {
return {error: "No previous user prompt found for retry"};
return { error: "No previous user prompt found for retry" };
}
const opener = await generateRoleplayOpener({
onThinking: captureThinking, onChunk, signal, generation, reasoning,
@@ -332,13 +332,13 @@ export function createChatService(repositories, modelService, config, webSearchP
sceneSummary: session.scenario_summary
});
if (!opener || containsUserVoiceInRoleplayOpener(opener, activeUserPersona)) {
return {error: "Failed to regenerate message"};
return { error: "Failed to regenerate message" };
}
return {fullReply: opener, thinking, promptMessageId: null};
return { fullReply: opener, thinking, promptMessageId: null };
}
const imageHistory = repositories.getRecentChatMessagesUpToId(chatId, promptMessage.id).reverse();
if (attachmentService?.hasImages(imageHistory)) onStatus?.({status: "analyzing"});
if (attachmentService?.hasImages(imageHistory)) onStatus?.({ status: "analyzing" });
await attachmentService?.analyzeContext(imageHistory, promptMessage.content, signal);
let sources = null;
let searchState = "skipped";
@@ -346,13 +346,13 @@ export function createChatService(repositories, modelService, config, webSearchP
const search = await retrieveWebSearch(promptMessage.content, webSearchProvider, {
signal,
referenceText: attachmentService?.searchContext(imageHistory, promptMessage.content) || "",
onSearching: () => onStatus?.({status: "searching"})
onSearching: () => onStatus?.({ status: "searching" })
});
sources = search.sources;
searchState = search.state;
}
onStatus?.({status: "generating", searchState, sources});
const {messagesPayload} = buildConversationContext({
onStatus?.({ status: "generating", searchState, sources });
const { messagesPayload } = buildConversationContext({
user,
chatId,
session,
@@ -363,20 +363,20 @@ export function createChatService(repositories, modelService, config, webSearchP
const styledPayload = appendRetryStyleInstruction(messagesPayload, retryStyle);
const fullReply = activePersona
? await generateValidatedRoleplayReply({selectedModel, messagesPayload: styledPayload, userPersona: activeUserPersona, signal, generation, onThinking: captureThinking, onChunk, reasoning})
? await generateValidatedRoleplayReply({ selectedModel, messagesPayload: styledPayload, userPersona: activeUserPersona, signal, generation, onThinking: captureThinking, onChunk, reasoning })
: onChunk
? await modelService.streamReply(selectedModel, styledPayload, onChunk, {signal, generation, onThinking: captureThinking, reasoning})
: await modelService.generateReply(selectedModel, styledPayload, {signal, generation, onThinking: captureThinking, reasoning});
? await modelService.streamReply(selectedModel, styledPayload, onChunk, { signal, generation, onThinking: captureThinking, reasoning })
: await modelService.generateReply(selectedModel, styledPayload, { signal, generation, onThinking: captureThinking, reasoning });
if (!fullReply) return {error: "Failed to regenerate message"};
return {fullReply, thinking, sources, searchState, promptMessageId: promptMessage.id};
if (!fullReply) return { error: "Failed to regenerate message" };
return { fullReply, thinking, sources, searchState, promptMessageId: promptMessage.id };
};
const generateChatTurn = ({user, chatId, message, model, attachments = [], reuseChatId = null, webSearch = false, reasoning = false, onStatus = null, onChunk = null, onThinking = null, signal = null, parentMessageId = undefined}) =>
const generateChatTurn = ({ user, chatId, message, model, attachments = [], reuseChatId = null, webSearch = false, reasoning = false, onStatus = null, onChunk = null, onThinking = null, signal = null, parentMessageId = undefined }) =>
withChatGenerationLock(chatId, async () => {
await (summaryQueues.get(chatId) || Promise.resolve());
const session = repositories.getChat(chatId, user);
if (!session) return {error: "Chat not found", status: 404};
if (!session) return { error: "Chat not found", status: 404 };
if (parentMessageId !== undefined) {
repositories.updateChatContextSummary(chatId, user, null, 0);
session.context_summary = null;
@@ -386,9 +386,9 @@ export function createChatService(repositories, modelService, config, webSearchP
? session.active_leaf_message_id ?? repositories.getLatestChatMessage(chatId)?.id ?? null
: parentMessageId;
if (effectiveParentId != null && !repositories.getChatMessage(chatId, effectiveParentId)) {
return {error: "Branch point not found", status: 404};
return { error: "Branch point not found", status: 404 };
}
const {model: effectiveModel, generation} = getGenerationSettings(session, model);
const { model: effectiveModel, generation } = getGenerationSettings(session, model);
const useReasoning = reasoning === true && supportsReasoning(effectiveModel);
signal?.throwIfAborted();
const insert = () => repositories.insertBranchChatMessage(chatId, "user", message, null, null, "pending", null, effectiveParentId);
@@ -406,17 +406,17 @@ export function createChatService(repositories, modelService, config, webSearchP
let payload;
const fullReply = await withActiveGeneration(chatId, signal, async (activeSignal) => {
activeSignal.throwIfAborted();
onStatus?.({status: "accepted"});
onStatus?.({ status: "accepted" });
const imageHistory = repositories.getRecentChatMessagesUpToId(chatId, userMessageId).reverse();
if (attachmentService?.hasImages(imageHistory)) {
onStatus?.({status: "analyzing"});
onStatus?.({ status: "analyzing" });
await attachmentService.analyzeContext(imageHistory, message, activeSignal);
}
if (webSearch) {
const search = await retrieveWebSearch(message, webSearchProvider, {
referenceText: attachmentService?.searchContext(imageHistory, message) || "",
signal: activeSignal,
onSearching: () => onStatus?.({status: "searching"})
onSearching: () => onStatus?.({ status: "searching" })
});
sources = search.sources;
searchState = search.state;
@@ -436,8 +436,8 @@ export function createChatService(repositories, modelService, config, webSearchP
}));
}
}
onStatus?.({status: "generating", searchState, ...(sources !== null ? {sourceCount: sources.length} : {})});
payload = buildConversationContext({user, chatId, session, message: null, sources});
onStatus?.({ status: "generating", searchState, ...(sources !== null ? { sourceCount: sources.length } : {}) });
payload = buildConversationContext({ user, chatId, session, message: null, sources });
return payload.activePersona
? generateValidatedRoleplayReply({
selectedModel: effectiveModel,
@@ -450,13 +450,13 @@ export function createChatService(repositories, modelService, config, webSearchP
reasoning: useReasoning
})
: onChunk
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, {signal: activeSignal, generation, onThinking: captureThinking, reasoning: useReasoning})
: modelService.generateReply(effectiveModel, payload.messagesPayload, {signal: activeSignal, generation, onThinking: captureThinking, reasoning: useReasoning});
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, { signal: activeSignal, generation, onThinking: captureThinking, reasoning: useReasoning })
: modelService.generateReply(effectiveModel, payload.messagesPayload, { signal: activeSignal, generation, onThinking: captureThinking, reasoning: useReasoning });
});
if (!fullReply) {
repositories.failChatTurn(chatId, userMessageId, "Failed to generate a valid character reply", webSearch ? userMessageId : effectiveParentId);
return {error: "Failed to generate a valid character reply", status: 500};
return { error: "Failed to generate a valid character reply", status: 500 };
}
const retryState = {
@@ -467,12 +467,12 @@ export function createChatService(repositories, modelService, config, webSearchP
};
repositories.completeChatTurn(chatId, userMessageId, fullReply, retryState, effectiveModel, sources, thinking);
repositories.touchChat(chatId, user);
scheduleContextSummaryUpdate({user, chatId, selectedModel: effectiveModel});
return {reply: fullReply, userMessageId, searchState, ...(sources ? {sources} : {})};
scheduleContextSummaryUpdate({ user, chatId, selectedModel: effectiveModel });
return { reply: fullReply, userMessageId, searchState, ...(sources ? { sources } : {}) };
} catch (error) {
repositories.failChatTurn(chatId, userMessageId, String(error?.message || "Model request failed").slice(0, 500), webSearch ? userMessageId : effectiveParentId);
repositories.touchChat(chatId, user);
if (error instanceof WebSearchError) return {error: error.message, status: error.status, searchState: "failed"};
if (error instanceof WebSearchError) return { error: error.message, status: error.status, searchState: "failed" };
throw error;
}
});
@@ -490,16 +490,16 @@ export function createChatService(repositories, modelService, config, webSearchP
};
});
const cloneChatThroughMessage = ({user, sourceChat, messageId = null}) => {
const cloneChatThroughMessage = ({ user, sourceChat, messageId = null }) => {
if (repositories.countChats(user) >= config.chatLimit) {
return {error: `Maximum of ${config.chatLimit} chats reached. Please delete an old chat to create a new one.`, status: 400};
return { error: `Maximum of ${config.chatLimit} chats reached. Please delete an old chat to create a new one.`, status: 400 };
}
const sourceMessages = messageId == null
? []
: repositories.listChatBranchThroughMessage(sourceChat.id, messageId);
if (messageId != null && !sourceMessages.some((message) => message.id === messageId)) {
return {error: "Message not found", status: 404};
return { error: "Message not found", status: 404 };
}
const sourceTitle = String(sourceChat.stored_title || sourceChat.title || "Chat").trim();
@@ -510,7 +510,7 @@ export function createChatService(repositories, modelService, config, webSearchP
sourceChat.user_persona_id ?? null,
sourceChat.scenario_prompt ?? null,
sourceChat.scenario_summary ?? null,
{folderName: sourceChat.folder_name ?? null}
{ folderName: sourceChat.folder_name ?? null }
);
try {
@@ -568,37 +568,37 @@ export function createChatService(repositories, modelService, config, webSearchP
containsUserVoiceInRoleplayOpener,
persistChatMessageRetryState,
buildMessagesPayload: buildConversationContext,
async sendChatMessage({user, chatId, message, model, attachments = [], webSearch = false, reasoning = false}) {
return generateChatTurn({user, chatId, message, model, attachments, webSearch, reasoning});
async sendChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning });
},
async streamChatMessage({user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null}) {
return generateChatTurn({user, chatId, message, model, attachments, webSearch, reasoning, onStatus, onChunk, onThinking, signal});
async streamChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, onStatus, onChunk, onThinking, signal });
},
isGenerating: (chatId) => chatGenerationQueues.has(chatId),
isRegeneratingMessage: (chatId, messageId) => regeneratingMessages.get(chatId) === messageId,
async retryMessage({user, chatId, selectedModel, targetMessage, retryStyle = null, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null, signal = null}) {
if (chatGenerationQueues.has(chatId)) return {error: "A response is already being generated", status: 409};
async retryMessage({ user, chatId, selectedModel, targetMessage, retryStyle = null, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null, signal = null }) {
if (chatGenerationQueues.has(chatId)) return { error: "A response is already being generated", status: 409 };
regeneratingMessages.set(chatId, targetMessage.id);
return withChatGenerationLock(chatId, async () => {
await (summaryQueues.get(chatId) || Promise.resolve());
const session = repositories.getChat(chatId, user);
if (!session) return {error: "Chat not found", status: 404};
if (!session) return { error: "Chat not found", status: 404 };
const freshTarget = repositories.getChatMessage(chatId, targetMessage.id);
const existingMessage = formatChatMessage(freshTarget);
if (!existingMessage) return {error: "Message not found", status: 404};
if (existingMessage.role !== "bot") return {error: "Only assistant messages can be regenerated", status: 400};
if (!existingMessage) return { error: "Message not found", status: 404 };
if (existingMessage.role !== "bot") return { error: "Only assistant messages can be regenerated", status: 400 };
if (existingMessage.versionCount - 1 >= maxRetries) {
return {error: `Maximum of ${maxRetries} retries reached`, status: 400};
return { error: `Maximum of ${maxRetries} retries reached`, status: 400 };
}
const {model, generation} = getGenerationSettings(session, selectedModel);
const { model, generation } = getGenerationSettings(session, selectedModel);
return withActiveGeneration(chatId, signal, async (activeSignal) => {
activeSignal.throwIfAborted();
const retryResult = await buildRetryPayload({
chatId, user, session, selectedModel: model, targetMessage: freshTarget, retryStyle,
signal: activeSignal, generation, webSearch, reasoning: reasoning === true && supportsReasoning(model), onChunk, onThinking, onStatus
});
if (retryResult.error) return {error: retryResult.error, status: 400};
if (retryResult.error) return { error: retryResult.error, status: 400 };
activeSignal.throwIfAborted();
repositories.saveMessageVersion(chatId, freshTarget.id, {
content: retryResult.fullReply, thinking: retryResult.thinking,
@@ -607,17 +607,17 @@ export function createChatService(repositories, modelService, config, webSearchP
repositories.updateChatContextSummary(chatId, user, null, 0);
repositories.touchChat(chatId, user);
const message = formatChatMessage(repositories.getChatMessage(chatId, freshTarget.id));
return {message, promptMessageId: retryResult.promptMessageId, retryStyle};
return { message, promptMessageId: retryResult.promptMessageId, retryStyle };
});
}).catch(error => {
if (error instanceof WebSearchError) return {error: error.message, status: error.status, searchState: "failed"};
if (error instanceof WebSearchError) return { error: error.message, status: error.status, searchState: "failed" };
throw error;
}).finally(() => regeneratingMessages.delete(chatId));
},
async editAndResendMessage({user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null}) {
if (!targetMessage || targetMessage.role !== "user") return {error: "Only user messages can be edited and resent", status: 400};
async editAndResendMessage({ user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null }) {
if (!targetMessage || targetMessage.role !== "user") return { error: "Only user messages can be edited and resent", status: 400 };
const sourceChat = repositories.getChat(chatId, user);
if (!sourceChat) return {error: "Chat not found", status: 404};
if (!sourceChat) return { error: "Chat not found", status: 404 };
const cloned = cloneChatThroughMessage({
user,
sourceChat,
@@ -636,7 +636,7 @@ export function createChatService(repositories, modelService, config, webSearchP
onChunk,
signal
});
if (generated.error) return {...generated, chat: cloned.chat};
if (generated.error) return { ...generated, chat: cloned.chat };
return {
...generated,
chat: repositories.getChat(cloned.chat.id, user),
@@ -645,29 +645,29 @@ export function createChatService(repositories, modelService, config, webSearchP
branchedFromMessageId: targetMessage.id
};
},
createBranchedChat({user, chatId, messageId}) {
createBranchedChat({ user, chatId, messageId }) {
const sourceChat = repositories.getChat(chatId, user);
if (!sourceChat) return {error: "Chat not found", status: 404};
if (!repositories.getChatMessage(chatId, messageId)) return {error: "Message not found", status: 404};
return cloneChatThroughMessage({user, sourceChat, messageId});
if (!sourceChat) return { error: "Chat not found", status: 404 };
if (!repositories.getChatMessage(chatId, messageId)) return { error: "Message not found", status: 404 };
return cloneChatThroughMessage({ user, sourceChat, messageId });
},
activateBranch({user, chatId, messageId, includeDescendants = false}) {
activateBranch({ user, chatId, messageId, includeDescendants = false }) {
const session = repositories.getChat(chatId, user);
if (!session) return {error: "Chat not found", status: 404};
if (!session) return { error: "Chat not found", status: 404 };
const message = repositories.getChatMessage(chatId, messageId);
if (!message) return {error: "Message not found", status: 404};
if (!message) return { error: "Message not found", status: 404 };
const leaf = includeDescendants ? repositories.getLatestDescendantMessage(chatId, messageId) : message;
repositories.updateChatActiveLeaf(chatId, user, leaf?.id || messageId);
repositories.updateChatContextSummary(chatId, user, null, 0);
return {messages: listActiveBranchMessages(chatId)};
return { messages: listActiveBranchMessages(chatId) };
},
listActiveBranchMessages,
stopGeneration({user, chatId}) {
if (!repositories.getChat(chatId, user)) return {error: "Chat not found", status: 404};
stopGeneration({ user, chatId }) {
if (!repositories.getChat(chatId, user)) return { error: "Chat not found", status: 404 };
const controller = activeGenerations.get(chatId);
if (!controller) return {stopped: false};
if (!controller) return { stopped: false };
controller.abort();
return {stopped: true};
return { stopped: true };
}
};
}