Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
CI / check (22) (push) Successful in 9m34s
CI / check (24) (push) Successful in 9m34s

This commit is contained in:
2026-10-06 00:32:18 +02:00
parent 1c0f06716e
commit 387c0ac2e7
40 changed files with 2374 additions and 1314 deletions
+19 -16
View File
@@ -27,10 +27,10 @@ import { supportsReasoning } from "../../public/js/modelCapabilities.js";
import {VISION_EVIDENCE_GUIDANCE} from "./visionAnalysis.js";
export function createChatService(repositories, modelService, config, webSearchProvider = new SearXNGSearchProvider(config), attachmentService = null, visionDebugService = null) {
const formatChatMessage = row => {
const formatChatMessage = (row, related = null) => {
const message = formatBaseChatMessage(row);
if (message?.role === "bot") {
const versions = repositories.listMessageVersions(row.id);
const versions = related?.versions ? related.versions.get(row.id) || [] : repositories.listMessageVersions(row.id);
message.versionCount = versions.length || 1;
message.versionIndex = versions.length ? Number(row.retry_active_index || 0) : 0;
if (versions.length) message.responseVersions = versions.map(version => ({
@@ -38,7 +38,7 @@ export function createChatService(repositories, modelService, config, webSearchP
sources: parseSources(version.sources), modelName: version.model_name, createdAt: version.created_at
}));
}
const attachments = row && attachmentService?.list(row.id);
const attachments = row && (related?.attachments ? related.attachments.get(row.id) : attachmentService?.list(row.id));
return message && attachments?.length ? { ...message, attachments } : message;
};
const chatGenerationQueues = new Map();
@@ -198,6 +198,9 @@ export function createChatService(repositories, modelService, config, webSearchP
const latestSummarized = summarizable[summarizable.length - 1];
if (!latestSummarized || !summary) return;
const current = repositories.getChat(chatId, user);
// A manual correction or branch switch during generation wins over the stale summary.
if (!current || current.context_summary !== session.context_summary || current.context_summary_message_id !== session.context_summary_message_id) return;
repositories.updateChatContextSummary(chatId, user, clampSummary(summary), latestSummarized.id);
};
@@ -573,18 +576,17 @@ export function createChatService(repositories, modelService, config, webSearchP
}
});
const listActiveBranchMessages = (chatId) => repositories.listActiveBranchMessages(chatId).map((row) => {
const message = formatChatMessage(row);
const siblings = repositories.listSiblingChatMessages(chatId, row.role, row.parent_message_id);
const siblingIndex = siblings.findIndex((sibling) => sibling.id === row.id);
return {
...message,
siblingIndex,
siblingCount: siblings.length,
previousSiblingId: siblingIndex > 0 ? siblings[siblingIndex - 1].id : null,
nextSiblingId: siblingIndex >= 0 && siblingIndex < siblings.length - 1 ? siblings[siblingIndex + 1].id : null
};
});
const formatHistory = (chatId, rows) => {
const metadata = repositories.getHistoryMetadata(chatId, rows.map(row => row.id));
return rows.map(row => ({...formatChatMessage(row, metadata), ...metadata.siblings.get(row.id)}));
};
const listActiveBranchMessages = chatId => formatHistory(chatId, repositories.listActiveBranchMessages(chatId));
const getHistoryPage = (chatId, options) => {
const result = repositories.getActiveBranchPage(chatId, options);
if (result.error) return result;
const {rows, ...details} = result;
return {...details, messages: formatHistory(chatId, rows)};
};
const cloneChatThroughMessage = ({ user, sourceChat, messageId = null }) => {
if (repositories.countChats(user) >= config.chatLimit) {
@@ -779,9 +781,10 @@ export function createChatService(repositories, modelService, config, webSearchP
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 getHistoryPage(chatId);
},
listActiveBranchMessages,
getHistoryPage,
stopGeneration({ user, chatId }) {
if (!repositories.getChat(chatId, user)) return { error: "Chat not found", status: 404 };
const controller = activeGenerations.get(chatId);
+7
View File
@@ -0,0 +1,7 @@
// bcrypt uses the first 72 UTF-8 bytes. Apply this only when creating a new hash:
// existing credentials must remain usable for login and current-password checks.
export function newPasswordError(password) {
return Buffer.byteLength(password, "utf8") > 72
? "Password must be at most 72 UTF-8 bytes (some characters use multiple bytes)"
: null;
}