diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index ca77acc7..95f6c584 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -145,6 +145,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan await this.MessageBus.SendMessage(this, Event.STARTUP_PLUGIN_SYSTEM); await this.themeProvider.WatchSystemDarkModeAsync(this.SystemThemeChanged); + this.CircuitState.ConnectionRestored += this.OnConnectionRestored; await this.UpdateThemeConfiguration(); this.LoadNavItems(); this.LoadEmbeddingItem(); @@ -583,6 +584,23 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan await this.UpdateThemeConfiguration(); } + /// + /// Reads the color theme anew once the browser connection of this circuit returned. + /// + /// + /// The browser reports a change of the system theme exactly once. Blazor drops that report while the + /// connection is down, which happens when the machine switches its theme during sleep and wakes up + /// again. Since the circuit survives the sleep (cf. the retention settings in Program.cs), no reload + /// reads the theme anew either, so AI Studio would keep the theme it had before the sleep. + ///

+ /// The update is deliberately not awaited: this handler runs while Blazor is still completing the + /// reconnection, and the answer to the JavaScript call inside can only arrive afterward. + ///
+ private void OnConnectionRestored() + { + this.InvokeAsync(this.UpdateThemeConfiguration).Observe($"{nameof(MainLayout)}: reading the color theme after the connection returned"); + } + private async Task UpdateThemeConfiguration() { if (this.FollowSystemTheme) @@ -677,6 +695,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan public void Dispose() { this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged; + this.CircuitState.ConnectionRestored -= this.OnConnectionRestored; this.MessageBus.Unregister(this); this.mandatoryInfoDialogSemaphore.Dispose(); } diff --git a/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs b/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs index 631d1dc8..a3cabfe8 100644 --- a/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs +++ b/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs @@ -28,6 +28,18 @@ public sealed class CircuitStateService /// public string CircuitId { get; private set; } = "n/a"; + /// + /// Occurs when the browser connection returned after it was lost. + /// + /// + /// It does not occur for the first connection of a circuit, only for the ones which follow a loss. Use it + /// to fetch again what the browser reports on its own: Blazor drops such reports while the connection is + /// down, and nothing sends them a second time. The event is raised while Blazor is still completing the + /// reconnection, though. A handler must not wait for JavaScript interop, because the browser's answer can + /// only be processed once the reconnection has finished. Start such work without awaiting it instead. + /// + public event Action? ConnectionRestored; + /// /// Called by the circuit handler when the circuit was opened. /// @@ -37,7 +49,17 @@ public sealed class CircuitStateService /// /// Called by the circuit handler when the browser connection was established or restored. /// - public void MarkAsConnected() => this.isConnected = true; + /// + /// A restored connection raises ConnectionRestored. Blazor never runs the handler's events of one circuit + /// concurrently, so reading and writing the state in two steps is safe here. + /// + public void MarkAsConnected() + { + var wasConnected = this.isConnected; + this.isConnected = true; + if (!wasConnected) + this.ConnectionRestored?.Invoke(); + } /// /// Called by the circuit handler when the browser connection was lost or the circuit ended. diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md index da3085df..61ddc6b8 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md @@ -88,5 +88,6 @@ - Fixed errors about a provider arriving as two messages at once, the second of which spoke of several attempts that were never made. You now get the single message which names the cause. - Fixed the button in the chat toolbar that deletes the current chat and starts a new one doing so without asking. It now asks for your confirmation first, just like the chat list does, because a deleted chat cannot be brought back. The button shows a delete icon in red now, instead of one that looked like a reload. - Fixed AI Studio following your system into light or dark mode even though you had chosen a fixed color theme in the app settings. +- Fixed AI Studio keeping its previous color theme after your computer woke up from sleep, when your system had switched between light and dark mode during that time. - Upgraded the Visual Briefing assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing assistant there. - Upgraded the vector database behind local RAG (Qdrant Edge) to version 0.8.0. diff --git a/app/Tests/Tools/CircuitStateServiceTests.cs b/app/Tests/Tools/CircuitStateServiceTests.cs new file mode 100644 index 00000000..7f414f68 --- /dev/null +++ b/app/Tests/Tools/CircuitStateServiceTests.cs @@ -0,0 +1,71 @@ +using AIStudio.Tools.Services; + +namespace AIStudio.Tests.Tools; + +/// +/// Checks when the circuit state reports that the browser connection came back. +/// +/// +/// The report is there to fetch again what the browser sent while the connection was down, because Blazor +/// drops it. The layout reads the color theme anew on it, for instance, since the machine may have switched +/// its theme during sleep. So it has to come after every loss, and only then: missing one leaves the app +/// with stale state, while a connection which was never lost has nothing to fetch again. +/// +[TestFixture] +public sealed class CircuitStateServiceTests +{ + [Test] + public void AConnectionWhichReturnsAfterALossIsReportedOnce() + { + var circuitState = new CircuitStateService(); + var numReports = 0; + circuitState.ConnectionRestored += () => numReports++; + + circuitState.MarkAsDisconnected(); + circuitState.MarkAsConnected(); + + Assert.That(numReports, Is.EqualTo(1), "The connection was lost and came back, so whatever the browser sent in between is gone."); + Assert.That(circuitState.IsConnected, Is.True); + } + + [Test] + public void TheFirstConnectionIsNotReported() + { + var circuitState = new CircuitStateService(); + var numReports = 0; + circuitState.ConnectionRestored += () => numReports++; + + circuitState.MarkAsConnected(); + + Assert.That(numReports, Is.Zero, "A circuit starts out connected, so its first connection has not lost anything."); + } + + [Test] + public void AConnectionWhichWasNotLostIsNotReportedAgain() + { + var circuitState = new CircuitStateService(); + var numReports = 0; + circuitState.ConnectionRestored += () => numReports++; + + circuitState.MarkAsDisconnected(); + circuitState.MarkAsConnected(); + circuitState.MarkAsConnected(); + + Assert.That(numReports, Is.EqualTo(1), "The second call follows a connection which was up all along."); + } + + [Test] + public void EveryLossIsReportedOnItsOwn() + { + var circuitState = new CircuitStateService(); + var numReports = 0; + circuitState.ConnectionRestored += () => numReports++; + + circuitState.MarkAsDisconnected(); + circuitState.MarkAsConnected(); + circuitState.MarkAsDisconnected(); + circuitState.MarkAsConnected(); + + Assert.That(numReports, Is.EqualTo(2), "The machine went to sleep twice, and each time something may have been lost."); + } +} \ No newline at end of file