Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions app/MindWork AI Studio/Assistants/AssistantBase.razor
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,25 @@

@if (this.ShowDedicatedProgress && this.IsProcessing)
{
@if (string.IsNullOrWhiteSpace(this.ProgressText))
@if (this.processingStartedAt is null && string.IsNullOrWhiteSpace(this.ProgressText))
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
else
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-2" />
<MudText Typo="Typo.body2" Class="mb-6">
@this.ProgressText
</MudText>
<MudStack Row="@true" AlignItems="@AlignItems.Center" Spacing="3" Class="mb-6">
@if (this.processingStartedAt is { } startedAt)
{
<ElapsedTime Start="@startedAt"/>
}
@if (!string.IsNullOrWhiteSpace(this.ProgressText))
{
<MudText Typo="Typo.body2">
@this.ProgressText
</MudText>
}
</MudStack>
}
}

Expand Down
11 changes: 11 additions & 0 deletions app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ protected virtual string? SendToChatVisibleUserPromptText
private AssistantSessionKey assistantSessionKey;
private MediaImportOwner CurrentMediaImportOwner => MediaImportOwner.ForAssistant(this.assistantSessionKey);
private Guid? assistantSessionId;

/// <summary>
/// When the running session started, which the time below the progress bar counts from; null while
/// no session runs. It comes from the session, so an instance which returns to a running session
/// goes on counting where the first one was.
/// </summary>
private DateTimeOffset? processingStartedAt;

private AssistantSessionSnapshot? pendingRenderedAssistantSessionSnapshot;
private AssistantSessionSnapshot? lastAttachedAssistantSessionSnapshot;

Expand Down Expand Up @@ -311,6 +319,7 @@ private async Task Start()
}

this.assistantSessionId = startedSession.SessionId;
this.processingStartedAt = startedSession.StartedAt;
await this.RefreshAssistantUIAsync();

