diff --git a/app/MindWork AI Studio/App.razor b/app/MindWork AI Studio/App.razor index 7df24793..e05a7749 100644 --- a/app/MindWork AI Studio/App.razor +++ b/app/MindWork AI Studio/App.razor @@ -21,6 +21,9 @@ + diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index b3d58859..b37b729b 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -154,12 +154,17 @@ internal sealed class Program // ReSharper restore AccessToDisposedClosure builder.Services.AddRazorComponents() - .AddInteractiveServerComponents() + .AddInteractiveServerComponents(options => + { + options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromDays(30); + options.DisconnectedCircuitMaxRetained = 2; + }) .AddHubOptions(options => { options.MaximumReceiveMessageSize = null; - options.ClientTimeoutInterval = TimeSpan.FromDays(14); + options.ClientTimeoutInterval = TimeSpan.FromSeconds(120); options.HandshakeTimeout = TimeSpan.FromSeconds(30); + options.KeepAliveInterval = TimeSpan.FromSeconds(30); }); builder.Services.AddSingleton(new HttpClient diff --git a/app/MindWork AI Studio/wwwroot/boot.js b/app/MindWork AI Studio/wwwroot/boot.js index d18dd7e9..4d605ee0 100644 --- a/app/MindWork AI Studio/wwwroot/boot.js +++ b/app/MindWork AI Studio/wwwroot/boot.js @@ -1,33 +1,73 @@ (() => { - const maximumRetryCount = 3; - const retryIntervalMilliseconds = 500; + const maximumRetryCount = 12; const reconnectModal = document.getElementById('reconnect-modal'); + const retryDelaysMilliseconds = [ + 0, + 1_000, + 2_000, + 5_000, + 10_000, + 15_000, + 30_000, + ]; + + let currentReconnectionProcess = null; + let isConnectionDown = false; + + const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); + + const getRetryDelayMilliseconds = attempt => retryDelaysMilliseconds[Math.min(attempt, retryDelaysMilliseconds.length - 1)]; + + const showReconnectModal = () => { + if (reconnectModal) + reconnectModal.style.display = 'flex'; + }; + + const hideReconnectModal = () => { + if (reconnectModal) + reconnectModal.style.display = 'none'; + }; + + const setReconnectModalText = text => { + if (reconnectModal) + reconnectModal.textContent = text; + }; const startReconnectionProcess = () => { - reconnectModal.style.display = 'block'; + showReconnectModal(); let isCanceled = false; + let forceAttempt = false; - (async () => { - for (let i = 0; i < maximumRetryCount; i++) { - reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`; + const waitForNextAttempt = async milliseconds => { + const startedAt = Date.now(); + while (!isCanceled && !forceAttempt && Date.now() - startedAt < milliseconds) + await delay(250); - await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds)); + forceAttempt = false; + }; - if (isCanceled) { + void (async () => { + for (let attempt = 0; attempt < maximumRetryCount && !isCanceled; attempt++) { + setReconnectModalText(`Reconnecting to AI Studio (${attempt + 1}/${maximumRetryCount})...`); + + const retryDelayMilliseconds = getRetryDelayMilliseconds(attempt); + if (retryDelayMilliseconds > 0) + await waitForNextAttempt(retryDelayMilliseconds); + + if (isCanceled) return; - } try { const result = await Blazor.reconnect(); - if (!result) { + if (result === false) { // The server was reached, but the connection was rejected; reload the page. location.reload(); return; } - // Successfully reconnected to the server. - return; + if (result === true) + return; } catch { // Didn't reach the server; try again. } @@ -40,25 +80,42 @@ return { cancel: () => { isCanceled = true; - reconnectModal.style.display = 'none'; + hideReconnectModal(); + }, + triggerImmediateAttempt: () => { + forceAttempt = true; }, }; }; - let currentReconnectionProcess = null; + const triggerReconnectAfterWake = () => { + if (isConnectionDown) + currentReconnectionProcess?.triggerImmediateAttempt(); + }; + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') + triggerReconnectAfterWake(); + }); + + globalThis.addEventListener('pageshow', triggerReconnectAfterWake); Blazor.start({ circuit: { reconnectionHandler: { - onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(), + onConnectionDown: () => { + isConnectionDown = true; + currentReconnectionProcess ??= startReconnectionProcess(); + }, onConnectionUp: () => { + isConnectionDown = false; currentReconnectionProcess?.cancel(); currentReconnectionProcess = null; } }, configureSignalR: function (builder) { - builder.withServerTimeout(1_200_000); + builder.withServerTimeout(120_000); builder.withKeepAliveInterval(30_000); }, } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 39b67be4..24e7b9e3 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -1,5 +1,6 @@ # v26.7.3, build 245 (2026-07-xx xx:xx UTC) - Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks. +- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. - Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. - Upgraded Rust to v1.97.0. - Upgraded Tauri to v2.11.5.