improve reasoning toggle tests and refactor model capability checks

This commit is contained in:
2026-09-22 10:30:03 +02:00
parent a8e348e0c7
commit 3b6c8969cc
15 changed files with 201 additions and 23 deletions
+5
View File
@@ -196,7 +196,12 @@
<div class="input-meta">
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
</div>
<div id="uploadPreviews" class="upload-previews" aria-live="polite" hidden></div>
<div class="input-container">
<input id="imagePicker" type="file" accept="image/png,image/jpeg,image/webp" multiple hidden>
<button id="attachImage" class="icon-button" type="button" aria-label="Attach image" title="Attach image">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8" cy="8" r="1.5"/><path d="m3 17 5-5 4 4 4-6 5 7"/></svg>
</button>
<button id="webSearch" class="icon-button" type="button" aria-label="Search the web" title="Search the web" aria-pressed="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18"/></svg>
</button>
+15 -6
View File
@@ -1,5 +1,6 @@
import {del, get, post, put, stream} from "./app/api.js?v=20260909-account-controls";
import {appendSources, createWebSearchToggle} from "./app/webSearch.js";
import {appendAttachments, createAttachmentComposer} from "./app/attachments.js";
import {createReasoningToggle, finishReasoning, updateReasoning} from "./app/reasoning.js?v=20260921-reasoning-capability";
import {closeChatDrawer, openChatDrawer} from "./app/chatDrawer.js?v=20260909-account-controls";
import {onboardingPrompts, requestedChatId} from "./app/constants.js?v=20260909-account-controls";
@@ -109,6 +110,7 @@ const webSearchToggle = createWebSearchToggle(webSearchButton);
const reasoningToggle = createReasoningToggle(document.getElementById("reasoning"));
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
const attachmentComposer = createAttachmentComposer({onChange: updateSendState, onError: message => setNotice(message, "error")});
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set();
let popupResolver = null, popupMode = null, popupLastFocus = null;
@@ -329,7 +331,7 @@ function updateComposerPlaceholder() {
function updateSendState() {
const hasText = Boolean(msgInput.value.trim());
sendBtn.disabled = isProcessing ? false : !hasText;
sendBtn.disabled = isProcessing ? false : attachmentComposer.pending || (!hasText && !attachmentComposer.attachments.length);
sendBtn.setAttribute("aria-label", isProcessing ? "Stop generation" : "Send message");
}
@@ -742,7 +744,9 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
actions.append(prevBtn, indexBadge, nextBtn);
}
msgDiv.append(header, body);
msgDiv.append(header);
appendAttachments(msgDiv, message.attachments);
msgDiv.append(body);
if (!isMessageUser) updateReasoning(msgDiv, message.thinking);
if (!isMessageUser) appendSources(msgDiv, message.sources);
if (message.deliveryStatus === "failed" && message.errorMessage) {
@@ -1364,6 +1368,7 @@ function setLoadingState(loading, overlayOptions = null) {
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";
@@ -1811,7 +1816,9 @@ async function checkSession() {
async function sendMessage() {
if (isProcessing) return;
const message = msgInput.value.trim();
if (attachmentComposer.pending) return;
const attachments = attachmentComposer.attachments;
const message = msgInput.value.trim() || (attachments.length ? "Describe the attached images." : "");
if (!message) return;
const webSearch = webSearchToggle.enabled;
const reasoning = reasoningToggle.enabled;
@@ -1821,7 +1828,8 @@ async function sendMessage() {
const autoTitle = message.length > 40 ? `${message.slice(0, 40)}...` : message;
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
}
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
const userMessage = {role: "user", content: message, attachments};
currentMessages.push(userMessage); addMessage(userMessage, true); updateChatActionState();
msgInput.value = "";
resizeComposerInput();
setLoadingState(true, false); setNotice("Generating reply...");
@@ -1832,14 +1840,15 @@ async function sendMessage() {
loadingLabel.textContent = "Preparing response...";
try {
let streamError = null;
await stream("/chat/stream", {message, model: modelSelect.value, chatId: activeChatId, webSearch, reasoning}, {
await stream("/chat/stream", {message, attachments: attachments.map(item => item.id), model: modelSelect.value, chatId: activeChatId, webSearch, reasoning}, {
onStatus: ({status, sourceCount, searchState}) => {
if (status === "accepted") {
attachmentComposer.clear();
webSearchToggle.reset();
reasoningToggle.reset();
return;
}
loadingLabel.textContent = status === "searching" ? "Searching the web..."
loadingLabel.textContent = status === "analyzing" ? "Analyzing images..." : status === "searching" ? "Searching the web..."
: searchState === "no_relevant_results" ? "No sufficiently relevant results. Generating response..."
: sourceCount === 0 ? "No web results found. Generating response..." : "Generating response...";
},
+12 -1
View File
@@ -456,7 +456,18 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
.input-meta, .input-container { width: min(940px, 100%); margin-inline: auto; }
.input-meta { margin-bottom: 5px; padding-left: 10px; }
.composer-hint { margin: 0; color: var(--text-muted); font-size: .72rem; }
.input-container { padding: 7px; display: grid; grid-template-columns: 44px 44px minmax(0, 1fr) 44px; grid-template-areas: "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; 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); }
#attachImage { grid-area: attach; }
.upload-previews { width: min(940px, 100%); margin: 0 auto 8px; display: flex; gap: 8px; flex-wrap: wrap; }
.upload-previews[hidden] { display: none; }
.upload-preview { display: flex; align-items: center; gap: 8px; padding: 6px; border: 1px solid var(--border-strong); border-radius: 12px; max-width: 100%; }
.upload-preview span { overflow-wrap: anywhere; font-size: 12px; }
.upload-preview img { width: 48px; height: 48px; object-fit: contain; border-radius: 6px; }
.message-images { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.message-images img { max-width: min(360px, 100%); max-height: 300px; object-fit: contain; border-radius: 12px; }
.message-images a { max-width: 100%; }
.image-drag-target { outline: 2px solid currentColor; }
@media (max-width: 480px) { .input-container { grid-template-columns: 36px 36px 36px minmax(0, 1fr) 36px; gap: 3px; } }
#webSearch, #reasoning { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 17px; }
#webSearch { grid-area: search; }
#reasoning { grid-area: reasoning; }