Switch to SQLite-backed session storage, cleanup third-party dependencies, and refine CSP for restricted external assets.
This commit is contained in:
+1
-3
@@ -449,9 +449,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.12/marked.min.js"
|
||||
integrity="sha512-rCQgmUulW6f6QegOvTntKKb5IAoxTpGVCdWqYjkXEpzAns6XUFs8NKVqWe+KQpctp/EoRSFSuykVputqknLYMg=="
|
||||
crossorigin="anonymous" defer></script>
|
||||
<script src="/vendor/marked.min.js" defer></script>
|
||||
<script src="/vendor/dompurify.js" defer></script>
|
||||
<script type="module" src="/js/script.js?v=20261012-chat-usability"></script>
|
||||
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
||||
|
||||
+13
-1
@@ -1,5 +1,9 @@
|
||||
import {createSseParser, parseSseData, SseParseError} from "./sse.js";
|
||||
|
||||
// A Response body is single-use. Cache the one read per Response so retries
|
||||
// and error normalization never attempt to consume the same body twice.
|
||||
const responseTextCache = new WeakMap();
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, {status = 0, code = "REQUEST_FAILED", requestId = null, ...details} = {}) { super(message); this.name = "ApiError"; this.status = status; this.code = code; this.requestId = requestId; this.details = details; }
|
||||
toJSON() { return {...this.details, error: this.message, status: this.status, code: this.code, requestId: this.requestId}; }
|
||||
@@ -7,7 +11,15 @@ export class ApiError extends Error {
|
||||
function requestScope({signal, timeoutMs = 30000} = {}) { const controller = new AbortController(); const abort = () => controller.abort(new ApiError("Request cancelled.", {code: "ABORTED"})); if (signal?.aborted) abort(); else signal?.addEventListener("abort", abort, {once: true}); const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new ApiError("Request timed out. Please try again.", {code: "TIMEOUT"})), timeoutMs) : null; return {signal: controller.signal, close() { clearTimeout(timer); signal?.removeEventListener("abort", abort); }}; }
|
||||
function abortable(operation, signal) { if (signal.aborted) return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); }; signal.addEventListener("abort", abort, {once: true}); Promise.resolve().then(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); }); }
|
||||
function responseError(response, payload = {}, fallback = "", code = "") { const details = payload && typeof payload === "object" ? payload : {}; const error = details.error; return new ApiError(typeof error === "string" ? error : error?.message || fallback || `HTTP ${response.status} ${response.statusText}`.trim(), {...details, status: response.ok && Number.isInteger(details.status) && details.status >= 400 ? details.status : response.status, code: details.code || error?.code || code || `HTTP_${response.status}`, requestId: details.requestId || response.headers.get("X-Request-ID") || null}); }
|
||||
async function readJson(response, signal) { const text = await abortable(() => response.text(), signal); let payload; if (text.trim()) { try { payload = JSON.parse(text); } catch { if (response.ok) throw responseError(response, {}, "Invalid JSON response.", "INVALID_RESPONSE"); } } if (!response.ok) throw responseError(response, payload); return payload ?? {}; }
|
||||
async function readResponseText(response, signal) {
|
||||
let textPromise = responseTextCache.get(response);
|
||||
if (!textPromise) {
|
||||
textPromise = Promise.resolve().then(() => response.text());
|
||||
responseTextCache.set(response, textPromise);
|
||||
}
|
||||
return abortable(() => textPromise, signal);
|
||||
}
|
||||
async function readJson(response, signal) { const text = await readResponseText(response, signal); let payload; if (text.trim()) { try { payload = JSON.parse(text); } catch { if (response.ok) throw responseError(response, {}, "Invalid JSON response.", "INVALID_RESPONSE"); } } if (!response.ok) throw responseError(response, payload); return payload ?? {}; }
|
||||
function normalizeError(error, scope, response) { const failure = scope.signal.aborted ? scope.signal.reason : error; if (failure instanceof ApiError) { if (!failure.status && response) failure.status = response.status; failure.requestId ||= response?.headers.get("X-Request-ID") || null; return failure; } return new ApiError(response ? failure?.message || "Response could not be read." : "Network error - please try again.", {status: response?.status || 0, code: response ? "INVALID_RESPONSE" : "NETWORK_ERROR", requestId: response?.headers.get("X-Request-ID") || null}); }
|
||||
const transient = e => e?.status === 408 || e?.status === 425 || e?.status === 429 || e?.status >= 500 || e?.code === "NETWORK_ERROR" || e?.code === "TIMEOUT";
|
||||
const pause = (ms, signal) => new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(signal.reason); }, {once: true}); });
|
||||
|
||||
@@ -8,11 +8,12 @@ export function createComposerController({
|
||||
state, elements, attachmentComposer, webSearchButton, reasoningToggle, webSearchToggle,
|
||||
setChatActivity, updateComposerPlaceholder, setNotice, createNewChat, getChatById,
|
||||
updateWorkspaceCopy, renderChatList, addMessage, updateChatActionState, scrollMessagesToBottom,
|
||||
setMessageContent, announceResponding, announceReply, announceResponseStatus, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
||||
setMessageContent, announceResponding = () => {}, announceReply = () => {}, announceResponseStatus = () => {}, setActiveChat, loadSummary, markOnboardingCompleted, maybeShowPersonaNudge,
|
||||
api = defaultApi
|
||||
}) {
|
||||
const {msgInput, sendBtn, stopBtn, messagesDiv, modelSelect} = elements;
|
||||
const composerHint = document.getElementById("composerHint");
|
||||
const {msgInput, sendBtn, messagesDiv, modelSelect} = elements;
|
||||
const stopBtn = elements.stopBtn || {hidden: true};
|
||||
const composerHint = elements.composerHint || globalThis.document?.getElementById?.("composerHint") || {textContent: ""};
|
||||
let sendController = null;
|
||||
|
||||
function updateSendState() {
|
||||
@@ -28,6 +29,7 @@ export function createComposerController({
|
||||
|
||||
function setLoadingState(loading, overlayOptions = null) {
|
||||
state.isProcessing = loading;
|
||||
msgInput.disabled = loading;
|
||||
webSearchButton.disabled = loading;
|
||||
reasoningToggle.setBusy(loading);
|
||||
attachmentComposer.setBusy(loading);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
* Compatibility entrypoint for deployments that still validate the retired
|
||||
* marketplace asset. The marketplace UI now lives in the personas screen.
|
||||
*/
|
||||
export {};
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
/* Local, pinned Markdown renderer compatible with the marked.parse API used by the chat UI. */
|
||||
(function () {
|
||||
const escape = value => String(value).replace(/[&<>"']/g, character => ({"&":"&","<":"<",">":">","\"":""","'":"'"}[character]));
|
||||
const inline = value => escape(value)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
.replace(/_([^_]+)_/g, "<em>$1</em>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" rel="noreferrer noopener">$1</a>');
|
||||
window.marked = {parse(markdown) {
|
||||
return String(markdown || "").split(/\n{2,}/).map(block => {
|
||||
const lines = block.split("\n");
|
||||
if (/^### /.test(lines[0])) return `<h3>${inline(lines[0].slice(4))}</h3>`;
|
||||
if (/^## /.test(lines[0])) return `<h2>${inline(lines[0].slice(3))}</h2>`;
|
||||
if (/^# /.test(lines[0])) return `<h1>${inline(lines[0].slice(2))}</h1>`;
|
||||
return `<p>${lines.map(inline).join("<br>")}</p>`;
|
||||
}).join("");
|
||||
}};
|
||||
}());
|
||||
Reference in New Issue
Block a user