Allow assistants to run in the background (#828)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-07-03 15:02:20 +02:00 committed by GitHub
parent fc53278c60
commit 508c7f9701
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 2464 additions and 168 deletions

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.Agenda; namespace AIStudio.Assistants.Agenda;
@ -185,6 +186,85 @@ public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
private string inputWhoIsPresenting = string.Empty; private string inputWhoIsPresenting = string.Empty;
private readonly List<string> contentLines = []; private readonly List<string> contentLines = [];
private static readonly AssistantSessionStateKey<string> INPUT_TOPIC_STATE_KEY = new(nameof(inputTopic));
private static readonly AssistantSessionStateKey<string> INPUT_NAME_STATE_KEY = new(nameof(inputName));
private static readonly AssistantSessionStateKey<string> INPUT_CONTENT_STATE_KEY = new(nameof(inputContent));
private static readonly AssistantSessionStateKey<string> INPUT_DURATION_STATE_KEY = new(nameof(inputDuration));
private static readonly AssistantSessionStateKey<string> INPUT_START_TIME_STATE_KEY = new(nameof(inputStartTime));
private static readonly AssistantSessionStateKey<HashSet<string>> SELECTED_FOCI_STATE_KEY = new(nameof(selectedFoci));
private static readonly AssistantSessionStateKey<HashSet<string>> JUST_BRIEFLY_STATE_KEY = new(nameof(justBriefly));
private static readonly AssistantSessionStateKey<string> INPUT_OBJECTIVE_STATE_KEY = new(nameof(inputObjective));
private static readonly AssistantSessionStateKey<string> INPUT_MODERATOR_STATE_KEY = new(nameof(inputModerator));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<bool> INTRODUCE_PARTICIPANTS_STATE_KEY = new(nameof(introduceParticipants));
private static readonly AssistantSessionStateKey<bool> IS_MEETING_VIRTUAL_STATE_KEY = new(nameof(isMeetingVirtual));
private static readonly AssistantSessionStateKey<string> INPUT_LOCATION_STATE_KEY = new(nameof(inputLocation));
private static readonly AssistantSessionStateKey<bool> GOING_TO_DINNER_STATE_KEY = new(nameof(goingToDinner));
private static readonly AssistantSessionStateKey<bool> DOING_SOCIAL_ACTIVITY_STATE_KEY = new(nameof(doingSocialActivity));
private static readonly AssistantSessionStateKey<bool> NEED_TO_ARRIVE_AND_DEPART_STATE_KEY = new(nameof(needToArriveAndDepart));
private static readonly AssistantSessionStateKey<int> DURATION_LUNCH_BREAK_STATE_KEY = new(nameof(durationLunchBreak));
private static readonly AssistantSessionStateKey<int> DURATION_BREAKS_STATE_KEY = new(nameof(durationBreaks));
private static readonly AssistantSessionStateKey<bool> ACTIVE_PARTICIPATION_STATE_KEY = new(nameof(activeParticipation));
private static readonly AssistantSessionStateKey<NumberParticipants> NUMBER_PARTICIPANTS_STATE_KEY = new(nameof(numberParticipants));
private static readonly AssistantSessionStateKey<string> INPUT_WHO_IS_PRESENTING_STATE_KEY = new(nameof(inputWhoIsPresenting));
private static readonly AssistantSessionStateKey<List<string>> CONTENT_LINES_STATE_KEY = new(nameof(contentLines));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -24,7 +24,7 @@
<InnerScrolling> <InnerScrolling>
<ChildContent> <ChildContent>
<MudForm @ref="@(this.Form)" @bind-IsValid="@(this.InputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2"> <MudForm @ref="@(this.Form)" @bind-IsValid="@(this.InputIsValid)" @bind-Errors="@(this.InputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-2"> <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-2">
@this.Description @this.Description
</MudText> </MudText>
@ -38,10 +38,10 @@
</CascadingValue> </CascadingValue>
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3"> <MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3">
<MudButton Disabled="@(this.SubmitDisabled || this.isProcessing)" Variant="Variant.Filled" OnClick="@(async () => await this.Start())" Style="@this.SubmitButtonStyle"> <MudButton Disabled="@(this.SubmitDisabled || this.IsProcessing)" Variant="Variant.Filled" OnClick="@(async () => await this.Start())" Style="@this.SubmitButtonStyle">
@this.SubmitText @this.SubmitText
</MudButton> </MudButton>
@if (this.isProcessing && this.CancellationTokenSource is not null) @if (this.IsProcessing)
{ {
<MudTooltip Text="@TB("Stop generation")"> <MudTooltip Text="@TB("Stop generation")">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@(async () => await this.CancelStreaming())"/> <MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@(async () => await this.CancelStreaming())"/>
@ -50,9 +50,9 @@
</MudStack> </MudStack>
} }
</MudForm> </MudForm>
<Issues IssuesData="@(this.inputIssues)"/> <Issues IssuesData="@(this.InputIssues)"/>
@if (this.ShowDedicatedProgress && this.isProcessing) @if (this.ShowDedicatedProgress && this.IsProcessing)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" /> <MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
} }
@ -63,9 +63,9 @@
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3"> <div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div> </div>
@if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null && this.resultingContentBlock.Content is not null) @if (this.ShowResult && !this.ShowEntireChatThread && this.ResultingContentBlock is not null && this.ResultingContentBlock.Content is not null)
{ {
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@this.resultingContentBlock.Content"/> <ContentBlockComponent Role="@(this.ResultingContentBlock.Role)" Type="@(this.ResultingContentBlock.ContentType)" Time="@(this.ResultingContentBlock.Time)" Content="@this.ResultingContentBlock.Content"/>
} }
@if(this.ShowResult && this.ShowEntireChatThread && this.ChatThread is not null) @if(this.ShowResult && this.ShowEntireChatThread && this.ChatThread is not null)

View File

@ -2,6 +2,8 @@ using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -36,6 +38,15 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
[Inject] [Inject]
private MudTheme ColorTheme { get; init; } = null!; private MudTheme ColorTheme { get; init; } = null!;
[Inject]
protected AssistantSessionService AssistantSessionService { get; init; } = null!;
/// <summary>
/// Gets the job service used to run assistant-created chats independently from the assistant UI.
/// </summary>
[Inject]
protected AIJobService AIJobService { get; init; } = null!;
protected abstract string Title { get; } protected abstract string Title { get; }
@ -45,7 +56,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected abstract Tools.Components Component { get; } protected abstract Tools.Components Component { get; }
protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch protected virtual Func<string> Result2Copy => () => this.ResultingContentBlock is null ? string.Empty : this.ResultingContentBlock.Content switch
{ {
ContentText textBlock => textBlock.Text, ContentText textBlock => textBlock.Text,
_ => string.Empty, _ => string.Empty,
@ -111,20 +122,29 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected virtual bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel); protected virtual bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
protected AIStudio.Settings.Provider ProviderSettings = Settings.Provider.NONE;
protected MudForm? Form;
protected bool InputIsValid;
protected Profile CurrentProfile = Profile.NO_PROFILE;
protected ChatTemplate CurrentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
protected ChatThread? ChatThread;
protected IContent? LastUserPrompt;
protected CancellationTokenSource? CancellationTokenSource;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6)); private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
protected MudForm? Form;
protected CancellationTokenSource? CancellationTokenSource;
private bool isDisposed;
private AssistantSessionKey assistantSessionKey;
private Guid? assistantSessionId;
private AssistantSessionSnapshot? pendingRenderedAssistantSessionSnapshot;
private ContentBlock? resultingContentBlock; /// <summary>
private string[] inputIssues = []; /// Gets whether the Blazor component instance has already been disposed.
private bool isProcessing; /// </summary>
protected bool IsAssistantComponentDisposed => this.isDisposed;
/// <summary>
/// Gets whether this component has attached an assistant session snapshot.
/// </summary>
protected bool HasAssistantSession => this.assistantSessionId is not null;
/// <summary>
/// Gets the assistant-specific identifier used to distinguish session slots.
/// </summary>
protected virtual string AssistantSessionInstanceId => this.GetType().FullName ?? this.Component.ToString();
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -150,6 +170,8 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component); this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
this.assistantSessionKey = new(this.Component, this.AssistantSessionInstanceId);
await this.AttachAssistantSessionIfAvailable();
} }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
@ -166,6 +188,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
// We don't want to show validation errors when the user opens the dialog. // We don't want to show validation errors when the user opens the dialog.
if(firstRender) if(firstRender)
this.Form?.ResetValidation(); this.Form?.ResetValidation();
if (this.pendingRenderedAssistantSessionSnapshot is { } snapshot)
{
this.pendingRenderedAssistantSessionSnapshot = null;
await this.OnAssistantSessionRenderedAsync(snapshot);
}
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
@ -191,12 +219,67 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
private async Task Start() 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 = await this.AssistantSessionService.TryBeginAsync(this.assistantSessionKey, this.Title, this.CancellationTokenSource, this.ChatThread, this.CaptureAssistantSessionState(), this);
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(); 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(), this);
if (!this.isDisposed)
_ = this.AssistantSessionService.TryTakeInactiveSnapshot(this.assistantSessionKey);
}
sessionCancellationTokenSource?.Dispose();
await this.RefreshAssistantUIAsync();
} }
this.CancellationTokenSource = null;
} }
private void TriggerFormChange(FormFieldChangedEventArgs _) private void TriggerFormChange(FormFieldChangedEventArgs _)
@ -221,10 +304,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
/// <param name="issue">The issue to add.</param> /// <param name="issue">The issue to add.</param>
protected void AddInputIssue(string issue) protected void AddInputIssue(string issue)
{ {
Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1); Array.Resize(ref this.InputIssues, this.InputIssues.Length + 1);
this.inputIssues[^1] = issue; this.InputIssues[^1] = issue;
this.InputIsValid = false; this.InputIsValid = false;
this.StateHasChanged(); _ = this.RefreshAssistantUIAsync();
} }
/// <summary> /// <summary>
@ -232,9 +315,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
/// </summary> /// </summary>
protected void ClearInputIssues() protected void ClearInputIssues()
{ {
this.inputIssues = []; this.InputIssues = [];
this.InputIsValid = true; this.InputIsValid = true;
this.StateHasChanged(); _ = this.RefreshAssistantUIAsync();
} }
protected void CreateChatThread() protected void CreateChatThread()
@ -310,7 +393,19 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
InitialRemoteWait = true, InitialRemoteWait = true,
}; };
this.resultingContentBlock = new ContentBlock aiText.StreamingEvent = async () =>
{
await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
};
aiText.StreamingDone = async () =>
{
await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
};
this.ResultingContentBlock = new ContentBlock
{ {
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
@ -321,12 +416,13 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
if (this.ChatThread is not null) if (this.ChatThread is not null)
{ {
this.ChatThread.Blocks.Add(this.resultingContentBlock); this.ChatThread.Blocks.Add(this.ResultingContentBlock);
this.ChatThread.SelectedProvider = this.ProviderSettings.Id; this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
} }
this.isProcessing = true; this.IsProcessing = true;
this.StateHasChanged(); await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
try try
{ {
@ -343,18 +439,19 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
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); 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)); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
if (this.resultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text)) if (this.ResultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text))
{ {
this.ChatThread?.Blocks.Remove(this.resultingContentBlock); this.ChatThread?.Blocks.Remove(this.ResultingContentBlock);
this.resultingContentBlock = null; this.ResultingContentBlock = null;
} }
return string.Empty; return string.Empty;
} }
finally finally
{ {
this.isProcessing = false; this.IsProcessing = this.assistantSessionId is not null && (this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey)?.IsActive ?? false);
this.StateHasChanged(); await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
if(manageCancellationLocally) if(manageCancellationLocally)
{ {
@ -363,12 +460,54 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
} }
} }
} }
/// <summary>
/// Starts the current assistant chat thread as a regular background-capable chat generation job.
/// </summary>
/// <remarks>
/// Use this when an assistant creates a chat and hands it over to the chat page instead of
/// rendering the answer inside the assistant UI.
/// </remarks>
/// <param name="time">The timestamp to use for the AI response block.</param>
/// <param name="hideContentFromUser">Whether the AI response block should be hidden from the user.</param>
/// <param name="isForeground">Whether the chat job should start as the current foreground job.</param>
/// <returns>A task that completes after the chat job was registered.</returns>
protected async Task StartChatGenerationJobAsync(DateTimeOffset time, bool hideContentFromUser = false, bool isForeground = true)
{
if (this.ChatThread is null)
return;
var aiText = new ContentText
{
InitialRemoteWait = true,
};
this.ResultingContentBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
HideFromUser = hideContentFromUser,
};
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
await this.CheckpointAssistantSession();
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
{
ChatThread = this.ChatThread,
AIText = aiText,
LastUserPrompt = this.LastUserPrompt,
ProviderSettings = this.ProviderSettings,
IsForeground = isForeground,
});
}
private async Task CancelStreaming() private async Task CancelStreaming()
{ {
if (this.CancellationTokenSource is not null) await this.AssistantSessionService.CancelAsync(this.assistantSessionKey, this);
if(!this.CancellationTokenSource.IsCancellationRequested)
await this.CancellationTokenSource.CancelAsync();
} }
protected async Task CopyToClipboard() protected async Task CopyToClipboard()
@ -434,15 +573,15 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
await this.DialogService.ShowAsync<TSettings>(null, dialogParameters, DialogOptions.FULLSCREEN); await this.DialogService.ShowAsync<TSettings>(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)) if (!this.CanSendToAssistant(destination))
return Task.CompletedTask; return;
var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch
{ {
false => sendToButton.GetText(), false => sendToButton.GetText(),
true => this.resultingContentBlock?.Content switch true => this.ResultingContentBlock?.Content switch
{ {
ContentText textBlock => textBlock.Text, ContentText textBlock => textBlock.Text,
_ => string.Empty, _ => string.Empty,
@ -450,6 +589,16 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
}; };
var sendToData = destination.GetData(); 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) switch (destination)
{ {
case Tools.Components.CHAT: case Tools.Components.CHAT:
@ -469,7 +618,6 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
} }
this.NavigationManager.NavigateTo(sendToData.Route); this.NavigationManager.NavigateTo(sendToData.Route);
return Task.CompletedTask;
} }
private bool CanSendToAssistant(Tools.Components component) private bool CanSendToAssistant(Tools.Components component)
@ -482,7 +630,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
private async Task InnerResetForm() private async Task InnerResetForm()
{ {
this.resultingContentBlock = null; 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; this.ProviderSettings = Settings.Provider.NONE;
await this.JsRuntime.ClearDiv(RESULT_DIV_ID); await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
@ -492,10 +645,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.ResetProviderAndProfileSelection(); this.ResetProviderAndProfileSelection();
this.InputIsValid = false; this.InputIsValid = false;
this.inputIssues = []; this.InputIssues = [];
this.Form?.ResetValidation(); this.Form?.ResetValidation();
this.StateHasChanged(); await this.RefreshAssistantUIAsync();
this.Form?.ResetValidation(); this.Form?.ResetValidation();
} }
@ -515,6 +668,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected override void DisposeResources() protected override void DisposeResources()
{ {
this.isDisposed = true;
try try
{ {
this.formChangeTimer.Stop(); this.formChangeTimer.Stop();
@ -529,4 +683,177 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
} }
#endregion #endregion
}
#region Assistant sessions
/// <summary>
/// Stores the current assistant UI and chat state in the active assistant session.
/// </summary>
/// <returns>A task that completes after the checkpoint was stored and published.</returns>
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(), this);
}
/// <summary>
/// Allows derived assistants to restore client-only UI after a session was attached.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was attached.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionAttachedAsync(AssistantSessionSnapshot snapshot) => Task.CompletedTask;
/// <summary>
/// Allows derived assistants to restore DOM-dependent client-only UI after an attached session was rendered.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was rendered.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionRenderedAsync(AssistantSessionSnapshot snapshot) => Task.CompletedTask;
/// <summary>
/// Handles assistant session change events for the current assistant instance.
/// </summary>
/// <typeparam name="T">The message payload type.</typeparam>
/// <param name="sendingComponent">The component that sent the message, if any.</param>
/// <param name="triggeredEvent">The event that was triggered.</param>
/// <param name="data">The message payload.</param>
/// <returns>A task that completes after the message was processed.</returns>
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (ReferenceEquals(sendingComponent, this))
return;
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);
if (triggeredEvent is Event.ASSISTANT_SESSION_FINISHED)
_ = this.AssistantSessionService.TryTakeInactiveSnapshot(this.assistantSessionKey);
}
break;
}
}
/// <summary>
/// Attaches the component to an existing assistant session if one is available.
/// </summary>
/// <returns>A task that completes after the session was attached.</returns>
private async Task AttachAssistantSessionIfAvailable()
{
var snapshot = this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey);
if (snapshot?.IsActive ?? false)
{
await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: true);
return;
}
snapshot = this.AssistantSessionService.TryTakeInactiveSnapshot(this.assistantSessionKey);
if (snapshot is null)
return;
await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: true);
}
/// <summary>
/// Applies an assistant session snapshot to this component instance.
/// </summary>
/// <param name="snapshot">The snapshot to attach.</param>
/// <param name="restoreClientOnlyContent">Whether derived assistants should restore client-only UI state.</param>
/// <returns>A task that completes after the component was refreshed.</returns>
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);
if (restoreClientOnlyContent)
this.pendingRenderedAssistantSessionSnapshot = snapshot;
await this.RefreshAssistantUIAsync();
}
/// <summary>
/// Refreshes the component when it is still mounted.
/// </summary>
/// <returns>A task that completes after the renderer was notified.</returns>
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.
}
}
/// <summary>
/// Captures the base assistant state and assistant-specific typed state values for session restore.
/// </summary>
/// <returns>A dictionary containing the current assistant state.</returns>
private Dictionary<string, IAssistantSessionSnapshotField> 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();
}
/// <summary>
/// Captures assistant-specific state values.
/// </summary>
/// <param name="state">The typed state writer to update.</param>
protected virtual void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) { }
/// <summary>
/// Restores the base assistant state and assistant-specific typed state values from a session snapshot.
/// </summary>
/// <param name="state">The captured assistant state to import.</param>
private void ImportAssistantSessionState(IReadOnlyDictionary<string, IAssistantSessionSnapshotField> 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);
}
/// <summary>
/// Restores assistant-specific state values.
/// </summary>
/// <param name="state">The typed state reader to read from.</param>
protected virtual void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) { }
#endregion
}