var sessionStatus = AssistantSessionStatus.COMPLETED;
Expand Down Expand Up @@ -343,6 +352,7 @@ private async Task Start()
finally
{
this.IsProcessing = false;
this.processingStartedAt = null;
var sessionCancellationTokenSource = this.CancellationTokenSource;
this.CancellationTokenSource = null;
if (this.assistantSessionId is { } sessionId)
Expand Down Expand Up @@ -1060,6 +1070,7 @@ private async Task AttachAssistantSession(AssistantSessionSnapshot snapshot, boo
this.lastAttachedAssistantSessionSnapshot = snapshot;
this.ChatThread = snapshot.ChatThread ?? this.ChatThread;
this.IsProcessing = snapshot.IsActive;
this.processingStartedAt = snapshot.IsActive ? snapshot.StartedAt : null;

if (!snapshot.IsActive)
this.CancellationTokenSource = null;
Expand Down
42 changes: 42 additions & 0 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3157,6 +3157,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::
-- Opened the visual briefing project folder.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T2964042492"] = "Opened the visual briefing project folder."

-- The visual briefing could not be exported. Please check that the target file is not open in another program.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T3136382170"] = "The visual briefing could not be exported. Please check that the target file is not open in another program."

-- Visual assets
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T3226971402"] = "Visual assets"

Expand Down Expand Up @@ -3286,6 +3289,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::
-- If you need help, report the problem and include the project ID.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T4292361710"] = "If you need help, report the problem and include the project ID."

-- The visual briefing could not be written completely.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T442972740"] = "The visual briefing could not be written completely."

-- The briefing was recompiled with the current AI Studio version.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGASSISTANT::T453632597"] = "The briefing was recompiled with the current AI Studio version."

Expand Down Expand Up @@ -4702,6 +4708,30 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T244391
-- Change what this tile opens
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T4272203100"] = "Change what this tile opens"

-- Could not open the file location.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T1118835751"] = "Could not open the file location."

-- Could not open the file: {0}
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T1290686840"] = "Could not open the file: {0}"

-- Open with the default program
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T1332010305"] = "Open with the default program"

-- Could not open the file location: {0}
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T1455637941"] = "Could not open the file location: {0}"

-- Show in the file manager
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T3007835369"] = "Show in the file manager"

-- Unknown error
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T3461425987"] = "Unknown error"

-- Could not open the file.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTACTIONS::T362546438"] = "Could not open the file."

-- Saving '{0}'...
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEEXPORTRUNNINGMESSAGE::T1063135893"] = "Saving '{0}'..."

-- LLMs can make mistakes. Check important information.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs can make mistakes. Check important information."

Expand Down Expand Up @@ -5266,6 +5296,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1594053136"]
-- Color theme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"] = "Color theme"

-- After you export a file, for example, an answer or a presentation, a message offers to show the file in your file manager or to open it. When enabled, this message closes after a few seconds, like other messages. When disabled, it stays until you close it.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T160366478"] = "After you export a file, for example, an answer or a presentation, a message offers to show the file in your file manager or to open it. When enabled, this message closes after a few seconds, like other messages. When disabled, it stays until you close it."

-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."

Expand Down Expand Up @@ -5317,6 +5350,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"]
-- Enterprise Administration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2277116008"] = "Enterprise Administration"

-- Close messages about exported files automatically?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2330147290"] = "Close messages about exported files automatically?"

-- Language behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Language behavior"

Expand Down Expand Up @@ -5344,6 +5380,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"]
-- No preview features selected.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2809641588"] = "No preview features selected."

-- The message stays until you close it
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2879147627"] = "The message stays until you close it"

-- This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2918560776"] = "This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information."

Expand Down Expand Up @@ -5413,6 +5452,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"]
-- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections."

-- The message closes after a few seconds
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4102409342"] = "The message closes after a few seconds"

-- Show details when suspicious content was removed?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4156872850"] = "Show details when suspicious content was removed?"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ private async Task CreatePresentationFileAsync()
var targetPath = response.SaveFilePath;
this.isWritingPresentation = true;
this.StateHasChanged();

// Pictures make this take seconds, since each of them is prepared anew:
await using var feedback = new FileExportFeedback();
feedback.ShowRunning(targetPath);
try
{
var options = new PresentationWriteOptions(this.variantLabel, this.footerText.Trim(), this.keepEmptyPicturePlaceholders);
Expand All @@ -582,7 +586,7 @@ private async Task CreatePresentationFileAsync()
await this.MessageBus.SendWarning(new(Icons.Material.Filled.HideImage, string.Format(T("These pictures could not be placed, because PowerPoint cannot show their format or they cannot be read: {0}"), string.Join(", ", names))));
}

await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Slideshow, T("The PowerPoint file was created.")));
await feedback.SucceededAsync(Icons.Material.Filled.Slideshow, T("The PowerPoint file was created."), targetPath, true);
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,10 @@ private async Task ExportAsync()
// why it gets no notice that AI generated it: any change would make it a version nobody checked.
//
await using var source = verified.Value.Stream;
await using var destination = new FileStream(response.SaveFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65_536, true);
await source.CopyToAsync(destination);
await using var feedback = new FileExportFeedback();
feedback.ShowRunning(response.SaveFilePath);
if (!await this.TryWriteExportAsync(source, response.SaveFilePath))
return;

var exportedVersion = this.selectedBriefing.Versions.First(version =>
version.RevisionId == this.selectedRevisionId);
Expand All @@ -150,7 +152,56 @@ private async Task ExportAsync()
exportedVersion.DocumentHash,
source.Length);

await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileDownload, T("The visual briefing was exported.")));
await feedback.SucceededAsync(Icons.Material.Filled.FileDownload, T("The visual briefing was exported."), response.SaveFilePath, true);
}

/// <summary>
/// Writes an exported version into the file the user chose, and tells the user when that fails.
/// </summary>
/// <remarks>
/// The file is written over rather than removed and created anew, which the Flatpak would not allow.
/// A file which cannot even be opened, e.g., because another program holds it, stays as it is. Once
/// writing has begun, a failure removes the file: half a briefing is worse than none.
/// </remarks>
/// <param name="source">The verified version to export.</param>
/// <param name="targetPath">The file the user chose.</param>
/// <returns>True, when the file was written completely.</returns>
private async Task<bool> TryWriteExportAsync(Stream source, string targetPath)
{
FileStream destination;
try
{
destination = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 65_536, true);
}
catch (Exception e)
{
this.Logger.LogError(e, "The visual briefing could not be exported, because the file '{FilePath}' cannot be opened for writing.", targetPath);
await this.MessageBus.SendError(new(Icons.Material.Filled.FileDownload, T("The visual briefing could not be exported. Please check that the target file is not open in another program.")));
return false;
}

try
{
await using (destination)
await source.CopyToAsync(destination);

return true;
}
catch (Exception e)
{
this.Logger.LogError(e, "Writing the visual briefing into the file '{FilePath}' failed.", targetPath);
try
{
File.Delete(targetPath);
}
catch (Exception deleteFailure)
{
this.Logger.LogWarning(deleteFailure, "The incomplete export '{FilePath}' cannot be deleted.", targetPath);
}

await this.MessageBus.SendError(new(Icons.Material.Filled.FileDownload, T("The visual briefing could not be written completely.")));
return false;
}
}

