Added the visual briefing assistant (#893)
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Sync Flatpak repo (push) Has been cancelled
Build and Release / Collect Flatpak artifacts (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-08-02 21:41:12 +02:00
1 parent df4663fff4
commit 58cc811a58
214 files changed
+18943 -353

No files matched your search

@@ -103,11 +103,29 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
private MediaImportOwner CurrentMediaImportOwner => MediaImportOwner.ForAssistant(new AssistantSessionKey(this.Component, this.AssistantSessionInstanceId));
private MediaImportSnapshot? MediaImportSnapshot => string.IsNullOrWhiteSpace(this.AssistantSessionInstanceId)
? this.MediaTranscriptionService.GetSnapshots().FirstOrDefault(snapshot =>
snapshot.Owner.Kind is MediaImportOwnerKind.ASSISTANT
&& snapshot.Owner.Id.StartsWith($"{this.Component}:", StringComparison.Ordinal))
: this.MediaTranscriptionService.GetSnapshot(this.CurrentMediaImportOwner);
private MediaImportSnapshot? MediaImportSnapshot => this.MediaTranscriptionService.GetSnapshots()
.FirstOrDefault(snapshot => this.OwnedByThisBlock(snapshot.Owner));
/// <summary>
/// Gets whether a media-import owner belongs to the assistant represented by this block.
/// </summary>
/// <remarks>
/// Owners that persist their own sources are keyed by the stored document rather than by an
/// assistant session, so this block aggregates all of them for its component. Without a session
/// instance we aggregate every owner of the component, otherwise we match the exact owner.
/// </remarks>
/// <param name="owner">The media-import owner to test.</param>
/// <returns><c>true</c> when this block represents the owner.</returns>
private bool OwnedByThisBlock(MediaImportOwner owner)
{
if (owner.Kind.PersistsOwnSources())
return owner.Kind == this.Component.MediaOwnerKind();
if (string.IsNullOrWhiteSpace(this.AssistantSessionInstanceId))
return owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.StartsWith($"{this.Component}:", StringComparison.Ordinal);
return owner == this.CurrentMediaImportOwner;
}
/// <summary>
/// Gets the assistant session indicator shown on top of the assistant icon.
@@ -140,11 +158,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
private void OnMediaImportStateChanged(MediaImportOwner owner)
{
var matches = string.IsNullOrWhiteSpace(this.AssistantSessionInstanceId)
? owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.StartsWith($"{this.Component}:", StringComparison.Ordinal)
: owner == this.CurrentMediaImportOwner;
if (matches)
if (this.OwnedByThisBlock(owner))
_ = this.InvokeAsync(this.StateHasChanged);
}
@@ -73,6 +73,12 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public AIStudio.Settings.Provider? Provider { get; set; }
/// <summary>
/// Gets or sets the optional picker and drop filter applied before standard attachment validation.
/// </summary>
[Parameter]
public FileTypeFilter[]? AllowedFileTypes { get; set; }
/// <summary>Optional persisted chat that can own transcript files immediately.</summary>
[Parameter]
public ChatThread? OwnerChat { get; set; }
@@ -178,7 +184,11 @@ public partial class AttachDocuments : MSGComponentBase
private async Task SyncCompletedMediaAttachmentsAsync()
{
var delivery = this.MediaTranscriptionService.GetPendingDelivery(this.EffectiveMediaImportTarget);
var completed = delivery?.Attachments ?? [];
// Owners that persist their own sources have already taken the media over when the batch
// started, so re-adding the delivered transcripts here would duplicate them.
var completed = this.EffectiveImportOwner.Kind.PersistsOwnSources()
? Array.Empty<FileAttachment>()
: delivery?.Attachments ?? [];
var pending = this.OwnerChat?.PendingMediaTranscripts ?? [];
var changed = false;
var ownerPendingChanged = false;
@@ -314,7 +324,7 @@ public partial class AttachDocuments : MSGComponentBase
this.isFileDialogOpen = true;
try
{
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"), this.AllowedFileTypes);
if (selectFiles.UserCancelled)
return;
@@ -407,6 +417,14 @@ public partial class AttachDocuments : MSGComponentBase
private async Task AddFileBatchAsync(IEnumerable<string> paths)
{
var pathList = paths.ToList();
if (this.AllowedFileTypes is { Length: > 0 })
{
var rejectedPaths = pathList.Where(path => !FileTypes.IsAllowedPath(path, this.AllowedFileTypes)).ToArray();
pathList.RemoveAll(path => rejectedPaths.Contains(path, StringComparer.Ordinal));
if (rejectedPaths.Length > 0)
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.T("Some files do not use an allowed format and were not attached.")));
}
var inaccessiblePaths = pathList.Where(path => !File.Exists(path)).ToList();
if (inaccessiblePaths.Count > 0)
{
@@ -434,12 +452,9 @@ public partial class AttachDocuments : MSGComponentBase
if (!canAddRegularFiles)
break;
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(
FileExtensionValidation.UseCase.ATTACHING_CONTENT,
path,
this.ValidateMediaFileTypes,
this.Provider))
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
continue;
this.DocumentPaths.Add(FileAttachment.FromPath(path));
}
@@ -480,6 +495,17 @@ public partial class AttachDocuments : MSGComponentBase
if (this.OwnerChat is null)
this.OwnerChat = await this.EnsureOwnerChatAsync(mediaPaths[0]);
// Owners that persist their own sources show the file right away and keep it next to the
// stored document, instead of waiting for the transcription to be delivered back.
if (this.EffectiveImportOwner.Kind.PersistsOwnSources())
{
foreach (var mediaPath in mediaPaths)
this.DocumentPaths.Add(FileAttachment.FromPath(mediaPath));
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
}
this.MediaTranscriptionService.TryStartAttachmentBatch(mediaPaths, this.EffectiveMediaImportTarget, this.OwnerChat);
}
@@ -38,10 +38,7 @@ public partial class MudCopyClipboardButton : ComponentBase
/// </summary>
[Parameter]
public Size Size { get; set; } = Size.Small;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
@@ -58,7 +55,7 @@ public partial class MudCopyClipboardButton : ComponentBase
/// </summary>
private async Task CopyToClipboard(string textContent)
{
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent);
await this.RustService.CopyText2Clipboard(textContent);
}
/// <summary>
@@ -73,16 +70,13 @@ public partial class MudCopyClipboardButton : ComponentBase
{
case ContentType.TEXT:
var textContent = (ContentText) contentToCopy;
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
await this.RustService.CopyText2Clipboard(textContent.Text);
break;
default:
this.Snackbar.Add(TB("Cannot copy this content type to clipboard."), Severity.Error, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = Color.Error;
});
// This component is no MSGComponentBase, so it uses the shared bus instance the same
// way FileExtensionValidation does:
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ContentCopy, TB("Cannot copy this content type to clipboard.")));
break;
}
}
@@ -0,0 +1,17 @@
<MudStepper ActiveIndex="@this.ActiveIndex"
ActiveIndexChanged="@this.ActiveIndexChanged"
CompletedStepColor="Color.Primary"
CurrentStepColor="Color.Primary"
ErrorStepColor="Color.Error"
NonLinear="@false"
ShowResetButton="@false"
OnPreviewInteraction="@this.PreviewInteractionAsync"
Class="@this.Classname">
<ChildContent>
@this.ChildContent
</ChildContent>
@* Empty on purpose: this is what keeps MudBlazor from rendering its Previous, Next, Skip, and
Complete buttons. The surrounding action bar still renders and is hidden through app.css. *@
<ActionContent Context="_">
</ActionContent>
</MudStepper>
@@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
/// <summary>
/// A stepper that leaves the step navigation to the application instead of the user.
/// </summary>
/// <remarks>
/// MudBlazor renders Previous, Next, Skip, and Complete buttons by default. AI Studio drives its
/// steppers from application state — a running build, or an install flow that advances when each
/// step succeeds — so those buttons have nothing to do and would only look broken when clicked.
/// This component removes them once instead of once per assistant, and carries the shared step
/// colors so the steppers stay visually consistent.
/// </remarks>
public partial class MudStepperWithoutActions : ComponentBase
{
/// <summary>
/// Gets or sets the step the stepper points at.
/// </summary>
[Parameter]
public int ActiveIndex { get; set; }
/// <summary>
/// Gets or sets the callback raised when the active step changed.
/// </summary>
[Parameter]
public EventCallback<int> ActiveIndexChanged { get; set; }
/// <summary>
/// Gets or sets whether the user must not change the active step.
/// </summary>
/// <remarks>
/// Set this when the displayed process runs on its own. The step headers stay visible, but
/// clicking them no longer moves the stepper away from the step the application selected.
/// </remarks>
[Parameter]
public bool ReadOnly { get; set; }
/// <summary>
/// Gets or sets additional CSS classes for the stepper.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the steps to render.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// The marker class that lets app.css hide the action bar MudBlazor renders around the actions.
/// </summary>
private const string MARKER_CLASS = "mud-stepper-without-actions";
private string Classname => string.IsNullOrWhiteSpace(this.Class) ? MARKER_CLASS : $"{MARKER_CLASS} {this.Class}";
/// <summary>
/// Blocks step changes that the user triggered while the stepper is read-only.
/// </summary>
/// <param name="args">The interaction to inspect.</param>
/// <returns>A completed task.</returns>
private Task PreviewInteractionAsync(StepperInteractionEventArgs args)
{
if (this.ReadOnly)
args.Cancel = true;
return Task.CompletedTask;
}
}
@@ -74,7 +74,7 @@ public partial class SettingsPanelApp : SettingsPanelBase
private async Task GenerateEncryptionSecret()
{
var secret = EnterpriseEncryption.GenerateSecret();
await this.RustService.CopyText2Clipboard(this.Snackbar, secret);
await this.RustService.CopyText2Clipboard(secret);
}
private string GetStartPageHelpText()
@@ -16,6 +16,4 @@ public abstract class SettingsPanelBase : MSGComponentBase
[Inject]
protected RustService RustService { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
}
@@ -47,7 +47,7 @@ public abstract class SettingsPanelProviderBase : SettingsPanelBase
else
{
// No encryption secret available - inform the user:
this.Snackbar.Add(TB("Cannot export the encrypted API key: No enterprise encryption secret is configured."), Severity.Warning);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Key, TB("Cannot export the encrypted API key: No enterprise encryption secret is configured.")));
}
}
}
@@ -56,6 +56,6 @@ public abstract class SettingsPanelProviderBase : SettingsPanelBase
if (string.IsNullOrWhiteSpace(luaCode))
return;
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
await this.RustService.CopyText2Clipboard(luaCode);
}
}
@@ -23,9 +23,6 @@ public partial class TextInfoLine : MSGComponentBase
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
#region Overrides of ComponentBase
@@ -43,5 +40,5 @@ public partial class TextInfoLine : MSGComponentBase
private string ClipboardTooltip => string.Format(T("Copy {0} to the clipboard"), this.ClipboardTooltipSubject);
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(content);
}
@@ -26,9 +26,6 @@ public partial class TextInfoLines : MSGComponentBase
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
#region Overrides of ComponentBase
@@ -46,7 +43,7 @@ public partial class TextInfoLines : MSGComponentBase
private string ClipboardTooltip => string.Format(T("Copy {0} to the clipboard"), this.ClipboardTooltipSubject);
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(content);
private string GetColor()
{
@@ -25,9 +25,6 @@ public partial class VoiceRecorder : MSGComponentBase
[Inject]
private GlobalShortcutService GlobalShortcutService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private VoiceRecordingAvailabilityService VoiceRecordingAvailabilityService { get; init; } = null!;
@@ -448,7 +445,7 @@ public partial class VoiceRecorder : MSGComponentBase
}
// Copy the transcribed text to the clipboard:
await this.RustService.CopyText2Clipboard(this.Snackbar, transcribedText);
await this.RustService.CopyText2Clipboard(transcribedText);
}
catch (Exception ex)