View File

@ -1,4 +1,7 @@
using AIStudio.Chat;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants; namespace AIStudio.Assistants;
@ -9,4 +12,25 @@ public abstract class AssistantLowerBase : MSGComponentBase
internal const string RESULT_DIV_ID = "assistantResult"; internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult"; internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult"; internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
protected static readonly AssistantSessionStateKey<AIStudio.Settings.Provider> PROVIDER_SETTINGS_STATE_KEY = new(nameof(ProviderSettings));
protected static readonly AssistantSessionStateKey<bool> INPUT_IS_VALID_STATE_KEY = new(nameof(InputIsValid));
protected static readonly AssistantSessionStateKey<Profile> CURRENT_PROFILE_STATE_KEY = new(nameof(CurrentProfile));
protected static readonly AssistantSessionStateKey<ChatTemplate> CURRENT_CHAT_TEMPLATE_STATE_KEY = new(nameof(CurrentChatTemplate));
protected static readonly AssistantSessionStateKey<ChatThread?> CHAT_THREAD_STATE_KEY = new(nameof(ChatThread));
protected static readonly AssistantSessionStateKey<IContent?> LAST_USER_PROMPT_STATE_KEY = new(nameof(LastUserPrompt));
protected static readonly AssistantSessionStateKey<ContentBlock?> RESULTING_CONTENT_BLOCK_STATE_KEY = new(nameof(ResultingContentBlock));
protected static readonly AssistantSessionStateKey<string[]> INPUT_ISSUES_STATE_KEY = new(nameof(InputIssues));
protected static readonly AssistantSessionStateKey<bool> IS_PROCESSING_STATE_KEY = new(nameof(IsProcessing));
protected AIStudio.Settings.Provider ProviderSettings = Settings.Provider.NONE;
protected bool InputIsValid;
protected Profile CurrentProfile = Profile.NO_PROFILE;
protected ChatTemplate CurrentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
protected ChatThread? ChatThread;
protected IContent? LastUserPrompt;
protected ContentBlock? ResultingContentBlock;
protected string[] InputIssues = [];
protected bool IsProcessing;
} }

View File

