make some overall QoL improvements
This commit is contained in:
+29
-16
@@ -11,7 +11,7 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
<script src="/js/theme-bootstrap.js?v=20260909-account-controls"></script>
|
<script src="/js/theme-bootstrap.js?v=20260909-account-controls"></script>
|
||||||
<link rel="stylesheet" href="/css/style.css?v=20260930-chat-usability" />
|
<link rel="stylesheet" href="/css/style.css?v=20261008-conversation-search" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/css/pages.css?v=20260909-account-controls">
|
<link rel="stylesheet" href="/css/pages.css?v=20260909-account-controls">
|
||||||
<link rel="stylesheet" href="/css/shell.css?v=20260909-account-controls">
|
<link rel="stylesheet" href="/css/shell.css?v=20260909-account-controls">
|
||||||
@@ -128,9 +128,11 @@
|
|||||||
<p id="modelHelpSummary" class="model-help-summary">Pick a
|
<p id="modelHelpSummary" class="model-help-summary">Pick a
|
||||||
model to see what it is best suited for.</p>
|
model to see what it is best suited for.</p>
|
||||||
</div>
|
</div>
|
||||||
<details class="generation-advanced">
|
</div>
|
||||||
<summary>Advanced generation settings</summary>
|
</details>
|
||||||
<div class="generation-settings-grid">
|
<details class="chat-drawer-section generation-advanced">
|
||||||
|
<summary class="chat-drawer-row"><span class="chat-drawer-row-label"><strong>Generation settings</strong></span><span class="chat-drawer-row-hint">Advanced</span></summary>
|
||||||
|
<div class="generation-settings-grid chat-drawer-disclosure-panel">
|
||||||
<label class="field"><span>Temperature</span><input
|
<label class="field"><span>Temperature</span><input
|
||||||
id="chatTemperature" type="number" min="0" max="2"
|
id="chatTemperature" type="number" min="0" max="2"
|
||||||
step="0.1" placeholder="Default" /></label>
|
step="0.1" placeholder="Default" /></label>
|
||||||
@@ -148,10 +150,8 @@
|
|||||||
generation settings</button>
|
generation settings</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<div class="chat-drawer-section">
|
<div class="chat-drawer-section">
|
||||||
<p class="dropdown-section-label">Chat</p>
|
<p class="dropdown-section-label">Conversation actions</p>
|
||||||
<div class="chat-drawer-action-list">
|
<div class="chat-drawer-action-list">
|
||||||
<button id="renameChat"
|
<button id="renameChat"
|
||||||
class="secondary-action chat-drawer-action" type="button"
|
class="secondary-action chat-drawer-action" type="button"
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<details class="chat-drawer-section chat-drawer-more">
|
<details class="chat-drawer-section chat-drawer-more">
|
||||||
<summary class="chat-drawer-row chat-drawer-row--compact">
|
<summary class="chat-drawer-row chat-drawer-row--compact">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<strong>Tools & workspace</strong>
|
<strong>Conversation tools</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-hint">Show</span>
|
<span class="chat-drawer-row-hint">Show</span>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -184,6 +184,11 @@
|
|||||||
<button id="managePersonaState"
|
<button id="managePersonaState"
|
||||||
class="secondary-action chat-drawer-action" type="button"
|
class="secondary-action chat-drawer-action" type="button"
|
||||||
data-chat-action="personaState">Roleplay state</button>
|
data-chat-action="personaState">Roleplay state</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="chat-drawer-section chat-drawer-more">
|
||||||
|
<summary class="chat-drawer-row chat-drawer-row--compact"><span class="chat-drawer-row-label"><strong>Workspace files</strong></span><span class="chat-drawer-row-hint">Show</span></summary>
|
||||||
|
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
|
||||||
<button id="backupWorkspace"
|
<button id="backupWorkspace"
|
||||||
class="secondary-action chat-drawer-action"
|
class="secondary-action chat-drawer-action"
|
||||||
type="button">Download workspace archive</button>
|
type="button">Download workspace archive</button>
|
||||||
@@ -197,7 +202,7 @@
|
|||||||
<details class="chat-drawer-section chat-drawer-danger">
|
<details class="chat-drawer-section chat-drawer-danger">
|
||||||
<summary class="chat-drawer-row chat-drawer-row--compact">
|
<summary class="chat-drawer-row chat-drawer-row--compact">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<strong>Clear conversation</strong>
|
<strong>Delete chat content</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-hint">Show</span>
|
<span class="chat-drawer-row-hint">Show</span>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -234,14 +239,21 @@
|
|||||||
<button id="loadOlderMessages" class="secondary-action" type="button" hidden>Load older messages</button>
|
<button id="loadOlderMessages" class="secondary-action" type="button" hidden>Load older messages</button>
|
||||||
<button id="loadLatestMessages" class="secondary-action" type="button" hidden>Back to latest messages</button>
|
<button id="loadLatestMessages" class="secondary-action" type="button" hidden>Back to latest messages</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="messages" class="messages-panel messages-panel-focus" role="log" aria-live="polite"
|
<div class="conversation-search" role="search" aria-label="Search this conversation">
|
||||||
aria-relevant="additions text"></div>
|
<label for="conversationSearch">Find in this chat</label>
|
||||||
|
<input id="conversationSearch" type="search" placeholder="Search messages" autocomplete="off" maxlength="200">
|
||||||
|
<span id="conversationSearchCount" role="status" aria-live="polite">0 matches</span>
|
||||||
|
<button id="conversationSearchPrevious" class="secondary-action" type="button" aria-label="Previous match" disabled>Previous</button>
|
||||||
|
<button id="conversationSearchNext" class="secondary-action" type="button" aria-label="Next match" disabled>Next</button>
|
||||||
|
</div>
|
||||||
|
<div id="messages" class="messages-panel messages-panel-focus" role="log" aria-live="off"></div>
|
||||||
|
<div id="replyAnnouncement" class="visually-hidden" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||||
<button id="jumpToLatest" class="jump-to-latest secondary-action hidden" type="button">Jump to latest</button>
|
<button id="jumpToLatest" class="jump-to-latest secondary-action hidden" type="button">Jump to latest</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-shell input-shell-focus">
|
<div class="input-shell input-shell-focus">
|
||||||
<div class="input-meta">
|
<div class="input-meta">
|
||||||
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
|
<p id="composerHint" class="composer-hint">Enter to send · Shift+Enter for new line</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadPreviews" class="upload-previews" aria-live="polite" hidden></div>
|
<div id="uploadPreviews" class="upload-previews" aria-live="polite" hidden></div>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
@@ -254,7 +266,7 @@
|
|||||||
<rect x="3" y="3" width="18" height="18" rx="3" />
|
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||||
<circle cx="8" cy="8" r="1.5" />
|
<circle cx="8" cy="8" r="1.5" />
|
||||||
<path d="m3 17 5-5 4 4 4-6 5 7" />
|
<path d="m3 17 5-5 4 4 4-6 5 7" />
|
||||||
</svg>
|
</svg><span class="composer-tool-label">Attach</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="webSearch" class="icon-button" type="button" aria-label="Automatic web search on — click to disable"
|
<button id="webSearch" class="icon-button" type="button" aria-label="Automatic web search on — click to disable"
|
||||||
title="Automatic web search on — click to disable" aria-pressed="true">
|
title="Automatic web search on — click to disable" aria-pressed="true">
|
||||||
@@ -263,7 +275,7 @@
|
|||||||
<circle cx="12" cy="12" r="9" />
|
<circle cx="12" cy="12" r="9" />
|
||||||
<ellipse cx="12" cy="12" rx="4" ry="9" />
|
<ellipse cx="12" cy="12" rx="4" ry="9" />
|
||||||
<path d="M3 12h18" />
|
<path d="M3 12h18" />
|
||||||
</svg>
|
</svg><span class="composer-tool-label">Web search</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="reasoning" class="icon-button" type="button" aria-label="Enable reasoning"
|
<button id="reasoning" class="icon-button" type="button" aria-label="Enable reasoning"
|
||||||
title="Enable reasoning" aria-pressed="false" disabled>
|
title="Enable reasoning" aria-pressed="false" disabled>
|
||||||
@@ -272,11 +284,12 @@
|
|||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M12 18V5a3 3 0 0 0-5.8-1A4 4 0 0 0 3 10a4 4 0 0 0 1 7.8A4 4 0 0 0 12 18ZM12 5a3 3 0 0 1 5.8-1A4 4 0 0 1 21 10a4 4 0 0 1-1 7.8A4 4 0 0 1 12 18M8 8a4 4 0 0 1-5 2m13-2a4 4 0 0 0 5 2M8 14a4 4 0 0 0-4 4m12-4a4 4 0 0 1 4 4" />
|
d="M12 18V5a3 3 0 0 0-5.8-1A4 4 0 0 0 3 10a4 4 0 0 0 1 7.8A4 4 0 0 0 12 18ZM12 5a3 3 0 0 1 5.8-1A4 4 0 0 1 21 10a4 4 0 0 1-1 7.8A4 4 0 0 1 12 18M8 8a4 4 0 0 1-5 2m13-2a4 4 0 0 0 5 2M8 14a4 4 0 0 0-4 4m12-4a4 4 0 0 1 4 4" />
|
||||||
</svg>
|
</svg><span class="composer-tool-label">Reasoning</span>
|
||||||
</button>
|
</button>
|
||||||
<label class="visually-hidden" for="msgInput">Message</label>
|
<label class="visually-hidden" for="msgInput">Message</label>
|
||||||
<textarea id="msgInput" rows="1" placeholder="Type your message..."></textarea>
|
<textarea id="msgInput" rows="1" placeholder="Type your message..."></textarea>
|
||||||
<button id="send" type="button" aria-label="Send message"></button>
|
<button id="send" type="button" aria-label="Send message"></button>
|
||||||
|
<button id="stopGeneration" class="secondary-action" type="button" aria-label="Stop generating reply" hidden>Stop</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -439,7 +452,7 @@
|
|||||||
integrity="sha512-rCQgmUulW6f6QegOvTntKKb5IAoxTpGVCdWqYjkXEpzAns6XUFs8NKVqWe+KQpctp/EoRSFSuykVputqknLYMg=="
|
integrity="sha512-rCQgmUulW6f6QegOvTntKKb5IAoxTpGVCdWqYjkXEpzAns6XUFs8NKVqWe+KQpctp/EoRSFSuykVputqknLYMg=="
|
||||||
crossorigin="anonymous" defer></script>
|
crossorigin="anonymous" defer></script>
|
||||||
<script src="/vendor/dompurify.js" defer></script>
|
<script src="/vendor/dompurify.js" defer></script>
|
||||||
<script type="module" src="/js/script.js?v=20261007-chat-csp-fix"></script>
|
<script type="module" src="/js/script.js?v=20261008-conversation-search"></script>
|
||||||
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -424,7 +424,14 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
.chat-header-actions { display: flex; align-items: center; }
|
.chat-header-actions { display: flex; align-items: center; }
|
||||||
.overflow-trigger { min-height: 38px; padding: 8px 12px; border-radius: 999px; font-size: .8rem; }
|
.overflow-trigger { min-height: 38px; padding: 8px 12px; border-radius: 999px; font-size: .8rem; }
|
||||||
|
|
||||||
.messages-wrap { min-height: 0; position: relative; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
.messages-wrap { min-height: 0; position: relative; display: grid; grid-template-rows: auto auto minmax(0, 1fr); }
|
||||||
|
.conversation-search { padding: 8px max(16px, calc((100% - 940px) / 2)); display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--border); }
|
||||||
|
.conversation-search label { flex: 0 0 auto; color: var(--text-soft); font-size: .76rem; font-weight: 700; }
|
||||||
|
.conversation-search input { min-width: 0; width: min(280px, 100%); min-height: 34px; }
|
||||||
|
.conversation-search span { min-width: 64px; color: var(--text-muted); font-size: .72rem; white-space: nowrap; }
|
||||||
|
.conversation-search button { min-height: 34px; padding: 5px 9px; font-size: .72rem; }
|
||||||
|
.conversation-search-match { background: #facc15; color: #1f2937; border-radius: 2px; }
|
||||||
|
.conversation-search-match.current { outline: 2px solid var(--brand); outline-offset: 1px; }
|
||||||
.messages-panel {
|
.messages-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -497,7 +504,10 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
.input-meta, .input-container { width: min(940px, 100%); margin-inline: auto; }
|
.input-meta, .input-container { width: min(940px, 100%); margin-inline: auto; }
|
||||||
.input-meta { margin-bottom: 5px; padding-left: 10px; }
|
.input-meta { margin-bottom: 5px; padding-left: 10px; }
|
||||||
.composer-hint { margin: 0; color: var(--text-muted); font-size: .72rem; }
|
.composer-hint { margin: 0; color: var(--text-muted); font-size: .72rem; }
|
||||||
.input-container { padding: 7px; display: grid; grid-template-columns: 44px 44px 44px minmax(0, 1fr) 44px; grid-template-areas: "attach search reasoning message send"; gap: 7px; align-items: end; background: var(--input-bg); border: 1px solid var(--border-strong); border-radius: 24px; box-shadow: var(--shadow-sm); }
|
.input-container { padding: 7px; display: grid; grid-template-columns: 44px 44px 44px minmax(0, 1fr) 44px 58px; grid-template-areas: "attach search reasoning message send stop"; gap: 7px; align-items: end; background: var(--input-bg); border: 1px solid var(--border-strong); border-radius: 24px; box-shadow: var(--shadow-sm); }
|
||||||
|
.input-container:not(:has(#stopGeneration:not([hidden]))) { grid-template-columns: 44px 44px 44px minmax(0, 1fr) 44px; grid-template-areas: "attach search reasoning message send"; }
|
||||||
|
.input-container [hidden] { display: none !important; }
|
||||||
|
.composer-tool-label { display: none; }
|
||||||
#attachImage { grid-area: attach; }
|
#attachImage { grid-area: attach; }
|
||||||
.upload-previews { width: min(940px, 100%); margin: 0 auto 8px; display: flex; gap: 8px; flex-wrap: wrap; }
|
.upload-previews { width: min(940px, 100%); margin: 0 auto 8px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
.upload-previews[hidden] { display: none; }
|
.upload-previews[hidden] { display: none; }
|
||||||
@@ -508,10 +518,6 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
.message-images img { max-width: min(360px, 100%); max-height: 300px; object-fit: contain; border-radius: 12px; }
|
.message-images img { max-width: min(360px, 100%); max-height: 300px; object-fit: contain; border-radius: 12px; }
|
||||||
.message-images a { max-width: 100%; }
|
.message-images a { max-width: 100%; }
|
||||||
.image-drag-target { outline: 2px solid currentColor; }
|
.image-drag-target { outline: 2px solid currentColor; }
|
||||||
@media (max-width: 480px) {
|
|
||||||
.input-container { grid-template-columns: repeat(3, 36px) minmax(0, 1fr) 44px; grid-template-areas: "message message message message send" "attach search reasoning . ."; gap: 4px; }
|
|
||||||
#attachImage, #webSearch, #reasoning { width: 36px; height: 36px; min-height: 36px; border-radius: 12px; }
|
|
||||||
}
|
|
||||||
#webSearch, #reasoning { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 17px; }
|
#webSearch, #reasoning { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 17px; }
|
||||||
#webSearch { grid-area: search; }
|
#webSearch { grid-area: search; }
|
||||||
#reasoning { grid-area: reasoning; }
|
#reasoning { grid-area: reasoning; }
|
||||||
@@ -524,7 +530,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
#msgInput { grid-area: message; min-height: 44px; max-height: 160px; padding: 11px 13px; resize: none; background: transparent; border: 0; box-shadow: none; }
|
#msgInput { grid-area: message; min-height: 44px; max-height: 160px; padding: 11px 13px; resize: none; background: transparent; border: 0; box-shadow: none; }
|
||||||
#send { grid-area: send; width: 44px; min-width: 44px; height: 44px; min-height: 44px; padding: 0; display: grid; place-items: center; border-radius: 17px; }
|
#send { grid-area: send; width: 44px; min-width: 44px; height: 44px; min-height: 44px; padding: 0; display: grid; place-items: center; border-radius: 17px; }
|
||||||
#send::after { content: "\2191"; font-size: 1.1rem; font-weight: 800; }
|
#send::after { content: "\2191"; font-size: 1.1rem; font-weight: 800; }
|
||||||
#send.loading::after { content: "■"; font-size: .85rem; }
|
#stopGeneration { grid-area: stop; width: 58px; height: 44px; min-height: 44px; padding: 0 7px; font-size: .76rem; font-weight: 800; border-radius: 15px; }
|
||||||
.generation-settings-grid { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
|
.generation-settings-grid { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
|
||||||
.generation-settings-grid .field-wide, .generation-settings-grid button { grid-column: 1 / -1; }
|
.generation-settings-grid .field-wide, .generation-settings-grid button { grid-column: 1 / -1; }
|
||||||
.generation-advanced { margin-top: 6px; border-top: 1px solid var(--border); }
|
.generation-advanced { margin-top: 6px; border-top: 1px solid var(--border); }
|
||||||
@@ -689,6 +695,14 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
.messages-empty-actions { display: grid; }
|
.messages-empty-actions { display: grid; }
|
||||||
.chat-drawer-action-list { grid-template-columns: 1fr; }
|
.chat-drawer-action-list { grid-template-columns: 1fr; }
|
||||||
.chat-drawer-action-list > * { grid-column: auto; }
|
.chat-drawer-action-list > * { grid-column: auto; }
|
||||||
|
.conversation-search { padding: 6px 10px; flex-wrap: wrap; gap: 5px; }
|
||||||
|
.conversation-search label { width: 100%; }
|
||||||
|
.conversation-search input { flex: 1 1 110px; }
|
||||||
|
.conversation-search span { min-width: 55px; }
|
||||||
|
.input-container { grid-template-columns: repeat(3, minmax(0, 1fr)) 44px 58px; grid-template-areas: "message message message send stop" "attach search reasoning . ."; gap: 4px; }
|
||||||
|
.input-container:not(:has(#stopGeneration:not([hidden]))) { grid-template-columns: repeat(3, minmax(0, 1fr)) 44px; grid-template-areas: "message message message send" "attach search reasoning ."; }
|
||||||
|
#attachImage, #webSearch, #reasoning { width: 100%; height: 49px; min-height: 49px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; border-radius: 12px; }
|
||||||
|
.composer-tool-label { display: inline; font-size: .63rem; line-height: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: none) { .msg-actions { opacity: 1; } }
|
@media (hover: none) { .msg-actions { opacity: 1; } }
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ export function createComposerController({
|
|||||||
state, elements, attachmentComposer, webSearchButton, reasoningToggle, webSearchToggle,
|
state, elements, attachmentComposer, webSearchButton, reasoningToggle, webSearchToggle,
|
||||||
setChatActivity, updateComposerPlaceholder, setNotice, createNewChat, getChatById,
|
setChatActivity, updateComposerPlaceholder, setNotice, createNewChat, getChatById,
|
||||||
updateWorkspaceCopy, renderChatList, addMessage, updateChatActionState, scrollMessagesToBottom,
|
updateWorkspaceCopy, renderChatList, addMessage, updateChatActionState, scrollMessagesToBottom,
|
||||||
setMessageContent, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
setMessageContent, announceResponding, announceReply, announceResponseStatus, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
||||||
api = defaultApi
|
api = defaultApi
|
||||||
}) {
|
}) {
|
||||||
const {msgInput, sendBtn, messagesDiv, modelSelect} = elements;
|
const {msgInput, sendBtn, stopBtn, messagesDiv, modelSelect} = elements;
|
||||||
|
const composerHint = document.getElementById("composerHint");
|
||||||
let sendController = null;
|
let sendController = null;
|
||||||
|
|
||||||
function updateSendState() {
|
function updateSendState() {
|
||||||
const hasText = Boolean(msgInput.value.trim());
|
const hasText = Boolean(msgInput.value.trim());
|
||||||
sendBtn.disabled = state.isProcessing ? false : attachmentComposer.pending || (!hasText && !attachmentComposer.attachments.length);
|
sendBtn.disabled = state.isProcessing || attachmentComposer.pending || (!hasText && !attachmentComposer.attachments.length);
|
||||||
sendBtn.setAttribute("aria-label", state.isProcessing ? "Stop generation" : "Send message");
|
stopBtn.hidden = !state.isProcessing;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeComposerInput() {
|
function resizeComposerInput() {
|
||||||
@@ -26,21 +27,22 @@ export function createComposerController({
|
|||||||
|
|
||||||
function setLoadingState(loading, overlayOptions = null) {
|
function setLoadingState(loading, overlayOptions = null) {
|
||||||
state.isProcessing = loading;
|
state.isProcessing = loading;
|
||||||
msgInput.disabled = loading;
|
|
||||||
webSearchButton.disabled = loading;
|
webSearchButton.disabled = loading;
|
||||||
reasoningToggle.setBusy(loading);
|
reasoningToggle.setBusy(loading);
|
||||||
attachmentComposer.setBusy(loading);
|
attachmentComposer.setBusy(loading);
|
||||||
sendBtn.classList.toggle("loading", loading);
|
|
||||||
messagesDiv.querySelectorAll('[data-message-action="regenerate"]').forEach(button => {
|
messagesDiv.querySelectorAll('[data-message-action="regenerate"]').forEach(button => {
|
||||||
button.disabled = loading || button.dataset.retryLimitReached === "true";
|
button.disabled = loading || button.dataset.retryLimitReached === "true";
|
||||||
});
|
});
|
||||||
messagesDiv.querySelectorAll('[data-message-action="retry-failed"]').forEach(button => { button.disabled = loading; });
|
messagesDiv.querySelectorAll('[data-message-action="retry-failed"]').forEach(button => { button.disabled = loading; });
|
||||||
if (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);
|
if (overlayOptions !== false) setChatActivity(true, overlayOptions || undefined);
|
||||||
} else {
|
} else {
|
||||||
setChatActivity(false);
|
setChatActivity(false);
|
||||||
updateComposerPlaceholder();
|
updateComposerPlaceholder();
|
||||||
|
composerHint.textContent = "Enter to send · Shift+Enter for new line";
|
||||||
}
|
}
|
||||||
updateSendState();
|
updateSendState();
|
||||||
}
|
}
|
||||||
@@ -76,16 +78,16 @@ export function createComposerController({
|
|||||||
let userCard, loadingMsg, rendering, completedReply = null;
|
let userCard, loadingMsg, rendering, completedReply = null;
|
||||||
let completedThinking = "", completedSources = null;
|
let completedThinking = "", completedSources = null;
|
||||||
const userMessage = {role: "user", content: message, attachments};
|
const userMessage = {role: "user", content: message, attachments};
|
||||||
|
if (!retryMessage) {
|
||||||
|
msgInput.value = "";
|
||||||
|
state.chatDrafts.set(chatId ?? "new", "");
|
||||||
|
resizeComposerInput();
|
||||||
|
}
|
||||||
const clearSentDraft = () => {
|
const clearSentDraft = () => {
|
||||||
if (state.chatDrafts.get(chatId)?.trim() === message) state.chatDrafts.delete(chatId);
|
|
||||||
attachmentComposer.clearSentDraft(chatId, attachments.map(item => item.id));
|
attachmentComposer.clearSentDraft(chatId, attachments.map(item => item.id));
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
setLoadingState(true, false);
|
setLoadingState(true, false);
|
||||||
if (retryMessage && !msgInput.value.trim()) {
|
|
||||||
msgInput.value = message;
|
|
||||||
resizeComposerInput();
|
|
||||||
}
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
const created = await createNewChat({preserveComposer: true, signal});
|
const created = await createNewChat({preserveComposer: true, signal});
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
@@ -152,6 +154,7 @@ export function createComposerController({
|
|||||||
state.currentMessages.push(reply);
|
state.currentMessages.push(reply);
|
||||||
if (loadingMsg?.isConnected) loadingMsg.replaceWith(addMessage(reply, false, false, {detached: true}));
|
if (loadingMsg?.isConnected) loadingMsg.replaceWith(addMessage(reply, false, false, {detached: true}));
|
||||||
else addMessage(reply);
|
else addMessage(reply);
|
||||||
|
announceReply(completedReply);
|
||||||
} else loadingMsg?.remove();
|
} else loadingMsg?.remove();
|
||||||
reasoningToggle.reset();
|
reasoningToggle.reset();
|
||||||
if (visible()) {
|
if (visible()) {
|
||||||
@@ -160,7 +163,6 @@ export function createComposerController({
|
|||||||
const refreshed = await setActiveChat(chatId);
|
const refreshed = await setActiveChat(chatId);
|
||||||
if (refreshed?.error) throw new Error(refreshed.error);
|
if (refreshed?.error) throw new Error(refreshed.error);
|
||||||
if (visible()) {
|
if (visible()) {
|
||||||
if (msgInput.value.trim() === message) { msgInput.value = ""; resizeComposerInput(); }
|
|
||||||
if (msgInput.value) state.chatDrafts.set(chatId, msgInput.value);
|
if (msgInput.value) state.chatDrafts.set(chatId, msgInput.value);
|
||||||
else state.chatDrafts.delete(chatId);
|
else state.chatDrafts.delete(chatId);
|
||||||
const currentAttachments = attachmentComposer.attachments;
|
const currentAttachments = attachmentComposer.attachments;
|
||||||
@@ -178,11 +180,16 @@ export function createComposerController({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
loadingMsg?.remove();
|
loadingMsg?.remove();
|
||||||
const messageText = signal.aborted ? "Generation stopped." : error?.message || "Unable to send message.";
|
const messageText = signal.aborted ? "Generation stopped." : error?.message || "Unable to send message.";
|
||||||
|
if (visible() && completedReply === null) announceResponseStatus(messageText);
|
||||||
if (completedReply !== null) {
|
if (completedReply !== null) {
|
||||||
setNotice(`Reply sent, but the conversation could not be refreshed: ${messageText}`, "error");
|
setNotice(`Reply sent, but the conversation could not be refreshed: ${messageText}`, "error");
|
||||||
} else {
|
} else {
|
||||||
if (visible()) {
|
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.deliveryStatus = "failed";
|
||||||
userMessage.errorMessage = messageText;
|
userMessage.errorMessage = messageText;
|
||||||
if (userCard?.isConnected) userCard.replaceWith(addMessage(userMessage, true, false, {detached: true}));
|
if (userCard?.isConnected) userCard.replaceWith(addMessage(userMessage, true, false, {detached: true}));
|
||||||
@@ -193,7 +200,7 @@ export function createComposerController({
|
|||||||
rendering?.dispose();
|
rendering?.dispose();
|
||||||
sendController = null;
|
sendController = null;
|
||||||
state.sendingChatId = null;
|
state.sendingChatId = null;
|
||||||
setLoadingState(false); msgInput.focus(); updateChatActionState();
|
setLoadingState(false); updateChatActionState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {post, put, del} from "./api.js";
|
import {post, put, del} from "./api.js";
|
||||||
import {regenerateInline} from "./regeneration.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) {
|
function getMessageIndexById(messageId) {
|
||||||
return conversation.currentMessages.findIndex((message) => message.id === messageId);
|
return conversation.currentMessages.findIndex((message) => message.id === messageId);
|
||||||
}
|
}
|
||||||
@@ -106,8 +106,12 @@ export function createMessageActions({conversation, setNotice, promptPopup, conf
|
|||||||
webSearch: webSearchToggle.enabled,
|
webSearch: webSearchToggle.enabled,
|
||||||
confirmed: true
|
confirmed: true
|
||||||
}, {timeoutMs: 660000});
|
}, {timeoutMs: 660000});
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) { announceResponseStatus(res.error); return setNotice(res.error, "error"); }
|
||||||
if (conversation.activeChatId === sourceChatId) await openCreatedChat(res.chat);
|
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");
|
return setNotice("Created the edited chat branch.", "success");
|
||||||
} finally {
|
} finally {
|
||||||
conversation.regenerationChatId = null;
|
conversation.regenerationChatId = null;
|
||||||
@@ -184,11 +188,13 @@ export function createMessageActions({conversation, setNotice, promptPopup, conf
|
|||||||
syncRetryStateFromMessage(conversation.currentMessages[index]);
|
syncRetryStateFromMessage(conversation.currentMessages[index]);
|
||||||
const rendered = addMessage(conversation.currentMessages[index], false, false, { messageIndex: index, detached: true });
|
const rendered = addMessage(conversation.currentMessages[index], false, false, { messageIndex: index, detached: true });
|
||||||
card.replaceChildren(...rendered.childNodes);
|
card.replaceChildren(...rendered.childNodes);
|
||||||
|
announceReply(message.content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
reasoningToggle.reset();
|
reasoningToggle.reset();
|
||||||
setNotice("Reply regenerated.", "success");
|
setNotice("Reply regenerated.", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
announceResponseStatus(error.message || "Unable to regenerate message.");
|
||||||
setNotice(error.message || "Unable to regenerate message.", "error");
|
setNotice(error.message || "Unable to regenerate message.", "error");
|
||||||
} finally {
|
} finally {
|
||||||
conversation.regenerationChatId = null;
|
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 {appendAttachments} from "./attachments.js?v=20260930-chat-usability";
|
||||||
import {updateReasoning} from "./reasoning.js?v=20260921-reasoning-capability";
|
import {updateReasoning} from "./reasoning.js?v=20260921-reasoning-capability";
|
||||||
import {appendSources} from "./webSearch.js";
|
import {appendSources} from "./webSearch.js";
|
||||||
|
import {get} from "./api.js";
|
||||||
|
|
||||||
export function createMessageRenderer({
|
export function createMessageRenderer({
|
||||||
getChatById,
|
getChatById,
|
||||||
@@ -17,17 +18,157 @@ export function createMessageRenderer({
|
|||||||
renderDrawerSummary,
|
renderDrawerSummary,
|
||||||
updateChatActionState,
|
updateChatActionState,
|
||||||
updateComposerPlaceholder,
|
updateComposerPlaceholder,
|
||||||
createNewChat
|
createNewChat,
|
||||||
|
openMessage
|
||||||
}) {
|
}) {
|
||||||
const jumpToLatest = document.getElementById("jumpToLatest");
|
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 scrollFrame = null;
|
||||||
|
|
||||||
let renderedChatId = null;
|
let renderedChatId = null;
|
||||||
|
|
||||||
let renderedMessages = new Map();
|
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 markdownParser = window.marked;
|
||||||
const purifier = window.DOMPurify;
|
const purifier = window.DOMPurify;
|
||||||
if (markdownParser?.parse && purifier?.sanitize) {
|
if (markdownParser?.parse && purifier?.sanitize) {
|
||||||
@@ -37,6 +178,11 @@ export function createMessageRenderer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMessageContent(el, content) {
|
||||||
|
renderMarkdown(el, content);
|
||||||
|
scheduleSearchRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
function scrollMessagesToBottom() {
|
function scrollMessagesToBottom() {
|
||||||
if (scrollFrame !== null) cancelAnimationFrame(scrollFrame);
|
if (scrollFrame !== null) cancelAnimationFrame(scrollFrame);
|
||||||
scrollFrame = requestAnimationFrame(() => {
|
scrollFrame = requestAnimationFrame(() => {
|
||||||
@@ -60,7 +206,7 @@ export function createMessageRenderer({
|
|||||||
msgDiv.setAttribute("aria-label", "Assistant is responding");
|
msgDiv.setAttribute("aria-label", "Assistant is responding");
|
||||||
msgDiv.innerHTML = `
|
msgDiv.innerHTML = `
|
||||||
<div class="msg-header">Assistant</div>
|
<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">
|
<div class="loading-bars">
|
||||||
<span class="loading-bar"></span>
|
<span class="loading-bar"></span>
|
||||||
<span class="loading-bar"></span>
|
<span class="loading-bar"></span>
|
||||||
@@ -251,6 +397,13 @@ export function createMessageRenderer({
|
|||||||
renderedMessages = nextRendered;
|
renderedMessages = nextRendered;
|
||||||
}
|
}
|
||||||
renderedChatId = state.activeChatId;
|
renderedChatId = state.activeChatId;
|
||||||
|
if (searchChatId !== state.activeChatId) {
|
||||||
|
searchNavigation++;
|
||||||
|
searchChatId = state.activeChatId;
|
||||||
|
selectedMatch = 0;
|
||||||
|
scheduleConversationSearch(true, true);
|
||||||
|
} else if (searchInput.value.trim()) scheduleConversationSearch();
|
||||||
|
refreshConversationSearch();
|
||||||
renderDrawerSummary();
|
renderDrawerSummary();
|
||||||
updateChatActionState();
|
updateChatActionState();
|
||||||
updateComposerPlaceholder();
|
updateComposerPlaceholder();
|
||||||
@@ -325,6 +478,9 @@ export function createMessageRenderer({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setMessageContent,
|
setMessageContent,
|
||||||
|
announceResponding,
|
||||||
|
announceResponseStatus,
|
||||||
|
announceReply,
|
||||||
scrollMessagesToBottom,
|
scrollMessagesToBottom,
|
||||||
updateJumpToLatest,
|
updateJumpToLatest,
|
||||||
addMessage,
|
addMessage,
|
||||||
|
|||||||
+13
-6
@@ -107,6 +107,7 @@ import {
|
|||||||
const conversation = createConversationState();
|
const conversation = createConversationState();
|
||||||
|
|
||||||
const headerModelButton = document.getElementById("headerModelButton");
|
const headerModelButton = document.getElementById("headerModelButton");
|
||||||
|
const stopBtn = document.getElementById("stopGeneration");
|
||||||
const webSearchButton = document.getElementById("webSearch");
|
const webSearchButton = document.getElementById("webSearch");
|
||||||
const webSearchToggle = createWebSearchToggle(webSearchButton);
|
const webSearchToggle = createWebSearchToggle(webSearchButton);
|
||||||
const reasoningToggle = createReasoningToggle(document.getElementById("reasoning"));
|
const reasoningToggle = createReasoningToggle(document.getElementById("reasoning"));
|
||||||
@@ -124,7 +125,7 @@ let availableModelsById = new Map();
|
|||||||
const personalitySelect = document.getElementById("personalitySelect");
|
const personalitySelect = document.getElementById("personalitySelect");
|
||||||
let personalities = [];
|
let personalities = [];
|
||||||
|
|
||||||
const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage, renderMessages} = createMessageRenderer({
|
const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage, renderMessages, announceResponding, announceReply, announceResponseStatus} = createMessageRenderer({
|
||||||
getChatById: (...args) => getChatById(...args),
|
getChatById: (...args) => getChatById(...args),
|
||||||
state: conversation,
|
state: conversation,
|
||||||
setNotice: (...args) => setNotice(...args),
|
setNotice: (...args) => setNotice(...args),
|
||||||
@@ -138,7 +139,8 @@ const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage
|
|||||||
renderDrawerSummary: (...args) => renderDrawerSummary(...args),
|
renderDrawerSummary: (...args) => renderDrawerSummary(...args),
|
||||||
updateChatActionState: (...args) => updateChatActionState(...args),
|
updateChatActionState: (...args) => updateChatActionState(...args),
|
||||||
updateComposerPlaceholder: (...args) => updateComposerPlaceholder(...args),
|
updateComposerPlaceholder: (...args) => updateComposerPlaceholder(...args),
|
||||||
createNewChat: (...args) => createNewChat(...args)
|
createNewChat: (...args) => createNewChat(...args),
|
||||||
|
openMessage: messageId => setActiveChat(conversation.activeChatId, {matchMessageId: messageId})
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -195,7 +197,7 @@ const {
|
|||||||
|
|
||||||
const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, sendMessage} = createComposerController({
|
const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, sendMessage} = createComposerController({
|
||||||
state: conversation,
|
state: conversation,
|
||||||
elements: {msgInput, sendBtn, messagesDiv, modelSelect},
|
elements: {msgInput, sendBtn, stopBtn, messagesDiv, modelSelect},
|
||||||
attachmentComposer,
|
attachmentComposer,
|
||||||
webSearchButton,
|
webSearchButton,
|
||||||
reasoningToggle,
|
reasoningToggle,
|
||||||
@@ -211,6 +213,9 @@ const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, se
|
|||||||
updateChatActionState: (...args) => updateChatActionState(...args),
|
updateChatActionState: (...args) => updateChatActionState(...args),
|
||||||
scrollMessagesToBottom: (...args) => scrollMessagesToBottom(...args),
|
scrollMessagesToBottom: (...args) => scrollMessagesToBottom(...args),
|
||||||
setMessageContent: (...args) => setMessageContent(...args),
|
setMessageContent: (...args) => setMessageContent(...args),
|
||||||
|
announceResponding,
|
||||||
|
announceReply,
|
||||||
|
announceResponseStatus,
|
||||||
setActiveChat: (...args) => setActiveChat(...args),
|
setActiveChat: (...args) => setActiveChat(...args),
|
||||||
loadSummary: (...args) => loadSummary(...args),
|
loadSummary: (...args) => loadSummary(...args),
|
||||||
markOnboardingCompleted: (...args) => markOnboardingCompleted(...args),
|
markOnboardingCompleted: (...args) => markOnboardingCompleted(...args),
|
||||||
@@ -221,7 +226,7 @@ const {exportCurrentChat, backupWorkspace, importWorkspaceFromFile} = createWork
|
|||||||
conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadChatSessions
|
conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadChatSessions
|
||||||
});
|
});
|
||||||
const {editChatMessage, deleteChatMessage, retryLatestAssistantMessage, switchResponseVersion, switchRetryVariant, branchFromMessage} = createMessageActions({
|
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});
|
const memoryPanel = createMemoryPanel({state: conversation});
|
||||||
@@ -373,6 +378,7 @@ function maybeShowOnboarding() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateComposerPlaceholder() {
|
function updateComposerPlaceholder() {
|
||||||
|
if (conversation.isProcessing) { msgInput.placeholder = "Draft your next message..."; return; }
|
||||||
const activeChat = getChatById(conversation.activeChatId);
|
const activeChat = getChatById(conversation.activeChatId);
|
||||||
if (!activeChat) {
|
if (!activeChat) {
|
||||||
msgInput.placeholder = "Create a chat to begin...";
|
msgInput.placeholder = "Create a chat to begin...";
|
||||||
@@ -979,8 +985,9 @@ function bindChatDrawerActions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendBtn.addEventListener("click", () => { void (conversation.isProcessing ? stopGeneration() : sendMessage()); });
|
sendBtn.addEventListener("click", () => { void sendMessage(); });
|
||||||
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { event.preventDefault(); 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 () {
|
msgInput.addEventListener("input", function () {
|
||||||
if (conversation.currentUsername) conversation.chatDrafts.set(conversation.activeChatId ?? "new", msgInput.value);
|
if (conversation.currentUsername) conversation.chatDrafts.set(conversation.activeChatId ?? "new", msgInput.value);
|
||||||
resizeComposerInput();
|
resizeComposerInput();
|
||||||
|
|||||||
@@ -228,6 +228,24 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
|||||||
return res.json({message: "Chat deleted"});
|
return res.json({message: "Chat deleted"});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/chats/:id/messages/search", (req, res) => {
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
if (!repositories.getChat(chatId, req.session.user)) return res.status(404).json({error: "Chat not found"});
|
||||||
|
const query = String(req.query.q || "").trim().slice(0, 200).toLocaleLowerCase();
|
||||||
|
if (!query) return res.json({matches: []});
|
||||||
|
const matches = [];
|
||||||
|
for (const message of repositories.listActiveBranchMessages(chatId)) {
|
||||||
|
const content = String(message.content || "").toLocaleLowerCase();
|
||||||
|
let at = content.indexOf(query);
|
||||||
|
let occurrence = 0;
|
||||||
|
while (at >= 0) {
|
||||||
|
matches.push({messageId: message.id, occurrence: occurrence++});
|
||||||
|
at = content.indexOf(query, at + query.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.json({matches});
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/chats/:id/messages", (req, res) => {
|
router.get("/chats/:id/messages", (req, res) => {
|
||||||
const chatId = Number(req.params.id);
|
const chatId = Number(req.params.id);
|
||||||
const chat = repositories.getChat(chatId, req.session.user);
|
const chat = repositories.getChat(chatId, req.session.user);
|
||||||
|
|||||||
Reference in New Issue
Block a user