/// <summary>
Expand Down
10 changes: 7 additions & 3 deletions app/MindWork AI Studio/Components/DataBackupPanel.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@
@if (backupResult.Success)
{
<MudAlert Severity="@(backupResult.IncludesDataSourceIndexes ? Severity.Success : Severity.Warning)" Dense="@true" Class="mt-3">
<MudText Typo="Typo.body2">
@string.Format(T("Backed up {0} files into: {1}"), backupResult.FileCount, backupResult.ArchivePath)
</MudText>
@* Opening a backup with the default program would only unpack it, so the file manager is the one way offered. *@
<MudStack Row="@true" AlignItems="@AlignItems.Center" Spacing="2">
<MudText Typo="Typo.body2">
@string.Format(T("Backed up {0} files into: {1}"), backupResult.FileCount, backupResult.ArchivePath)
</MudText>
<FileExportActions FilePath="@backupResult.ArchivePath" OffersOpening="@false"/>
</MudStack>
@if (!backupResult.IncludesDataSourceIndexes)
{
<MudText Typo="Typo.body2">
Expand Down
8 changes: 8 additions & 0 deletions app/MindWork AI Studio/Components/ElapsedTime.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@inherits MSGComponentBase

<MudStack Row="@true" AlignItems="@AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Timer" Size="Size.Small"/>
<MudText Typo="Typo.body2">
@Format(DateTimeOffset.Now - this.Start)
</MudText>
</MudStack>
75 changes: 75 additions & 0 deletions app/MindWork AI Studio/Components/ElapsedTime.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Components;

namespace AIStudio.Components;

/// <summary>
/// Shows how long something has been running, counting up every second.
/// </summary>
/// <remarks>
/// A model which thinks for minutes shows nothing for just as long, and the time is what tells the user
/// that it still works. The component owns its timer and renders only itself once a second, not the
/// page around it: an assistant would otherwise render its entire form every second.
/// </remarks>
public partial class ElapsedTime : MSGComponentBase
{
private static readonly TimeSpan TICK_INTERVAL = TimeSpan.FromSeconds(1);

/// <summary>
/// When it started.
/// </summary>
[Parameter]
public DateTimeOffset Start { get; set; }

/// <summary>Stops counting.</summary>
private readonly CancellationTokenSource tickCancellation = new();

#region Overrides of ComponentBase

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
this.TickAsync(this.tickCancellation.Token).Observe($"{nameof(ElapsedTime)}: counting the elapsed time");
}

#endregion

#region Overrides of MSGComponentBase

protected override void DisposeResources()
{
this.tickCancellation.Cancel();
this.tickCancellation.Dispose();
base.DisposeResources();
}

#endregion

/// <summary>
/// Writes a time span as minutes and seconds, and with the hours in front from the first hour on.
/// </summary>
/// <param name="elapsed">The time span; a negative one, e.g., after the clock was set back, counts as none.</param>
/// <returns>The time span, such as 4:07 or 1:02:09.</returns>
internal static string Format(TimeSpan elapsed)
{
if (elapsed < TimeSpan.Zero)
elapsed = TimeSpan.Zero;

var hours = (int)elapsed.TotalHours;
return hours > 0
? $"{hours}:{elapsed.Minutes:00}:{elapsed.Seconds:00}"
: $"{elapsed.Minutes}:{elapsed.Seconds:00}";
}

private async Task TickAsync(CancellationToken token)
{
using var timer = new PeriodicTimer(TICK_INTERVAL);
try
{
while (await timer.WaitForNextTickAsync(token))
await this.InvokeAsync(this.StateHasChanged);
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
}
}
}
13 changes: 13 additions & 0 deletions app/MindWork AI Studio/Components/FileExportActions.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@inherits MSGComponentBase

<MudStack Row="@true" AlignItems="@AlignItems.Center" Spacing="0">
<MudTooltip Text="@T("Show in the file manager")" Placement="Placement.Top">
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Small" OnClick="@this.ShowInFileManager"/>
</MudTooltip>
@if (this.OffersOpening)
{
<MudTooltip Text="@T("Open with the default program")" Placement="Placement.Top">
<MudIconButton Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" OnClick="@this.OpenWithDefaultProgram"/>
</MudTooltip>
}
</MudStack>
Loading
Loading