@ -3,6 +3,7 @@ using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.BiasDay; namespace AIStudio.Assistants.BiasDay;
@ -66,6 +67,25 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAss
private Bias biasOfTheDay = BiasCatalog.NONE; private Bias biasOfTheDay = BiasCatalog.NONE;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS; private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<Bias> BIAS_OF_THE_DAY_STATE_KEY = new(nameof(biasOfTheDay));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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) private string? ValidateTargetLanguage(CommonLanguages language)
{ {
@ -149,8 +169,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAss
Please tell me about the bias of the day. Please tell me about the bias of the day.
""", true); """, true);
// Start the AI response without waiting for it to finish: await this.StartChatGenerationJobAsync(time);
_ = this.AddAIResponseAsync(time);
await this.SendToAssistant(Tools.Components.CHAT, default); await this.SendToAssistant(Tools.Components.CHAT, default);
} }
} }

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.Coding; namespace AIStudio.Assistants.Coding;
@ -59,6 +60,28 @@ public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding>
private bool provideCompilerMessages; private bool provideCompilerMessages;
private string compilerMessages = string.Empty; private string compilerMessages = string.Empty;
private string questions = string.Empty; private string questions = string.Empty;
private static readonly AssistantSessionStateKey<List<CodingContext>> CODING_CONTEXTS_STATE_KEY = new(nameof(codingContexts));
private static readonly AssistantSessionStateKey<bool> PROVIDE_COMPILER_MESSAGES_STATE_KEY = new(nameof(provideCompilerMessages));
private static readonly AssistantSessionStateKey<string> COMPILER_MESSAGES_STATE_KEY = new(nameof(compilerMessages));
private static readonly AssistantSessionStateKey<string> QUESTIONS_STATE_KEY = new(nameof(questions));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -7,6 +7,7 @@ using AIStudio.Dialogs.Settings;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -279,6 +280,55 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
private ProfilePreselection policyPreselectedProfile = ProfilePreselection.NoProfile; private ProfilePreselection policyPreselectedProfile = ProfilePreselection.NoProfile;
private HashSet<FileAttachment> loadedDocumentPaths = []; private HashSet<FileAttachment> loadedDocumentPaths = [];
private readonly List<ConfigurationSelectData<string>> availableLLMProviders = new(); private readonly List<ConfigurationSelectData<string>> availableLLMProviders = new();
private static readonly AssistantSessionStateKey<DataDocumentAnalysisPolicy?> SELECTED_POLICY_STATE_KEY = new(nameof(selectedPolicy));
private static readonly AssistantSessionStateKey<bool> POLICY_IS_PROTECTED_STATE_KEY = new(nameof(policyIsProtected));
private static readonly AssistantSessionStateKey<bool> POLICY_HIDE_POLICY_DEFINITION_STATE_KEY = new(nameof(policyHidePolicyDefinition));
private static readonly AssistantSessionStateKey<bool> POLICY_DEFINITION_EXPANDED_STATE_KEY = new(nameof(policyDefinitionExpanded));
private static readonly AssistantSessionStateKey<string> POLICY_NAME_STATE_KEY = new(nameof(policyName));
private static readonly AssistantSessionStateKey<string> POLICY_DESCRIPTION_STATE_KEY = new(nameof(policyDescription));
private static readonly AssistantSessionStateKey<string> POLICY_ANALYSIS_RULES_STATE_KEY = new(nameof(policyAnalysisRules));
private static readonly AssistantSessionStateKey<string> POLICY_OUTPUT_RULES_STATE_KEY = new(nameof(policyOutputRules));
private static readonly AssistantSessionStateKey<ConfidenceLevel> POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY = new(nameof(policyMinimumProviderConfidence));
private static readonly AssistantSessionStateKey<string> POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY = new(nameof(policyPreselectedProviderId));
private static readonly AssistantSessionStateKey<ProfilePreselection> POLICY_PRESELECTED_PROFILE_STATE_KEY = new(nameof(policyPreselectedProfile));
private static readonly AssistantSessionStateKey<HashSet<FileAttachment>> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths));
private static readonly AssistantSessionStateKey<List<ConfigurationSelectData<string>>> AVAILABLE_LLM_PROVIDERS_STATE_KEY = new(nameof(availableLLMProviders));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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; private bool IsNoPolicySelectedOrProtected => this.selectedPolicy is null || this.selectedPolicy.IsProtected;
@ -515,7 +565,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
break; break;
} }
return Task.CompletedTask; return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
} }
#endregion #endregion

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.PluginSystem.Assistants.DataModel; using AIStudio.Tools.PluginSystem.Assistants.DataModel;
@ -27,6 +28,11 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
// Reuse chat-level provider filtering/preselection instead of NONE. // Reuse chat-level provider filtering/preselection instead of NONE.
protected override Tools.Components Component => Tools.Components.CHAT; protected override Tools.Components Component => Tools.Components.CHAT;
/// <summary>
/// Gets the plugin ID as the assistant session instance ID.
/// </summary>
protected override string AssistantSessionInstanceId => this.assistantPlugin is null ? base.AssistantSessionInstanceId : this.assistantPlugin.Id.ToString();
private string title = string.Empty; private string title = string.Empty;
private string description = string.Empty; private string description = string.Empty;
private string systemPrompt = string.Empty; private string systemPrompt = string.Empty;
@ -44,6 +50,61 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
private string securityMessage = string.Empty; private string securityMessage = string.Empty;
private bool isSecurityBlocked; private bool isSecurityBlocked;
private const string ASSISTANT_QUERY_KEY = "assistantId"; private const string ASSISTANT_QUERY_KEY = "assistantId";
private static readonly AssistantSessionStateKey<string> TITLE_STATE_KEY = new(nameof(title));
private static readonly AssistantSessionStateKey<string> DESCRIPTION_STATE_KEY = new(nameof(description));
private static readonly AssistantSessionStateKey<string> SYSTEM_PROMPT_STATE_KEY = new(nameof(systemPrompt));
private static readonly AssistantSessionStateKey<bool> ALLOW_PROFILES_STATE_KEY = new(nameof(allowProfiles));
private static readonly AssistantSessionStateKey<string> SUBMIT_TEXT_STATE_KEY = new(nameof(submitText));
private static readonly AssistantSessionStateKey<bool> SHOW_FOOTER_PROFILE_SELECTION_STATE_KEY = new(nameof(showFooterProfileSelection));
private static readonly AssistantSessionStateKey<PluginAssistants?> ASSISTANT_PLUGIN_STATE_KEY = new(nameof(assistantPlugin));
private static readonly AssistantSessionStateKey<AssistantState> ASSISTANT_STATE_STATE_KEY = new(nameof(assistantState));
private static readonly AssistantSessionStateKey<Dictionary<string, string>> IMAGE_CACHE_STATE_KEY = new(nameof(imageCache));
private static readonly AssistantSessionStateKey<HashSet<string>> EXECUTING_BUTTON_ACTIONS_STATE_KEY = new(nameof(executingButtonActions));
private static readonly AssistantSessionStateKey<HashSet<string>> EXECUTING_SWITCH_ACTIONS_STATE_KEY = new(nameof(executingSwitchActions));
private static readonly AssistantSessionStateKey<string> PLUGIN_PATH_STATE_KEY = new(nameof(pluginPath));
private static readonly AssistantSessionStateKey<PluginAssistantAudit?> AUDIT_STATE_KEY = new(nameof(audit));
private static readonly AssistantSessionStateKey<string> SECURITY_MESSAGE_STATE_KEY = new(nameof(securityMessage));
private static readonly AssistantSessionStateKey<bool> IS_SECURITY_BLOCKED_STATE_KEY = new(nameof(isSecurityBlocked));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Implementation of AssistantBase

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.EMail; namespace AIStudio.Assistants.EMail;
@ -78,6 +79,46 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private bool provideHistory; private bool provideHistory;
private string inputHistory = string.Empty; private string inputHistory = string.Empty;
private static readonly AssistantSessionStateKey<WritingStyles> SELECTED_WRITING_STYLE_STATE_KEY = new(nameof(selectedWritingStyle));
private static readonly AssistantSessionStateKey<string> INPUT_GREETING_STATE_KEY = new(nameof(inputGreeting));
private static readonly AssistantSessionStateKey<string> INPUT_BULLET_POINTS_STATE_KEY = new(nameof(inputBulletPoints));
private static readonly AssistantSessionStateKey<List<string>> BULLET_POINTS_LINES_STATE_KEY = new(nameof(bulletPointsLines));
private static readonly AssistantSessionStateKey<HashSet<string>> SELECTED_FOCI_STATE_KEY = new(nameof(selectedFoci));
private static readonly AssistantSessionStateKey<string> INPUT_NAME_STATE_KEY = new(nameof(inputName));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<bool> PROVIDE_HISTORY_STATE_KEY = new(nameof(provideHistory));
private static readonly AssistantSessionStateKey<string> INPUT_HISTORY_STATE_KEY = new(nameof(inputHistory));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -41,7 +41,7 @@
} }
else else
{ {
<MudList Disabled="@this.AreServerPresetsBlocked" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged"> <MudList Disabled="@this.AreServerPresetControlsDisabled" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged">
@foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers) @foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers)
{ {
<MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server"> <MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server">
@ -52,10 +52,10 @@ else
} }
<MudStack Row="@true" Class="mt-1"> <MudStack Row="@true" Class="mt-1">
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary"> <MudButton Disabled="@this.AreServerPresetControlsDisabled" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
@T("Add ERI server preset") @T("Add ERI server preset")
</MudButton> </MudButton>
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error"> <MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetControlsDisabled || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
@T("Delete this server preset") @T("Delete this server preset")
</MudButton> </MudButton>
</MudStack> </MudStack>
@ -82,18 +82,18 @@ else
</MudJustifiedText> </MudJustifiedText>
} }
<MudTextSwitch Label="@T("Should we automatically save any input made?")" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="@T("Yes, please save my inputs")" LabelOff="@T("No, I will enter everything again or configure it manually in the settings")" /> <MudTextSwitch Label="@T("Should we automatically save any input made?")" Disabled="@this.AreServerPresetControlsDisabled" @bind-Value="@this.autoSave" LabelOn="@T("Yes, please save my inputs")" LabelOff="@T("No, I will enter everything again or configure it manually in the settings")" />
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/> <hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/>
<MudText Typo="Typo.h4" Class="mt-6 mb-1"> <MudText Typo="Typo.h4" Class="mt-6 mb-1">
@T("Common ERI server settings") @T("Common ERI server settings")
</MudText> </MudText>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="@T("ERI server name")" HelperText="@T("Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/> <MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="@T("ERI server name")" HelperText="@T("Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="@T("ERI server description")" HelperText="@T("Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="@T("ERI server description")" HelperText="@T("Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" Class="mb-3"> <MudStack Row="@true" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="@T("Programming language")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage"> <MudSelect Disabled="@this.IsERIInputDisabled" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="@T("Programming language")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
@foreach (var language in Enum.GetValues<ProgrammingLanguages>()) @foreach (var language in Enum.GetValues<ProgrammingLanguages>())
{ {
<MudSelectItem Value="@language"> <MudSelectItem Value="@language">
@ -103,12 +103,12 @@ else
</MudSelect> </MudSelect>
@if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER) @if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER)
{ {
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="@T("Other language")" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="@T("Other language")" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
} }
</MudStack> </MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3"> <MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="@T("ERI specification version")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion"> <MudSelect Disabled="@this.IsERIInputDisabled" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="@T("ERI specification version")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion">
@foreach (var version in Enum.GetValues<ERIVersion>()) @foreach (var version in Enum.GetValues<ERIVersion>())
{ {
<MudSelectItem Value="@version"> <MudSelectItem Value="@version">
@ -116,7 +116,7 @@ else
</MudSelectItem> </MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@(!this.selectedERIVersion.WasSpecificationSelected() || this.IsNoneERIServerSelected)" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank"> <MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationDownloadDisabled" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> @T("Download specification") <MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> @T("Download specification")
</MudButton> </MudButton>
</MudStack> </MudStack>
@ -126,7 +126,7 @@ else
</MudText> </MudText>
<MudStack Row="@false" Spacing="1" Class="mb-3"> <MudStack Row="@false" Spacing="1" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="@T("Data source")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged"> <MudSelect Disabled="@this.IsERIInputDisabled" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="@T("Data source")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged">
@foreach (var dataSource in Enum.GetValues<DataSources>()) @foreach (var dataSource in Enum.GetValues<DataSources>())
{ {
<MudSelectItem Value="@dataSource"> <MudSelectItem Value="@dataSource">
@ -136,21 +136,21 @@ else
</MudSelect> </MudSelect>
@if (this.selectedDataSource is DataSources.CUSTOM) @if (this.selectedDataSource is DataSources.CUSTOM)
{ {
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="@T("Describe your data source")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="@T("Describe your data source")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
} }
</MudStack> </MudStack>
@if(this.selectedDataSource > DataSources.FILE_SYSTEM) @if(this.selectedDataSource > DataSources.FILE_SYSTEM)
{ {
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceProductName" Label="@T("Data source: product name")" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.dataSourceProductName" Label="@T("Data source: product name")" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
} }
@if (this.NeedHostnamePort()) @if (this.NeedHostnamePort())
{ {
<div class="mb-3"> <div class="mb-3">
<MudStack Row="@true"> <MudStack Row="@true">
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceHostname" Label="@T("Data source: hostname")" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.dataSourceHostname" Label="@T("Data source: hostname")" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudNumericField Disabled="@this.IsNoneERIServerSelected" Label="@T("Data source: port")" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/> <MudNumericField Disabled="@this.IsERIInputDisabled" Label="@T("Data source: port")" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/>
</MudStack> </MudStack>
@if (this.dataSourcePort < 1024) @if (this.dataSourcePort < 1024)
{ {
@ -168,7 +168,7 @@ else
<MudStack Row="@false" Spacing="1" Class="mb-1"> <MudStack Row="@false" Spacing="1" Class="mb-1">
<MudSelectExtended <MudSelectExtended
T="Auth" T="Auth"
Disabled="@this.IsNoneERIServerSelected" Disabled="@this.IsERIInputDisabled"
ShrinkLabel="@true" ShrinkLabel="@true"
MultiSelection="@true" MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionAuthText" MultiSelectionTextFunc="@this.GetMultiSelectionAuthText"
@ -185,12 +185,12 @@ else
</MudSelectItemExtended> </MudSelectItemExtended>
} }
</MudSelectExtended> </MudSelectExtended>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</MudStack> </MudStack>
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS)) @if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
{ {
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="@T("Operating system on which your ERI will run")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1"> <MudSelect Disabled="@this.IsERIInputDisabled" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="@T("Operating system on which your ERI will run")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1">
@foreach (var os in Enum.GetValues<OperatingSystem>()) @foreach (var os in Enum.GetValues<OperatingSystem>())
{ {
<MudSelectItem Value="@os"> <MudSelectItem Value="@os">
@ -204,7 +204,7 @@ else
@T("Data protection settings") @T("Data protection settings")
</MudText> </MudText>
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="@T("Allowed LLM providers for this data source")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1"> <MudSelect Disabled="@this.IsERIInputDisabled" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="@T("Allowed LLM providers for this data source")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1">
@foreach (var option in Enum.GetValues<AllowedLLMProviders>()) @foreach (var option in Enum.GetValues<AllowedLLMProviders>())
{ {
<MudSelectItem Value="@option"> <MudSelectItem Value="@option">
@ -227,7 +227,7 @@ else
@if (!this.IsNoneERIServerSelected) @if (!this.IsNoneERIServerSelected)
{ {
<MudTable Items="@this.embeddings" Hover="@true" Class="border-dashed border rounded-lg"> <MudTable Items="@this.EmbeddingRows" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup> <ColGroup>
<col/> <col/>
<col style="width: 34em;"/> <col style="width: 34em;"/>
@ -243,10 +243,10 @@ else
<MudTd>@context.EmbeddingType</MudTd> <MudTd>@context.EmbeddingType</MudTd>
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)" Disabled="@this.IsProcessing">
@T("Edit") @T("Edit")
</MudButton> </MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)"> <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)" Disabled="@this.IsProcessing">
@T("Delete") @T("Delete")
</MudButton> </MudButton>
</MudStack> </MudStack>
@ -262,7 +262,7 @@ else
} }
} }
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding"> <MudButton Disabled="@this.IsERIInputDisabled" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding">
@T("Add Embedding Method") @T("Add Embedding Method")
</MudButton> </MudButton>
@ -276,7 +276,7 @@ else
@if (!this.IsNoneERIServerSelected) @if (!this.IsNoneERIServerSelected)
{ {
<MudTable Items="@this.retrievalProcesses" Hover="@true" Class="border-dashed border rounded-lg"> <MudTable Items="@this.RetrievalProcessRows" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup> <ColGroup>
<col/> <col/>
<col style="width: 34em;"/> <col style="width: 34em;"/>
@ -289,10 +289,10 @@ else
<MudTd>@context.Name</MudTd> <MudTd>@context.Name</MudTd>
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)" Disabled="@this.IsProcessing">
@T("Edit") @T("Edit")
</MudButton> </MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)"> <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)" Disabled="@this.IsProcessing">
@T("Delete") @T("Delete")
</MudButton> </MudButton>
</MudStack> </MudStack>
@ -308,7 +308,7 @@ else
} }
} }
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess"> <MudButton Disabled="@this.IsERIInputDisabled" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess">
@T("Add Retrieval Process") @T("Add Retrieval Process")
</MudButton> </MudButton>
@ -316,7 +316,7 @@ else
@T("You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.") @T("You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.")
</MudJustifiedText> </MudJustifiedText>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="@T("(Optional) Additional libraries")" HelperText="@T("Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.additionalLibraries" Label="@T("(Optional) Additional libraries")" HelperText="@T("Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1"> <MudText Typo="Typo.h4" Class="mt-9 mb-1">
@T("Provider selection for generation") @T("Provider selection for generation")
@ -330,7 +330,7 @@ else
<b>@T("Important:")</b> @T("The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.") <b>@T("However, generating all the files takes a certain amount of time.")</b> @T("Local or self-hosted models may work without these limitations and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.") <b>@T("Important:")</b> @T("The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.") <b>@T("However, generating all the files takes a certain amount of time.")</b> @T("Local or self-hosted models may work without these limitations and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.")
</MudJustifiedText> </MudJustifiedText>
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider" Disabled="@this.IsProcessing"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1"> <MudText Typo="Typo.h4" Class="mt-9 mb-1">
@T("Write code to file system") @T("Write code to file system")
@ -344,5 +344,5 @@ else
@T("When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All other files you have created remain. Then, the AI generates the new files.") <b>@T("But beware:")</b> @T("It may happen that the AI generates a file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore, you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization, you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.") @T("When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All other files you have created remain. Then, the AI generates the new files.") <b>@T("But beware:")</b> @T("It may happen that the AI generates a file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore, you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization, you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.")
</MudJustifiedText> </MudJustifiedText>
<MudTextSwitch Label="@T("Should we write the generated code to the file system?")" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="@T("Yes, please write or update all generated code to the file system")" LabelOff="@T("No, just show me the code")" /> <MudTextSwitch Label="@T("Should we write the generated code to the file system?")" Disabled="@this.IsERIInputDisabled" @bind-Value="@this.writeToFilesystem" LabelOn="@T("Yes, please write or update all generated code to the file system")" LabelOff="@T("No, just show me the code")" />
<SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" /> <SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@this.IsBaseDirectorySelectionDisabled" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" />

View File

@ -5,6 +5,7 @@ using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -291,7 +292,17 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
} }
} }
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new ButtonData
{
Text = T("Open in chat"),
Icon = Icons.Material.Filled.Chat,
Color = Color.Default,
AsyncAction = this.OpenInChat,
DisabledActionParam = () => !this.CanOpenInChat,
},
];
protected override bool ShowEntireChatThread => true; protected override bool ShowEntireChatThread => true;
@ -307,6 +318,22 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
{ {
SystemPrompt = this.SystemPrompt, SystemPrompt = this.SystemPrompt,
}; };
/// <summary>
/// Indicates whether the generated ERI conversation can be opened in the chat view.
/// </summary>
private bool CanOpenInChat => !this.IsProcessing && this.ChatThread is { Blocks.Count: > 0 };
/// <summary>
/// Opens the generated ERI conversation in the chat view when a finished conversation is available.
/// </summary>
private async Task OpenInChat()
{
if (!this.CanOpenInChat)
return;
await this.SendToAssistant(Tools.Components.CHAT, default);
}
protected override void ResetForm() protected override void ResetForm()
{ {
@ -449,17 +476,110 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private bool writeToFilesystem; private bool writeToFilesystem;
private string baseDirectory = string.Empty; private string baseDirectory = string.Empty;
private List<string> previouslyGeneratedFiles = new(); private List<string> previouslyGeneratedFiles = new();
private static readonly AssistantSessionStateKey<DataERIServer?> SELECTED_ERI_SERVER_STATE_KEY = new(nameof(selectedERIServer));
private static readonly AssistantSessionStateKey<bool> AUTO_SAVE_STATE_KEY = new(nameof(autoSave));
private static readonly AssistantSessionStateKey<string> SERVER_NAME_STATE_KEY = new(nameof(serverName));
private static readonly AssistantSessionStateKey<string> SERVER_DESCRIPTION_STATE_KEY = new(nameof(serverDescription));
private static readonly AssistantSessionStateKey<ERIVersion> SELECTED_ERI_VERSION_STATE_KEY = new(nameof(selectedERIVersion));
private static readonly AssistantSessionStateKey<string?> ERI_SPECIFICATION_STATE_KEY = new(nameof(eriSpecification));
private static readonly AssistantSessionStateKey<ProgrammingLanguages> SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(selectedProgrammingLanguage));
private static readonly AssistantSessionStateKey<string> OTHER_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(otherProgrammingLanguage));
private static readonly AssistantSessionStateKey<DataSources> SELECTED_DATA_SOURCE_STATE_KEY = new(nameof(selectedDataSource));
private static readonly AssistantSessionStateKey<string> OTHER_DATA_SOURCE_STATE_KEY = new(nameof(otherDataSource));
private static readonly AssistantSessionStateKey<string> DATA_SOURCE_PRODUCT_NAME_STATE_KEY = new(nameof(dataSourceProductName));
private static readonly AssistantSessionStateKey<string> DATA_SOURCE_HOSTNAME_STATE_KEY = new(nameof(dataSourceHostname));
private static readonly AssistantSessionStateKey<int?> DATA_SOURCE_PORT_STATE_KEY = new(nameof(dataSourcePort));
private static readonly AssistantSessionStateKey<bool> USER_TYPED_PORT_STATE_KEY = new(nameof(userTypedPort));
private static readonly AssistantSessionStateKey<HashSet<Auth>> SELECTED_AUTHENTICATION_METHODS_STATE_KEY = new(nameof(selectedAuthenticationMethods));
private static readonly AssistantSessionStateKey<string> AUTH_DESCRIPTION_STATE_KEY = new(nameof(authDescription));
private static readonly AssistantSessionStateKey<OperatingSystem> SELECTED_OPERATING_SYSTEM_STATE_KEY = new(nameof(selectedOperatingSystem));
private static readonly AssistantSessionStateKey<AllowedLLMProviders> ALLOWED_LLM_PROVIDERS_STATE_KEY = new(nameof(allowedLLMProviders));
private static readonly AssistantSessionStateKey<List<EmbeddingInfo>> EMBEDDINGS_STATE_KEY = new(nameof(embeddings));
private static readonly AssistantSessionStateKey<List<RetrievalInfo>> RETRIEVAL_PROCESSES_STATE_KEY = new(nameof(retrievalProcesses));
private static readonly AssistantSessionStateKey<string> ADDITIONAL_LIBRARIES_STATE_KEY = new(nameof(additionalLibraries));
private static readonly AssistantSessionStateKey<bool> WRITE_TO_FILESYSTEM_STATE_KEY = new(nameof(writeToFilesystem));
private static readonly AssistantSessionStateKey<string> BASE_DIRECTORY_STATE_KEY = new(nameof(baseDirectory));
private static readonly AssistantSessionStateKey<List<string>> PREVIOUSLY_GENERATED_FILES_STATE_KEY = new(nameof(previouslyGeneratedFiles));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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; private bool AreServerPresetsBlocked => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions;
/// <summary>
/// Gets whether ERI server preset controls should be disabled.
/// </summary>
private bool AreServerPresetControlsDisabled => this.AreServerPresetsBlocked || this.IsProcessing;
private void SelectedERIServerChanged(DataERIServer? server) private void SelectedERIServerChanged(DataERIServer? server)
{ {
if (this.IsProcessing)
return;
this.selectedERIServer = server; this.selectedERIServer = server;
this.ResetForm(); this.ResetForm();
} }
private async Task AddERIServer() private async Task AddERIServer()
{ {
if (this.IsProcessing)
return;
this.SettingsManager.ConfigurationData.ERI.ERIServers.Add(new () this.SettingsManager.ConfigurationData.ERI.ERIServers.Add(new ()
{ {
ServerName = string.Format(T("ERI Server {0}"), DateTimeOffset.UtcNow), ServerName = string.Format(T("ERI Server {0}"), DateTimeOffset.UtcNow),
@ -470,6 +590,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task RemoveERIServer() private async Task RemoveERIServer()
{ {
if (this.IsProcessing)
return;
if(this.selectedERIServer is null) if(this.selectedERIServer is null)
return; return;
@ -493,6 +616,31 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private bool IsNoneERIServerSelected => this.selectedERIServer is null; private bool IsNoneERIServerSelected => this.selectedERIServer is null;
/// <summary>
/// Gets whether ERI configuration input controls should be disabled.
/// </summary>
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
/// <summary>
/// Gets whether the selected ERI specification cannot be downloaded.
/// </summary>
private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
/// <summary>
/// Gets whether the generated-code target directory selection should be disabled.
/// </summary>
private bool IsBaseDirectorySelectionDisabled => this.IsERIInputDisabled || !this.writeToFilesystem;
/// <summary>
/// Gets a stable row snapshot for the embedding-method table.
/// </summary>
private EmbeddingInfo[] EmbeddingRows => this.embeddings.ToArray();
/// <summary>
/// Gets a stable row snapshot for the retrieval-process table.
/// </summary>
private RetrievalInfo[] RetrievalProcessRows => this.retrievalProcesses.ToArray();
/// <summary> /// <summary>
/// Gets called when the server name was changed by typing. /// Gets called when the server name was changed by typing.
/// </summary> /// </summary>
@ -780,6 +928,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task AddEmbedding() private async Task AddEmbedding()
{ {
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<EmbeddingMethodDialog> var dialogParameters = new DialogParameters<EmbeddingMethodDialog>
{ {
{ x => x.IsEditing, false }, { x => x.IsEditing, false },
@ -798,6 +949,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task EditEmbedding(EmbeddingInfo embeddingInfo) private async Task EditEmbedding(EmbeddingInfo embeddingInfo)
{ {
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<EmbeddingMethodDialog> var dialogParameters = new DialogParameters<EmbeddingMethodDialog>
{ {
{ x => x.DataEmbeddingName, embeddingInfo.EmbeddingName }, { x => x.DataEmbeddingName, embeddingInfo.EmbeddingName },
@ -823,6 +977,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task DeleteEmbedding(EmbeddingInfo embeddingInfo) private async Task DeleteEmbedding(EmbeddingInfo embeddingInfo)
{ {
if (this.IsProcessing)
return;
var message = this.retrievalProcesses.Any(n => n.Embeddings?.Contains(embeddingInfo) is true) var message = this.retrievalProcesses.Any(n => n.Embeddings?.Contains(embeddingInfo) is true)
? string.Format(T("The embedding '{0}' is used in one or more retrieval processes. Are you sure you want to delete it?"), embeddingInfo.EmbeddingName) ? string.Format(T("The embedding '{0}' is used in one or more retrieval processes. Are you sure you want to delete it?"), embeddingInfo.EmbeddingName)
: string.Format(T("Are you sure you want to delete the embedding '{0}'?"), embeddingInfo.EmbeddingName); : string.Format(T("Are you sure you want to delete the embedding '{0}'?"), embeddingInfo.EmbeddingName);
@ -845,6 +1002,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task AddRetrievalProcess() private async Task AddRetrievalProcess()
{ {
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<RetrievalProcessDialog> var dialogParameters = new DialogParameters<RetrievalProcessDialog>
{ {
{ x => x.IsEditing, false }, { x => x.IsEditing, false },
@ -864,6 +1024,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task EditRetrievalProcess(RetrievalInfo retrievalInfo) private async Task EditRetrievalProcess(RetrievalInfo retrievalInfo)
{ {
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<RetrievalProcessDialog> var dialogParameters = new DialogParameters<RetrievalProcessDialog>
{ {
{ x => x.DataName, retrievalInfo.Name }, { x => x.DataName, retrievalInfo.Name },
@ -890,6 +1053,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
private async Task DeleteRetrievalProcess(RetrievalInfo retrievalInfo) private async Task DeleteRetrievalProcess(RetrievalInfo retrievalInfo)
{ {
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
{ {
{ x => x.Message, string.Format(T("Are you sure you want to delete the retrieval process '{0}'?"), retrievalInfo.Name) }, { x => x.Message, string.Format(T("Are you sure you want to delete the retrieval process '{0}'?"), retrievalInfo.Name) },
@ -949,6 +1115,10 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
this.AddInputIssue(T("Please describe at least one retrieval process.")); this.AddInputIssue(T("Please describe at least one retrieval process."));
return; return;
} }
var writeToFilesystemSnapshot = this.writeToFilesystem;
var baseDirectorySnapshot = this.baseDirectory;
var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray();
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient); this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient);
if (string.IsNullOrWhiteSpace(this.eriSpecification)) if (string.IsNullOrWhiteSpace(this.eriSpecification))
@ -990,9 +1160,9 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
var fileListAnswer = await this.AddAIResponseAsync(time, true); var fileListAnswer = await this.AddAIResponseAsync(time, true);
// Is this an update of the ERI server? If so, we need to delete the previously generated files: // Is this an update of the ERI server? If so, we need to delete the previously generated files:
if (this.writeToFilesystem && this.previouslyGeneratedFiles.Count > 0 && !string.IsNullOrWhiteSpace(fileListAnswer)) if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0 && !string.IsNullOrWhiteSpace(fileListAnswer))
{ {
foreach (var file in this.previouslyGeneratedFiles) foreach (var file in previouslyGeneratedFilesSnapshot)
{ {
try try
{ {
@ -1014,7 +1184,8 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
} }
var generatedFiles = new List<string>(); var generatedFiles = new List<string>();
foreach (var file in this.ExtractFiles(fileListAnswer)) var filesToGenerate = this.ExtractFiles(fileListAnswer).ToArray();
foreach (var file in filesToGenerate)
{ {
this.Logger.LogInformation($"The LLM want to create the file: '{file}'"); this.Logger.LogInformation($"The LLM want to create the file: '{file}'");
@ -1034,15 +1205,15 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
``` ```
""", true); """, true);
var generatedCodeMarkdown = await this.AddAIResponseAsync(time); var generatedCodeMarkdown = await this.AddAIResponseAsync(time);
if (this.writeToFilesystem) if (writeToFilesystemSnapshot)
{ {
var desiredFilePath = Path.Join(this.baseDirectory, file); var desiredFilePath = Path.Join(baseDirectorySnapshot, file);
// Security check: ensure that the desired file path is inside the base directory. // Security check: ensure that the desired file path is inside the base directory.
// We cannot trust the beginning of the file path because it would be possible // We cannot trust the beginning of the file path because it would be possible
// to escape by using `..` in the file path. // to escape by using `..` in the file path.
if (!desiredFilePath.StartsWith(this.baseDirectory, StringComparison.InvariantCultureIgnoreCase) || desiredFilePath.Contains("..")) if (!desiredFilePath.StartsWith(baseDirectorySnapshot, StringComparison.InvariantCultureIgnoreCase) || desiredFilePath.Contains(".."))
this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{this.baseDirectory}'."); this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{baseDirectorySnapshot}'.");
else else
{ {
@ -1077,7 +1248,7 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
} }
} }
if(this.writeToFilesystem) if(writeToFilesystemSnapshot)
{ {
this.previouslyGeneratedFiles = generatedFiles; this.previouslyGeneratedFiles = generatedFiles;
this.selectedERIServer!.PreviouslyGeneratedFiles = generatedFiles; this.selectedERIServer!.PreviouslyGeneratedFiles = generatedFiles;
@ -1096,6 +1267,5 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
like Docker. like Docker.
""", true); """, true);
await this.AddAIResponseAsync(time); await this.AddAIResponseAsync(time);
await this.SendToAssistant(Tools.Components.CHAT, default);
} }
} }

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.GrammarSpelling; namespace AIStudio.Assistants.GrammarSpelling;
@ -84,6 +85,28 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
private CommonLanguages selectedTargetLanguage; private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private string correctedText = string.Empty; private string correctedText = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<string> CORRECTED_TEXT_STATE_KEY = new(nameof(correctedText));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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) private string? ValidateText(string text)
{ {
@ -127,6 +150,13 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
var time = this.AddUserRequest(this.inputText); var time = this.AddUserRequest(this.inputText);
this.correctedText = await this.AddAIResponseAsync(time); this.correctedText = await this.AddAIResponseAsync(time);
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText); if (!this.IsAssistantComponentDisposed)
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText);
}
protected override async Task OnAssistantSessionRenderedAsync(AssistantSessionSnapshot snapshot)
{
if (!snapshot.IsActive && !string.IsNullOrWhiteSpace(this.inputText) && !string.IsNullOrWhiteSpace(this.correctedText))
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText);
} }
} }

View File

@ -2,8 +2,8 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N> @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" SelectionUpdated="_ => this.OnChangedLanguage()" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" SelectionUpdated="_ => this.OnChangedLanguage()" Disabled="@this.IsProcessing" />
<ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="@T("Select the language plugin used for comparision.")"/> <ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="@T("Select the language plugin used for comparision.")" Disabled="@(() => this.IsProcessing)"/>
@if (this.isLoading) @if (this.isLoading)
{ {
<MudText Typo="Typo.body1" Class="mb-6"> <MudText Typo="Typo.body1" Class="mb-6">
@ -20,7 +20,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.AddedContentText @this.AddedContentText
</MudText> </MudText>
<MudTable Items="@this.addedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.AddedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>
@ -50,7 +50,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.RemovedContentText @this.RemovedContentText
</MudText> </MudText>
<MudTable Items="@this.removedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.RemovedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>
@ -94,7 +94,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.LocalizedContentText @this.LocalizedContentText
</MudText> </MudText>
<MudTable Items="@this.localizedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.LocalizedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>

View File

@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
@ -117,32 +118,87 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
private Dictionary<string, string> removedContent = []; private Dictionary<string, string> removedContent = [];
private Dictionary<string, string> localizedContent = []; private Dictionary<string, string> localizedContent = [];
private StringBuilder finalLuaCode = new(); private StringBuilder finalLuaCode = new();
private string? activeSystemPromptLanguage;
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<bool> IS_LOADING_STATE_KEY = new(nameof(isLoading));
private static readonly AssistantSessionStateKey<string> LOADING_ISSUE_STATE_KEY = new(nameof(loadingIssue));
private static readonly AssistantSessionStateKey<bool> LOCALIZATION_POSSIBLE_STATE_KEY = new(nameof(localizationPossible));
private static readonly AssistantSessionStateKey<string> SEARCH_STRING_STATE_KEY = new(nameof(searchString));
private static readonly AssistantSessionStateKey<Guid> SELECTED_LANGUAGE_PLUGIN_ID_STATE_KEY = new(nameof(selectedLanguagePluginId));
private static readonly AssistantSessionStateKey<ILanguagePlugin?> SELECTED_LANGUAGE_PLUGIN_STATE_KEY = new(nameof(selectedLanguagePlugin));
private static readonly AssistantSessionStateKey<Dictionary<string, string>> ADDED_CONTENT_STATE_KEY = new(nameof(addedContent));
private static readonly AssistantSessionStateKey<Dictionary<string, string>> REMOVED_CONTENT_STATE_KEY = new(nameof(removedContent));
private static readonly AssistantSessionStateKey<Dictionary<string, string>> LOCALIZED_CONTENT_STATE_KEY = new(nameof(localizedContent));
private static readonly AssistantSessionStateKey<string> FINAL_LUA_CODE_STATE_KEY = new(nameof(finalLuaCode));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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<SettingsDialogI18N> #region Overrides of AssistantBase<SettingsDialogI18N>
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
if (this.HasAssistantSession)
return;
await this.OnLanguagePluginChanged(this.selectedLanguagePluginId); await this.OnLanguagePluginChanged(this.selectedLanguagePluginId);
await this.LoadData();
} }
#endregion #endregion
private string SystemPromptLanguage() => this.selectedTargetLanguage switch private string SystemPromptLanguage() => this.activeSystemPromptLanguage ?? (this.selectedTargetLanguage switch
{ {
CommonLanguages.OTHER => this.customTargetLanguage, CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}", _ => $"{this.selectedTargetLanguage.Name()}",
}; });
private async Task OnLanguagePluginChanged(Guid pluginId) private async Task OnLanguagePluginChanged(Guid pluginId)
{ {
if (this.IsProcessing)
return;
this.selectedLanguagePluginId = pluginId; this.selectedLanguagePluginId = pluginId;
await this.OnChangedLanguage(); await this.OnChangedLanguage();
} }
private async Task OnChangedLanguage() private async Task OnChangedLanguage()
{ {
if (this.IsProcessing)
return;
this.finalLuaCode.Clear(); this.finalLuaCode.Clear();
this.localizedContent.Clear(); this.localizedContent.Clear();
this.localizationPossible = false; this.localizationPossible = false;
@ -261,6 +317,21 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count; private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count;
/// <summary>
/// Gets a stable row snapshot for the added-content table.
/// </summary>
private KeyValuePair<string, string>[] AddedContentRows => this.addedContent.ToArray();
/// <summary>
/// Gets a stable row snapshot for the removed-content table.
/// </summary>
private KeyValuePair<string, string>[] RemovedContentRows => this.removedContent.ToArray();
/// <summary>
/// Gets a stable row snapshot for the localized-content table.
/// </summary>
private KeyValuePair<string, string>[] LocalizedContentRows => this.localizedContent.ToArray();
private string AddedContentText => string.Format(T("Added Content ({0} entries)"), this.addedContent.Count); private string AddedContentText => string.Format(T("Added Content ({0} entries)"), this.addedContent.Count);
private string RemovedContentText => string.Format(T("Removed Content ({0} entries)"), this.removedContent.Count); private string RemovedContentText => string.Format(T("Removed Content ({0} entries)"), this.removedContent.Count);
@ -279,68 +350,87 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag()) if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
return; return;
this.localizedContent.Clear(); var addedContentSnapshot = this.addedContent.ToArray();
if (this.selectedTargetLanguage is not CommonLanguages.EN_US) var removedContentSnapshot = this.removedContent.ToArray();
{ var removedContentKeys = removedContentSnapshot.Select(keyValuePair => keyValuePair.Key).ToHashSet(StringComparer.Ordinal);
// Phase 1: Translate added content var selectedLanguageContentSnapshot = this.selectedLanguagePlugin.Content.ToArray();
await this.Phase1TranslateAddedContent(); var baseLanguageContentSnapshot = PluginFactory.BaseLanguage.Content.ToArray();
}
else
{
// Case: no translation needed
this.localizedContent = this.addedContent.ToDictionary();
}
if(this.CancellationTokenSource!.IsCancellationRequested) this.localizedContent.Clear();
return; this.activeSystemPromptLanguage = this.SystemPromptLanguage();
try
//
// Now, we have localized the added content. Next, we must merge
// the localized content with the existing content. However, we
// must skip the removed content. We use the localizedContent
// dictionary for the final result:
//
foreach (var keyValuePair in this.selectedLanguagePlugin.Content)
{ {
if (this.CancellationTokenSource!.IsCancellationRequested) if (this.selectedTargetLanguage is not CommonLanguages.EN_US)
break; {
// Phase 1: Translate added content
await this.Phase1TranslateAddedContent(addedContentSnapshot);
}
else
{
// Case: no translation needed
this.localizedContent = addedContentSnapshot.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value, StringComparer.Ordinal);
}
if(this.CancellationTokenSource!.IsCancellationRequested)
return;
if (this.localizedContent.ContainsKey(keyValuePair.Key)) //
continue; // Now, we have localized the added content. Next, we must merge
// the localized content with the existing content. However, we
// must skip the removed content. We use the localizedContent
// dictionary for the final result:
//
foreach (var keyValuePair in selectedLanguageContentSnapshot)
{
if (this.CancellationTokenSource!.IsCancellationRequested)
break;
if (this.localizedContent.ContainsKey(keyValuePair.Key))
continue;
if (removedContentKeys.Contains(keyValuePair.Key))
continue;
this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value);
}
if(this.CancellationTokenSource!.IsCancellationRequested)
return;
if (this.removedContent.ContainsKey(keyValuePair.Key)) //
continue; // Phase 2: Create the Lua code. We want to use the base language
// for the comments, though:
//
var commentContent = addedContentSnapshot.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value, StringComparer.Ordinal);
foreach (var keyValuePair in baseLanguageContentSnapshot)
{
if (this.CancellationTokenSource!.IsCancellationRequested)
break;
if (removedContentKeys.Contains(keyValuePair.Key))
continue;
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value); this.Phase2CreateLuaCode(commentContent);
} }
finally
if(this.CancellationTokenSource!.IsCancellationRequested)
return;
//
// Phase 2: Create the Lua code. We want to use the base language
// for the comments, though:
//
var commentContent = new Dictionary<string, string>(this.addedContent);
foreach (var keyValuePair in PluginFactory.BaseLanguage.Content)
{ {
if (this.CancellationTokenSource!.IsCancellationRequested) this.activeSystemPromptLanguage = null;
break;
if (this.removedContent.ContainsKey(keyValuePair.Key))
continue;
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
} }
this.Phase2CreateLuaCode(commentContent);
} }
private async Task Phase1TranslateAddedContent() /// <summary>
/// Translates the added text content from a stable snapshot.
/// </summary>
/// <param name="addedContentSnapshot">The added text entries captured when the job started.</param>
/// <returns>A task that completes when all added text entries were translated or cancellation was requested.</returns>
private async Task Phase1TranslateAddedContent(KeyValuePair<string, string>[] addedContentSnapshot)
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
var minimumTime = TimeSpan.FromMilliseconds(500); var minimumTime = TimeSpan.FromMilliseconds(500);
foreach (var keyValuePair in this.addedContent) foreach (var keyValuePair in addedContentSnapshot)
{ {
if(this.CancellationTokenSource!.IsCancellationRequested) if(this.CancellationTokenSource!.IsCancellationRequested)
break; break;

View File

@ -310,6 +310,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T180921696"] = "Reset"
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "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} -- Assistant - {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistant - {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistant - {0}"
@ -748,6 +754,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1565111217"] = "Relat
-- Please select an ERI specification version for the ERI server. -- Please select an ERI specification version for the ERI server.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Please select an ERI specification version for the ERI server." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Please select an ERI specification version for the ERI server."
-- Open in chat
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1664804142"] = "Open in chat"
-- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user). -- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user)." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user)."
@ -1981,6 +1990,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T349928509"] = "The ima
-- Open Settings -- Open Settings
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings"
-- Assistant is still running.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T2719896610"] = "Assistant is still running."
-- Assistant was canceled. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3390934621"] = "Assistant was canceled. Open it to review the result."
-- Assistant failed. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assistant failed. Open it to review the result."
-- The result is ready.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
-- Show or hide the detailed security information. -- Show or hide the detailed security information.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.IconFinder; namespace AIStudio.Assistants.IconFinder;
@ -56,6 +57,22 @@ public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconF
private string inputContext = string.Empty; private string inputContext = string.Empty;
private IconSources selectedIconSource; private IconSources selectedIconSource;
private static readonly AssistantSessionStateKey<string> INPUT_CONTEXT_STATE_KEY = new(nameof(inputContext));
private static readonly AssistantSessionStateKey<IconSources> SELECTED_ICON_SOURCE_STATE_KEY = new(nameof(selectedIconSource));
/// <inheritdoc />
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
{
state.Set(INPUT_CONTEXT_STATE_KEY, this.inputContext);
state.Set(SELECTED_ICON_SOURCE_STATE_KEY, this.selectedIconSource);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.JobPosting; namespace AIStudio.Assistants.JobPosting;
@ -128,6 +129,49 @@ public partial class AssistantJobPostings : AssistantBaseCore<SettingsDialogJobP
private string inputCountryLegalFramework = string.Empty; private string inputCountryLegalFramework = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS; private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_MANDATORY_INFORMATION_STATE_KEY = new(nameof(inputMandatoryInformation));
private static readonly AssistantSessionStateKey<string> INPUT_JOB_DESCRIPTION_STATE_KEY = new(nameof(inputJobDescription));
private static readonly AssistantSessionStateKey<string> INPUT_QUALIFICATIONS_STATE_KEY = new(nameof(inputQualifications));
private static readonly AssistantSessionStateKey<string> INPUT_RESPONSIBILITIES_STATE_KEY = new(nameof(inputResponsibilities));
private static readonly AssistantSessionStateKey<string> INPUT_COMPANY_NAME_STATE_KEY = new(nameof(inputCompanyName));
private static readonly AssistantSessionStateKey<string> INPUT_ENTRY_DATE_STATE_KEY = new(nameof(inputEntryDate));
private static readonly AssistantSessionStateKey<string> INPUT_VALID_UNTIL_STATE_KEY = new(nameof(inputValidUntil));
private static readonly AssistantSessionStateKey<string> INPUT_WORK_LOCATION_STATE_KEY = new(nameof(inputWorkLocation));
private static readonly AssistantSessionStateKey<string> INPUT_COUNTRY_LEGAL_FRAMEWORK_STATE_KEY = new(nameof(inputCountryLegalFramework));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.LegalCheck; namespace AIStudio.Assistants.LegalCheck;
@ -59,6 +60,31 @@ public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegal
private bool isAgentRunning; private bool isAgentRunning;
private string inputLegalDocument = string.Empty; private string inputLegalDocument = string.Empty;
private string inputQuestions = string.Empty; private string inputQuestions = string.Empty;
private static readonly AssistantSessionStateKey<bool> SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader));
private static readonly AssistantSessionStateKey<bool> USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent));
private static readonly AssistantSessionStateKey<bool> IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning));
private static readonly AssistantSessionStateKey<string> INPUT_LEGAL_DOCUMENT_STATE_KEY = new(nameof(inputLegalDocument));
private static readonly AssistantSessionStateKey<string> INPUT_QUESTIONS_STATE_KEY = new(nameof(inputQuestions));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,5 +1,6 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.MyTasks; namespace AIStudio.Assistants.MyTasks;
@ -59,6 +60,25 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
private string inputText = string.Empty; private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS; private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
#if !DEBUG #if !DEBUG
@ -176,6 +177,67 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
private string recStructureMarkers = string.Empty; private string recStructureMarkers = string.Empty;
private string recRoleDefinition = string.Empty; private string recRoleDefinition = string.Empty;
private string recLanguageChoice = string.Empty; private string recLanguageChoice = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_PROMPT_STATE_KEY = new(nameof(inputPrompt));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<string> IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects));
private static readonly AssistantSessionStateKey<bool> USE_CUSTOM_PROMPT_GUIDE_STATE_KEY = new(nameof(useCustomPromptGuide));
private static readonly AssistantSessionStateKey<HashSet<FileAttachment>> CUSTOM_PROMPT_GUIDE_FILES_STATE_KEY = new(nameof(customPromptGuideFiles));
private static readonly AssistantSessionStateKey<string> CURRENT_CUSTOM_PROMPT_GUIDE_PATH_STATE_KEY = new(nameof(currentCustomPromptGuidePath));
private static readonly AssistantSessionStateKey<string> CUSTOM_PROMPTING_GUIDELINE_CONTENT_STATE_KEY = new(nameof(customPromptingGuidelineContent));
private static readonly AssistantSessionStateKey<bool> IS_LOADING_CUSTOM_PROMPT_GUIDE_STATE_KEY = new(nameof(isLoadingCustomPromptGuide));
private static readonly AssistantSessionStateKey<bool> HAS_UPDATED_DEFAULT_RECOMMENDATIONS_STATE_KEY = new(nameof(hasUpdatedDefaultRecommendations));
private static readonly AssistantSessionStateKey<string> OPTIMIZED_PROMPT_STATE_KEY = new(nameof(optimizedPrompt));
private static readonly AssistantSessionStateKey<string> REC_CLARITY_DIRECTNESS_STATE_KEY = new(nameof(recClarityDirectness));
private static readonly AssistantSessionStateKey<string> REC_EXAMPLES_CONTEXT_STATE_KEY = new(nameof(recExamplesContext));
private static readonly AssistantSessionStateKey<string> REC_SEQUENTIAL_STEPS_STATE_KEY = new(nameof(recSequentialSteps));
private static readonly AssistantSessionStateKey<string> REC_STRUCTURE_MARKERS_STATE_KEY = new(nameof(recStructureMarkers));
private static readonly AssistantSessionStateKey<string> REC_ROLE_DEFINITION_STATE_KEY = new(nameof(recRoleDefinition));
private static readonly AssistantSessionStateKey<string> REC_LANGUAGE_CHOICE_STATE_KEY = new(nameof(recLanguageChoice));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 ShowUpdatedPromptGuidelinesIndicator => !this.useCustomPromptGuide && this.hasUpdatedDefaultRecommendations;
private bool CanPreviewCustomPromptGuide => this.useCustomPromptGuide && this.customPromptGuideFiles.Count > 0; private bool CanPreviewCustomPromptGuide => this.useCustomPromptGuide && this.customPromptGuideFiles.Count > 0;

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.RewriteImprove; namespace AIStudio.Assistants.RewriteImprove;
@ -91,6 +92,34 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
private string rewrittenText = string.Empty; private string rewrittenText = string.Empty;
private WritingStyles selectedWritingStyle; private WritingStyles selectedWritingStyle;
private SentenceStructure selectedSentenceStructure; private SentenceStructure selectedSentenceStructure;
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<string> REWRITTEN_TEXT_STATE_KEY = new(nameof(rewrittenText));
private static readonly AssistantSessionStateKey<WritingStyles> SELECTED_WRITING_STYLE_STATE_KEY = new(nameof(selectedWritingStyle));
private static readonly AssistantSessionStateKey<SentenceStructure> SELECTED_SENTENCE_STRUCTURE_STATE_KEY = new(nameof(selectedSentenceStructure));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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) private string? ValidateText(string text)
{ {
@ -134,6 +163,13 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
var time = this.AddUserRequest(this.inputText); var time = this.AddUserRequest(this.inputText);
this.rewrittenText = await this.AddAIResponseAsync(time); this.rewrittenText = await this.AddAIResponseAsync(time);
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.rewrittenText); if (!this.IsAssistantComponentDisposed)
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.rewrittenText);
}
protected override async Task OnAssistantSessionRenderedAsync(AssistantSessionSnapshot snapshot)
{
if (!snapshot.IsActive && !string.IsNullOrWhiteSpace(this.inputText) && !string.IsNullOrWhiteSpace(this.rewrittenText))
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.rewrittenText);
} }
} }

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.SlideBuilder; namespace AIStudio.Assistants.SlideBuilder;
@ -197,6 +198,58 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
private int calculatedNumberOfSlides; private int calculatedNumberOfSlides;
private string importantAspects = string.Empty; private string importantAspects = string.Empty;
private HashSet<FileAttachment> loadedDocumentPaths = []; private HashSet<FileAttachment> loadedDocumentPaths = [];
private static readonly AssistantSessionStateKey<string> INPUT_TITLE_STATE_KEY = new(nameof(inputTitle));
private static readonly AssistantSessionStateKey<string> INPUT_CONTENT_STATE_KEY = new(nameof(inputContent));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<AudienceProfile> SELECTED_AUDIENCE_PROFILE_STATE_KEY = new(nameof(selectedAudienceProfile));
private static readonly AssistantSessionStateKey<AudienceAgeGroup> SELECTED_AUDIENCE_AGE_GROUP_STATE_KEY = new(nameof(selectedAudienceAgeGroup));
private static readonly AssistantSessionStateKey<AudienceOrganizationalLevel> SELECTED_AUDIENCE_ORGANIZATIONAL_LEVEL_STATE_KEY = new(nameof(selectedAudienceOrganizationalLevel));
private static readonly AssistantSessionStateKey<AudienceExpertise> SELECTED_AUDIENCE_EXPERTISE_STATE_KEY = new(nameof(selectedAudienceExpertise));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<int> NUMBER_OF_SHEETS_STATE_KEY = new(nameof(numberOfSheets));
private static readonly AssistantSessionStateKey<int> NUMBER_OF_BULLET_POINTS_STATE_KEY = new(nameof(numberOfBulletPoints));
private static readonly AssistantSessionStateKey<int> TIME_SPECIFICATION_STATE_KEY = new(nameof(timeSpecification));
private static readonly AssistantSessionStateKey<int> CALCULATED_NUMBER_OF_SLIDES_STATE_KEY = new(nameof(calculatedNumberOfSlides));
private static readonly AssistantSessionStateKey<string> IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects));
private static readonly AssistantSessionStateKey<HashSet<FileAttachment>> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.Synonym; namespace AIStudio.Assistants.Synonym;
@ -103,6 +104,28 @@ public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonym
private string inputContext = string.Empty; private string inputContext = string.Empty;
private CommonLanguages selectedLanguage; private CommonLanguages selectedLanguage;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<string> INPUT_CONTEXT_STATE_KEY = new(nameof(inputContext));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_LANGUAGE_STATE_KEY = new(nameof(selectedLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.TextSummarizer; namespace AIStudio.Assistants.TextSummarizer;
@ -72,6 +73,43 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
private Complexity selectedComplexity; private Complexity selectedComplexity;
private string expertInField = string.Empty; private string expertInField = string.Empty;
private string importantAspects = string.Empty; private string importantAspects = string.Empty;
private static readonly AssistantSessionStateKey<bool> SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader));
private static readonly AssistantSessionStateKey<bool> USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent));
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<bool> IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<Complexity> SELECTED_COMPLEXITY_STATE_KEY = new(nameof(selectedComplexity));
private static readonly AssistantSessionStateKey<string> EXPERT_IN_FIELD_STATE_KEY = new(nameof(expertInField));
private static readonly AssistantSessionStateKey<string> IMPORTANT_ASPECTS_STATE_KEY = new(nameof(importantAspects));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantSessions;
namespace AIStudio.Assistants.Translation; namespace AIStudio.Assistants.Translation;
@ -79,6 +80,40 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
private string inputTextLastTranslation = string.Empty; private string inputTextLastTranslation = string.Empty;
private CommonLanguages selectedTargetLanguage; private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<bool> SHOW_WEB_CONTENT_READER_STATE_KEY = new(nameof(showWebContentReader));
private static readonly AssistantSessionStateKey<bool> USE_CONTENT_CLEANER_AGENT_STATE_KEY = new(nameof(useContentCleanerAgent));
private static readonly AssistantSessionStateKey<bool> LIVE_TRANSLATION_STATE_KEY = new(nameof(liveTranslation));
private static readonly AssistantSessionStateKey<bool> IS_AGENT_RUNNING_STATE_KEY = new(nameof(isAgentRunning));
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_LAST_TRANSLATION_STATE_KEY = new(nameof(inputTextLastTranslation));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 #region Overrides of ComponentBase

