diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs index c6513cc2..b31bd188 100644 --- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs +++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs @@ -1,6 +1,7 @@ using System.Text; using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.Agenda; @@ -185,6 +186,85 @@ public partial class AssistantAgenda : AssistantBaseCore private string inputWhoIsPresenting = string.Empty; private readonly List contentLines = []; + private static readonly AssistantSessionStateKey INPUT_TOPIC_STATE_KEY = new(nameof(inputTopic)); + private static readonly AssistantSessionStateKey INPUT_NAME_STATE_KEY = new(nameof(inputName)); + private static readonly AssistantSessionStateKey INPUT_CONTENT_STATE_KEY = new(nameof(inputContent)); + private static readonly AssistantSessionStateKey INPUT_DURATION_STATE_KEY = new(nameof(inputDuration)); + private static readonly AssistantSessionStateKey INPUT_START_TIME_STATE_KEY = new(nameof(inputStartTime)); + private static readonly AssistantSessionStateKey> SELECTED_FOCI_STATE_KEY = new(nameof(selectedFoci)); + private static readonly AssistantSessionStateKey> JUST_BRIEFLY_STATE_KEY = new(nameof(justBriefly)); + private static readonly AssistantSessionStateKey INPUT_OBJECTIVE_STATE_KEY = new(nameof(inputObjective)); + private static readonly AssistantSessionStateKey INPUT_MODERATOR_STATE_KEY = new(nameof(inputModerator)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey INTRODUCE_PARTICIPANTS_STATE_KEY = new(nameof(introduceParticipants)); + private static readonly AssistantSessionStateKey IS_MEETING_VIRTUAL_STATE_KEY = new(nameof(isMeetingVirtual)); + private static readonly AssistantSessionStateKey INPUT_LOCATION_STATE_KEY = new(nameof(inputLocation)); + private static readonly AssistantSessionStateKey GOING_TO_DINNER_STATE_KEY = new(nameof(goingToDinner)); + private static readonly AssistantSessionStateKey DOING_SOCIAL_ACTIVITY_STATE_KEY = new(nameof(doingSocialActivity)); + private static readonly AssistantSessionStateKey NEED_TO_ARRIVE_AND_DEPART_STATE_KEY = new(nameof(needToArriveAndDepart)); + private static readonly AssistantSessionStateKey DURATION_LUNCH_BREAK_STATE_KEY = new(nameof(durationLunchBreak)); + private static readonly AssistantSessionStateKey DURATION_BREAKS_STATE_KEY = new(nameof(durationBreaks)); + private static readonly AssistantSessionStateKey ACTIVE_PARTICIPATION_STATE_KEY = new(nameof(activeParticipation)); + private static readonly AssistantSessionStateKey NUMBER_PARTICIPANTS_STATE_KEY = new(nameof(numberParticipants)); + private static readonly AssistantSessionStateKey INPUT_WHO_IS_PRESENTING_STATE_KEY = new(nameof(inputWhoIsPresenting)); + private static readonly AssistantSessionStateKey> CONTENT_LINES_STATE_KEY = new(nameof(contentLines)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TOPIC_STATE_KEY, this.inputTopic); + state.Set(INPUT_NAME_STATE_KEY, this.inputName); + state.Set(INPUT_CONTENT_STATE_KEY, this.inputContent); + state.Set(INPUT_DURATION_STATE_KEY, this.inputDuration); + state.Set(INPUT_START_TIME_STATE_KEY, this.inputStartTime); + state.SetHashSet(SELECTED_FOCI_STATE_KEY, this.selectedFoci); + state.SetHashSet(JUST_BRIEFLY_STATE_KEY, this.justBriefly); + state.Set(INPUT_OBJECTIVE_STATE_KEY, this.inputObjective); + state.Set(INPUT_MODERATOR_STATE_KEY, this.inputModerator); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(INTRODUCE_PARTICIPANTS_STATE_KEY, this.introduceParticipants); + state.Set(IS_MEETING_VIRTUAL_STATE_KEY, this.isMeetingVirtual); + state.Set(INPUT_LOCATION_STATE_KEY, this.inputLocation); + state.Set(GOING_TO_DINNER_STATE_KEY, this.goingToDinner); + state.Set(DOING_SOCIAL_ACTIVITY_STATE_KEY, this.doingSocialActivity); + state.Set(NEED_TO_ARRIVE_AND_DEPART_STATE_KEY, this.needToArriveAndDepart); + state.Set(DURATION_LUNCH_BREAK_STATE_KEY, this.durationLunchBreak); + state.Set(DURATION_BREAKS_STATE_KEY, this.durationBreaks); + state.Set(ACTIVE_PARTICIPATION_STATE_KEY, this.activeParticipation); + state.Set(NUMBER_PARTICIPANTS_STATE_KEY, this.numberParticipants); + state.Set(INPUT_WHO_IS_PRESENTING_STATE_KEY, this.inputWhoIsPresenting); + state.SetList(CONTENT_LINES_STATE_KEY, this.contentLines); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TOPIC_STATE_KEY, value => this.inputTopic = value); + state.Restore(INPUT_NAME_STATE_KEY, value => this.inputName = value); + state.Restore(INPUT_CONTENT_STATE_KEY, value => this.inputContent = value); + state.Restore(INPUT_DURATION_STATE_KEY, value => this.inputDuration = value); + state.Restore(INPUT_START_TIME_STATE_KEY, value => this.inputStartTime = value); + state.Restore(SELECTED_FOCI_STATE_KEY, value => this.selectedFoci = value); + state.Restore(JUST_BRIEFLY_STATE_KEY, value => this.justBriefly = value); + state.Restore(INPUT_OBJECTIVE_STATE_KEY, value => this.inputObjective = value); + state.Restore(INPUT_MODERATOR_STATE_KEY, value => this.inputModerator = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(INTRODUCE_PARTICIPANTS_STATE_KEY, value => this.introduceParticipants = value); + state.Restore(IS_MEETING_VIRTUAL_STATE_KEY, value => this.isMeetingVirtual = value); + state.Restore(INPUT_LOCATION_STATE_KEY, value => this.inputLocation = value); + state.Restore(GOING_TO_DINNER_STATE_KEY, value => this.goingToDinner = value); + state.Restore(DOING_SOCIAL_ACTIVITY_STATE_KEY, value => this.doingSocialActivity = value); + state.Restore(NEED_TO_ARRIVE_AND_DEPART_STATE_KEY, value => this.needToArriveAndDepart = value); + state.Restore(DURATION_LUNCH_BREAK_STATE_KEY, value => this.durationLunchBreak = value); + state.Restore(DURATION_BREAKS_STATE_KEY, value => this.durationBreaks = value); + state.Restore(ACTIVE_PARTICIPATION_STATE_KEY, value => this.activeParticipation = value); + state.Restore(NUMBER_PARTICIPANTS_STATE_KEY, value => this.numberParticipants = value); + state.Restore(INPUT_WHO_IS_PRESENTING_STATE_KEY, value => this.inputWhoIsPresenting = value); + state.RestoreList(CONTENT_LINES_STATE_KEY, this.contentLines); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 796de962..8adb8418 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -41,7 +41,7 @@ @this.SubmitText - @if (this.isProcessing && this.CancellationTokenSource is not null) + @if (this.isProcessing) { diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 79f650bb..d309c825 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -2,6 +2,7 @@ using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -36,6 +37,9 @@ public abstract partial class AssistantBase : AssistantLowerBase wher [Inject] private MudTheme ColorTheme { get; init; } = null!; + + [Inject] + protected AssistantSessionService AssistantSessionService { get; init; } = null!; protected abstract string Title { get; } @@ -119,12 +123,35 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected ChatThread? ChatThread; protected IContent? LastUserPrompt; protected CancellationTokenSource? CancellationTokenSource; + + private static readonly AssistantSessionStateKey PROVIDER_SETTINGS_STATE_KEY = new(nameof(ProviderSettings)); + private static readonly AssistantSessionStateKey INPUT_IS_VALID_STATE_KEY = new(nameof(InputIsValid)); + private static readonly AssistantSessionStateKey CURRENT_PROFILE_STATE_KEY = new(nameof(CurrentProfile)); + private static readonly AssistantSessionStateKey CURRENT_CHAT_TEMPLATE_STATE_KEY = new(nameof(CurrentChatTemplate)); + private static readonly AssistantSessionStateKey CHAT_THREAD_STATE_KEY = new(nameof(ChatThread)); + private static readonly AssistantSessionStateKey LAST_USER_PROMPT_STATE_KEY = new(nameof(LastUserPrompt)); + private static readonly AssistantSessionStateKey RESULTING_CONTENT_BLOCK_STATE_KEY = new(nameof(resultingContentBlock)); + private static readonly AssistantSessionStateKey INPUT_ISSUES_STATE_KEY = new(nameof(inputIssues)); + private static readonly AssistantSessionStateKey IS_PROCESSING_STATE_KEY = new(nameof(isProcessing)); private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6)); private ContentBlock? resultingContentBlock; private string[] inputIssues = []; private bool isProcessing; + private bool isDisposed; + private AssistantSessionKey assistantSessionKey; + private Guid? assistantSessionId; + + /// + /// Gets whether the Blazor component instance has already been disposed. + /// + protected bool IsAssistantComponentDisposed => this.isDisposed; + + /// + /// Gets the assistant-specific identifier used to distinguish session slots. + /// + protected virtual string AssistantSessionInstanceId => this.GetType().FullName ?? this.Component.ToString(); #region Overrides of ComponentBase @@ -150,6 +177,8 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component); + this.assistantSessionKey = new(this.Component, this.AssistantSessionInstanceId); + await this.AttachAssistantSessionIfAvailable(); } protected override async Task OnParametersSetAsync() @@ -191,12 +220,63 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private async Task Start() { - using (this.CancellationTokenSource = new()) + var activeSession = this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey); + if (activeSession?.IsActive ?? false) + { + await this.AttachAssistantSession(activeSession, restoreClientOnlyContent: true); + return; + } + + this.CancellationTokenSource = new(); + this.isProcessing = true; + var startedSession = this.AssistantSessionService.TryBegin(this.assistantSessionKey, this.Title, this.CancellationTokenSource, this.ChatThread, this.CaptureAssistantSessionState()); + if (startedSession.IsActive is not true || startedSession.Key != this.assistantSessionKey) + { + this.CancellationTokenSource.Dispose(); + this.CancellationTokenSource = null; + return; + } + + this.assistantSessionId = startedSession.SessionId; + await this.RefreshAssistantUIAsync(); + + var sessionStatus = AssistantSessionStatus.COMPLETED; + var errorMessage = string.Empty; + try { await this.SubmitAction(); + + if (this.CancellationTokenSource?.IsCancellationRequested ?? false) + sessionStatus = AssistantSessionStatus.CANCELED; + } + catch (OperationCanceledException) + { + sessionStatus = AssistantSessionStatus.CANCELED; + } + catch (ProviderRequestException e) + { + sessionStatus = AssistantSessionStatus.FAILED; + errorMessage = e.UserMessage; + this.Logger.LogError(e, "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", this.Title, e.StatusCode, e.ReasonPhrase, e.ResponseBody); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage)); + } + catch (Exception e) + { + sessionStatus = AssistantSessionStatus.FAILED; + errorMessage = e.Message; + this.Logger.LogError(e, "The assistant session '{AssistantTitle}' failed.", this.Title); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(this.TB("The assistant failed. The message is: '{0}'"), e.Message))); + } + finally + { + this.isProcessing = false; + var sessionCancellationTokenSource = this.CancellationTokenSource; + this.CancellationTokenSource = null; + if (this.assistantSessionId is { } sessionId) + await this.AssistantSessionService.CompleteAsync(this.assistantSessionKey, sessionId, sessionStatus, errorMessage, this.ChatThread, this.CaptureAssistantSessionState()); + sessionCancellationTokenSource?.Dispose(); + await this.RefreshAssistantUIAsync(); } - - this.CancellationTokenSource = null; } private void TriggerFormChange(FormFieldChangedEventArgs _) @@ -224,7 +304,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1); this.inputIssues[^1] = issue; this.InputIsValid = false; - this.StateHasChanged(); + _ = this.RefreshAssistantUIAsync(); } /// @@ -234,7 +314,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher { this.inputIssues = []; this.InputIsValid = true; - this.StateHasChanged(); + _ = this.RefreshAssistantUIAsync(); } protected void CreateChatThread() @@ -310,6 +390,18 @@ public abstract partial class AssistantBase : AssistantLowerBase wher InitialRemoteWait = true, }; + aiText.StreamingEvent = async () => + { + await this.CheckpointAssistantSession(); + await this.RefreshAssistantUIAsync(); + }; + + aiText.StreamingDone = async () => + { + await this.CheckpointAssistantSession(); + await this.RefreshAssistantUIAsync(); + }; + this.resultingContentBlock = new ContentBlock { Time = time, @@ -326,7 +418,8 @@ public abstract partial class AssistantBase : AssistantLowerBase wher } this.isProcessing = true; - this.StateHasChanged(); + await this.CheckpointAssistantSession(); + await this.RefreshAssistantUIAsync(); try { @@ -353,8 +446,9 @@ public abstract partial class AssistantBase : AssistantLowerBase wher } finally { - this.isProcessing = false; - this.StateHasChanged(); + this.isProcessing = this.assistantSessionId is not null && (this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey)?.IsActive ?? false); + await this.CheckpointAssistantSession(); + await this.RefreshAssistantUIAsync(); if(manageCancellationLocally) { @@ -366,9 +460,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private async Task CancelStreaming() { - if (this.CancellationTokenSource is not null) - if(!this.CancellationTokenSource.IsCancellationRequested) - await this.CancellationTokenSource.CancelAsync(); + await this.AssistantSessionService.CancelAsync(this.assistantSessionKey); } protected async Task CopyToClipboard() @@ -434,10 +526,10 @@ public abstract partial class AssistantBase : AssistantLowerBase wher await this.DialogService.ShowAsync(null, dialogParameters, DialogOptions.FULLSCREEN); } - protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) + protected async Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) { if (!this.CanSendToAssistant(destination)) - return Task.CompletedTask; + return; var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch { @@ -450,6 +542,16 @@ public abstract partial class AssistantBase : AssistantLowerBase wher }; var sendToData = destination.GetData(); + if (destination is not Tools.Components.CHAT && this.AssistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && snapshot.Key.Component == destination)) + { + await this.MessageBus.SendWarning(new(Icons.Material.Filled.Apps, this.TB("This assistant is already running. AI Studio opens the running session instead."))); + this.NavigationManager.NavigateTo(sendToData.Route); + return; + } + + if (destination is not Tools.Components.CHAT) + await this.AssistantSessionService.ClearInactiveSessionsForComponentAsync(destination); + switch (destination) { case Tools.Components.CHAT: @@ -469,7 +571,6 @@ public abstract partial class AssistantBase : AssistantLowerBase wher } this.NavigationManager.NavigateTo(sendToData.Route); - return Task.CompletedTask; } private bool CanSendToAssistant(Tools.Components component) @@ -482,6 +583,11 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private async Task InnerResetForm() { + if (this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey)?.IsActive ?? false) + return; + + await this.AssistantSessionService.ClearAsync(this.assistantSessionKey); + this.assistantSessionId = null; this.resultingContentBlock = null; this.ProviderSettings = Settings.Provider.NONE; @@ -495,7 +601,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.inputIssues = []; this.Form?.ResetValidation(); - this.StateHasChanged(); + await this.RefreshAssistantUIAsync(); this.Form?.ResetValidation(); } @@ -515,6 +621,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected override void DisposeResources() { + this.isDisposed = true; try { this.formChangeTimer.Stop(); @@ -529,4 +636,153 @@ public abstract partial class AssistantBase : AssistantLowerBase wher } #endregion -} + + #region Assistant sessions + + /// + /// Stores the current assistant UI and chat state in the active assistant session. + /// + /// A task that completes after the checkpoint was stored and published. + private Task CheckpointAssistantSession() + { + if (this.assistantSessionId is null) + return Task.CompletedTask; + + return this.AssistantSessionService.CheckpointAsync(this.assistantSessionKey, this.assistantSessionId.Value, this.Title, this.ChatThread, this.CaptureAssistantSessionState()); + } + + /// + /// Allows derived assistants to restore client-only UI after a session was attached. + /// + /// The assistant session snapshot that was attached. + /// A task that completes after derived UI restore work has finished. + protected virtual Task OnAssistantSessionAttachedAsync(AssistantSessionSnapshot snapshot) => Task.CompletedTask; + + /// + /// Handles assistant session change events for the current assistant instance. + /// + /// The message payload type. + /// The component that sent the message, if any. + /// The event that was triggered. + /// The message payload. + /// A task that completes after the message was processed. + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + switch (triggeredEvent) + { + case Event.ASSISTANT_SESSION_CHANGED: + case Event.ASSISTANT_SESSION_FINISHED: + if (data is AssistantSessionSnapshot snapshot && snapshot.Key == this.assistantSessionKey) + await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: triggeredEvent is Event.ASSISTANT_SESSION_FINISHED); + break; + } + } + + /// + /// Attaches the component to an existing assistant session if one is available. + /// + /// A task that completes after the session was attached. + private async Task AttachAssistantSessionIfAvailable() + { + var snapshot = this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey); + if (snapshot is null) + return; + + await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: true); + } + + /// + /// Applies an assistant session snapshot to this component instance. + /// + /// The snapshot to attach. + /// Whether derived assistants should restore client-only UI state. + /// A task that completes after the component was refreshed. + private async Task AttachAssistantSession(AssistantSessionSnapshot snapshot, bool restoreClientOnlyContent) + { + this.assistantSessionId = snapshot.SessionId; + this.ImportAssistantSessionState(snapshot.State); + this.ChatThread = snapshot.ChatThread ?? this.ChatThread; + this.isProcessing = snapshot.IsActive; + + if (!snapshot.IsActive) + this.CancellationTokenSource = null; + + if (restoreClientOnlyContent) + await this.OnAssistantSessionAttachedAsync(snapshot); + + await this.RefreshAssistantUIAsync(); + } + + /// + /// Refreshes the component when it is still mounted. + /// + /// A task that completes after the renderer was notified. + private async Task RefreshAssistantUIAsync() + { + if (this.isDisposed) + return; + + try + { + await this.InvokeAsync(this.StateHasChanged); + } + catch (InvalidOperationException) + { + // The component may already have left the renderer while a background session is finishing. + } + } + + /// + /// Captures the base assistant state and assistant-specific typed state values for session restore. + /// + /// A dictionary containing the current assistant state. + private Dictionary CaptureAssistantSessionState() + { + var state = new AssistantSessionStateWriter(); + state.Set(PROVIDER_SETTINGS_STATE_KEY, this.ProviderSettings); + state.Set(INPUT_IS_VALID_STATE_KEY, this.InputIsValid); + state.Set(CURRENT_PROFILE_STATE_KEY, this.CurrentProfile); + state.Set(CURRENT_CHAT_TEMPLATE_STATE_KEY, this.CurrentChatTemplate); + state.Set(CHAT_THREAD_STATE_KEY, this.ChatThread); + state.Set(LAST_USER_PROMPT_STATE_KEY, this.LastUserPrompt); + state.Set(RESULTING_CONTENT_BLOCK_STATE_KEY, this.resultingContentBlock); + state.Set(INPUT_ISSUES_STATE_KEY, this.inputIssues); + state.Set(IS_PROCESSING_STATE_KEY, this.isProcessing); + this.CaptureCustomAssistantSessionState(state); + + return state.ToDictionary(); + } + + /// + /// Captures assistant-specific state values. + /// + /// The typed state writer to update. + protected virtual void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) { } + + /// + /// Restores the base assistant state and assistant-specific typed state values from a session snapshot. + /// + /// The captured assistant state to import. + private void ImportAssistantSessionState(IReadOnlyDictionary state) + { + var reader = new AssistantSessionStateReader(state, this.Title); + reader.Restore(PROVIDER_SETTINGS_STATE_KEY, value => this.ProviderSettings = value); + reader.Restore(INPUT_IS_VALID_STATE_KEY, value => this.InputIsValid = value); + reader.Restore(CURRENT_PROFILE_STATE_KEY, value => this.CurrentProfile = value); + reader.Restore(CURRENT_CHAT_TEMPLATE_STATE_KEY, value => this.CurrentChatTemplate = value); + reader.Restore(CHAT_THREAD_STATE_KEY, value => this.ChatThread = value); + reader.Restore(LAST_USER_PROMPT_STATE_KEY, value => this.LastUserPrompt = value); + reader.Restore(RESULTING_CONTENT_BLOCK_STATE_KEY, value => this.resultingContentBlock = value); + reader.Restore(INPUT_ISSUES_STATE_KEY, value => this.inputIssues = value); + reader.Restore(IS_PROCESSING_STATE_KEY, value => this.isProcessing = value); + this.RestoreCustomAssistantSessionState(reader); + } + + /// + /// Restores assistant-specific state values. + /// + /// The typed state reader to read from. + protected virtual void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) { } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs index d1930b8e..c470a29f 100644 --- a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs @@ -3,6 +3,7 @@ using System.Text; using AIStudio.Chat; using AIStudio.Dialogs.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.BiasDay; @@ -66,6 +67,25 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore BIAS_OF_THE_DAY_STATE_KEY = new(nameof(biasOfTheDay)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(BIAS_OF_THE_DAY_STATE_KEY, this.biasOfTheDay); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(BIAS_OF_THE_DAY_STATE_KEY, value => this.biasOfTheDay = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + } private string? ValidateTargetLanguage(CommonLanguages language) { diff --git a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs index 5e8a3753..bb55421a 100644 --- a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs +++ b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs @@ -1,6 +1,7 @@ using System.Text; using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.Coding; @@ -59,6 +60,28 @@ public partial class AssistantCoding : AssistantBaseCore private bool provideCompilerMessages; private string compilerMessages = string.Empty; private string questions = string.Empty; + private static readonly AssistantSessionStateKey> CODING_CONTEXTS_STATE_KEY = new(nameof(codingContexts)); + private static readonly AssistantSessionStateKey PROVIDE_COMPILER_MESSAGES_STATE_KEY = new(nameof(provideCompilerMessages)); + private static readonly AssistantSessionStateKey COMPILER_MESSAGES_STATE_KEY = new(nameof(compilerMessages)); + private static readonly AssistantSessionStateKey QUESTIONS_STATE_KEY = new(nameof(questions)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.SetList(CODING_CONTEXTS_STATE_KEY, this.codingContexts); + state.Set(PROVIDE_COMPILER_MESSAGES_STATE_KEY, this.provideCompilerMessages); + state.Set(COMPILER_MESSAGES_STATE_KEY, this.compilerMessages); + state.Set(QUESTIONS_STATE_KEY, this.questions); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.RestoreList(CODING_CONTEXTS_STATE_KEY, this.codingContexts); + state.Restore(PROVIDE_COMPILER_MESSAGES_STATE_KEY, value => this.provideCompilerMessages = value); + state.Restore(COMPILER_MESSAGES_STATE_KEY, value => this.compilerMessages = value); + state.Restore(QUESTIONS_STATE_KEY, value => this.questions = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index 987c9f5c..436c5c4d 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -7,6 +7,7 @@ using AIStudio.Dialogs.Settings; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.AssistantSessions; using Microsoft.AspNetCore.Components; @@ -279,6 +280,55 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore loadedDocumentPaths = []; private readonly List> availableLLMProviders = new(); + private static readonly AssistantSessionStateKey SELECTED_POLICY_STATE_KEY = new(nameof(selectedPolicy)); + private static readonly AssistantSessionStateKey POLICY_IS_PROTECTED_STATE_KEY = new(nameof(policyIsProtected)); + private static readonly AssistantSessionStateKey POLICY_HIDE_POLICY_DEFINITION_STATE_KEY = new(nameof(policyHidePolicyDefinition)); + private static readonly AssistantSessionStateKey POLICY_DEFINITION_EXPANDED_STATE_KEY = new(nameof(policyDefinitionExpanded)); + private static readonly AssistantSessionStateKey POLICY_NAME_STATE_KEY = new(nameof(policyName)); + private static readonly AssistantSessionStateKey POLICY_DESCRIPTION_STATE_KEY = new(nameof(policyDescription)); + private static readonly AssistantSessionStateKey POLICY_ANALYSIS_RULES_STATE_KEY = new(nameof(policyAnalysisRules)); + private static readonly AssistantSessionStateKey POLICY_OUTPUT_RULES_STATE_KEY = new(nameof(policyOutputRules)); + private static readonly AssistantSessionStateKey POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY = new(nameof(policyMinimumProviderConfidence)); + private static readonly AssistantSessionStateKey POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY = new(nameof(policyPreselectedProviderId)); + private static readonly AssistantSessionStateKey POLICY_PRESELECTED_PROFILE_STATE_KEY = new(nameof(policyPreselectedProfile)); + private static readonly AssistantSessionStateKey> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths)); + private static readonly AssistantSessionStateKey>> AVAILABLE_LLM_PROVIDERS_STATE_KEY = new(nameof(availableLLMProviders)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SELECTED_POLICY_STATE_KEY, this.selectedPolicy); + state.Set(POLICY_IS_PROTECTED_STATE_KEY, this.policyIsProtected); + state.Set(POLICY_HIDE_POLICY_DEFINITION_STATE_KEY, this.policyHidePolicyDefinition); + state.Set(POLICY_DEFINITION_EXPANDED_STATE_KEY, this.policyDefinitionExpanded); + state.Set(POLICY_NAME_STATE_KEY, this.policyName); + state.Set(POLICY_DESCRIPTION_STATE_KEY, this.policyDescription); + state.Set(POLICY_ANALYSIS_RULES_STATE_KEY, this.policyAnalysisRules); + state.Set(POLICY_OUTPUT_RULES_STATE_KEY, this.policyOutputRules); + state.Set(POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY, this.policyMinimumProviderConfidence); + state.Set(POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY, this.policyPreselectedProviderId); + state.Set(POLICY_PRESELECTED_PROFILE_STATE_KEY, this.policyPreselectedProfile); + state.SetHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); + state.SetList(AVAILABLE_LLM_PROVIDERS_STATE_KEY, this.availableLLMProviders); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SELECTED_POLICY_STATE_KEY, value => this.selectedPolicy = value); + state.Restore(POLICY_IS_PROTECTED_STATE_KEY, value => this.policyIsProtected = value); + state.Restore(POLICY_HIDE_POLICY_DEFINITION_STATE_KEY, value => this.policyHidePolicyDefinition = value); + state.Restore(POLICY_DEFINITION_EXPANDED_STATE_KEY, value => this.policyDefinitionExpanded = value); + state.Restore(POLICY_NAME_STATE_KEY, value => this.policyName = value); + state.Restore(POLICY_DESCRIPTION_STATE_KEY, value => this.policyDescription = value); + state.Restore(POLICY_ANALYSIS_RULES_STATE_KEY, value => this.policyAnalysisRules = value); + state.Restore(POLICY_OUTPUT_RULES_STATE_KEY, value => this.policyOutputRules = value); + state.Restore(POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY, value => this.policyMinimumProviderConfidence = value); + state.Restore(POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY, value => this.policyPreselectedProviderId = value); + state.Restore(POLICY_PRESELECTED_PROFILE_STATE_KEY, value => this.policyPreselectedProfile = value); + state.RestoreHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); + state.RestoreList(AVAILABLE_LLM_PROVIDERS_STATE_KEY, this.availableLLMProviders); + } private bool IsNoPolicySelectedOrProtected => this.selectedPolicy is null || this.selectedPolicy.IsProtected; @@ -515,7 +565,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore // Reuse chat-level provider filtering/preselection instead of NONE. protected override Tools.Components Component => Tools.Components.CHAT; + /// + /// Gets the plugin ID as the assistant session instance ID. + /// + protected override string AssistantSessionInstanceId => this.assistantPlugin is null ? base.AssistantSessionInstanceId : this.assistantPlugin.Id.ToString(); + private string title = string.Empty; private string description = string.Empty; private string systemPrompt = string.Empty; @@ -44,6 +50,61 @@ public partial class AssistantDynamic : AssistantBaseCore private string securityMessage = string.Empty; private bool isSecurityBlocked; private const string ASSISTANT_QUERY_KEY = "assistantId"; + private static readonly AssistantSessionStateKey TITLE_STATE_KEY = new(nameof(title)); + private static readonly AssistantSessionStateKey DESCRIPTION_STATE_KEY = new(nameof(description)); + private static readonly AssistantSessionStateKey SYSTEM_PROMPT_STATE_KEY = new(nameof(systemPrompt)); + private static readonly AssistantSessionStateKey ALLOW_PROFILES_STATE_KEY = new(nameof(allowProfiles)); + private static readonly AssistantSessionStateKey SUBMIT_TEXT_STATE_KEY = new(nameof(submitText)); + private static readonly AssistantSessionStateKey SHOW_FOOTER_PROFILE_SELECTION_STATE_KEY = new(nameof(showFooterProfileSelection)); + private static readonly AssistantSessionStateKey ASSISTANT_PLUGIN_STATE_KEY = new(nameof(assistantPlugin)); + private static readonly AssistantSessionStateKey ASSISTANT_STATE_STATE_KEY = new(nameof(assistantState)); + private static readonly AssistantSessionStateKey> IMAGE_CACHE_STATE_KEY = new(nameof(imageCache)); + private static readonly AssistantSessionStateKey> EXECUTING_BUTTON_ACTIONS_STATE_KEY = new(nameof(executingButtonActions)); + private static readonly AssistantSessionStateKey> EXECUTING_SWITCH_ACTIONS_STATE_KEY = new(nameof(executingSwitchActions)); + private static readonly AssistantSessionStateKey PLUGIN_PATH_STATE_KEY = new(nameof(pluginPath)); + private static readonly AssistantSessionStateKey AUDIT_STATE_KEY = new(nameof(audit)); + private static readonly AssistantSessionStateKey SECURITY_MESSAGE_STATE_KEY = new(nameof(securityMessage)); + private static readonly AssistantSessionStateKey IS_SECURITY_BLOCKED_STATE_KEY = new(nameof(isSecurityBlocked)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(TITLE_STATE_KEY, this.title); + state.Set(DESCRIPTION_STATE_KEY, this.description); + state.Set(SYSTEM_PROMPT_STATE_KEY, this.systemPrompt); + state.Set(ALLOW_PROFILES_STATE_KEY, this.allowProfiles); + state.Set(SUBMIT_TEXT_STATE_KEY, this.submitText); + state.Set(SHOW_FOOTER_PROFILE_SELECTION_STATE_KEY, this.showFooterProfileSelection); + state.Set(ASSISTANT_PLUGIN_STATE_KEY, this.assistantPlugin); + state.Set(ASSISTANT_STATE_STATE_KEY, this.assistantState.Clone()); + state.SetDictionary(IMAGE_CACHE_STATE_KEY, this.imageCache); + state.SetHashSet(EXECUTING_BUTTON_ACTIONS_STATE_KEY, this.executingButtonActions); + state.SetHashSet(EXECUTING_SWITCH_ACTIONS_STATE_KEY, this.executingSwitchActions); + state.Set(PLUGIN_PATH_STATE_KEY, this.pluginPath); + state.Set(AUDIT_STATE_KEY, this.audit); + state.Set(SECURITY_MESSAGE_STATE_KEY, this.securityMessage); + state.Set(IS_SECURITY_BLOCKED_STATE_KEY, this.isSecurityBlocked); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(TITLE_STATE_KEY, value => this.title = value); + state.Restore(DESCRIPTION_STATE_KEY, value => this.description = value); + state.Restore(SYSTEM_PROMPT_STATE_KEY, value => this.systemPrompt = value); + state.Restore(ALLOW_PROFILES_STATE_KEY, value => this.allowProfiles = value); + state.Restore(SUBMIT_TEXT_STATE_KEY, value => this.submitText = value); + state.Restore(SHOW_FOOTER_PROFILE_SELECTION_STATE_KEY, value => this.showFooterProfileSelection = value); + state.Restore(ASSISTANT_PLUGIN_STATE_KEY, value => this.assistantPlugin = value); + state.Restore(ASSISTANT_STATE_STATE_KEY, value => this.assistantState.CopyFrom(value)); + state.RestoreDictionary(IMAGE_CACHE_STATE_KEY, this.imageCache); + state.RestoreHashSet(EXECUTING_BUTTON_ACTIONS_STATE_KEY, this.executingButtonActions); + state.RestoreHashSet(EXECUTING_SWITCH_ACTIONS_STATE_KEY, this.executingSwitchActions); + state.Restore(PLUGIN_PATH_STATE_KEY, value => this.pluginPath = value); + state.Restore(AUDIT_STATE_KEY, value => this.audit = value); + state.Restore(SECURITY_MESSAGE_STATE_KEY, value => this.securityMessage = value); + state.Restore(IS_SECURITY_BLOCKED_STATE_KEY, value => this.isSecurityBlocked = value); + } #region Implementation of AssistantBase diff --git a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs index 4c1e1158..ee5d233a 100644 --- a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs +++ b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs @@ -1,6 +1,7 @@ using System.Text; using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.EMail; @@ -78,6 +79,46 @@ public partial class AssistantEMail : AssistantBaseCore SELECTED_WRITING_STYLE_STATE_KEY = new(nameof(selectedWritingStyle)); + private static readonly AssistantSessionStateKey INPUT_GREETING_STATE_KEY = new(nameof(inputGreeting)); + private static readonly AssistantSessionStateKey INPUT_BULLET_POINTS_STATE_KEY = new(nameof(inputBulletPoints)); + private static readonly AssistantSessionStateKey> BULLET_POINTS_LINES_STATE_KEY = new(nameof(bulletPointsLines)); + private static readonly AssistantSessionStateKey> SELECTED_FOCI_STATE_KEY = new(nameof(selectedFoci)); + private static readonly AssistantSessionStateKey INPUT_NAME_STATE_KEY = new(nameof(inputName)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey PROVIDE_HISTORY_STATE_KEY = new(nameof(provideHistory)); + private static readonly AssistantSessionStateKey INPUT_HISTORY_STATE_KEY = new(nameof(inputHistory)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SELECTED_WRITING_STYLE_STATE_KEY, this.selectedWritingStyle); + state.Set(INPUT_GREETING_STATE_KEY, this.inputGreeting); + state.Set(INPUT_BULLET_POINTS_STATE_KEY, this.inputBulletPoints); + state.SetList(BULLET_POINTS_LINES_STATE_KEY, this.bulletPointsLines); + state.SetHashSet(SELECTED_FOCI_STATE_KEY, this.selectedFoci); + state.Set(INPUT_NAME_STATE_KEY, this.inputName); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(PROVIDE_HISTORY_STATE_KEY, this.provideHistory); + state.Set(INPUT_HISTORY_STATE_KEY, this.inputHistory); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SELECTED_WRITING_STYLE_STATE_KEY, value => this.selectedWritingStyle = value); + state.Restore(INPUT_GREETING_STATE_KEY, value => this.inputGreeting = value); + state.Restore(INPUT_BULLET_POINTS_STATE_KEY, value => this.inputBulletPoints = value); + state.RestoreList(BULLET_POINTS_LINES_STATE_KEY, this.bulletPointsLines); + state.Restore(SELECTED_FOCI_STATE_KEY, value => this.selectedFoci = value); + state.Restore(INPUT_NAME_STATE_KEY, value => this.inputName = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(PROVIDE_HISTORY_STATE_KEY, value => this.provideHistory = value); + state.Restore(INPUT_HISTORY_STATE_KEY, value => this.inputHistory = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index a4c204c9..96f3f953 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -5,6 +5,7 @@ using AIStudio.Chat; using AIStudio.Dialogs; using AIStudio.Dialogs.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.AssistantSessions; using Microsoft.AspNetCore.Components; @@ -449,6 +450,88 @@ public partial class AssistantERI : AssistantBaseCore private bool writeToFilesystem; private string baseDirectory = string.Empty; private List previouslyGeneratedFiles = new(); + private static readonly AssistantSessionStateKey SELECTED_ERI_SERVER_STATE_KEY = new(nameof(selectedERIServer)); + private static readonly AssistantSessionStateKey AUTO_SAVE_STATE_KEY = new(nameof(autoSave)); + private static readonly AssistantSessionStateKey SERVER_NAME_STATE_KEY = new(nameof(serverName)); + private static readonly AssistantSessionStateKey SERVER_DESCRIPTION_STATE_KEY = new(nameof(serverDescription)); + private static readonly AssistantSessionStateKey SELECTED_ERI_VERSION_STATE_KEY = new(nameof(selectedERIVersion)); + private static readonly AssistantSessionStateKey ERI_SPECIFICATION_STATE_KEY = new(nameof(eriSpecification)); + private static readonly AssistantSessionStateKey SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(selectedProgrammingLanguage)); + private static readonly AssistantSessionStateKey OTHER_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(otherProgrammingLanguage)); + private static readonly AssistantSessionStateKey SELECTED_DATA_SOURCE_STATE_KEY = new(nameof(selectedDataSource)); + private static readonly AssistantSessionStateKey OTHER_DATA_SOURCE_STATE_KEY = new(nameof(otherDataSource)); + private static readonly AssistantSessionStateKey DATA_SOURCE_PRODUCT_NAME_STATE_KEY = new(nameof(dataSourceProductName)); + private static readonly AssistantSessionStateKey DATA_SOURCE_HOSTNAME_STATE_KEY = new(nameof(dataSourceHostname)); + private static readonly AssistantSessionStateKey DATA_SOURCE_PORT_STATE_KEY = new(nameof(dataSourcePort)); + private static readonly AssistantSessionStateKey USER_TYPED_PORT_STATE_KEY = new(nameof(userTypedPort)); + private static readonly AssistantSessionStateKey> SELECTED_AUTHENTICATION_METHODS_STATE_KEY = new(nameof(selectedAuthenticationMethods)); + private static readonly AssistantSessionStateKey AUTH_DESCRIPTION_STATE_KEY = new(nameof(authDescription)); + private static readonly AssistantSessionStateKey SELECTED_OPERATING_SYSTEM_STATE_KEY = new(nameof(selectedOperatingSystem)); + private static readonly AssistantSessionStateKey ALLOWED_LLM_PROVIDERS_STATE_KEY = new(nameof(allowedLLMProviders)); + private static readonly AssistantSessionStateKey> EMBEDDINGS_STATE_KEY = new(nameof(embeddings)); + private static readonly AssistantSessionStateKey> RETRIEVAL_PROCESSES_STATE_KEY = new(nameof(retrievalProcesses)); + private static readonly AssistantSessionStateKey ADDITIONAL_LIBRARIES_STATE_KEY = new(nameof(additionalLibraries)); + private static readonly AssistantSessionStateKey WRITE_TO_FILESYSTEM_STATE_KEY = new(nameof(writeToFilesystem)); + private static readonly AssistantSessionStateKey BASE_DIRECTORY_STATE_KEY = new(nameof(baseDirectory)); + private static readonly AssistantSessionStateKey> PREVIOUSLY_GENERATED_FILES_STATE_KEY = new(nameof(previouslyGeneratedFiles)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SELECTED_ERI_SERVER_STATE_KEY, this.selectedERIServer); + state.Set(AUTO_SAVE_STATE_KEY, this.autoSave); + state.Set(SERVER_NAME_STATE_KEY, this.serverName); + state.Set(SERVER_DESCRIPTION_STATE_KEY, this.serverDescription); + state.Set(SELECTED_ERI_VERSION_STATE_KEY, this.selectedERIVersion); + state.Set(ERI_SPECIFICATION_STATE_KEY, this.eriSpecification); + state.Set(SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY, this.selectedProgrammingLanguage); + state.Set(OTHER_PROGRAMMING_LANGUAGE_STATE_KEY, this.otherProgrammingLanguage); + state.Set(SELECTED_DATA_SOURCE_STATE_KEY, this.selectedDataSource); + state.Set(OTHER_DATA_SOURCE_STATE_KEY, this.otherDataSource); + state.Set(DATA_SOURCE_PRODUCT_NAME_STATE_KEY, this.dataSourceProductName); + state.Set(DATA_SOURCE_HOSTNAME_STATE_KEY, this.dataSourceHostname); + state.Set(DATA_SOURCE_PORT_STATE_KEY, this.dataSourcePort); + state.Set(USER_TYPED_PORT_STATE_KEY, this.userTypedPort); + state.SetHashSet(SELECTED_AUTHENTICATION_METHODS_STATE_KEY, this.selectedAuthenticationMethods); + state.Set(AUTH_DESCRIPTION_STATE_KEY, this.authDescription); + state.Set(SELECTED_OPERATING_SYSTEM_STATE_KEY, this.selectedOperatingSystem); + state.Set(ALLOWED_LLM_PROVIDERS_STATE_KEY, this.allowedLLMProviders); + state.SetList(EMBEDDINGS_STATE_KEY, this.embeddings); + state.SetList(RETRIEVAL_PROCESSES_STATE_KEY, this.retrievalProcesses); + state.Set(ADDITIONAL_LIBRARIES_STATE_KEY, this.additionalLibraries); + state.Set(WRITE_TO_FILESYSTEM_STATE_KEY, this.writeToFilesystem); + state.Set(BASE_DIRECTORY_STATE_KEY, this.baseDirectory); + state.SetList(PREVIOUSLY_GENERATED_FILES_STATE_KEY, this.previouslyGeneratedFiles); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SELECTED_ERI_SERVER_STATE_KEY, value => this.selectedERIServer = value); + state.Restore(AUTO_SAVE_STATE_KEY, value => this.autoSave = value); + state.Restore(SERVER_NAME_STATE_KEY, value => this.serverName = value); + state.Restore(SERVER_DESCRIPTION_STATE_KEY, value => this.serverDescription = value); + state.Restore(SELECTED_ERI_VERSION_STATE_KEY, value => this.selectedERIVersion = value); + state.Restore(ERI_SPECIFICATION_STATE_KEY, value => this.eriSpecification = value); + state.Restore(SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY, value => this.selectedProgrammingLanguage = value); + state.Restore(OTHER_PROGRAMMING_LANGUAGE_STATE_KEY, value => this.otherProgrammingLanguage = value); + state.Restore(SELECTED_DATA_SOURCE_STATE_KEY, value => this.selectedDataSource = value); + state.Restore(OTHER_DATA_SOURCE_STATE_KEY, value => this.otherDataSource = value); + state.Restore(DATA_SOURCE_PRODUCT_NAME_STATE_KEY, value => this.dataSourceProductName = value); + state.Restore(DATA_SOURCE_HOSTNAME_STATE_KEY, value => this.dataSourceHostname = value); + state.Restore(DATA_SOURCE_PORT_STATE_KEY, value => this.dataSourcePort = value); + state.Restore(USER_TYPED_PORT_STATE_KEY, value => this.userTypedPort = value); + state.Restore(SELECTED_AUTHENTICATION_METHODS_STATE_KEY, value => this.selectedAuthenticationMethods = value); + state.Restore(AUTH_DESCRIPTION_STATE_KEY, value => this.authDescription = value); + state.Restore(SELECTED_OPERATING_SYSTEM_STATE_KEY, value => this.selectedOperatingSystem = value); + state.Restore(ALLOWED_LLM_PROVIDERS_STATE_KEY, value => this.allowedLLMProviders = value); + state.RestoreList(EMBEDDINGS_STATE_KEY, this.embeddings); + state.RestoreList(RETRIEVAL_PROCESSES_STATE_KEY, this.retrievalProcesses); + state.Restore(ADDITIONAL_LIBRARIES_STATE_KEY, value => this.additionalLibraries = value); + state.Restore(WRITE_TO_FILESYSTEM_STATE_KEY, value => this.writeToFilesystem = value); + state.Restore(BASE_DIRECTORY_STATE_KEY, value => this.baseDirectory = value); + state.RestoreList(PREVIOUSLY_GENERATED_FILES_STATE_KEY, this.previouslyGeneratedFiles); + } private bool AreServerPresetsBlocked => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions; diff --git a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs index 9f90a0fa..6ba1e0eb 100644 --- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs +++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.GrammarSpelling; @@ -84,6 +85,28 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey CORRECTED_TEXT_STATE_KEY = new(nameof(correctedText)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(CORRECTED_TEXT_STATE_KEY, this.correctedText); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(CORRECTED_TEXT_STATE_KEY, value => this.correctedText = value); + } private string? ValidateText(string text) { @@ -127,6 +150,13 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore private Dictionary removedContent = []; private Dictionary localizedContent = []; private StringBuilder finalLuaCode = new(); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey IS_LOADING_STATE_KEY = new(nameof(isLoading)); + private static readonly AssistantSessionStateKey LOADING_ISSUE_STATE_KEY = new(nameof(loadingIssue)); + private static readonly AssistantSessionStateKey LOCALIZATION_POSSIBLE_STATE_KEY = new(nameof(localizationPossible)); + private static readonly AssistantSessionStateKey SEARCH_STRING_STATE_KEY = new(nameof(searchString)); + private static readonly AssistantSessionStateKey SELECTED_LANGUAGE_PLUGIN_ID_STATE_KEY = new(nameof(selectedLanguagePluginId)); + private static readonly AssistantSessionStateKey SELECTED_LANGUAGE_PLUGIN_STATE_KEY = new(nameof(selectedLanguagePlugin)); + private static readonly AssistantSessionStateKey> ADDED_CONTENT_STATE_KEY = new(nameof(addedContent)); + private static readonly AssistantSessionStateKey> REMOVED_CONTENT_STATE_KEY = new(nameof(removedContent)); + private static readonly AssistantSessionStateKey> LOCALIZED_CONTENT_STATE_KEY = new(nameof(localizedContent)); + private static readonly AssistantSessionStateKey FINAL_LUA_CODE_STATE_KEY = new(nameof(finalLuaCode)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(IS_LOADING_STATE_KEY, this.isLoading); + state.Set(LOADING_ISSUE_STATE_KEY, this.loadingIssue); + state.Set(LOCALIZATION_POSSIBLE_STATE_KEY, this.localizationPossible); + state.Set(SEARCH_STRING_STATE_KEY, this.searchString); + state.Set(SELECTED_LANGUAGE_PLUGIN_ID_STATE_KEY, this.selectedLanguagePluginId); + state.Set(SELECTED_LANGUAGE_PLUGIN_STATE_KEY, this.selectedLanguagePlugin); + state.SetDictionary(ADDED_CONTENT_STATE_KEY, this.addedContent); + state.SetDictionary(REMOVED_CONTENT_STATE_KEY, this.removedContent); + state.SetDictionary(LOCALIZED_CONTENT_STATE_KEY, this.localizedContent); + state.SetStringBuilder(FINAL_LUA_CODE_STATE_KEY, this.finalLuaCode); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(IS_LOADING_STATE_KEY, value => this.isLoading = value); + state.Restore(LOADING_ISSUE_STATE_KEY, value => this.loadingIssue = value); + state.Restore(LOCALIZATION_POSSIBLE_STATE_KEY, value => this.localizationPossible = value); + state.Restore(SEARCH_STRING_STATE_KEY, value => this.searchString = value); + state.Restore(SELECTED_LANGUAGE_PLUGIN_ID_STATE_KEY, value => this.selectedLanguagePluginId = value); + state.Restore(SELECTED_LANGUAGE_PLUGIN_STATE_KEY, value => this.selectedLanguagePlugin = value); + state.RestoreDictionary(ADDED_CONTENT_STATE_KEY, this.addedContent); + state.RestoreDictionary(REMOVED_CONTENT_STATE_KEY, this.removedContent); + state.RestoreDictionary(LOCALIZED_CONTENT_STATE_KEY, this.localizedContent); + state.RestoreStringBuilder(FINAL_LUA_CODE_STATE_KEY, this.finalLuaCode); + } #region Overrides of AssistantBase diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index bd5d8535..c33e351a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -310,6 +310,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T180921696"] = "Reset" -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Please select a provider." +-- The assistant failed. The message is: '{0}' +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1833836281"] = "The assistant failed. The message is: '{0}'" + +-- This assistant is already running. AI Studio opens the running session instead. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T2575715765"] = "This assistant is already running. AI Studio opens the running session instead." + -- Assistant - {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistant - {0}" diff --git a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs index b6a3e5ad..1134c175 100644 --- a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs +++ b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.IconFinder; @@ -56,6 +57,22 @@ public partial class AssistantIconFinder : AssistantBaseCore INPUT_CONTEXT_STATE_KEY = new(nameof(inputContext)); + private static readonly AssistantSessionStateKey SELECTED_ICON_SOURCE_STATE_KEY = new(nameof(selectedIconSource)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_CONTEXT_STATE_KEY, this.inputContext); + state.Set(SELECTED_ICON_SOURCE_STATE_KEY, this.selectedIconSource); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_CONTEXT_STATE_KEY, value => this.inputContext = value); + state.Restore(SELECTED_ICON_SOURCE_STATE_KEY, value => this.selectedIconSource = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs index 9d44eae7..d8826a8c 100644 --- a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs +++ b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.JobPosting; @@ -128,6 +129,49 @@ public partial class AssistantJobPostings : AssistantBaseCore INPUT_MANDATORY_INFORMATION_STATE_KEY = new(nameof(inputMandatoryInformation)); + private static readonly AssistantSessionStateKey INPUT_JOB_DESCRIPTION_STATE_KEY = new(nameof(inputJobDescription)); + private static readonly AssistantSessionStateKey INPUT_QUALIFICATIONS_STATE_KEY = new(nameof(inputQualifications)); + private static readonly AssistantSessionStateKey INPUT_RESPONSIBILITIES_STATE_KEY = new(nameof(inputResponsibilities)); + private static readonly AssistantSessionStateKey INPUT_COMPANY_NAME_STATE_KEY = new(nameof(inputCompanyName)); + private static readonly AssistantSessionStateKey INPUT_ENTRY_DATE_STATE_KEY = new(nameof(inputEntryDate)); + private static readonly AssistantSessionStateKey INPUT_VALID_UNTIL_STATE_KEY = new(nameof(inputValidUntil)); + private static readonly AssistantSessionStateKey INPUT_WORK_LOCATION_STATE_KEY = new(nameof(inputWorkLocation)); + private static readonly AssistantSessionStateKey INPUT_COUNTRY_LEGAL_FRAMEWORK_STATE_KEY = new(nameof(inputCountryLegalFramework)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_MANDATORY_INFORMATION_STATE_KEY, this.inputMandatoryInformation); + state.Set(INPUT_JOB_DESCRIPTION_STATE_KEY, this.inputJobDescription); + state.Set(INPUT_QUALIFICATIONS_STATE_KEY, this.inputQualifications); + state.Set(INPUT_RESPONSIBILITIES_STATE_KEY, this.inputResponsibilities); + state.Set(INPUT_COMPANY_NAME_STATE_KEY, this.inputCompanyName); + state.Set(INPUT_ENTRY_DATE_STATE_KEY, this.inputEntryDate); + state.Set(INPUT_VALID_UNTIL_STATE_KEY, this.inputValidUntil); + state.Set(INPUT_WORK_LOCATION_STATE_KEY, this.inputWorkLocation); + state.Set(INPUT_COUNTRY_LEGAL_FRAMEWORK_STATE_KEY, this.inputCountryLegalFramework); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_MANDATORY_INFORMATION_STATE_KEY, value => this.inputMandatoryInformation = value); + state.Restore(INPUT_JOB_DESCRIPTION_STATE_KEY, value => this.inputJobDescription = value); + state.Restore(INPUT_QUALIFICATIONS_STATE_KEY, value => this.inputQualifications = value); + state.Restore(INPUT_RESPONSIBILITIES_STATE_KEY, value => this.inputResponsibilities = value); + state.Restore(INPUT_COMPANY_NAME_STATE_KEY, value => this.inputCompanyName = value); + state.Restore(INPUT_ENTRY_DATE_STATE_KEY, value => this.inputEntryDate = value); + state.Restore(INPUT_VALID_UNTIL_STATE_KEY, value => this.inputValidUntil = value); + state.Restore(INPUT_WORK_LOCATION_STATE_KEY, value => this.inputWorkLocation = value); + state.Restore(INPUT_COUNTRY_LEGAL_FRAMEWORK_STATE_KEY, value => this.inputCountryLegalFramework = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs index a7c01bca..7c9cce5e 100644 --- a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs +++ b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.LegalCheck; @@ -59,6 +60,31 @@ public partial class AssistantLegalCheck : AssistantBaseCore SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader)); + private static readonly AssistantSessionStateKey USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent)); + private static readonly AssistantSessionStateKey IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning)); + private static readonly AssistantSessionStateKey INPUT_LEGAL_DOCUMENT_STATE_KEY = new(nameof(inputLegalDocument)); + private static readonly AssistantSessionStateKey INPUT_QUESTIONS_STATE_KEY = new(nameof(inputQuestions)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SHOW_WEB_CONTENT_READER_STATE_KEY, this.showWebContentReader); + state.Set(USE_CONTENT_CLEANER_AGENT_STATE_KEY, this.useContentCleanerAgent); + state.Set(IS_AGENT_RUNNING_STATE_KEY, this.isAgentRunning); + state.Set(INPUT_LEGAL_DOCUMENT_STATE_KEY, this.inputLegalDocument); + state.Set(INPUT_QUESTIONS_STATE_KEY, this.inputQuestions); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SHOW_WEB_CONTENT_READER_STATE_KEY, value => this.showWebContentReader = value); + state.Restore(USE_CONTENT_CLEANER_AGENT_STATE_KEY, value => this.useContentCleanerAgent = value); + state.Restore(IS_AGENT_RUNNING_STATE_KEY, value => this.isAgentRunning = value); + state.Restore(INPUT_LEGAL_DOCUMENT_STATE_KEY, value => this.inputLegalDocument = value); + state.Restore(INPUT_QUESTIONS_STATE_KEY, value => this.inputQuestions = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs index ff5ab87f..18b25880 100644 --- a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs +++ b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Dialogs.Settings; using AIStudio.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.MyTasks; @@ -59,6 +60,25 @@ public partial class AssistantMyTasks : AssistantBaseCore private string inputText = string.Empty; private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS; private string customTargetLanguage = string.Empty; + private static readonly AssistantSessionStateKey INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs index b1df8944..7a5156b2 100644 --- a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using AIStudio.Chat; using AIStudio.Dialogs; using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; using Microsoft.AspNetCore.Components; #if !DEBUG @@ -176,6 +177,67 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore INPUT_PROMPT_STATE_KEY = new(nameof(inputPrompt)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects)); + private static readonly AssistantSessionStateKey USE_CUSTOM_PROMPT_GUIDE_STATE_KEY = new(nameof(useCustomPromptGuide)); + private static readonly AssistantSessionStateKey> CUSTOM_PROMPT_GUIDE_FILES_STATE_KEY = new(nameof(customPromptGuideFiles)); + private static readonly AssistantSessionStateKey CURRENT_CUSTOM_PROMPT_GUIDE_PATH_STATE_KEY = new(nameof(currentCustomPromptGuidePath)); + private static readonly AssistantSessionStateKey CUSTOM_PROMPTING_GUIDELINE_CONTENT_STATE_KEY = new(nameof(customPromptingGuidelineContent)); + private static readonly AssistantSessionStateKey IS_LOADING_CUSTOM_PROMPT_GUIDE_STATE_KEY = new(nameof(isLoadingCustomPromptGuide)); + private static readonly AssistantSessionStateKey HAS_UPDATED_DEFAULT_RECOMMENDATIONS_STATE_KEY = new(nameof(hasUpdatedDefaultRecommendations)); + private static readonly AssistantSessionStateKey OPTIMIZED_PROMPT_STATE_KEY = new(nameof(optimizedPrompt)); + private static readonly AssistantSessionStateKey REC_CLARITY_DIRECTNESS_STATE_KEY = new(nameof(recClarityDirectness)); + private static readonly AssistantSessionStateKey REC_EXAMPLES_CONTEXT_STATE_KEY = new(nameof(recExamplesContext)); + private static readonly AssistantSessionStateKey REC_SEQUENTIAL_STEPS_STATE_KEY = new(nameof(recSequentialSteps)); + private static readonly AssistantSessionStateKey REC_STRUCTURE_MARKERS_STATE_KEY = new(nameof(recStructureMarkers)); + private static readonly AssistantSessionStateKey REC_ROLE_DEFINITION_STATE_KEY = new(nameof(recRoleDefinition)); + private static readonly AssistantSessionStateKey REC_LANGUAGE_CHOICE_STATE_KEY = new(nameof(recLanguageChoice)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_PROMPT_STATE_KEY, this.inputPrompt); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(IMPORTANT_ASPECTS_STATE_KEY, this.importantAspects); + state.Set(USE_CUSTOM_PROMPT_GUIDE_STATE_KEY, this.useCustomPromptGuide); + state.SetHashSet(CUSTOM_PROMPT_GUIDE_FILES_STATE_KEY, this.customPromptGuideFiles); + state.Set(CURRENT_CUSTOM_PROMPT_GUIDE_PATH_STATE_KEY, this.currentCustomPromptGuidePath); + state.Set(CUSTOM_PROMPTING_GUIDELINE_CONTENT_STATE_KEY, this.customPromptingGuidelineContent); + state.Set(IS_LOADING_CUSTOM_PROMPT_GUIDE_STATE_KEY, this.isLoadingCustomPromptGuide); + state.Set(HAS_UPDATED_DEFAULT_RECOMMENDATIONS_STATE_KEY, this.hasUpdatedDefaultRecommendations); + state.Set(OPTIMIZED_PROMPT_STATE_KEY, this.optimizedPrompt); + state.Set(REC_CLARITY_DIRECTNESS_STATE_KEY, this.recClarityDirectness); + state.Set(REC_EXAMPLES_CONTEXT_STATE_KEY, this.recExamplesContext); + state.Set(REC_SEQUENTIAL_STEPS_STATE_KEY, this.recSequentialSteps); + state.Set(REC_STRUCTURE_MARKERS_STATE_KEY, this.recStructureMarkers); + state.Set(REC_ROLE_DEFINITION_STATE_KEY, this.recRoleDefinition); + state.Set(REC_LANGUAGE_CHOICE_STATE_KEY, this.recLanguageChoice); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_PROMPT_STATE_KEY, value => this.inputPrompt = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(IMPORTANT_ASPECTS_STATE_KEY, value => this.importantAspects = value); + state.Restore(USE_CUSTOM_PROMPT_GUIDE_STATE_KEY, value => this.useCustomPromptGuide = value); + state.RestoreHashSet(CUSTOM_PROMPT_GUIDE_FILES_STATE_KEY, this.customPromptGuideFiles); + state.Restore(CURRENT_CUSTOM_PROMPT_GUIDE_PATH_STATE_KEY, value => this.currentCustomPromptGuidePath = value); + state.Restore(CUSTOM_PROMPTING_GUIDELINE_CONTENT_STATE_KEY, value => this.customPromptingGuidelineContent = value); + state.Restore(IS_LOADING_CUSTOM_PROMPT_GUIDE_STATE_KEY, value => this.isLoadingCustomPromptGuide = value); + state.Restore(HAS_UPDATED_DEFAULT_RECOMMENDATIONS_STATE_KEY, value => this.hasUpdatedDefaultRecommendations = value); + state.Restore(OPTIMIZED_PROMPT_STATE_KEY, value => this.optimizedPrompt = value); + state.Restore(REC_CLARITY_DIRECTNESS_STATE_KEY, value => this.recClarityDirectness = value); + state.Restore(REC_EXAMPLES_CONTEXT_STATE_KEY, value => this.recExamplesContext = value); + state.Restore(REC_SEQUENTIAL_STEPS_STATE_KEY, value => this.recSequentialSteps = value); + state.Restore(REC_STRUCTURE_MARKERS_STATE_KEY, value => this.recStructureMarkers = value); + state.Restore(REC_ROLE_DEFINITION_STATE_KEY, value => this.recRoleDefinition = value); + state.Restore(REC_LANGUAGE_CHOICE_STATE_KEY, value => this.recLanguageChoice = value); + } private bool ShowUpdatedPromptGuidelinesIndicator => !this.useCustomPromptGuide && this.hasUpdatedDefaultRecommendations; private bool CanPreviewCustomPromptGuide => this.useCustomPromptGuide && this.customPromptGuideFiles.Count > 0; diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs index 81eaa6b3..53975561 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.RewriteImprove; @@ -91,6 +92,34 @@ public partial class AssistantRewriteImprove : AssistantBaseCore INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey REWRITTEN_TEXT_STATE_KEY = new(nameof(rewrittenText)); + private static readonly AssistantSessionStateKey SELECTED_WRITING_STYLE_STATE_KEY = new(nameof(selectedWritingStyle)); + private static readonly AssistantSessionStateKey SELECTED_SENTENCE_STRUCTURE_STATE_KEY = new(nameof(selectedSentenceStructure)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(REWRITTEN_TEXT_STATE_KEY, this.rewrittenText); + state.Set(SELECTED_WRITING_STYLE_STATE_KEY, this.selectedWritingStyle); + state.Set(SELECTED_SENTENCE_STRUCTURE_STATE_KEY, this.selectedSentenceStructure); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(REWRITTEN_TEXT_STATE_KEY, value => this.rewrittenText = value); + state.Restore(SELECTED_WRITING_STYLE_STATE_KEY, value => this.selectedWritingStyle = value); + state.Restore(SELECTED_SENTENCE_STRUCTURE_STATE_KEY, value => this.selectedSentenceStructure = value); + } private string? ValidateText(string text) { @@ -134,6 +163,13 @@ public partial class AssistantRewriteImprove : AssistantBaseCore loadedDocumentPaths = []; + private static readonly AssistantSessionStateKey INPUT_TITLE_STATE_KEY = new(nameof(inputTitle)); + private static readonly AssistantSessionStateKey INPUT_CONTENT_STATE_KEY = new(nameof(inputContent)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey SELECTED_AUDIENCE_PROFILE_STATE_KEY = new(nameof(selectedAudienceProfile)); + private static readonly AssistantSessionStateKey SELECTED_AUDIENCE_AGE_GROUP_STATE_KEY = new(nameof(selectedAudienceAgeGroup)); + private static readonly AssistantSessionStateKey SELECTED_AUDIENCE_ORGANIZATIONAL_LEVEL_STATE_KEY = new(nameof(selectedAudienceOrganizationalLevel)); + private static readonly AssistantSessionStateKey SELECTED_AUDIENCE_EXPERTISE_STATE_KEY = new(nameof(selectedAudienceExpertise)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey NUMBER_OF_SHEETS_STATE_KEY = new(nameof(numberOfSheets)); + private static readonly AssistantSessionStateKey NUMBER_OF_BULLET_POINTS_STATE_KEY = new(nameof(numberOfBulletPoints)); + private static readonly AssistantSessionStateKey TIME_SPECIFICATION_STATE_KEY = new(nameof(timeSpecification)); + private static readonly AssistantSessionStateKey CALCULATED_NUMBER_OF_SLIDES_STATE_KEY = new(nameof(calculatedNumberOfSlides)); + private static readonly AssistantSessionStateKey IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects)); + private static readonly AssistantSessionStateKey> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TITLE_STATE_KEY, this.inputTitle); + state.Set(INPUT_CONTENT_STATE_KEY, this.inputContent); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(SELECTED_AUDIENCE_PROFILE_STATE_KEY, this.selectedAudienceProfile); + state.Set(SELECTED_AUDIENCE_AGE_GROUP_STATE_KEY, this.selectedAudienceAgeGroup); + state.Set(SELECTED_AUDIENCE_ORGANIZATIONAL_LEVEL_STATE_KEY, this.selectedAudienceOrganizationalLevel); + state.Set(SELECTED_AUDIENCE_EXPERTISE_STATE_KEY, this.selectedAudienceExpertise); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(NUMBER_OF_SHEETS_STATE_KEY, this.numberOfSheets); + state.Set(NUMBER_OF_BULLET_POINTS_STATE_KEY, this.numberOfBulletPoints); + state.Set(TIME_SPECIFICATION_STATE_KEY, this.timeSpecification); + state.Set(CALCULATED_NUMBER_OF_SLIDES_STATE_KEY, this.calculatedNumberOfSlides); + state.Set(IMPORTANT_ASPECTS_STATE_KEY, this.importantAspects); + state.SetHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TITLE_STATE_KEY, value => this.inputTitle = value); + state.Restore(INPUT_CONTENT_STATE_KEY, value => this.inputContent = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(SELECTED_AUDIENCE_PROFILE_STATE_KEY, value => this.selectedAudienceProfile = value); + state.Restore(SELECTED_AUDIENCE_AGE_GROUP_STATE_KEY, value => this.selectedAudienceAgeGroup = value); + state.Restore(SELECTED_AUDIENCE_ORGANIZATIONAL_LEVEL_STATE_KEY, value => this.selectedAudienceOrganizationalLevel = value); + state.Restore(SELECTED_AUDIENCE_EXPERTISE_STATE_KEY, value => this.selectedAudienceExpertise = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(NUMBER_OF_SHEETS_STATE_KEY, value => this.numberOfSheets = value); + state.Restore(NUMBER_OF_BULLET_POINTS_STATE_KEY, value => this.numberOfBulletPoints = value); + state.Restore(TIME_SPECIFICATION_STATE_KEY, value => this.timeSpecification = value); + state.Restore(CALCULATED_NUMBER_OF_SLIDES_STATE_KEY, value => this.calculatedNumberOfSlides = value); + state.Restore(IMPORTANT_ASPECTS_STATE_KEY, value => this.importantAspects = value); + state.RestoreHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs index d778d9a1..3acc0b08 100644 --- a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs +++ b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.Synonym; @@ -103,6 +104,28 @@ public partial class AssistantSynonyms : AssistantBaseCore INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey INPUT_CONTEXT_STATE_KEY = new(nameof(inputContext)); + private static readonly AssistantSessionStateKey SELECTED_LANGUAGE_STATE_KEY = new(nameof(selectedLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(INPUT_CONTEXT_STATE_KEY, this.inputContext); + state.Set(SELECTED_LANGUAGE_STATE_KEY, this.selectedLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(INPUT_CONTEXT_STATE_KEY, value => this.inputContext = value); + state.Restore(SELECTED_LANGUAGE_STATE_KEY, value => this.selectedLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs index 0c2097b1..62356f83 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.TextSummarizer; @@ -72,6 +73,43 @@ public partial class AssistantTextSummarizer : AssistantBaseCore SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader)); + private static readonly AssistantSessionStateKey USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent)); + private static readonly AssistantSessionStateKey INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + private static readonly AssistantSessionStateKey SELECTED_COMPLEXITY_STATE_KEY = new(nameof(selectedComplexity)); + private static readonly AssistantSessionStateKey EXPERT_IN_FIELD_STATE_KEY = new(nameof(expertInField)); + private static readonly AssistantSessionStateKey IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SHOW_WEB_CONTENT_READER_STATE_KEY, this.showWebContentReader); + state.Set(USE_CONTENT_CLEANER_AGENT_STATE_KEY, this.useContentCleanerAgent); + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(IS_AGENT_RUNNING_STATE_KEY, this.isAgentRunning); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + state.Set(SELECTED_COMPLEXITY_STATE_KEY, this.selectedComplexity); + state.Set(EXPERT_IN_FIELD_STATE_KEY, this.expertInField); + state.Set(IMPORTANT_ASPECTS_STATE_KEY, this.importantAspects); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SHOW_WEB_CONTENT_READER_STATE_KEY, value => this.showWebContentReader = value); + state.Restore(USE_CONTENT_CLEANER_AGENT_STATE_KEY, value => this.useContentCleanerAgent = value); + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(IS_AGENT_RUNNING_STATE_KEY, value => this.isAgentRunning = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + state.Restore(SELECTED_COMPLEXITY_STATE_KEY, value => this.selectedComplexity = value); + state.Restore(EXPERT_IN_FIELD_STATE_KEY, value => this.expertInField = value); + state.Restore(IMPORTANT_ASPECTS_STATE_KEY, value => this.importantAspects = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs index 690f8d21..b368f186 100644 --- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs +++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; namespace AIStudio.Assistants.Translation; @@ -79,6 +80,40 @@ public partial class AssistantTranslation : AssistantBaseCore SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader)); + private static readonly AssistantSessionStateKey USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent)); + private static readonly AssistantSessionStateKey LIVE_TRANSLATION_STATE_KEY = new(nameof(liveTranslation)); + private static readonly AssistantSessionStateKey IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning)); + private static readonly AssistantSessionStateKey INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey INPUT_TEXT_LAST_TRANSLATION_STATE_KEY = new(nameof(inputTextLastTranslation)); + private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); + private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(SHOW_WEB_CONTENT_READER_STATE_KEY, this.showWebContentReader); + state.Set(USE_CONTENT_CLEANER_AGENT_STATE_KEY, this.useContentCleanerAgent); + state.Set(LIVE_TRANSLATION_STATE_KEY, this.liveTranslation); + state.Set(IS_AGENT_RUNNING_STATE_KEY, this.isAgentRunning); + state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.Set(INPUT_TEXT_LAST_TRANSLATION_STATE_KEY, this.inputTextLastTranslation); + state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); + state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); + } + + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(SHOW_WEB_CONTENT_READER_STATE_KEY, value => this.showWebContentReader = value); + state.Restore(USE_CONTENT_CLEANER_AGENT_STATE_KEY, value => this.useContentCleanerAgent = value); + state.Restore(LIVE_TRANSLATION_STATE_KEY, value => this.liveTranslation = value); + state.Restore(IS_AGENT_RUNNING_STATE_KEY, value => this.isAgentRunning = value); + state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.Restore(INPUT_TEXT_LAST_TRANSLATION_STATE_KEY, value => this.inputTextLastTranslation = value); + state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); + state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); + } #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Components/AssistantBlock.razor.cs b/app/MindWork AI Studio/Components/AssistantBlock.razor.cs index dde37267..74683559 100644 --- a/app/MindWork AI Studio/Components/AssistantBlock.razor.cs +++ b/app/MindWork AI Studio/Components/AssistantBlock.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Dialogs.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.AssistantSessions; using Microsoft.AspNetCore.Components; using DialogOptions = AIStudio.Dialogs.DialogOptions; @@ -31,6 +32,12 @@ public partial class AssistantBlock : MSGComponentBase where TSetting [Parameter] public Tools.Components Component { get; set; } = Tools.Components.NONE; + /// + /// Gets or sets the optional assistant session instance ID represented by this block. + /// + [Parameter] + public string AssistantSessionInstanceId { get; set; } = string.Empty; + [Parameter] public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE; @@ -39,6 +46,9 @@ public partial class AssistantBlock : MSGComponentBase where TSetting [Inject] private IDialogService DialogService { get; init; } = null!; + + [Inject] + private AssistantSessionService AssistantSessionService { get; init; } = null!; private async Task OpenSettingsDialog() { @@ -50,7 +60,7 @@ public partial class AssistantBlock : MSGComponentBase where TSetting await this.DialogService.ShowAsync(T("Open Settings"), dialogParameters, DialogOptions.FULLSCREEN); } - private string BorderColor => this.SettingsManager.IsDarkMode switch + private string BorderColor => this.HasActiveSession ? this.ColorTheme.GetCurrentPalette(this.SettingsManager).Warning.Value : this.SettingsManager.IsDarkMode switch { true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight, false => this.ColorTheme.GetCurrentPalette(this.SettingsManager).Primary.Value, @@ -61,4 +71,27 @@ public partial class AssistantBlock : MSGComponentBase where TSetting private bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature); private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel); -} + + /// + /// Gets whether this block represents an active assistant session. + /// + private bool HasActiveSession => string.IsNullOrWhiteSpace(this.AssistantSessionInstanceId) + ? this.AssistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && snapshot.Key.Component == this.Component) + : this.AssistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && snapshot.Key.InstanceId == this.AssistantSessionInstanceId); + + /// + /// Refreshes the block when assistant session activity changes. + /// + /// The message payload type. + /// The component that sent the message, if any. + /// The event that was triggered. + /// The message payload. + /// A task that completes after the message was processed. + protected override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + if (triggeredEvent is Event.ASSISTANT_SESSION_CHANGED or Event.ASSISTANT_SESSION_FINISHED) + this.StateHasChanged(); + + return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 57dc70dc..f01a70c5 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -2,6 +2,7 @@ using AIStudio.Dialogs; using AIStudio.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.AIJobs; +using AIStudio.Tools.AssistantSessions; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -30,6 +31,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan [Inject] private AIJobService AIJobService { get; init; } = null!; + + [Inject] + private AssistantSessionService AssistantSessionService { get; init; } = null!; [Inject] private ISnackbar Snackbar { get; init; } = null!; @@ -102,7 +106,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR, Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED, Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, - Event.CHAT_GENERATION_CHANGED, + Event.CHAT_GENERATION_CHANGED, Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED, ]); // Set the snackbar for the update service: @@ -228,6 +232,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan case Event.AI_JOB_CHANGED: case Event.AI_JOB_FINISHED: case Event.CHAT_GENERATION_CHANGED: + case Event.ASSISTANT_SESSION_CHANGED: + case Event.ASSISTANT_SESSION_FINISHED: this.LoadNavItems(); this.StateHasChanged(); break; @@ -344,7 +350,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true); yield return new(T("Chat"), this.AIJobService.HasActiveJobs ? Icons.Material.Filled.Chat : Icons.Material.Outlined.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false); - yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false); + yield return new(T("Assistants"), this.AssistantSessionService.HasActiveSessions ? Icons.Material.Filled.Apps : Icons.Material.Outlined.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false); if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager)) yield return new(T("Writer"), Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false); diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index cec6c561..feca92fc 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -47,6 +47,7 @@ Description="@T(assistantPlugin.Description)" Icon="@Icons.Material.Filled.FindInPage" Disabled="@(!securityState.CanStartAssistant)" + AssistantSessionInstanceId="@assistantPlugin.Id.ToString()" Link="@($"{Routes.ASSISTANT_DYNAMIC}?assistantId={assistantPlugin.Id}")"> diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 95ba5490..2690e684 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -3,6 +3,7 @@ using AIStudio.Agents.AssistantAudit; using AIStudio.Settings; using AIStudio.Tools.Databases; using AIStudio.Tools.AIJobs; +using AIStudio.Tools.AssistantSessions; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.Services; @@ -130,6 +131,7 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionKey.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionKey.cs new file mode 100644 index 00000000..677e3b43 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionKey.cs @@ -0,0 +1,34 @@ +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Identifies one logical assistant session slot. +/// +public readonly record struct AssistantSessionKey +{ + /// + /// Initializes a new assistant session key. + /// + /// The application component the assistant belongs to. + /// The assistant-specific instance ID, such as a component type or plugin ID. + public AssistantSessionKey(Components component, string instanceId) + { + this.Component = component; + this.InstanceId = instanceId; + } + + /// + /// Gets the application component the assistant belongs to. + /// + public Components Component { get; init; } + + /// + /// Gets the assistant-specific instance ID, such as a component type or plugin ID. + /// + public string InstanceId { get; init; } + + /// + /// Converts the key into a compact diagnostic string. + /// + /// The component and instance ID joined by a colon. + public override string ToString() => $"{this.Component}:{this.InstanceId}"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs new file mode 100644 index 00000000..ff5363b1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs @@ -0,0 +1,320 @@ +using System.Collections.Concurrent; + +using AIStudio.Chat; + +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Keeps assistant sessions alive while their Blazor components are not mounted. +/// +/// The message bus used to publish assistant session changes. +public sealed class AssistantSessionService(MessageBus messageBus) +{ + /// + /// Mutable runtime state owned exclusively by . + /// + /// + /// This type intentionally exists in addition to . + /// It holds runtime-only data such as the cancellation token source and lock, while + /// snapshots are copied DTOs for UI components and message bus payloads. + /// + private sealed class AssistantSessionState + { + /// + /// Identifies this concrete run of an assistant session. + /// + public required Guid SessionId { get; init; } + + /// + /// Identifies the assistant and logical session slot this run belongs to. + /// + public required AssistantSessionKey Key { get; init; } + + /// + /// Cancels the active assistant run. + /// + public required CancellationTokenSource CancellationTokenSource { get; init; } + + /// + /// Stores when the session run started. + /// + public required DateTimeOffset StartedAt { get; init; } + + /// + /// Stores when the session state was last changed. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// Stores when the session reached a terminal state. + /// + public DateTimeOffset? FinishedAt { get; set; } + + /// + /// Stores the user-visible assistant title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Stores the current lifecycle state of the session. + /// + public AssistantSessionStatus Status { get; set; } + + /// + /// Stores the user-visible error message for failed sessions. + /// + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// Stores the current assistant chat thread, including streamed output. + /// + public ChatThread? ChatThread { get; set; } + + /// + /// Stores the assistant component state captured from the running UI instance. + /// + public Dictionary State { get; set; } = new(StringComparer.Ordinal); + + /// + /// Guards mutable fields while snapshots are created or updates are applied. + /// + public readonly Lock SyncRoot = new(); + } + + /// + /// Stores one assistant session per session key. + /// + private readonly ConcurrentDictionary sessions = new(); + + /// + /// Gets whether at least one assistant session is still active. + /// + public bool HasActiveSessions => this.sessions.Values.Any(session => session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING); + + /// + /// Gets copied snapshots for all known assistant sessions. + /// + /// Session snapshots ordered by newest update first. + public IReadOnlyCollection GetSnapshots() + { + return this.sessions.Values + .Select(CreateSnapshot) + .OrderByDescending(snapshot => snapshot.UpdatedAt) + .ToList(); + } + + /// + /// Tries to get the current snapshot for an assistant session key. + /// + /// The assistant session key to look up. + /// The current snapshot, or null when no session exists. + public AssistantSessionSnapshot? TryGetSnapshot(AssistantSessionKey key) + { + return this.sessions.TryGetValue(key, out var session) ? CreateSnapshot(session) : null; + } + + /// + /// Starts a new assistant session when no active session exists for the key. + /// + /// The assistant session key. + /// The user-visible assistant title. + /// The cancellation token source owned by the new runtime session. + /// The current assistant chat thread, if one already exists. + /// The initial assistant component state. + /// The new session snapshot, or the existing active session snapshot. + public AssistantSessionSnapshot TryBegin(AssistantSessionKey key, string title, CancellationTokenSource cancellationTokenSource, ChatThread? chatThread, Dictionary state) + { + if (this.sessions.TryGetValue(key, out var existing) && existing.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING) + return CreateSnapshot(existing); + + var now = DateTimeOffset.Now; + var session = new AssistantSessionState + { + SessionId = Guid.NewGuid(), + Key = key, + CancellationTokenSource = cancellationTokenSource, + StartedAt = now, + UpdatedAt = now, + Title = title, + Status = AssistantSessionStatus.RUNNING, + ChatThread = chatThread, + State = state, + }; + + this.sessions[key] = session; + _ = this.NotifyChangedAsync(session); + return CreateSnapshot(session); + } + + /// + /// Updates a running assistant session with the latest UI and chat state. + /// + /// The assistant session key. + /// The concrete run ID that is allowed to write the checkpoint. + /// The current user-visible assistant title. + /// The current assistant chat thread. + /// The current assistant component state. + public async Task CheckpointAsync(AssistantSessionKey key, Guid sessionId, string title, ChatThread? chatThread, Dictionary state) + { + if (!this.sessions.TryGetValue(key, out var session)) + return; + + lock (session.SyncRoot) + { + if (session.SessionId != sessionId) + return; + + session.Title = title; + session.ChatThread = chatThread; + session.State = state; + session.UpdatedAt = DateTimeOffset.Now; + } + + await this.NotifyChangedAsync(session); + } + + /// + /// Requests cancellation for an active assistant session. + /// + /// The assistant session key to cancel. + public async Task CancelAsync(AssistantSessionKey key) + { + if (!this.sessions.TryGetValue(key, out var session)) + return; + + lock (session.SyncRoot) + { + if (session.Status is not AssistantSessionStatus.RUNNING) + return; + + session.Status = AssistantSessionStatus.CANCELING; + session.UpdatedAt = DateTimeOffset.Now; + } + + try + { + if (!session.CancellationTokenSource.IsCancellationRequested) + await session.CancellationTokenSource.CancelAsync(); + } + catch (ObjectDisposedException) + { + return; + } + + await this.NotifyChangedAsync(session); + } + + /// + /// Moves an assistant session into a terminal state and publishes completion. + /// + /// The assistant session key. + /// The concrete run ID that is allowed to complete the session. + /// The terminal status to store. + /// The user-visible error message for failed sessions. + /// The final assistant chat thread. + /// The final assistant component state. + public async Task CompleteAsync(AssistantSessionKey key, Guid sessionId, AssistantSessionStatus status, string errorMessage, ChatThread? chatThread, Dictionary state) + { + if (!this.sessions.TryGetValue(key, out var session)) + return; + + lock (session.SyncRoot) + { + if (session.SessionId != sessionId) + return; + + session.Status = status; + session.ErrorMessage = errorMessage; + session.ChatThread = chatThread; + session.State = state; + session.UpdatedAt = DateTimeOffset.Now; + session.FinishedAt = session.UpdatedAt; + } + + await this.NotifyChangedAsync(session); + await messageBus.SendMessage(null, Event.ASSISTANT_SESSION_FINISHED, CreateSnapshot(session)); + + try + { + session.CancellationTokenSource.Dispose(); + } + catch + { + // ignore + } + } + + /// + /// Clears an inactive assistant session. + /// + /// The assistant session key to clear. + public async Task ClearAsync(AssistantSessionKey key) + { + if (!this.sessions.TryGetValue(key, out var session)) + return; + + if (session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING) + return; + + lock (session.SyncRoot) + { + session.Status = AssistantSessionStatus.NONE; + session.ChatThread = null; + session.State = new(StringComparer.Ordinal); + session.UpdatedAt = DateTimeOffset.Now; + session.FinishedAt = session.UpdatedAt; + } + + this.sessions.TryRemove(key, out _); + await messageBus.SendMessage(null, Event.ASSISTANT_SESSION_CHANGED, CreateSnapshot(session)); + } + + /// + /// Clears all inactive sessions for a component. + /// + /// The component whose inactive sessions should be cleared. + public async Task ClearInactiveSessionsForComponentAsync(Components component) + { + var matchingKeys = this.sessions + .Where(pair => pair.Key.Component == component && pair.Value.Status is not AssistantSessionStatus.RUNNING and not AssistantSessionStatus.CANCELING) + .Select(pair => pair.Key) + .ToList(); + + foreach (var key in matchingKeys) + await this.ClearAsync(key); + } + + /// + /// Publishes an assistant session change event. + /// + /// The runtime session whose copied snapshot should be published. + private async Task NotifyChangedAsync(AssistantSessionState session) + { + await messageBus.SendMessage(null, Event.ASSISTANT_SESSION_CHANGED, CreateSnapshot(session)); + } + + /// + /// Creates a copied, external snapshot from the internal runtime state. + /// + /// The runtime session to copy. + /// A snapshot safe to send to UI components. + private static AssistantSessionSnapshot CreateSnapshot(AssistantSessionState session) + { + lock (session.SyncRoot) + { + return new() + { + SessionId = session.SessionId, + Key = session.Key, + Title = session.Title, + Status = session.Status, + StartedAt = session.StartedAt, + UpdatedAt = session.UpdatedAt, + FinishedAt = session.FinishedAt, + ErrorMessage = session.ErrorMessage, + ChatThread = session.ChatThread, + State = new Dictionary(session.State, StringComparer.Ordinal), + }; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshot.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshot.cs new file mode 100644 index 00000000..b5676df7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshot.cs @@ -0,0 +1,68 @@ +using AIStudio.Chat; + +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Immutable-style view of an assistant session for UI consumers and message bus events. +/// +/// +/// The service creates snapshots by copying its internal runtime state. Consumers must +/// treat the contained chat thread and state objects as read-only views of the session. +/// +public sealed record AssistantSessionSnapshot +{ + /// + /// Identifies the concrete run represented by this snapshot. + /// + public required Guid SessionId { get; init; } + + /// + /// Identifies the assistant and logical session slot represented by this snapshot. + /// + public required AssistantSessionKey Key { get; init; } + + /// + /// Gets the user-visible assistant title. + /// + public required string Title { get; init; } + + /// + /// Gets the current lifecycle status. + /// + public required AssistantSessionStatus Status { get; init; } + + /// + /// Gets when the session run started. + /// + public required DateTimeOffset StartedAt { get; init; } + + /// + /// Gets when the session was last changed. + /// + public required DateTimeOffset UpdatedAt { get; init; } + + /// + /// Gets when the session reached a terminal state. + /// + public DateTimeOffset? FinishedAt { get; init; } + + /// + /// Gets the user-visible error message for failed sessions. + /// + public string ErrorMessage { get; init; } = string.Empty; + + /// + /// Gets the assistant chat thread captured for this session. + /// + public ChatThread? ChatThread { get; init; } + + /// + /// Gets the assistant component state captured for this session. + /// + public IReadOnlyDictionary State { get; init; } = new Dictionary(); + + /// + /// Gets whether the session is still running or canceling. + /// + public bool IsActive => this.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshotField.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshotField.cs new file mode 100644 index 00000000..053bfb11 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionSnapshotField.cs @@ -0,0 +1,45 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Stores one typed value in an assistant session snapshot. +/// +/// The captured value type. +public sealed record AssistantSessionSnapshotField : IAssistantSessionSnapshotField +{ + /// + /// Initializes a new typed snapshot field. + /// + /// The captured value. + public AssistantSessionSnapshotField(T value) + { + this.Value = value; + } + + /// + /// Gets the captured value. + /// + public T Value { get; } + + /// + public Type ValueType => typeof(T); + + /// + public bool TryRead(out TValue? value) + { + if (this.Value is TValue typedValue) + { + value = typedValue; + return true; + } + + if (this.Value is null && default(TValue) is null) + { + value = default; + return true; + } + + value = default; + return false; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateKey.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateKey.cs new file mode 100644 index 00000000..8cab0f0c --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateKey.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Identifies a typed assistant session state value. +/// +/// The value type stored for this key. +public readonly record struct AssistantSessionStateKey +{ + /// + /// Initializes a new assistant session state key. + /// + /// The stable dictionary name used in assistant session snapshots. + public AssistantSessionStateKey(string name) + { + this.Name = name; + } + + /// + /// Gets the stable dictionary name used in assistant session snapshots. + /// + public string Name { get; } + + /// + /// Returns the stable dictionary name. + /// + /// The stable dictionary name. + public override string ToString() => this.Name; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateReader.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateReader.cs new file mode 100644 index 00000000..4b826ed6 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateReader.cs @@ -0,0 +1,111 @@ +using System.Text; + +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Restores typed assistant session state values from a snapshot. +/// +/// The captured snapshot fields. +/// The user-visible assistant title. +public sealed class AssistantSessionStateReader(IReadOnlyDictionary fields, string assistantTitle) +{ + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); + + /// + /// Restores a typed value when it exists in the snapshot. + /// + /// The value type. + /// The typed state key. + /// The action that applies the restored value. + public void Restore(AssistantSessionStateKey key, Action apply) + { + if (this.TryRead(key, out var value)) + apply(value!); + } + + /// + /// Restores a list into an existing list instance. + /// + /// The list item type. + /// The typed state key. + /// The existing list to update. + public void RestoreList(AssistantSessionStateKey> key, List target) + { + this.Restore(key, values => + { + target.Clear(); + target.AddRange(values); + }); + } + + /// + /// Restores a hash set into an existing hash set instance. + /// + /// The set item type. + /// The typed state key. + /// The existing hash set to update. + public void RestoreHashSet(AssistantSessionStateKey> key, HashSet target) + { + this.Restore(key, values => + { + target.Clear(); + target.UnionWith(values); + }); + } + + /// + /// Restores a dictionary into an existing dictionary instance. + /// + /// The dictionary key type. + /// The dictionary value type. + /// The typed state key. + /// The existing dictionary to update. + public void RestoreDictionary(AssistantSessionStateKey> key, Dictionary target) where TKey : notnull + { + this.Restore(key, values => + { + target.Clear(); + foreach (var (itemKey, itemValue) in values) + target[itemKey] = itemValue; + }); + } + + /// + /// Restores text into an existing string builder instance. + /// + /// The typed state key. + /// The existing string builder to update. + public void RestoreStringBuilder(AssistantSessionStateKey key, StringBuilder target) + { + this.Restore(key, value => + { + target.Clear(); + target.Append(value); + }); + } + + /// + /// Tries to read a typed value from the snapshot. + /// + /// The requested value type. + /// The typed state key. + /// The restored value when reading succeeds. + /// true when a value exists and matches the requested type; otherwise, false. + private bool TryRead(AssistantSessionStateKey key, out T? value) + { + value = default; + if (!fields.TryGetValue(key.Name, out var field)) + return false; + + if (field.TryRead(out value)) + return true; + + LOG.LogWarning( + "Could not restore assistant session field '{FieldStateKey}' for assistant '{AssistantTitle}'. ExpectedType='{ExpectedType}', CapturedValueType='{CapturedValueType}'. The current value is kept.", + key.Name, + assistantTitle, + typeof(T).FullName, + field.ValueType.FullName); + return false; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateWriter.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateWriter.cs new file mode 100644 index 00000000..e57d3c6c --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStateWriter.cs @@ -0,0 +1,78 @@ +using System.Text; + +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Collects typed assistant session state values for a snapshot. +/// +public sealed class AssistantSessionStateWriter +{ + /// + /// Stores captured fields by their stable dictionary names. + /// + private readonly Dictionary fields = new(StringComparer.Ordinal); + + /// + /// Stores a typed state value. + /// + /// The value type. + /// The typed state key. + /// The captured value. + public void Set(AssistantSessionStateKey key, T value) + { + this.fields[key.Name] = new AssistantSessionSnapshotField(value); + } + + /// + /// Stores a list copy. + /// + /// The list item type. + /// The typed state key. + /// The values to copy. + public void SetList(AssistantSessionStateKey> key, IEnumerable values) + { + this.Set(key, values.ToList()); + } + + /// + /// Stores a hash set copy. + /// + /// The set item type. + /// The typed state key. + /// The values to copy. + public void SetHashSet(AssistantSessionStateKey> key, IEnumerable values) + { + this.Set(key, values.ToHashSet()); + } + + /// + /// Stores a dictionary copy. + /// + /// The dictionary key type. + /// The dictionary value type. + /// The typed state key. + /// The values to copy. + public void SetDictionary(AssistantSessionStateKey> key, IDictionary values) where TKey : notnull + { + this.Set(key, new Dictionary(values)); + } + + /// + /// Stores the current text from a string builder. + /// + /// The typed state key. + /// The string builder to read. + public void SetStringBuilder(AssistantSessionStateKey key, StringBuilder value) + { + this.Set(key, value.ToString()); + } + + /// + /// Returns the captured fields as a dictionary. + /// + /// A copied dictionary containing the captured fields. + public Dictionary ToDictionary() + { + return new(this.fields, StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStatus.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStatus.cs new file mode 100644 index 00000000..96f3485f --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionStatus.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Describes the lifecycle state of an assistant session. +/// +public enum AssistantSessionStatus +{ + /// + /// No session state is available. + /// + NONE, + + /// + /// The assistant session is running. + /// + RUNNING, + + /// + /// Cancellation was requested and the assistant is shutting down. + /// + CANCELING, + + /// + /// The assistant session completed successfully. + /// + COMPLETED, + + /// + /// The assistant session was canceled. + /// + CANCELED, + + /// + /// The assistant session failed. + /// + FAILED, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/IAssistantSessionSnapshotField.cs b/app/MindWork AI Studio/Tools/AssistantSessions/IAssistantSessionSnapshotField.cs new file mode 100644 index 00000000..c64780a9 --- /dev/null +++ b/app/MindWork AI Studio/Tools/AssistantSessions/IAssistantSessionSnapshotField.cs @@ -0,0 +1,20 @@ +namespace AIStudio.Tools.AssistantSessions; + +/// +/// Provides a typed value stored in an assistant session snapshot. +/// +public interface IAssistantSessionSnapshotField +{ + /// + /// Gets the type used when the value was captured. + /// + Type ValueType { get; } + + /// + /// Tries to read the captured value as the requested type. + /// + /// The requested value type. + /// The typed value when reading succeeds. + /// true when the captured value matches ; otherwise, false. + bool TryRead(out T? value); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 15ee6183..dbc737e4 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -144,6 +144,16 @@ public enum Event /// Notifies receivers that chat generation state changed. /// CHAT_GENERATION_CHANGED, + + /// + /// Notifies receivers that an assistant session changed. + /// + ASSISTANT_SESSION_CHANGED, + + /// + /// Notifies receivers that an assistant session finished. + /// + ASSISTANT_SESSION_FINISHED, // Workspace events: /// diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantState.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantState.cs index be172190..23adc194 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantState.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantState.cs @@ -30,6 +30,49 @@ public sealed class AssistantState this.Times.Clear(); } + /// + /// Copies all dynamic assistant state values from another state instance. + /// + /// The state instance to copy from. + public void CopyFrom(AssistantState other) + { + this.Clear(); + CopyDictionary(other.Text, this.Text); + CopyDictionary(other.SingleSelect, this.SingleSelect); + CopyDictionary(other.MultiSelect, this.MultiSelect); + CopyDictionary(other.Booleans, this.Booleans); + CopyDictionary(other.WebContent, this.WebContent); + CopyDictionary(other.FileContent, this.FileContent); + CopyDictionary(other.Colors, this.Colors); + CopyDictionary(other.Dates, this.Dates); + CopyDictionary(other.DateRanges, this.DateRanges); + CopyDictionary(other.Times, this.Times); + } + + /// + /// Creates a copy of the dynamic assistant state. + /// + /// A copied assistant state instance. + public AssistantState Clone() + { + var clone = new AssistantState(); + clone.CopyFrom(this); + return clone; + } + + /// + /// Copies all entries from one dictionary into another dictionary. + /// + /// The dictionary key type. + /// The dictionary value type. + /// The source dictionary. + /// The target dictionary. + private static void CopyDictionary(Dictionary source, Dictionary target) where TKey : notnull + { + foreach (var (key, value) in source) + target[key] = value; + } + public bool TryApplyValue(string fieldName, LuaValue value, out string expectedType) { expectedType = string.Empty; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md index 435b1554..dcd358f8 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md @@ -1,2 +1,3 @@ # v26.6.3, build 243 (2026-06-xx xx:xx UTC) +- Improved assistants so running tasks can continue when you leave the assistant and return later. - Improved the chat experience by automatically focusing the message composer again when it becomes available. Thanks, Dominic Neuburg (`donework`), for the contribution. \ No newline at end of file