moved js files in public into js and css into css folder
This commit is contained in:
+96
-96
@@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user