View File

@ -7,7 +7,17 @@
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<MudStack AlignItems="AlignItems.Center" Row="@true"> <MudStack AlignItems="AlignItems.Center" Row="@true">
<MudIcon Icon="@this.Icon" Size="Size.Large" Color="Color.Primary"/> <MudElement HtmlTag="span" Style="position: relative; display: inline-flex; line-height: 1;">
<MudIcon Icon="@this.Icon" Size="Size.Large" Color="Color.Primary"/>
@if (this.AssistantSessionIndicator is { } indicator)
{
<MudTooltip Text="@indicator.Tooltip">
<MudElement HtmlTag="span" Style="position: absolute; right: -0.45rem; bottom: -0.3rem; display: inline-flex; background-color: var(--mud-palette-surface); border-radius: 50%; padding: 1px;">
<MudIcon Icon="@indicator.Icon" Size="Size.Small" Color="@indicator.Color"/>
</MudElement>
</MudTooltip>
}
</MudElement>
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.Name @this.Name
</MudText> </MudText>

View File

@ -1,5 +1,6 @@
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions; using DialogOptions = AIStudio.Dialogs.DialogOptions;
@ -7,6 +8,14 @@ namespace AIStudio.Components;
public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent
{ {
/// <summary>
/// Describes the assistant session indicator shown on top of the assistant icon.
/// </summary>
/// <param name="Icon">The icon that communicates the session status.</param>
/// <param name="Color">The color that communicates the session status.</param>
/// <param name="Tooltip">The tooltip text that explains the session status.</param>
private sealed record AssistantSessionIndicatorData(string Icon, Color Color, string Tooltip);
[Parameter] [Parameter]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -31,6 +40,12 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
[Parameter] [Parameter]
public Tools.Components Component { get; set; } = Tools.Components.NONE; public Tools.Components Component { get; set; } = Tools.Components.NONE;
/// <summary>
/// Gets or sets the optional assistant session instance ID represented by this block.
/// </summary>
[Parameter]
public string AssistantSessionInstanceId { get; set; } = string.Empty;
[Parameter] [Parameter]
public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE; public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE;
@ -39,6 +54,9 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject]
private AssistantSessionService AssistantSessionService { get; init; } = null!;
private async Task OpenSettingsDialog() private async Task OpenSettingsDialog()
{ {
@ -50,15 +68,50 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
await this.DialogService.ShowAsync<TSettings>(T("Open Settings"), dialogParameters, DialogOptions.FULLSCREEN); await this.DialogService.ShowAsync<TSettings>(T("Open Settings"), dialogParameters, DialogOptions.FULLSCREEN);
} }
private string BorderColor => this.SettingsManager.IsDarkMode switch private string BorderColor => this.AssistantSessionSnapshot?.IsActive is true ? this.ColorTheme.GetActivityIndicatorColor(this.SettingsManager) : this.SettingsManager.IsDarkMode switch
{ {
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight, true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayDefault,
false => this.ColorTheme.GetCurrentPalette(this.SettingsManager).Primary.Value, false => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayDefault,
}; };
private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;"; private string BlockStyle => $"border-width: 3px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
private bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature); private bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature);
private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel); private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
}
/// <summary>
/// Gets the newest assistant session snapshot represented by this block.
/// </summary>
private AssistantSessionSnapshot? AssistantSessionSnapshot => string.IsNullOrWhiteSpace(this.AssistantSessionInstanceId)
? this.AssistantSessionService.GetSnapshots().FirstOrDefault(snapshot => snapshot.Key.Component == this.Component)
: this.AssistantSessionService.GetSnapshots().FirstOrDefault(snapshot => snapshot.Key.InstanceId == this.AssistantSessionInstanceId);
/// <summary>
/// Gets the assistant session indicator shown on top of the assistant icon.
/// </summary>
private AssistantSessionIndicatorData? AssistantSessionIndicator => this.AssistantSessionSnapshot?.Status switch
{
AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING => new(Icons.Material.Filled.ChangeCircle, Color.Info, this.T("Assistant is still running.")),
AssistantSessionStatus.COMPLETED => new(Icons.Material.Filled.TaskAlt, Color.Success, this.T("The result is ready.")),
AssistantSessionStatus.FAILED => new(Icons.Material.Filled.Error, Color.Error, this.T("Assistant failed. Open it to review the result.")),
AssistantSessionStatus.CANCELED => new(Icons.Material.Filled.Cancel, Color.Warning, this.T("Assistant was canceled. Open it to review the result.")),
_ => null,
};
/// <summary>
/// Refreshes the block when assistant session activity changes.
/// </summary>
/// <typeparam name="T">The message payload type.</typeparam>
/// <param name="sendingComponent">The component that sent the message, if any.</param>
/// <param name="triggeredEvent">The event that was triggered.</param>
/// <param name="data">The message payload.</param>
/// <returns>A task that completes after the message was processed.</returns>
protected override Task ProcessIncomingMessage<T>(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);
}
}

