import {post, put, del} from "./api.js"; import {regenerateInline} from "./regeneration.js"; export function createMessageActions({conversation, setNotice, promptPopup, confirmPopup, setActiveChat, openCreatedChat, setLoadingState, loadSummary, renderMessages, addMessage, setMessageContent, announceReply, announceResponseStatus, webSearchToggle, reasoningToggle, modelSelect, messagesDiv, msgInput}) { function getMessageIndexById(messageId) { return conversation.currentMessages.findIndex((message) => message.id === messageId); } async function resolveMessageTarget(messageId, fallbackIndex = -1) { if (messageId) { const index = getMessageIndexById(messageId); if (index >= 0) { return { index, message: conversation.currentMessages[index] }; } } if (conversation.activeChatId) { await setActiveChat(conversation.activeChatId); } if (messageId) { const index = getMessageIndexById(messageId); if (index >= 0) { return { index, message: conversation.currentMessages[index] }; } } if (fallbackIndex >= 0 && fallbackIndex < conversation.currentMessages.length) { return { index: fallbackIndex, message: conversation.currentMessages[fallbackIndex] }; } return null; } function getMessageEndpoint(target, suffix = "") { if (!conversation.activeChatId || !target) return ""; if (target.message?.id) return `/chats/${conversation.activeChatId}/messages/${target.message.id}${suffix}`; if (target.index >= 0) return `/chats/${conversation.activeChatId}/messages/by-index/${target.index}${suffix}`; return ""; } function getPreviousUserMessageId(messageId) { const index = getMessageIndexById(messageId); if (index <= 0) return null; for (let i = index - 1; i >= 0; i -= 1) { if (conversation.currentMessages[i].role === "user" && conversation.currentMessages[i].id) { return conversation.currentMessages[i].id; } } return null; } function getRetryStateFromMessage(message) { const variants = Array.isArray(message?.retryVariants) && message.retryVariants.length ? [...message.retryVariants] : [message?.content || ""]; const requestedActiveIndex = Number(message?.retryActiveIndex); const activeIndex = Number.isInteger(requestedActiveIndex) ? Math.min(Math.max(requestedActiveIndex, 0), variants.length - 1) : 0; return { variants, activeIndex, retriesUsed: Number(message?.retryRetriesUsed || 0), promptMessageId: message?.retryPromptMessageId ?? getPreviousUserMessageId(message?.id) }; } function syncRetryStateFromMessage(message) { if (!message?.id) return null; const state = getRetryStateFromMessage(message); conversation.messageRetryState.set(message.id, state); return state; } async function editChatMessage(messageId, fallbackIndex = -1) { const target = await resolveMessageTarget(messageId, fallbackIndex); if (!target) return setNotice("Message is not ready yet. Try again.", "error"); const { index, message } = target; const sourceChatId = conversation.activeChatId; const nextContent = await promptPopup({ eyebrow: message.role === "user" ? "Your message" : "Assistant message", title: "Edit message", description: "Update this message.", label: "Message", value: message.content, confirmLabel: "Save" }); const content = (nextContent || "").trim(); if (!content) return; if (message.role === "user") { const confirmed = await confirmPopup({ eyebrow: "Create branch", title: "Create an edited chat branch?", description: "This creates a new chat from the messages before this one and sends your edited message there. The original chat stays unchanged.", confirmLabel: "Create branch" }); if (!confirmed || conversation.isProcessing) return; conversation.regenerationChatId = sourceChatId; setLoadingState(true, { eyebrow: "Branching", title: "Editing and resending", detail: "Creating and opening a new chat branch." }); try { const res = await post(`/chats/${sourceChatId}/messages/${message.id}/edit-resend`, { chatId: sourceChatId, message: content, model: modelSelect.value, webSearch: webSearchToggle.enabled, confirmed: true }, {timeoutMs: 660000}); if (res.error) { announceResponseStatus(res.error); return setNotice(res.error, "error"); } if (conversation.activeChatId === sourceChatId) { await openCreatedChat(res.chat); const reply = conversation.currentMessages.at(-1); if (reply?.role === "bot") announceReply(reply.content); } return setNotice("Created the edited chat branch.", "success"); } finally { conversation.regenerationChatId = null; setLoadingState(false); } } const endpoint = getMessageEndpoint(target); if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error"); const res = await put(endpoint, { content }); if (res.error || !res.message) { return setNotice(res.error || "Unable to edit message.", "error"); } conversation.currentMessages[index] = { ...conversation.currentMessages[index], ...res.message }; syncRetryStateFromMessage(conversation.currentMessages[index]); renderMessages(); setNotice("Message updated.", "success"); } async function deleteChatMessage(messageId, fallbackIndex = -1) { const target = await resolveMessageTarget(messageId, fallbackIndex); if (!target) return setNotice("Message is not ready yet. Try again.", "error"); const { message } = target; const confirmed = await confirmPopup({ eyebrow: message.role === "user" ? "Your message" : "Assistant message", title: "Delete message", description: "Delete this message from the chat?", confirmLabel: "Delete", danger: true }); if (!confirmed) return; const endpoint = getMessageEndpoint(target); if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error"); const res = await del(endpoint); if (res.error) return setNotice(res.error, "error"); conversation.currentMessages = conversation.currentMessages.filter((item) => item.id !== message.id); if (message.id) { conversation.messageRetryState.delete(message.id); for (const [key, state] of conversation.messageRetryState.entries()) { if (state.promptMessageId === message.id) { conversation.messageRetryState.delete(key); } } } renderMessages(); await loadSummary(); setNotice("Message deleted.", "success"); } async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) { if (conversation.isProcessing) return; const target = await resolveMessageTarget(messageId, fallbackIndex); if (conversation.isProcessing) return; if (!target) return setNotice("Message is not ready yet. Try again.", "error"); if (target.message.role !== "bot") return setNotice("Only assistant messages can be regenerated.", "error"); const retriesUsed = Math.max(0, Number(target.message.versionCount || 1) - 1); if (retriesUsed >= 5) { return setNotice("Retry limit reached (5).", "error"); } const chatId = conversation.activeChatId; const card = messagesDiv.querySelector(`[data-message-id="${target.message.id}"]`); if (!card) return; const payload = { model: modelSelect.value, webSearch: webSearchToggle.enabled, reasoning: reasoningToggle.enabled }; conversation.regenerationChatId = chatId; setLoadingState(true, false); try { await regenerateInline({ card, scroller: messagesDiv, endpoint: `/chats/${chatId}/messages/${target.message.id}/retry/stream`, payload, renderContent: setMessageContent, complete: message => { if (conversation.activeChatId !== chatId) return; const index = getMessageIndexById(message.id); if (index < 0) return; conversation.currentMessages[index] = { ...conversation.currentMessages[index], ...message, thinking: message.thinking, sources: message.sources }; syncRetryStateFromMessage(conversation.currentMessages[index]); const rendered = addMessage(conversation.currentMessages[index], false, false, { messageIndex: index, detached: true }); card.replaceChildren(...rendered.childNodes); announceReply(message.content); } }); reasoningToggle.reset(); setNotice("Reply regenerated.", "success"); } catch (error) { announceResponseStatus(error.message || "Unable to regenerate message."); setNotice(error.message || "Unable to regenerate message.", "error"); } finally { conversation.regenerationChatId = null; setLoadingState(false); } } async function switchResponseVersion(messageId, direction, fallbackIndex = -1) { const target = await resolveMessageTarget(messageId, fallbackIndex); if (!target) return setNotice("Message is not ready yet. Try again.", "error"); if (Number(target.message.versionCount) > 1) { const chatId = conversation.activeChatId; const card = messagesDiv.querySelector(`[data-message-id="${messageId}"]`); if (!card || card.getAttribute("aria-busy") === "true") return; const buttons = [...card.querySelectorAll("button")].map(button => [button, button.disabled]); card.setAttribute("aria-busy", "true"); buttons.forEach(([button]) => { button.disabled = true; }); try { const res = await post(`/chats/${chatId}/messages/${messageId}/versions/${target.message.versionIndex + direction}`, {}); if (res.error) return setNotice(res.error, "error"); if (conversation.activeChatId !== chatId) return; const index = getMessageIndexById(messageId); if (index < 0) return; conversation.currentMessages[index] = { ...conversation.currentMessages[index], ...res.message, thinking: res.message.thinking, sources: res.message.sources }; syncRetryStateFromMessage(conversation.currentMessages[index]); const rendered = addMessage(conversation.currentMessages[index], false, false, { messageIndex: index, detached: true }); card.replaceChildren(...rendered.childNodes); } finally { card.removeAttribute("aria-busy"); buttons.forEach(([button, disabled]) => { button.disabled = disabled; }); } } } async function switchRetryVariant(messageId, direction, fallbackIndex = -1) { const target = await resolveMessageTarget(messageId, fallbackIndex); if (!target) return setNotice("Message is not ready yet. Try again.", "error"); const siblingId = direction < 0 ? target.message.previousSiblingId : target.message.nextSiblingId; if (!siblingId) return; const res = await post(`/chats/${conversation.activeChatId}/branches/${siblingId}/activate`, { includeDescendants: true }); if (res.error) return setNotice(res.error, "error"); conversation.currentMessages = res.messages || []; conversation.historyPage = res.pagination || null; renderMessages(); } async function branchFromMessage(messageId) { if (!conversation.activeChatId || !messageId) return; const confirmed = await confirmPopup({ eyebrow: "Create branch", title: "Create a new chat branch?", description: "The new chat will contain the conversation through this message. The original chat stays unchanged.", confirmLabel: "Create branch" }); if (!confirmed) return; const sourceChatId = conversation.activeChatId; const res = await post(`/chats/${sourceChatId}/messages/${messageId}/branch`, { confirmed: true }); if (res.error) return setNotice(res.error, "error"); await openCreatedChat(res.chat); msgInput.focus(); setNotice("Created and opened a new chat branch.", "success"); } return {editChatMessage, deleteChatMessage, retryLatestAssistantMessage, switchResponseVersion, switchRetryVariant, branchFromMessage}; }