add generation cancellation middleware and refactor chat/message workflows to support HTTP client disconnections
This commit is contained in:
@@ -40,54 +40,60 @@ export function createConversationController({
|
||||
|
||||
async function setActiveChat(id, {preserveComposer = false, matchMessageId = null} = {}) {
|
||||
const openRequest = ++state.chatOpenRequest;
|
||||
if (!preserveComposer && id !== state.activeChatId) {
|
||||
if (state.activeChatId != null) {
|
||||
state.chatDrafts.set(state.activeChatId, msgInput.value);
|
||||
attachmentComposer.saveDraft(state.activeChatId);
|
||||
msgInput.value = state.chatDrafts.get(id) || "";
|
||||
attachmentComposer.restoreDraft(id);
|
||||
resizeComposerInput();
|
||||
} else if (!msgInput.value && !attachmentComposer.pending && !attachmentComposer.attachments.length) {
|
||||
msgInput.value = state.chatDrafts.get(id) || "";
|
||||
attachmentComposer.restoreDraft(id);
|
||||
resizeComposerInput();
|
||||
}
|
||||
}
|
||||
if (isMobileLayout()) closeMobileSidebar();
|
||||
setChatLoading(true, "Opening chat...");
|
||||
setChatActivity(true, {
|
||||
eyebrow: "Opening chat",
|
||||
title: "Loading conversation",
|
||||
eyebrow: "Opening chat", title: "Loading conversation",
|
||||
detail: "Fetching messages and restoring the current scene."
|
||||
});
|
||||
state.activeChatId = id;
|
||||
history.replaceState(null, "", `/app/chats/${id}`);
|
||||
state.messageRetryState = new Map();
|
||||
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
||||
let res = await get(`/chats/${id}/messages`);
|
||||
if (openRequest !== state.chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
|
||||
if (res.error) {
|
||||
setChatLoading(false);
|
||||
setChatActivity(false);
|
||||
return setNotice(res.error, "error");
|
||||
try {
|
||||
let res = await get(`/chats/${id}/messages`);
|
||||
if (openRequest !== state.chatOpenRequest) return;
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
|
||||
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
|
||||
if (openRequest !== state.chatOpenRequest) return;
|
||||
if (activated.error) return setNotice(activated.error, "error");
|
||||
res = await get(`/chats/${id}/messages`);
|
||||
if (openRequest !== state.chatOpenRequest) return;
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
}
|
||||
// Commit identity, content and drafts together, only for the latest successful load.
|
||||
if (!preserveComposer && id !== state.activeChatId) {
|
||||
if (state.activeChatId != null) {
|
||||
state.chatDrafts.set(state.activeChatId, msgInput.value);
|
||||
attachmentComposer.saveDraft(state.activeChatId);
|
||||
msgInput.value = state.chatDrafts.get(id) || "";
|
||||
attachmentComposer.restoreDraft(id);
|
||||
resizeComposerInput();
|
||||
} else if (!msgInput.value && !attachmentComposer.pending && !attachmentComposer.attachments.length) {
|
||||
msgInput.value = state.chatDrafts.get(id) || "";
|
||||
attachmentComposer.restoreDraft(id);
|
||||
resizeComposerInput();
|
||||
}
|
||||
}
|
||||
const existing = getChatById(id);
|
||||
if (res.chat && existing) Object.assign(existing, res.chat);
|
||||
state.activeChatId = id;
|
||||
state.currentMessages = res.messages || [];
|
||||
history.replaceState(null, "", `/app/chats/${id}`);
|
||||
state.messageRetryState = new Map();
|
||||
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
||||
populateGenerationSettings(res.chat || existing);
|
||||
renderMessages();
|
||||
if (matchMessageId) requestAnimationFrame(() => {
|
||||
if (openRequest !== state.chatOpenRequest) return;
|
||||
const target = [...messagesDiv.querySelectorAll("[data-message-id]")]
|
||||
.find(node => Number(node.dataset.messageId) === Number(matchMessageId));
|
||||
if (target) { target.scrollIntoView({block: "center"}); updateJumpToLatest(); }
|
||||
});
|
||||
updateSendState();
|
||||
} finally {
|
||||
if (openRequest === state.chatOpenRequest) {
|
||||
setChatLoading(false);
|
||||
setChatActivity(false);
|
||||
}
|
||||
}
|
||||
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
|
||||
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
|
||||
if (!activated.error) res = await get(`/chats/${id}/messages`);
|
||||
if (openRequest !== state.chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
|
||||
}
|
||||
const existing = getChatById(id);
|
||||
if (res.chat && existing) Object.assign(existing, res.chat);
|
||||
populateGenerationSettings(res.chat || existing);
|
||||
state.currentMessages = res.messages || []; renderMessages();
|
||||
if (matchMessageId) requestAnimationFrame(() => {
|
||||
const target = [...messagesDiv.querySelectorAll("[data-message-id]")]
|
||||
.find(node => Number(node.dataset.messageId) === Number(matchMessageId));
|
||||
if (target) { target.scrollIntoView({block: "center"}); updateJumpToLatest(); }
|
||||
});
|
||||
updateSendState();
|
||||
setChatLoading(false);
|
||||
setChatActivity(false);
|
||||
}
|
||||
|
||||
async function loadChatSessions() {
|
||||
|
||||
+1
-1
@@ -554,7 +554,7 @@ async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPr
|
||||
const res = await post(`/personas/market/${marketId}/chat`, {
|
||||
userPersonaId,
|
||||
scenarioPrompt: scenarioPrompt.trim()
|
||||
});
|
||||
}, {timeoutMs: 660000});
|
||||
if (res.error || !res.chat) {
|
||||
setMarketActivity(false);
|
||||
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||
|
||||
@@ -385,7 +385,7 @@ export function createPersonaController({
|
||||
userPersonaId,
|
||||
scenarioPrompt,
|
||||
model: modelSelect.value
|
||||
});
|
||||
}, {timeoutMs: 660000});
|
||||
roleplayStarterConfirm.disabled = false;
|
||||
if (res.error || !res.chat) {
|
||||
setChatLoading(false);
|
||||
|
||||
+7
-5
@@ -496,6 +496,7 @@ 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",
|
||||
@@ -513,8 +514,8 @@ async function editChatMessage(messageId, fallbackIndex = -1) {
|
||||
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) return;
|
||||
const sourceChatId = conversation.activeChatId;
|
||||
if (!confirmed || conversation.isProcessing) return;
|
||||
conversation.regenerationChatId = sourceChatId;
|
||||
setLoadingState(true, {
|
||||
eyebrow: "Branching",
|
||||
title: "Editing and resending",
|
||||
@@ -526,11 +527,12 @@ async function editChatMessage(messageId, fallbackIndex = -1) {
|
||||
message: content,
|
||||
model: modelSelect.value,
|
||||
confirmed: true
|
||||
});
|
||||
}, {timeoutMs: 660000});
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
await openCreatedChat(res.chat);
|
||||
return setNotice("Created and opened the edited chat branch.", "success");
|
||||
if (conversation.activeChatId === sourceChatId) await openCreatedChat(res.chat);
|
||||
return setNotice("Created the edited chat branch.", "success");
|
||||
} finally {
|
||||
conversation.regenerationChatId = null;
|
||||
setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user