View File

@ -2,7 +2,7 @@
@inherits EnumSelectionBase @inherits EnumSelectionBase
<MudStack Row="@true" Class="mb-3"> <MudStack Row="@true" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection"> <MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection" Disabled="@this.Disabled">
@foreach (var value in Enum.GetValues<T>()) @foreach (var value in Enum.GetValues<T>())
{ {
<MudSelectItem Value="@value"> <MudSelectItem Value="@value">
@ -12,6 +12,6 @@
</MudSelect> </MudSelect>
@if (this.AllowOther && this.Value.Equals(this.OtherValue)) @if (this.AllowOther && this.Value.Equals(this.OtherValue))
{ {
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/> <MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true" Disabled="@this.Disabled"/>
} }
</MudStack> </MudStack>

View File

@ -38,6 +38,12 @@ public partial class EnumSelection<T> : EnumSelectionBase where T : struct, Enum
[Parameter] [Parameter]
public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown; public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown;
/// <summary>
/// Gets or sets whether the selection controls are disabled.
/// </summary>
[Parameter]
public bool Disabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets the custom name function for selecting the display name of an enum value. /// Gets or sets the custom name function for selecting the display name of an enum value.

View File

@ -1,6 +1,6 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits MSGComponentBase @inherits MSGComponentBase
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined"> <MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
@foreach (var provider in this.GetAvailableProviders()) @foreach (var provider in this.GetAvailableProviders())
{ {
<MudSelectItem Value="@provider"/> <MudSelectItem Value="@provider"/>

View File

@ -20,6 +20,12 @@ public partial class ProviderSelection : MSGComponentBase
[Parameter] [Parameter]
public Func<AIStudio.Settings.Provider, string?> ValidateProvider { get; set; } = _ => null; public Func<AIStudio.Settings.Provider, string?> ValidateProvider { get; set; } = _ => null;
/// <summary>
/// Gets or sets whether provider selection is disabled.
/// </summary>
[Parameter]
public bool Disabled { get; set; }
[Parameter] [Parameter]
public ConfidenceLevel ExplicitMinimumConfidence { get; set; } = ConfidenceLevel.UNKNOWN; public ConfidenceLevel ExplicitMinimumConfidence { get; set; } = ConfidenceLevel.UNKNOWN;

View File

@ -2,6 +2,7 @@ using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AIJobs; using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -30,6 +31,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
[Inject] [Inject]
private AIJobService AIJobService { get; init; } = null!; private AIJobService AIJobService { get; init; } = null!;
[Inject]
private AssistantSessionService AssistantSessionService { get; init; } = null!;
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; 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.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.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.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: // 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_CHANGED:
case Event.AI_JOB_FINISHED: case Event.AI_JOB_FINISHED:
case Event.CHAT_GENERATION_CHANGED: case Event.CHAT_GENERATION_CHANGED:
case Event.ASSISTANT_SESSION_CHANGED:
case Event.ASSISTANT_SESSION_FINISHED:
this.LoadNavItems(); this.LoadNavItems();
this.StateHasChanged(); this.StateHasChanged();
break; break;
@ -341,18 +347,26 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private IEnumerable<NavBarItem> GetNavItems() private IEnumerable<NavBarItem> GetNavItems()
{ {
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager); var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
var activityIndicatorLightColor = this.ColorTheme.GetActivityIndicatorLightColor();
var activityIndicatorDarkColor = this.ColorTheme.GetActivityIndicatorDarkColor();
var defaultLightColor = palette.DarkLighten;
var defaultDarkColor = palette.GrayLight;
var chatLightColor = this.AIJobService.HasActiveJobs ? activityIndicatorLightColor : defaultLightColor;
var chatDarkColor = this.AIJobService.HasActiveJobs ? activityIndicatorDarkColor : defaultDarkColor;
var assistantsLightColor = this.AssistantSessionService.HasActiveSessions ? activityIndicatorLightColor : defaultLightColor;
var assistantsDarkColor = this.AssistantSessionService.HasActiveSessions ? activityIndicatorDarkColor : defaultDarkColor;
yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true); yield return new(T("Home"), Icons.Material.Filled.Home, defaultLightColor, defaultDarkColor, 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("Chat"), Icons.Material.Filled.Chat, chatLightColor, chatDarkColor, Routes.CHAT, false);
yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false); yield return new(T("Assistants"), Icons.Material.Filled.Apps, assistantsLightColor, assistantsDarkColor, Routes.ASSISTANTS, false);
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager)) 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); yield return new(T("Writer"), Icons.Material.Filled.Create, defaultLightColor, defaultDarkColor, Routes.WRITER, false);
yield return new(T("Plugins"), Icons.Material.TwoTone.Extension, palette.DarkLighten, palette.GrayLight, Routes.PLUGINS, false); yield return new(T("Plugins"), Icons.Material.TwoTone.Extension, defaultLightColor, defaultDarkColor, Routes.PLUGINS, false);
yield return new(T("Supporters"), Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false); yield return new(T("Supporters"), Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false);
yield return new(T("Information"), Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false); yield return new(T("Information"), Icons.Material.Filled.Info, defaultLightColor, defaultDarkColor, Routes.ABOUT, false);
yield return new(T("Settings"), Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false); yield return new(T("Settings"), Icons.Material.Filled.Settings, defaultLightColor, defaultDarkColor, Routes.SETTINGS, false);
} }
private async Task ShowUpdateDialog() private async Task ShowUpdateDialog()

