make some overall QoL improvements
This commit is contained in:
@@ -7,16 +7,17 @@ export function createComposerController({
|
||||
state, elements, attachmentComposer, webSearchButton, reasoningToggle, webSearchToggle,
|
||||
setChatActivity, updateComposerPlaceholder, setNotice, createNewChat, getChatById,
|
||||
updateWorkspaceCopy, renderChatList, addMessage, updateChatActionState, scrollMessagesToBottom,
|
||||
setMessageContent, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
||||
setMessageContent, announceResponding, announceReply, announceResponseStatus, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
||||
api = defaultApi
|
||||
}) {
|
||||
const {msgInput, sendBtn, messagesDiv, modelSelect} = elements;
|
||||
const {msgInput, sendBtn, stopBtn, messagesDiv, modelSelect} = elements;
|
||||
const composerHint = document.getElementById("composerHint");
|
||||
let sendController = null;
|
||||
|
||||
function updateSendState() {
|
||||
const hasText = Boolean(msgInput.value.trim());
|
||||
sendBtn.disabled = state.isProcessing ? false : attachmentComposer.pending || (!hasText && !attachmentComposer.attachments.length);
|
||||
sendBtn.setAttribute("aria-label", state.isProcessing ? "Stop generation" : "Send message");
|
||||
sendBtn.disabled = state.isProcessing || attachmentComposer.pending || (!hasText && !attachmentComposer.attachments.length);
|
||||
stopBtn.hidden = !state.isProcessing;
|
||||
}
|
||||
|
||||
function resizeComposerInput() {
|
||||
@@ -26,21 +27,22 @@ export function createComposerController({
|
||||
|
||||
function setLoadingState(loading, overlayOptions = null) {
|
||||
state.isProcessing = loading;
|
||||
msgInput.disabled = loading;
|
||||
webSearchButton.disabled = loading;
|
||||
reasoningToggle.setBusy(loading);
|
||||
attachmentComposer.setBusy(loading);
|
||||
sendBtn.classList.toggle("loading", loading);
|
||||
messagesDiv.querySelectorAll('[data-message-action="regenerate"]').forEach(button => {
|
||||
button.disabled = loading || button.dataset.retryLimitReached === "true";
|
||||
});
|
||||
messagesDiv.querySelectorAll('[data-message-action="retry-failed"]').forEach(button => { button.disabled = loading; });
|
||||
if (loading) {
|
||||
msgInput.placeholder = "Processing response...";
|
||||
msgInput.placeholder = "Draft your next message...";
|
||||
composerHint.textContent = "Draft your next message · Enter for new line";
|
||||
announceResponding();
|
||||
if (overlayOptions !== false) setChatActivity(true, overlayOptions || undefined);
|
||||
} else {
|
||||
setChatActivity(false);
|
||||
updateComposerPlaceholder();
|
||||
composerHint.textContent = "Enter to send · Shift+Enter for new line";
|
||||
}
|
||||
updateSendState();
|
||||
}
|
||||
@@ -76,16 +78,16 @@ export function createComposerController({
|
||||
let userCard, loadingMsg, rendering, completedReply = null;
|
||||
let completedThinking = "", completedSources = null;
|
||||
const userMessage = {role: "user", content: message, attachments};
|
||||
if (!retryMessage) {
|
||||
msgInput.value = "";
|
||||
state.chatDrafts.set(chatId ?? "new", "");
|
||||
resizeComposerInput();
|
||||
}
|
||||
const clearSentDraft = () => {
|
||||
if (state.chatDrafts.get(chatId)?.trim() === message) state.chatDrafts.delete(chatId);
|
||||
attachmentComposer.clearSentDraft(chatId, attachments.map(item => item.id));
|
||||
};
|
||||
try {
|
||||
setLoadingState(true, false);
|
||||
if (retryMessage && !msgInput.value.trim()) {
|
||||
msgInput.value = message;
|
||||
resizeComposerInput();
|
||||
}
|
||||
if (!chatId) {
|
||||
const created = await createNewChat({preserveComposer: true, signal});
|
||||
signal.throwIfAborted();
|
||||
@@ -152,6 +154,7 @@ export function createComposerController({
|
||||
state.currentMessages.push(reply);
|
||||
if (loadingMsg?.isConnected) loadingMsg.replaceWith(addMessage(reply, false, false, {detached: true}));
|
||||
else addMessage(reply);
|
||||
announceReply(completedReply);
|
||||
} else loadingMsg?.remove();
|
||||
reasoningToggle.reset();
|
||||
if (visible()) {
|
||||
@@ -160,7 +163,6 @@ export function createComposerController({
|
||||
const refreshed = await setActiveChat(chatId);
|
||||
if (refreshed?.error) throw new Error(refreshed.error);
|
||||
if (visible()) {
|
||||
if (msgInput.value.trim() === message) { msgInput.value = ""; resizeComposerInput(); }
|
||||
if (msgInput.value) state.chatDrafts.set(chatId, msgInput.value);
|
||||
else state.chatDrafts.delete(chatId);
|
||||
const currentAttachments = attachmentComposer.attachments;
|
||||
@@ -178,11 +180,16 @@ export function createComposerController({
|
||||
} catch (error) {
|
||||
loadingMsg?.remove();
|
||||
const messageText = signal.aborted ? "Generation stopped." : error?.message || "Unable to send message.";
|
||||
if (visible() && completedReply === null) announceResponseStatus(messageText);
|
||||
if (completedReply !== null) {
|
||||
setNotice(`Reply sent, but the conversation could not be refreshed: ${messageText}`, "error");
|
||||
} else {
|
||||
if (visible()) {
|
||||
if (!msgInput.value.trim()) { msgInput.value = message; resizeComposerInput(); }
|
||||
if (!msgInput.value.trim()) {
|
||||
msgInput.value = message;
|
||||
state.chatDrafts.set(state.activeChatId ?? "new", message);
|
||||
resizeComposerInput();
|
||||
}
|
||||
userMessage.deliveryStatus = "failed";
|
||||
userMessage.errorMessage = messageText;
|
||||
if (userCard?.isConnected) userCard.replaceWith(addMessage(userMessage, true, false, {detached: true}));
|
||||
@@ -193,7 +200,7 @@ export function createComposerController({
|
||||
rendering?.dispose();
|
||||
sendController = null;
|
||||
state.sendingChatId = null;
|
||||
setLoadingState(false); msgInput.focus(); updateChatActionState();
|
||||
setLoadingState(false); updateChatActionState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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, webSearchToggle, reasoningToggle, modelSelect, messagesDiv, msgInput}) {
|
||||
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);
|
||||
}
|
||||
@@ -106,8 +106,12 @@ export function createMessageActions({conversation, setNotice, promptPopup, conf
|
||||
webSearch: webSearchToggle.enabled,
|
||||
confirmed: true
|
||||
}, {timeoutMs: 660000});
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
if (conversation.activeChatId === sourceChatId) await openCreatedChat(res.chat);
|
||||
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;
|
||||
@@ -184,11 +188,13 @@ export function createMessageActions({conversation, setNotice, promptPopup, conf
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {messagesDiv, msgInput} from "./dom.js?v=20260909-account-controls";
|
||||
import {appendAttachments} from "./attachments.js?v=20260930-chat-usability";
|
||||
import {updateReasoning} from "./reasoning.js?v=20260921-reasoning-capability";
|
||||
import {appendSources} from "./webSearch.js";
|
||||
import {get} from "./api.js";
|
||||
|
||||
export function createMessageRenderer({
|
||||
getChatById,
|
||||
@@ -17,17 +18,157 @@ export function createMessageRenderer({
|
||||
renderDrawerSummary,
|
||||
updateChatActionState,
|
||||
updateComposerPlaceholder,
|
||||
createNewChat
|
||||
createNewChat,
|
||||
openMessage
|
||||
}) {
|
||||
const jumpToLatest = document.getElementById("jumpToLatest");
|
||||
const searchInput = document.getElementById("conversationSearch");
|
||||
const searchCount = document.getElementById("conversationSearchCount");
|
||||
const previousMatch = document.getElementById("conversationSearchPrevious");
|
||||
const nextMatch = document.getElementById("conversationSearchNext");
|
||||
const replyAnnouncement = document.getElementById("replyAnnouncement");
|
||||
|
||||
let scrollFrame = null;
|
||||
|
||||
let renderedChatId = null;
|
||||
|
||||
let renderedMessages = new Map();
|
||||
let searchMatches = [];
|
||||
let conversationMatches = null;
|
||||
let selectedMatch = 0;
|
||||
let searchChatId = null;
|
||||
let searchFrame = null;
|
||||
let searchTimer = null;
|
||||
let searchRequest = 0;
|
||||
let searchNavigation = 0;
|
||||
|
||||
function setMessageContent(el, content) {
|
||||
function clearSearchMarks() {
|
||||
messagesDiv.querySelectorAll("mark.conversation-search-match").forEach(mark => {
|
||||
mark.replaceWith(document.createTextNode(mark.textContent));
|
||||
});
|
||||
messagesDiv.normalize();
|
||||
}
|
||||
|
||||
function updateSearchCount() {
|
||||
const total = conversationMatches?.length ?? searchMatches.length;
|
||||
const label = total ? `${selectedMatch + 1} of ${total} matches` : "0 matches";
|
||||
if (searchCount.textContent !== label) searchCount.textContent = label;
|
||||
previousMatch.disabled = nextMatch.disabled = !total;
|
||||
const target = conversationMatches?.[selectedMatch];
|
||||
searchMatches.forEach((mark, index) => mark.classList.toggle("current", target
|
||||
? String(target.messageId) === mark.dataset.messageId && Number(mark.dataset.occurrence) === target.occurrence
|
||||
: index === selectedMatch));
|
||||
}
|
||||
|
||||
function refreshConversationSearch() {
|
||||
if (searchFrame !== null) { cancelAnimationFrame(searchFrame); searchFrame = null; }
|
||||
clearSearchMarks();
|
||||
searchMatches = [];
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
const needle = query.toLocaleLowerCase();
|
||||
const occurrences = new Map();
|
||||
messagesDiv.querySelectorAll(".msg-content").forEach(content => {
|
||||
const messageId = content.closest(".msg")?.dataset.messageId || "";
|
||||
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
nodes.forEach(node => {
|
||||
const value = node.nodeValue;
|
||||
const lower = value.toLocaleLowerCase();
|
||||
let start = lower.indexOf(needle);
|
||||
if (start < 0) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let cursor = 0;
|
||||
while (start >= 0) {
|
||||
fragment.append(document.createTextNode(value.slice(cursor, start)));
|
||||
const mark = document.createElement("mark");
|
||||
mark.className = "conversation-search-match";
|
||||
mark.textContent = value.slice(start, start + query.length);
|
||||
mark.dataset.messageId = messageId;
|
||||
mark.dataset.occurrence = String(occurrences.get(messageId) || 0);
|
||||
occurrences.set(messageId, (occurrences.get(messageId) || 0) + 1);
|
||||
fragment.append(mark);
|
||||
searchMatches.push(mark);
|
||||
cursor = start + query.length;
|
||||
start = lower.indexOf(needle, cursor);
|
||||
}
|
||||
fragment.append(document.createTextNode(value.slice(cursor)));
|
||||
node.replaceWith(fragment);
|
||||
});
|
||||
});
|
||||
}
|
||||
selectedMatch = Math.min(selectedMatch, Math.max(0, (conversationMatches?.length ?? searchMatches.length) - 1));
|
||||
updateSearchCount();
|
||||
}
|
||||
|
||||
function scheduleConversationSearch(reset = false, focusMatch = false) {
|
||||
clearTimeout(searchTimer);
|
||||
const request = ++searchRequest;
|
||||
const chatId = state.activeChatId;
|
||||
const query = searchInput.value.trim();
|
||||
if (reset || !query) conversationMatches = null;
|
||||
updateSearchCount();
|
||||
if (!chatId || !query) return;
|
||||
searchTimer = setTimeout(async () => {
|
||||
const result = await get(`/chats/${chatId}/messages/search?q=${encodeURIComponent(query)}`);
|
||||
if (request !== searchRequest || chatId !== state.activeChatId || query !== searchInput.value.trim()) return;
|
||||
if (result.error) { searchCount.textContent = "Search unavailable"; return; }
|
||||
conversationMatches = result.matches || [];
|
||||
selectedMatch = Math.min(selectedMatch, Math.max(0, conversationMatches.length - 1));
|
||||
updateSearchCount();
|
||||
if (focusMatch && conversationMatches.length) void moveSearchMatch(0);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function scheduleSearchRefresh() {
|
||||
if (!searchInput.value.trim() || searchFrame !== null) return;
|
||||
searchFrame = requestAnimationFrame(refreshConversationSearch);
|
||||
}
|
||||
|
||||
async function moveSearchMatch(direction) {
|
||||
const navigation = ++searchNavigation;
|
||||
const hits = conversationMatches || searchMatches;
|
||||
if (!hits.length) return;
|
||||
selectedMatch = (selectedMatch + direction + hits.length) % hits.length;
|
||||
updateSearchCount();
|
||||
const hit = conversationMatches?.[selectedMatch];
|
||||
if (hit && !searchMatches.some(mark => mark.dataset.messageId === String(hit.messageId))) {
|
||||
await openMessage(hit.messageId);
|
||||
if (navigation !== searchNavigation || state.activeChatId !== searchChatId) return;
|
||||
refreshConversationSearch();
|
||||
}
|
||||
const mark = hit
|
||||
? searchMatches.find(item => item.dataset.messageId === String(hit.messageId) && Number(item.dataset.occurrence) === hit.occurrence)
|
||||
|| searchMatches.find(item => item.dataset.messageId === String(hit.messageId))
|
||||
: searchMatches[selectedMatch];
|
||||
mark?.scrollIntoView({block: "center", behavior: "smooth"});
|
||||
}
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
searchNavigation++;
|
||||
selectedMatch = 0;
|
||||
conversationMatches = null;
|
||||
refreshConversationSearch();
|
||||
searchMatches[0]?.scrollIntoView({block: "center", behavior: "smooth"});
|
||||
scheduleConversationSearch(false, true);
|
||||
});
|
||||
searchInput.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") { event.preventDefault(); moveSearchMatch(event.shiftKey ? -1 : 1); }
|
||||
});
|
||||
previousMatch.addEventListener("click", () => moveSearchMatch(-1));
|
||||
nextMatch.addEventListener("click", () => moveSearchMatch(1));
|
||||
|
||||
function announceResponding() { replyAnnouncement.textContent = "Assistant is responding."; }
|
||||
function announceResponseStatus(status) { replyAnnouncement.textContent = status; }
|
||||
function announceReply(content) {
|
||||
const plain = document.createElement("div");
|
||||
renderMarkdown(plain, content || "");
|
||||
replyAnnouncement.textContent = `Assistant: ${plain.textContent.trim() || "Response complete."}`;
|
||||
if (searchInput.value.trim()) scheduleConversationSearch();
|
||||
}
|
||||
|
||||
function renderMarkdown(el, content) {
|
||||
const markdownParser = window.marked;
|
||||
const purifier = window.DOMPurify;
|
||||
if (markdownParser?.parse && purifier?.sanitize) {
|
||||
@@ -37,6 +178,11 @@ export function createMessageRenderer({
|
||||
}
|
||||
}
|
||||
|
||||
function setMessageContent(el, content) {
|
||||
renderMarkdown(el, content);
|
||||
scheduleSearchRefresh();
|
||||
}
|
||||
|
||||
function scrollMessagesToBottom() {
|
||||
if (scrollFrame !== null) cancelAnimationFrame(scrollFrame);
|
||||
scrollFrame = requestAnimationFrame(() => {
|
||||
@@ -60,7 +206,7 @@ export function createMessageRenderer({
|
||||
msgDiv.setAttribute("aria-label", "Assistant is responding");
|
||||
msgDiv.innerHTML = `
|
||||
<div class="msg-header">Assistant</div>
|
||||
<div class="loading-shell" aria-live="polite" aria-label="Assistant is responding">
|
||||
<div class="loading-shell" aria-hidden="true">
|
||||
<div class="loading-bars">
|
||||
<span class="loading-bar"></span>
|
||||
<span class="loading-bar"></span>
|
||||
@@ -251,6 +397,13 @@ export function createMessageRenderer({
|
||||
renderedMessages = nextRendered;
|
||||
}
|
||||
renderedChatId = state.activeChatId;
|
||||
if (searchChatId !== state.activeChatId) {
|
||||
searchNavigation++;
|
||||
searchChatId = state.activeChatId;
|
||||
selectedMatch = 0;
|
||||
scheduleConversationSearch(true, true);
|
||||
} else if (searchInput.value.trim()) scheduleConversationSearch();
|
||||
refreshConversationSearch();
|
||||
renderDrawerSummary();
|
||||
updateChatActionState();
|
||||
updateComposerPlaceholder();
|
||||
@@ -325,6 +478,9 @@ export function createMessageRenderer({
|
||||
|
||||
return {
|
||||
setMessageContent,
|
||||
announceResponding,
|
||||
announceResponseStatus,
|
||||
announceReply,
|
||||
scrollMessagesToBottom,
|
||||
updateJumpToLatest,
|
||||
addMessage,
|
||||
|
||||
+13
-6
@@ -107,6 +107,7 @@ import {
|
||||
const conversation = createConversationState();
|
||||
|
||||
const headerModelButton = document.getElementById("headerModelButton");
|
||||
const stopBtn = document.getElementById("stopGeneration");
|
||||
const webSearchButton = document.getElementById("webSearch");
|
||||
const webSearchToggle = createWebSearchToggle(webSearchButton);
|
||||
const reasoningToggle = createReasoningToggle(document.getElementById("reasoning"));
|
||||
@@ -124,7 +125,7 @@ let availableModelsById = new Map();
|
||||
const personalitySelect = document.getElementById("personalitySelect");
|
||||
let personalities = [];
|
||||
|
||||
const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage, renderMessages} = createMessageRenderer({
|
||||
const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage, renderMessages, announceResponding, announceReply, announceResponseStatus} = createMessageRenderer({
|
||||
getChatById: (...args) => getChatById(...args),
|
||||
state: conversation,
|
||||
setNotice: (...args) => setNotice(...args),
|
||||
@@ -138,7 +139,8 @@ const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage
|
||||
renderDrawerSummary: (...args) => renderDrawerSummary(...args),
|
||||
updateChatActionState: (...args) => updateChatActionState(...args),
|
||||
updateComposerPlaceholder: (...args) => updateComposerPlaceholder(...args),
|
||||
createNewChat: (...args) => createNewChat(...args)
|
||||
createNewChat: (...args) => createNewChat(...args),
|
||||
openMessage: messageId => setActiveChat(conversation.activeChatId, {matchMessageId: messageId})
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -195,7 +197,7 @@ const {
|
||||
|
||||
const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, sendMessage} = createComposerController({
|
||||
state: conversation,
|
||||
elements: {msgInput, sendBtn, messagesDiv, modelSelect},
|
||||
elements: {msgInput, sendBtn, stopBtn, messagesDiv, modelSelect},
|
||||
attachmentComposer,
|
||||
webSearchButton,
|
||||
reasoningToggle,
|
||||
@@ -211,6 +213,9 @@ const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, se
|
||||
updateChatActionState: (...args) => updateChatActionState(...args),
|
||||
scrollMessagesToBottom: (...args) => scrollMessagesToBottom(...args),
|
||||
setMessageContent: (...args) => setMessageContent(...args),
|
||||
announceResponding,
|
||||
announceReply,
|
||||
announceResponseStatus,
|
||||
setActiveChat: (...args) => setActiveChat(...args),
|
||||
loadSummary: (...args) => loadSummary(...args),
|
||||
markOnboardingCompleted: (...args) => markOnboardingCompleted(...args),
|
||||
@@ -221,7 +226,7 @@ const {exportCurrentChat, backupWorkspace, importWorkspaceFromFile} = createWork
|
||||
conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadChatSessions
|
||||
});
|
||||
const {editChatMessage, deleteChatMessage, retryLatestAssistantMessage, switchResponseVersion, switchRetryVariant, branchFromMessage} = createMessageActions({
|
||||
conversation, setNotice, promptPopup, confirmPopup, setActiveChat, openCreatedChat, setLoadingState, loadSummary, renderMessages, addMessage, setMessageContent, webSearchToggle, reasoningToggle, modelSelect, messagesDiv, msgInput
|
||||
conversation, setNotice, promptPopup, confirmPopup, setActiveChat, openCreatedChat, setLoadingState, loadSummary, renderMessages, addMessage, setMessageContent, announceReply, announceResponseStatus, webSearchToggle, reasoningToggle, modelSelect, messagesDiv, msgInput
|
||||
});
|
||||
|
||||
const memoryPanel = createMemoryPanel({state: conversation});
|
||||
@@ -373,6 +378,7 @@ function maybeShowOnboarding() {
|
||||
}
|
||||
|
||||
function updateComposerPlaceholder() {
|
||||
if (conversation.isProcessing) { msgInput.placeholder = "Draft your next message..."; return; }
|
||||
const activeChat = getChatById(conversation.activeChatId);
|
||||
if (!activeChat) {
|
||||
msgInput.placeholder = "Create a chat to begin...";
|
||||
@@ -979,8 +985,9 @@ function bindChatDrawerActions() {
|
||||
});
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => { void (conversation.isProcessing ? stopGeneration() : sendMessage()); });
|
||||
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { event.preventDefault(); void sendMessage(); } });
|
||||
sendBtn.addEventListener("click", () => { void sendMessage(); });
|
||||
stopBtn.addEventListener("click", () => { void stopGeneration(); });
|
||||
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey && !event.isComposing && !conversation.isProcessing) { event.preventDefault(); void sendMessage(); } });
|
||||
msgInput.addEventListener("input", function () {
|
||||
if (conversation.currentUsername) conversation.chatDrafts.set(conversation.activeChatId ?? "new", msgInput.value);
|
||||
resizeComposerInput();
|
||||
|
||||
Reference in New Issue
Block a user