View File

@ -4,5 +4,10 @@ namespace AIStudio.Layout;
public record NavBarItem(string Name, string Icon, string IconLightColor, string IconDarkColor, string Path, bool MatchAll) public record NavBarItem(string Name, string Icon, string IconLightColor, string IconDarkColor, string Path, bool MatchAll)
{ {
/// <summary>
/// Gets the CSS style that applies the current theme-aware icon color.
/// </summary>
/// <param name="settingsManager">The settings manager used to read the current theme.</param>
/// <returns>The CSS style for the nav item icon color.</returns>
public string SetColorStyle(SettingsManager settingsManager) => $"--custom-icon-color: {(settingsManager.IsDarkMode ? this.IconDarkColor : this.IconLightColor)};"; public string SetColorStyle(SettingsManager settingsManager) => $"--custom-icon-color: {(settingsManager.IsDarkMode ? this.IconDarkColor : this.IconLightColor)};";
} }

View File

@ -47,6 +47,7 @@
Description="@T(assistantPlugin.Description)" Description="@T(assistantPlugin.Description)"
Icon="@Icons.Material.Filled.FindInPage" Icon="@Icons.Material.Filled.FindInPage"
Disabled="@(!securityState.CanStartAssistant)" Disabled="@(!securityState.CanStartAssistant)"
AssistantSessionInstanceId="@assistantPlugin.Id.ToString()"
Link="@($"{Routes.ASSISTANT_DYNAMIC}?assistantId={assistantPlugin.Id}")"> Link="@($"{Routes.ASSISTANT_DYNAMIC}?assistantId={assistantPlugin.Id}")">
<SecurityBadge> <SecurityBadge>
<AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true" /> <AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true" />

View File

@ -312,6 +312,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T180921696"] = "Zurückset
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Bitte wählen Sie einen Anbieter aus." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Bitte wählen Sie einen Anbieter aus."
-- The assistant failed. The message is: '{0}'
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1833836281"] = "Der Assistent ist fehlgeschlagen. Die Meldung lautet: „{0}“"
-- This assistant is already running. AI Studio opens the running session instead.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T2575715765"] = "Dieser Assistent läuft bereits. AI Studio öffnet stattdessen die laufende Sitzung."
-- Assistant - {0} -- Assistant - {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistent {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistent {0}"
@ -750,6 +756,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1565111217"] = "Relev
-- Please select an ERI specification version for the ERI server. -- Please select an ERI specification version for the ERI server.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Bitte wählen Sie eine Version der ERI-Spezifikation für den ERI-Server aus." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Bitte wählen Sie eine Version der ERI-Spezifikation für den ERI-Server aus."
-- Open in chat
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1664804142"] = "Im Chat öffnen"
-- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user). -- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports unterhalb von 1024 sind für Systemdienste reserviert. Ihr ERI-Server muss mit erhöhten Rechten (als Root-Benutzer) ausgeführt werden." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports unterhalb von 1024 sind für Systemdienste reserviert. Ihr ERI-Server muss mit erhöhten Rechten (als Root-Benutzer) ausgeführt werden."
@ -1983,6 +1992,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T349928509"] = "Das Bil
-- Open Settings -- Open Settings
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Einstellungen öffnen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Einstellungen öffnen"
-- Assistant is still running.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T2719896610"] = "Assistent läuft noch."
-- Assistant was canceled. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3390934621"] = "Assistent wurde abgebrochen. Öffnen Sie ihn, um das Ergebnis zu überprüfen."
-- Assistant failed. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assistent fehlgeschlagen. Öffnen Sie ihn, um das Ergebnis zu überprüfen."
-- The result is ready.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "Das Ergebnis ist fertig."
-- Show or hide the detailed security information. -- Show or hide the detailed security information.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Detaillierte Sicherheitsinformationen anzeigen oder ausblenden." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Detaillierte Sicherheitsinformationen anzeigen oder ausblenden."

View File

@ -312,6 +312,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T180921696"] = "Reset"
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "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} -- Assistant - {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistant - {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T3043922"] = "Assistant - {0}"
@ -750,6 +756,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1565111217"] = "Relat
-- Please select an ERI specification version for the ERI server. -- Please select an ERI specification version for the ERI server.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Please select an ERI specification version for the ERI server." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1612890554"] = "Please select an ERI specification version for the ERI server."
-- Open in chat
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T1664804142"] = "Open in chat"
-- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user). -- Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user)." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T168780641"] = "Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user)."
@ -1983,6 +1992,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T349928509"] = "The ima
-- Open Settings -- Open Settings
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings"
-- Assistant is still running.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T2719896610"] = "Assistant is still running."
-- Assistant was canceled. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3390934621"] = "Assistant was canceled. Open it to review the result."
-- Assistant failed. Open it to review the result.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assistant failed. Open it to review the result."
-- The result is ready.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
-- Show or hide the detailed security information. -- Show or hide the detailed security information.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."

View File

@ -3,6 +3,7 @@ using AIStudio.Agents.AssistantAudit;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Databases; using AIStudio.Tools.Databases;
using AIStudio.Tools.AIJobs; using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -130,6 +131,7 @@ internal sealed class Program
builder.Services.AddSingleton<SettingsManager>(); builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>(); builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddSingleton<AIJobService>(); builder.Services.AddSingleton<AIJobService>();
builder.Services.AddSingleton<AssistantSessionService>();
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>(); builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
builder.Services.AddSingleton<DataSourceService>(); builder.Services.AddSingleton<DataSourceService>();
builder.Services.AddScoped<PandocAvailabilityService>(); builder.Services.AddScoped<PandocAvailabilityService>();

View File

@ -0,0 +1,34 @@
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Identifies one logical assistant session slot.
/// </summary>
public readonly record struct AssistantSessionKey
{
/// <summary>
/// Initializes a new assistant session key.
/// </summary>
/// <param name="component">The application component the assistant belongs to.</param>
/// <param name="instanceId">The assistant-specific instance ID, such as a component type or plugin ID.</param>
public AssistantSessionKey(Components component, string instanceId)
{
this.Component = component;
this.InstanceId = instanceId;
}
/// <summary>
/// Gets the application component the assistant belongs to.
/// </summary>
public Components Component { get; init; }
/// <summary>
/// Gets the assistant-specific instance ID, such as a component type or plugin ID.
/// </summary>
public string InstanceId { get; init; }
/// <summary>
/// Converts the key into a compact diagnostic string.
/// </summary>
/// <returns>The component and instance ID joined by a colon.</returns>
public override string ToString() => $"{this.Component}:{this.InstanceId}";
}

View File

@ -0,0 +1,365 @@
using System.Collections.Concurrent;
using AIStudio.Chat;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Keeps assistant sessions alive while their Blazor components are not mounted.
/// </summary>
/// <param name="messageBus">The message bus used to publish assistant session changes.</param>
public sealed class AssistantSessionService(MessageBus messageBus)
{
/// <summary>
/// Mutable runtime state owned exclusively by <see cref="AssistantSessionService"/>.
/// </summary>
/// <remarks>
/// This type intentionally exists in addition to <see cref="AssistantSessionSnapshot"/>.
/// 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.
/// </remarks>
private sealed class AssistantSessionState
{
/// <summary>
/// Identifies this concrete run of an assistant session.
/// </summary>
public required Guid SessionId { get; init; }
/// <summary>
/// Identifies the assistant and logical session slot this run belongs to.
/// </summary>
public required AssistantSessionKey Key { get; init; }
/// <summary>
/// Cancels the active assistant run.
/// </summary>
public required CancellationTokenSource CancellationTokenSource { get; init; }
/// <summary>
/// Stores when the session run started.
/// </summary>
public required DateTimeOffset StartedAt { get; init; }
/// <summary>
/// Stores when the session state was last changed.
/// </summary>
public DateTimeOffset UpdatedAt { get; set; }
/// <summary>
/// Stores when the session reached a terminal state.
/// </summary>
public DateTimeOffset? FinishedAt { get; set; }
/// <summary>
/// Stores the user-visible assistant title.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Stores the current lifecycle state of the session.
/// </summary>
public AssistantSessionStatus Status { get; set; }
/// <summary>
/// Stores the user-visible error message for failed sessions.
/// </summary>
public string ErrorMessage { get; set; } = string.Empty;
/// <summary>
/// Stores the current assistant chat thread, including streamed output.
/// </summary>
public ChatThread? ChatThread { get; set; }
/// <summary>
/// Stores the assistant component state captured from the running UI instance.
/// </summary>
public Dictionary<string, IAssistantSessionSnapshotField> State { get; set; } = new(StringComparer.Ordinal);
/// <summary>
/// Guards mutable fields while snapshots are created or updates are applied.
/// </summary>
public readonly Lock SyncRoot = new();
}
/// <summary>
/// Stores one assistant session per session key.
/// </summary>
private readonly ConcurrentDictionary<AssistantSessionKey, AssistantSessionState> sessions = new();
/// <summary>
/// Gets whether at least one assistant session is still active.
/// </summary>
public bool HasActiveSessions => this.sessions.Values.Any(session => session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING);
/// <summary>
/// Gets copied snapshots for all known assistant sessions.
/// </summary>
/// <returns>Session snapshots ordered by newest update first.</returns>
public IReadOnlyCollection<AssistantSessionSnapshot> GetSnapshots()
{
return this.sessions.Values
.Select(CreateSnapshot)
.OrderByDescending(snapshot => snapshot.UpdatedAt)
.ToList();
}
/// <summary>
/// Tries to get the current snapshot for an assistant session key.
/// </summary>
/// <param name="key">The assistant session key to look up.</param>
/// <returns>The current snapshot, or <c>null</c> when no session exists.</returns>
public AssistantSessionSnapshot? TryGetSnapshot(AssistantSessionKey key)
{
return this.sessions.TryGetValue(key, out var session) ? CreateSnapshot(session) : null;
}
/// <summary>
/// Tries to get and remove an inactive assistant session snapshot.
/// </summary>
/// <remarks>
/// This method intentionally does not publish a change event. It is used when
/// a UI instance consumes a finished session exactly once and should keep the
/// restored result locally until the user leaves or resets the assistant.
/// </remarks>
/// <param name="key">The assistant session key to look up and remove.</param>
/// <returns>The removed inactive snapshot, or <c>null</c> when no inactive session exists.</returns>
public AssistantSessionSnapshot? TryTakeInactiveSnapshot(AssistantSessionKey key)
{
if (!this.sessions.TryGetValue(key, out var session))
return null;
AssistantSessionSnapshot snapshot;
lock (session.SyncRoot)
{
if (session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING)
return null;
snapshot = CreateSnapshotWithoutLock(session);
}
return ((ICollection<KeyValuePair<AssistantSessionKey, AssistantSessionState>>)this.sessions).Remove(new(key, session)) ? snapshot : null;
}
/// <summary>
/// Starts a new assistant session when no active session exists for the key.
/// </summary>
/// <param name="key">The assistant session key.</param>
/// <param name="title">The user-visible assistant title.</param>
/// <param name="cancellationTokenSource">The cancellation token source owned by the new runtime session.</param>
/// <param name="chatThread">The current assistant chat thread, if one already exists.</param>
/// <param name="state">The initial assistant component state.</param>
/// <param name="sendingComponent">The component that initiated the session start.</param>
/// <returns>The new session snapshot, or the existing active session snapshot.</returns>
public async Task<AssistantSessionSnapshot> TryBeginAsync(AssistantSessionKey key, string title, CancellationTokenSource cancellationTokenSource, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
{
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;
var snapshot = CreateSnapshot(session);
await this.NotifyChangedAsync(session, sendingComponent);
return snapshot;
}
/// <summary>
/// Updates a running assistant session with the latest UI and chat state.
/// </summary>
/// <param name="key">The assistant session key.</param>
/// <param name="sessionId">The concrete run ID that is allowed to write the checkpoint.</param>
/// <param name="title">The current user-visible assistant title.</param>
/// <param name="chatThread">The current assistant chat thread.</param>
/// <param name="state">The current assistant component state.</param>
/// <param name="sendingComponent">The component that initiated the checkpoint.</param>
public async Task CheckpointAsync(AssistantSessionKey key, Guid sessionId, string title, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
{
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, sendingComponent);
}
/// <summary>
/// Requests cancellation for an active assistant session.
/// </summary>
/// <param name="key">The assistant session key to cancel.</param>
/// <param name="sendingComponent">The component that initiated the cancellation.</param>
public async Task CancelAsync(AssistantSessionKey key, ComponentBase? sendingComponent = null)
{
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, sendingComponent);
}
/// <summary>
/// Moves an assistant session into a terminal state and publishes completion.
/// </summary>
/// <param name="key">The assistant session key.</param>
/// <param name="sessionId">The concrete run ID that is allowed to complete the session.</param>
/// <param name="status">The terminal status to store.</param>
/// <param name="errorMessage">The user-visible error message for failed sessions.</param>
/// <param name="chatThread">The final assistant chat thread.</param>
/// <param name="state">The final assistant component state.</param>
/// <param name="sendingComponent">The component that initiated the completion.</param>
public async Task CompleteAsync(AssistantSessionKey key, Guid sessionId, AssistantSessionStatus status, string errorMessage, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
{
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, sendingComponent);
await messageBus.SendMessage(sendingComponent, Event.ASSISTANT_SESSION_FINISHED, CreateSnapshot(session));
try
{
session.CancellationTokenSource.Dispose();
}
catch
{
// ignore
}
}
/// <summary>
/// Clears an inactive assistant session.
/// </summary>
/// <param name="key">The assistant session key to clear.</param>
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));
}
/// <summary>
/// Clears all inactive sessions for a component.
/// </summary>
/// <param name="component">The component whose inactive sessions should be cleared.</param>
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);
}
/// <summary>
/// Publishes an assistant session change event.
/// </summary>
/// <param name="session">The runtime session whose copied snapshot should be published.</param>
/// <param name="sendingComponent">The component that initiated the session change.</param>
private async Task NotifyChangedAsync(AssistantSessionState session, ComponentBase? sendingComponent = null)
{
await messageBus.SendMessage(sendingComponent, Event.ASSISTANT_SESSION_CHANGED, CreateSnapshot(session));
}
/// <summary>
/// Creates a copied, external snapshot from the internal runtime state.
/// </summary>
/// <param name="session">The runtime session to copy.</param>
/// <returns>A snapshot safe to send to UI components.</returns>
private static AssistantSessionSnapshot CreateSnapshot(AssistantSessionState session)
{
lock (session.SyncRoot)
{
return CreateSnapshotWithoutLock(session);
}
}
/// <summary>
/// Creates a copied, external snapshot while the caller already holds the session lock.
/// </summary>
/// <param name="session">The runtime session to copy.</param>
/// <returns>A snapshot safe to send to UI components.</returns>
private static AssistantSessionSnapshot CreateSnapshotWithoutLock(AssistantSessionState session)
{
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<string, IAssistantSessionSnapshotField>(session.State, StringComparer.Ordinal),
};
}
}

View File

@ -0,0 +1,68 @@
using AIStudio.Chat;
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Immutable-style view of an assistant session for UI consumers and message bus events.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed record AssistantSessionSnapshot
{
/// <summary>
/// Identifies the concrete run represented by this snapshot.
/// </summary>
public required Guid SessionId { get; init; }
/// <summary>
/// Identifies the assistant and logical session slot represented by this snapshot.
/// </summary>
public required AssistantSessionKey Key { get; init; }
/// <summary>
/// Gets the user-visible assistant title.
/// </summary>
public required string Title { get; init; }
/// <summary>
/// Gets the current lifecycle status.
/// </summary>
public required AssistantSessionStatus Status { get; init; }
/// <summary>
/// Gets when the session run started.
/// </summary>
public required DateTimeOffset StartedAt { get; init; }
/// <summary>
/// Gets when the session was last changed.
/// </summary>
public required DateTimeOffset UpdatedAt { get; init; }
/// <summary>
/// Gets when the session reached a terminal state.
/// </summary>
public DateTimeOffset? FinishedAt { get; init; }
/// <summary>
/// Gets the user-visible error message for failed sessions.
/// </summary>
public string ErrorMessage { get; init; } = string.Empty;
/// <summary>
/// Gets the assistant chat thread captured for this session.
/// </summary>
public ChatThread? ChatThread { get; init; }
/// <summary>
/// Gets the assistant component state captured for this session.
/// </summary>
public IReadOnlyDictionary<string, IAssistantSessionSnapshotField> State { get; init; } = new Dictionary<string, IAssistantSessionSnapshotField>();
/// <summary>
/// Gets whether the session is still running or canceling.
/// </summary>
public bool IsActive => this.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING;
}

View File

@ -0,0 +1,45 @@
// ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Stores one typed value in an assistant session snapshot.
/// </summary>
/// <typeparam name="T">The captured value type.</typeparam>
public sealed record AssistantSessionSnapshotField<T> : IAssistantSessionSnapshotField
{
/// <summary>
/// Initializes a new typed snapshot field.
/// </summary>
/// <param name="value">The captured value.</param>
public AssistantSessionSnapshotField(T value)
{
this.Value = value;
}
/// <summary>
/// Gets the captured value.
/// </summary>
public T Value { get; }
/// <inheritdoc />
public Type ValueType => typeof(T);
/// <inheritdoc />
public bool TryRead<TValue>(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;
}
}

View File

@ -0,0 +1,28 @@
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Identifies a typed assistant session state value.
/// </summary>
/// <typeparam name="T">The value type stored for this key.</typeparam>
public readonly record struct AssistantSessionStateKey<T>
{
/// <summary>
/// Initializes a new assistant session state key.
/// </summary>
/// <param name="name">The stable dictionary name used in assistant session snapshots.</param>
public AssistantSessionStateKey(string name)
{
this.Name = name;
}
/// <summary>
/// Gets the stable dictionary name used in assistant session snapshots.
/// </summary>
public string Name { get; }
/// <summary>
/// Returns the stable dictionary name.
/// </summary>
/// <returns>The stable dictionary name.</returns>
public override string ToString() => this.Name;
}

View File

@ -0,0 +1,111 @@
using System.Text;
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Restores typed assistant session state values from a snapshot.
/// </summary>
/// <param name="fields">The captured snapshot fields.</param>
/// <param name="assistantTitle">The user-visible assistant title.</param>
public sealed class AssistantSessionStateReader(IReadOnlyDictionary<string, IAssistantSessionSnapshotField> fields, string assistantTitle)
{
private static readonly ILogger<AssistantSessionStateReader> LOG = Program.LOGGER_FACTORY.CreateLogger<AssistantSessionStateReader>();
/// <summary>
/// Restores a typed value when it exists in the snapshot.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="apply">The action that applies the restored value.</param>
public void Restore<T>(AssistantSessionStateKey<T> key, Action<T> apply)
{
if (this.TryRead(key, out var value))
apply(value!);
}
/// <summary>
/// Restores a list into an existing list instance.
/// </summary>
/// <typeparam name="T">The list item type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="target">The existing list to update.</param>
public void RestoreList<T>(AssistantSessionStateKey<List<T>> key, List<T> target)
{
this.Restore(key, values =>
{
target.Clear();
target.AddRange(values);
});
}
/// <summary>
/// Restores a hash set into an existing hash set instance.
/// </summary>
/// <typeparam name="T">The set item type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="target">The existing hash set to update.</param>
public void RestoreHashSet<T>(AssistantSessionStateKey<HashSet<T>> key, HashSet<T> target)
{
this.Restore(key, values =>
{
target.Clear();
target.UnionWith(values);
});
}
/// <summary>
/// Restores a dictionary into an existing dictionary instance.
/// </summary>
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="target">The existing dictionary to update.</param>
public void RestoreDictionary<TKey, TValue>(AssistantSessionStateKey<Dictionary<TKey, TValue>> key, Dictionary<TKey, TValue> target) where TKey : notnull
{
this.Restore(key, values =>
{
target.Clear();
foreach (var (itemKey, itemValue) in values)
target[itemKey] = itemValue;
});
}
/// <summary>
/// Restores text into an existing string builder instance.
/// </summary>
/// <param name="key">The typed state key.</param>
/// <param name="target">The existing string builder to update.</param>
public void RestoreStringBuilder(AssistantSessionStateKey<string> key, StringBuilder target)
{
this.Restore(key, value =>
{
target.Clear();
target.Append(value);
});
}
/// <summary>
/// Tries to read a typed value from the snapshot.
/// </summary>
/// <typeparam name="T">The requested value type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="value">The restored value when reading succeeds.</param>
/// <returns><c>true</c> when a value exists and matches the requested type; otherwise, <c>false</c>.</returns>
private bool TryRead<T>(AssistantSessionStateKey<T> 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;
}
}

View File

@ -0,0 +1,78 @@
using System.Text;
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Collects typed assistant session state values for a snapshot.
/// </summary>
public sealed class AssistantSessionStateWriter
{
/// <summary>
/// Stores captured fields by their stable dictionary names.
/// </summary>
private readonly Dictionary<string, IAssistantSessionSnapshotField> fields = new(StringComparer.Ordinal);
/// <summary>
/// Stores a typed state value.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="value">The captured value.</param>
public void Set<T>(AssistantSessionStateKey<T> key, T value)
{
this.fields[key.Name] = new AssistantSessionSnapshotField<T>(value);
}
/// <summary>
/// Stores a list copy.
/// </summary>
/// <typeparam name="T">The list item type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="values">The values to copy.</param>
public void SetList<T>(AssistantSessionStateKey<List<T>> key, IEnumerable<T> values)
{
this.Set(key, values.ToList());
}
/// <summary>
/// Stores a hash set copy.
/// </summary>
/// <typeparam name="T">The set item type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="values">The values to copy.</param>
public void SetHashSet<T>(AssistantSessionStateKey<HashSet<T>> key, IEnumerable<T> values)
{
this.Set(key, values.ToHashSet());
}
/// <summary>
/// Stores a dictionary copy.
/// </summary>
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
/// <param name="key">The typed state key.</param>
/// <param name="values">The values to copy.</param>
public void SetDictionary<TKey, TValue>(AssistantSessionStateKey<Dictionary<TKey, TValue>> key, IDictionary<TKey, TValue> values) where TKey : notnull
{
this.Set(key, new Dictionary<TKey, TValue>(values));
}
/// <summary>
/// Stores the current text from a string builder.
/// </summary>
/// <param name="key">The typed state key.</param>
/// <param name="value">The string builder to read.</param>
public void SetStringBuilder(AssistantSessionStateKey<string> key, StringBuilder value)
{
this.Set(key, value.ToString());
}
/// <summary>
/// Returns the captured fields as a dictionary.
/// </summary>
/// <returns>A copied dictionary containing the captured fields.</returns>
public Dictionary<string, IAssistantSessionSnapshotField> ToDictionary()
{
return new(this.fields, StringComparer.Ordinal);
}
}

View File

@ -0,0 +1,37 @@
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Describes the lifecycle state of an assistant session.
/// </summary>
public enum AssistantSessionStatus
{
/// <summary>
/// No session state is available.
/// </summary>
NONE,
/// <summary>
/// The assistant session is running.
/// </summary>
RUNNING,
/// <summary>
/// Cancellation was requested and the assistant is shutting down.
/// </summary>
CANCELING,
/// <summary>
/// The assistant session completed successfully.
/// </summary>
COMPLETED,
/// <summary>
/// The assistant session was canceled.
/// </summary>
CANCELED,
/// <summary>
/// The assistant session failed.
/// </summary>
FAILED,
}

View File

@ -0,0 +1,20 @@
namespace AIStudio.Tools.AssistantSessions;
/// <summary>
/// Provides a typed value stored in an assistant session snapshot.
/// </summary>
public interface IAssistantSessionSnapshotField
{
/// <summary>
/// Gets the type used when the value was captured.
/// </summary>
Type ValueType { get; }
/// <summary>
/// Tries to read the captured value as the requested type.
/// </summary>
/// <typeparam name="T">The requested value type.</typeparam>
/// <param name="value">The typed value when reading succeeds.</param>
/// <returns><c>true</c> when the captured value matches <typeparamref name="T"/>; otherwise, <c>false</c>.</returns>
bool TryRead<T>(out T? value);
}

View File

@ -144,6 +144,16 @@ public enum Event
/// Notifies receivers that chat generation state changed. /// Notifies receivers that chat generation state changed.
/// </summary> /// </summary>
CHAT_GENERATION_CHANGED, CHAT_GENERATION_CHANGED,
/// <summary>
/// Notifies receivers that an assistant session changed.
/// </summary>
ASSISTANT_SESSION_CHANGED,
/// <summary>
/// Notifies receivers that an assistant session finished.
/// </summary>
ASSISTANT_SESSION_FINISHED,
// Workspace events: // Workspace events:
/// <summary> /// <summary>

View File

@ -9,4 +9,14 @@ public static class MudThemeExtensions
true => theme.PaletteDark, true => theme.PaletteDark,
false => theme.PaletteLight, false => theme.PaletteLight,
}; };
public static string GetActivityIndicatorColor(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch
{
true => theme.GetActivityIndicatorDarkColor(),
false => theme.GetActivityIndicatorLightColor(),
};
public static string GetActivityIndicatorLightColor(this MudTheme theme) => theme.PaletteLight.Info.Value;
public static string GetActivityIndicatorDarkColor(this MudTheme theme) => theme.PaletteDark.InfoLighten;
} }

View File

@ -30,6 +30,49 @@ public sealed class AssistantState
this.Times.Clear(); this.Times.Clear();
} }
/// <summary>
/// Copies all dynamic assistant state values from another state instance.
/// </summary>
/// <param name="other">The state instance to copy from.</param>
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);
}
/// <summary>
/// Creates a copy of the dynamic assistant state.
/// </summary>
/// <returns>A copied assistant state instance.</returns>
public AssistantState Clone()
{
var clone = new AssistantState();
clone.CopyFrom(this);
return clone;
}
/// <summary>
/// Copies all entries from one dictionary into another dictionary.
/// </summary>
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
/// <param name="source">The source dictionary.</param>
/// <param name="target">The target dictionary.</param>
private static void CopyDictionary<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> target) where TKey : notnull
{
foreach (var (key, value) in source)
target[key] = value;
}
public bool TryApplyValue(string fieldName, LuaValue value, out string expectedType) public bool TryApplyValue(string fieldName, LuaValue value, out string expectedType)
{ {
expectedType = string.Empty; expectedType = string.Empty;

View File

@ -1,10 +1,18 @@
window.generateDiff = function (text1, text2, divDiff, divLegend) { window.generateDiff = function (text1, text2, divDiff, divLegend) {
let wikEdDiff = new WikEdDiff(); let wikEdDiff = new WikEdDiff();
let targetDiv = document.getElementById(divDiff) let targetDiv = document.getElementById(divDiff)
if (!targetDiv) {
return;
}
targetDiv.innerHTML = wikEdDiff.diff(text1, text2); targetDiv.innerHTML = wikEdDiff.diff(text1, text2);
targetDiv.classList.add('mud-typography-body1', 'improvedDiff'); targetDiv.classList.add('mud-typography-body1', 'improvedDiff');
let legend = document.getElementById(divLegend); let legend = document.getElementById(divLegend);
if (!legend) {
return;
}
legend.innerHTML = ` legend.innerHTML = `
<div class="legend mt-2"> <div class="legend mt-2">
<h3>Legend</h3> <h3>Legend</h3>
@ -20,6 +28,10 @@ window.generateDiff = function (text1, text2, divDiff, divLegend) {
window.clearDiv = function (divName) { window.clearDiv = function (divName) {
let targetDiv = document.getElementById(divName); let targetDiv = document.getElementById(divName);
if (!targetDiv) {
return;
}
targetDiv.innerHTML = ''; targetDiv.innerHTML = '';
} }

View File

@ -1,2 +1,5 @@
# v26.6.3, build 243 (2026-06-xx xx:xx UTC) # v26.6.3, build 243 (2026-06-xx xx:xx UTC)
- Improved all assistants, so running tasks can continue when you leave the assistant and return later.
- Improved the assistant overview so it shows which assistants are still running or have a result ready.
- Improved the activity indicators for running chats and assistants so they use a consistent blue highlight.
- Improved the chat experience by automatically focusing the message composer again when it becomes available. Thanks, Dominic Neuburg (`donework`), for the contribution. - Improved the chat experience by automatically focusing the message composer again when it becomes available. Thanks, Dominic Neuburg (`donework`), for the contribution.

View File

@ -227,6 +227,9 @@ public sealed class ThisUsageAnalyzer : DiagnosticAnalyzer
} }
// Also check for conditional access expressions (e.g., instance?.Member): // Also check for conditional access expressions (e.g., instance?.Member):
if (node.Parent is MemberBindingExpressionSyntax)
return true;
if (node.Parent is ConditionalAccessExpressionSyntax) if (node.Parent is ConditionalAccessExpressionSyntax)
return true; return true;