mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-16 13:53:48 +00:00
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
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:
1 parent
df4663fff4
commit
58cc811a58
214 files changed
+18943
-353
No files matched your search
@@ -61,6 +61,7 @@ public static class AssistantVisibilityExtensions
|
||||
Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT,
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT,
|
||||
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
|
||||
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Marks JSON serializer options whose exact byte output is hashed into stored data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Options carrying this attribute are frozen: changing how they serialize changes every hash ever
|
||||
/// computed with them, which turns previously valid stored data into data that fails its integrity
|
||||
/// check. Because that failure looks like corruption rather than like a code change, the rule
|
||||
/// MWAIS0010 requires such options to be written out in full at their own declaration and to carry no
|
||||
/// converters. Sharing a factory with non-hashed options is what allows a change meant for one of them
|
||||
/// to reach the other unnoticed.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class CanonicalJsonConfigurationAttribute : Attribute;
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the JSON shape of a type whose serialized form is hashed into stored data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A Roslyn analyzer only ever sees the current code, so it cannot notice that a property was added
|
||||
/// yesterday. Declaring the expected shape here gives it something to compare against: rule MWAIS0011
|
||||
/// derives a signature from the properties, their JSON names, their types, and their ignore conditions,
|
||||
/// and fails the build when it no longer matches. The point is not the value itself but the moment it
|
||||
/// forces — updating it is the step where somebody has to decide whether existing stored data may stop
|
||||
/// being readable, and the changed value makes that decision visible in the diff.
|
||||
/// Only types whose JSON is hashed directly carry this attribute. The artifact envelopes around them do
|
||||
/// not, because the parts of them that reach a hash are named one by one in
|
||||
/// <c>VisualBriefingPayloadHash</c>, where changing a type breaks the build on its own.
|
||||
/// Attributes that affect reading rather than writing, such as <c>JsonRequired</c>, are not part of the
|
||||
/// signature: they cannot change the bytes that were hashed.
|
||||
/// </remarks>
|
||||
/// <param name="signature">The expected shape signature, reported by MWAIS0011 whenever it changes.</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class CanonicalJsonShapeAttribute(string signature) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expected shape signature.
|
||||
/// </summary>
|
||||
public string Signature { get; } = signature;
|
||||
}
|
||||
@@ -36,4 +36,5 @@ public enum Components
|
||||
AGENT_RETRIEVAL_CONTEXT_VALIDATION,
|
||||
AGENT_ASSISTANT_PLUGIN_AUDIT,
|
||||
LOG_VIEWER_ASSISTANT,
|
||||
VISUAL_BRIEFING_ASSISTANT,
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
@@ -8,7 +10,53 @@ namespace AIStudio.Tools;
|
||||
public static class ComponentsExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the preview feature a component belongs to. Components that are generally available
|
||||
/// return <see cref="PreviewFeatures.NONE"/>. This is the single place that maps a component to
|
||||
/// its preview feature, so visibility checks never need to special-case one assistant.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to look up.</param>
|
||||
/// <returns>The required preview feature.</returns>
|
||||
public static PreviewFeatures RequiredPreviewFeature(this Components component) => component switch
|
||||
{
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
|
||||
|
||||
_ => PreviewFeatures.NONE,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a component owns exactly one assistant session slot, so that a running session
|
||||
/// blocks starting another one and inactive sessions can be cleared as a group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Components return <c>false</c> for two different reasons. The chat has no assistant sessions
|
||||
/// at all. The visual briefing assistant keys its sessions per briefing, so it owns one slot per
|
||||
/// stored briefing rather than one per component. Both must be excluded from the single-slot
|
||||
/// checks, which is why this is a capability and not a component comparison.
|
||||
/// </remarks>
|
||||
/// <param name="component">The component to look up.</param>
|
||||
/// <returns><c>true</c> when the component owns exactly one session slot.</returns>
|
||||
public static bool HasSingleSessionSlot(this Components component) => component switch
|
||||
{
|
||||
Components.CHAT => false,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of media-import owner a component creates for its attachments.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to look up.</param>
|
||||
/// <returns>The media-import owner kind.</returns>
|
||||
public static MediaImportOwnerKind MediaOwnerKind(this Components component) => component switch
|
||||
{
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
|
||||
|
||||
_ => MediaImportOwnerKind.ASSISTANT,
|
||||
};
|
||||
|
||||
public static bool AllowSendTo(this Components component) => component switch
|
||||
{
|
||||
Components.NONE => false,
|
||||
@@ -50,6 +98,7 @@ public static class ComponentsExtensions
|
||||
Components.I18N_ASSISTANT => TB("Localization Assistant"),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Planner Assistant"),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"),
|
||||
Components.META_ASSISTANT => TB("Assistant Builder"),
|
||||
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
|
||||
|
||||
@@ -75,6 +124,7 @@ public static class ComponentsExtensions
|
||||
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
|
||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS),
|
||||
Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER),
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING),
|
||||
|
||||
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
|
||||
|
||||
@@ -99,6 +149,7 @@ public static class ComponentsExtensions
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
|
||||
|
||||
// The minimum confidence for the Document Analysis Assistant is set per policy.
|
||||
// We do this inside the Document Analysis Assistant component:
|
||||
@@ -129,6 +180,7 @@ public static class ComponentsExtensions
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null,
|
||||
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected provider at the component level.
|
||||
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
|
||||
@@ -159,6 +211,7 @@ public static class ComponentsExtensions
|
||||
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty,
|
||||
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty,
|
||||
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty,
|
||||
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile,
|
||||
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty,
|
||||
|
||||
// The Document Analysis Assistant does not have a preselected profile at the component level.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct DataInfoMessage(string Icon, string Message)
|
||||
{
|
||||
public void Show(ISnackbar snackbar)
|
||||
{
|
||||
var icon = this.Icon;
|
||||
snackbar.Add(this.Message, Severity.Info, config =>
|
||||
{
|
||||
config.Icon = icon;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 10_000;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,11 @@ public enum Event
|
||||
/// </summary>
|
||||
SHOW_SUCCESS,
|
||||
|
||||
/// <summary>
|
||||
/// Requests display of an informational notification.
|
||||
/// </summary>
|
||||
SHOW_INFO,
|
||||
|
||||
/// <summary>
|
||||
/// Carries an event received from the Tauri runtime.
|
||||
/// </summary>
|
||||
@@ -302,5 +307,10 @@ public enum Event
|
||||
/// <summary>
|
||||
/// Sends content to the slide builder assistant.
|
||||
/// </summary>
|
||||
SEND_TO_SLIDE_BUILDER_ASSISTANT
|
||||
SEND_TO_SLIDE_BUILDER_ASSISTANT,
|
||||
|
||||
/// <summary>
|
||||
/// Sends content to the Visual Briefing Assistant.
|
||||
/// </summary>
|
||||
SEND_TO_VISUAL_BRIEFING_ASSISTANT
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Persists a completed media transcript for a feature-specific owner.
|
||||
/// </summary>
|
||||
public interface IMediaTranscriptStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this storage handles the specified media-import owner.
|
||||
/// </summary>
|
||||
/// <param name="owner">The feature-specific media-import owner.</param>
|
||||
/// <returns><see langword="true"/> when the transcript can be stored.</returns>
|
||||
bool CanStore(MediaImportOwner owner);
|
||||
|
||||
/// <summary>
|
||||
/// Persists a completed transcript and returns the attachment exposed to the calling workflow.
|
||||
/// </summary>
|
||||
/// <param name="target">The stable media-import target.</param>
|
||||
/// <param name="originalMediaPath">The original media path.</param>
|
||||
/// <param name="transcript">The completed transcript text.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The attachment representing the persisted transcript.</returns>
|
||||
Task<FileAttachment> StoreAsync(
|
||||
MediaImportTarget target,
|
||||
string originalMediaPath,
|
||||
string transcript,
|
||||
CancellationToken token);
|
||||
}
|
||||
@@ -8,4 +8,11 @@ public readonly record struct MediaImportOwner(MediaImportOwnerKind Kind, string
|
||||
public static MediaImportOwner ForChat(Guid chatId) => new(MediaImportOwnerKind.CHAT, chatId.ToString("N"));
|
||||
|
||||
public static MediaImportOwner ForAssistant(AssistantSessionKey key) => new(MediaImportOwnerKind.ASSISTANT, key.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a persistent media-import owner for a visual briefing.
|
||||
/// </summary>
|
||||
/// <param name="briefingId">The stable briefing identifier.</param>
|
||||
/// <returns>The media-import owner.</returns>
|
||||
public static MediaImportOwner ForVisualBriefing(Guid briefingId) => new(MediaImportOwnerKind.VISUAL_BRIEFING, briefingId.ToString("D"));
|
||||
}
|
||||
@@ -5,4 +5,17 @@ public enum MediaImportOwnerKind
|
||||
{
|
||||
CHAT,
|
||||
ASSISTANT,
|
||||
|
||||
/// <summary>
|
||||
/// Identifies persistent media transcripts owned by a visual briefing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A visual briefing cannot use <see cref="ASSISTANT"/>: that kind is keyed by an assistant
|
||||
/// session, which ends when the user navigates away or closes the app. A briefing is a stored
|
||||
/// document that outlives both, and its transcripts are stored next to it. The owner is
|
||||
/// therefore keyed by the briefing ID, see <see cref="MediaImportOwner.ForVisualBriefing"/>.
|
||||
/// This is what lets AI Studio re-associate transcripts with the right briefing after a
|
||||
/// restart, and what lets the UI show a running import on the briefing it belongs to.
|
||||
/// </remarks>
|
||||
VISUAL_BRIEFING,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>Capabilities of a media-import owner kind.</summary>
|
||||
public static class MediaImportOwnerKindExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the owner stores its own source list and transcripts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Owners that persist their own sources take the attached media over immediately and keep it
|
||||
/// next to the stored document, see <see cref="MediaImportOwnerKind.VISUAL_BRIEFING"/>. The
|
||||
/// attachment control must therefore neither wait for the transcription to finish before showing
|
||||
/// the file, nor deliver the completed transcripts back into its own list afterwards, because
|
||||
/// the owner already holds them. All other owners rely on that delivery instead.
|
||||
/// </remarks>
|
||||
/// <param name="kind">The owner kind to look up.</param>
|
||||
/// <returns><c>true</c> when the owner persists its own sources.</returns>
|
||||
public static bool PersistsOwnSources(this MediaImportOwnerKind kind) => kind is MediaImportOwnerKind.VISUAL_BRIEFING;
|
||||
}
|
||||
@@ -91,6 +91,8 @@ public sealed class MessageBus
|
||||
|
||||
public Task SendSuccess(DataSuccessMessage dataSuccessMessage) => this.SendMessage(null, Event.SHOW_SUCCESS, dataSuccessMessage);
|
||||
|
||||
public Task SendInfo(DataInfoMessage dataInfoMessage) => this.SendMessage(null, Event.SHOW_INFO, dataInfoMessage);
|
||||
|
||||
public void DeferMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data = default)
|
||||
{
|
||||
if (this.deferredMessages.TryGetValue(triggeredEvent, out var queue))
|
||||
|
||||
@@ -31,6 +31,11 @@ public static class FileTypes
|
||||
public static readonly FileTypeFilter LUA = FileTypeFilter.Leaf("Lua", "lua");
|
||||
public static readonly FileTypeFilter PHP = FileTypeFilter.Leaf("PHP", "php");
|
||||
public static readonly FileTypeFilter WEB = FileTypeFilter.Leaf("HTML/CSS", "html", "css");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the standalone HTML filter used for visual briefing import and export.
|
||||
/// </summary>
|
||||
public static readonly FileTypeFilter VISUAL_BRIEFING_HTML = FileTypeFilter.Leaf(TB("Visual briefing"), "html");
|
||||
public static readonly FileTypeFilter APP = FileTypeFilter.Leaf("Swift/Kotlin", "swift", "kt");
|
||||
public static readonly FileTypeFilter SHELL = FileTypeFilter.Leaf("Shell", "sh", "bash", "zsh");
|
||||
public static readonly FileTypeFilter LOG = FileTypeFilter.Leaf("Log", "log");
|
||||
@@ -60,6 +65,12 @@ public static class FileTypes
|
||||
// Media hierarchy
|
||||
public static readonly FileTypeFilter IMAGE = FileTypeFilter.Leaf(TB("Image"),
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "webp", "heic");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prototype visual-asset image formats.
|
||||
/// </summary>
|
||||
public static readonly FileTypeFilter VISUAL_BRIEFING_IMAGE = FileTypeFilter.Leaf(TB("Visual briefing image"),
|
||||
"jpg", "jpeg", "png", "webp");
|
||||
public static readonly FileTypeFilter AUDIO = FileTypeFilter.Leaf(TB("Audio"),
|
||||
"mp3", "wav", "wave", "aac", "flac", "ogg", "opus", "m4a", "m4b", "wma", "alac", "aif", "aiff", "caf");
|
||||
public static readonly FileTypeFilter VIDEO = FileTypeFilter.Leaf(TB("Video"),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Contains a locally prepared image.
|
||||
/// </summary>
|
||||
/// <param name="DataUrl">The prepared image as a Data URL.</param>
|
||||
/// <param name="MimeType">The preserved supported image MIME type.</param>
|
||||
/// <param name="Width">The prepared pixel width.</param>
|
||||
/// <param name="Height">The prepared pixel height.</param>
|
||||
/// <param name="WasResized">Whether the maximum-edge policy resized the image.</param>
|
||||
public sealed record ImagePrepareResponse(
|
||||
string DataUrl,
|
||||
string MimeType,
|
||||
uint Width,
|
||||
uint Height,
|
||||
bool WasResized);
|
||||
@@ -6,15 +6,13 @@ namespace AIStudio.Tools.Services;
|
||||
/// Wire up the clipboard service to copy Markdown to the clipboard.
|
||||
/// We use our own Rust-based clipboard service for this.
|
||||
/// </summary>
|
||||
public sealed class MarkdownClipboardService(RustService rust, ISnackbar snackbar) : IMudMarkdownClipboardService
|
||||
public sealed class MarkdownClipboardService(RustService rust) : IMudMarkdownClipboardService
|
||||
{
|
||||
private ISnackbar Snackbar { get; } = snackbar;
|
||||
|
||||
private RustService Rust { get; } = rust;
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when the user wants to copy the Markdown to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="text">The Markdown text to copy.</param>
|
||||
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.Snackbar, text);
|
||||
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(text);
|
||||
}
|
||||
@@ -10,7 +10,11 @@ namespace AIStudio.Tools.Services;
|
||||
/// <summary>
|
||||
/// Coordinates serialized visible media imports and independent voice transcriptions.
|
||||
/// </summary>
|
||||
public sealed class MediaTranscriptionService(RustService rustService, SettingsManager settingsManager, ILogger<MediaTranscriptionService> logger) : IDisposable
|
||||
public sealed class MediaTranscriptionService(
|
||||
RustService rustService,
|
||||
SettingsManager settingsManager,
|
||||
IEnumerable<IMediaTranscriptStorage> transcriptStorages,
|
||||
ILogger<MediaTranscriptionService> logger) : IDisposable
|
||||
{
|
||||
private const string NORMALIZED_OUTPUT_EXTENSION = ".webm";
|
||||
private const string NORMALIZED_OUTPUT_FORMAT = "webm";
|
||||
@@ -294,12 +298,18 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
continue;
|
||||
}
|
||||
|
||||
var isPersistedChat = ownerChat is not null && WorkspaceBehaviour.IsChatExisting(new LoadChat(ownerChat.WorkspaceId, ownerChat.ChatId));
|
||||
var attachment = isPersistedChat
|
||||
? await WorkspaceBehaviour.CreateManagedTranscriptAsync(ownerChat!, mediaPath, result.Text)
|
||||
: await ManagedTranscriptAttachment.CreateStagedAsync(mediaPath, result.Text);
|
||||
var persistentStorage = transcriptStorages.FirstOrDefault(storage => storage.CanStore(target.Owner));
|
||||
var isPersistedChat = persistentStorage is null &&
|
||||
ownerChat is not null &&
|
||||
WorkspaceBehaviour.IsChatExisting(new LoadChat(ownerChat.WorkspaceId, ownerChat.ChatId));
|
||||
|
||||
var attachment = persistentStorage is not null
|
||||
? await persistentStorage.StoreAsync(target, mediaPath, result.Text, batchToken)
|
||||
: isPersistedChat
|
||||
? await WorkspaceBehaviour.CreateManagedTranscriptAsync(ownerChat!, mediaPath, result.Text)
|
||||
: await ManagedTranscriptAttachment.CreateStagedAsync(mediaPath, result.Text);
|
||||
|
||||
if (ownerChat is not null && attachment is { } managed
|
||||
if (persistentStorage is null && ownerChat is not null && attachment is ManagedTranscriptAttachment managed
|
||||
&& ownerChat.PendingMediaTranscripts.All(existing => existing.FilePath != managed.FilePath))
|
||||
ownerChat.PendingMediaTranscripts.Add(managed);
|
||||
|
||||
@@ -443,6 +453,11 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
var normalizedPath = Path.Combine(Path.GetTempPath(), "mindwork-ai-studio-media", $"{operation.Id:N}.webm");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(normalizedPath)!);
|
||||
|
||||
// Logged next to the operation ID: users recognize the file they picked, whereas an ID only
|
||||
// helps when correlating log lines. The name alone is enough and keeps full paths out of
|
||||
// logs that get shared in bug reports.
|
||||
var fileName = Path.GetFileName(mediaPath);
|
||||
|
||||
try
|
||||
{
|
||||
var normalized = await this.NormalizeAsync(mediaPath, normalizedPath, operation, updateImportState);
|
||||
@@ -454,13 +469,20 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
var uploadContractError = await ValidateNormalizedProviderUploadAsync(normalized.Result, normalizedPath, operation.Cancellation.Token);
|
||||
if (uploadContractError is not null)
|
||||
{
|
||||
logger.LogError("Refusing the transcription provider upload because the normalized media contract validation failed: {Diagnostic}", uploadContractError);
|
||||
logger.LogError(
|
||||
"Refusing the transcription provider upload for '{FileName}' (operation {OperationId}) because the normalized media contract validation failed: {Diagnostic}",
|
||||
fileName,
|
||||
operation.Id,
|
||||
uploadContractError);
|
||||
return MediaTranscriptionResult.Failed(TB("The media pipeline ended without an output file."));
|
||||
}
|
||||
|
||||
if (!normalized.Result.HasAudibleSignal)
|
||||
{
|
||||
logger.LogInformation("Skipping transcription for '{MediaPath}' because its maximum audio peak does not exceed the practical-silence threshold.", mediaPath);
|
||||
logger.LogInformation(
|
||||
"Skipping media transcription for '{FileName}' (operation {OperationId}) because its maximum audio peak does not exceed the practical-silence threshold.",
|
||||
fileName,
|
||||
operation.Id);
|
||||
return MediaTranscriptionResult.NoAudibleSignal(TB("The audio track contains no audible signal, so there is nothing to transcribe."));
|
||||
}
|
||||
|
||||
@@ -480,10 +502,10 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
var reductionPercent = sourceSize > 0
|
||||
? (1.0 - (double)normalizedSize / sourceSize) * 100.0
|
||||
: 0.0;
|
||||
logger.LogInformation("Transcribing normalized WebM/Opus media '{NormalizedPath}' ({NormalizedSize} bytes; source '{SourcePath}' {SourceSize} bytes; size reduction {ReductionPercent:F1}%) with provider '{Provider}' and model '{Model}'.",
|
||||
normalizedPath,
|
||||
logger.LogInformation("Transcribing normalized WebM/Opus media '{FileName}' for operation {OperationId} ({NormalizedSize} bytes; source {SourceSize} bytes; size reduction {ReductionPercent:F1}%) with provider '{Provider}' and model '{Model}'.",
|
||||
fileName,
|
||||
operation.Id,
|
||||
normalizedSize,
|
||||
mediaPath,
|
||||
sourceSize,
|
||||
reductionPercent,
|
||||
providerSettings.UsedLLMProvider,
|
||||
@@ -493,7 +515,11 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
operation.Cancellation.Token.ThrowIfCancellationRequested();
|
||||
if (!providerResult.Success)
|
||||
{
|
||||
logger.LogWarning("The transcription provider failed for '{MediaPath}': {Diagnostic}", mediaPath, providerResult.ErrorMessage);
|
||||
logger.LogWarning(
|
||||
"The transcription provider failed for '{FileName}' (operation {OperationId}): {Diagnostic}",
|
||||
fileName,
|
||||
operation.Id,
|
||||
providerResult.ErrorMessage);
|
||||
return MediaTranscriptionResult.Failed(TB("The transcription provider could not transcribe the media file."));
|
||||
}
|
||||
|
||||
@@ -505,7 +531,11 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Media transcription failed for '{MediaPath}'.", mediaPath);
|
||||
logger.LogError(
|
||||
"Media transcription failed for '{FileName}' (operation {OperationId}). ExceptionType={ExceptionType}",
|
||||
fileName,
|
||||
operation.Id,
|
||||
exception.GetType().Name);
|
||||
return MediaTranscriptionResult.Failed(TB("The media file could not be transcribed."));
|
||||
}
|
||||
finally
|
||||
@@ -622,7 +652,11 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
|
||||
case MediaJobPhase.FAILED:
|
||||
if (mediaEvent.Error is not null)
|
||||
logger.LogWarning("Rust media normalization failed for '{MediaPath}' with {Code}: {Diagnostic}", mediaPath, mediaEvent.Error.Code, mediaEvent.Error.Message);
|
||||
logger.LogWarning(
|
||||
"Rust media normalization failed for operation {OperationId} with {Code}: {Diagnostic}",
|
||||
operation.Id,
|
||||
mediaEvent.Error.Code,
|
||||
mediaEvent.Error.Message);
|
||||
|
||||
return (null, mediaEvent.Error);
|
||||
|
||||
@@ -785,7 +819,9 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not delete operation-owned temporary media file '{Path}'.", path);
|
||||
logger.LogWarning(
|
||||
"Could not delete an operation-owned temporary media file. ExceptionType={ExceptionType}",
|
||||
exception.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -807,12 +843,15 @@ public sealed class MediaTranscriptionService(RustService rustService, SettingsM
|
||||
foreach (var oldPath in new DirectoryInfo(diagnosticDirectory).EnumerateFiles("*.webm").OrderByDescending(file => file.LastWriteTimeUtc).Skip(10))
|
||||
oldPath.Delete();
|
||||
|
||||
logger.LogInformation("Retained normalized media diagnostic '{DiagnosticPath}'.", diagnosticPath);
|
||||
logger.LogInformation("Retained normalized media diagnostic for operation {OperationId}.", operationId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not retain normalized media diagnostic for operation '{OperationId}'.", operationId);
|
||||
logger.LogWarning(
|
||||
"Could not retain normalized media diagnostic for operation {OperationId}. ExceptionType={ExceptionType}",
|
||||
operationId,
|
||||
exception.GetType().Name);
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
|
||||
@@ -7,13 +7,16 @@ public sealed partial class RustService
|
||||
/// <summary>
|
||||
/// Tries to copy the given text to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||
/// <remarks>
|
||||
/// The outcome is reported through the message bus. Callers used to hand in their snackbar, which
|
||||
/// was the reason most components injected one at all, and it meant this notification was styled
|
||||
/// here instead of together with every other notification of the app.
|
||||
/// </remarks>
|
||||
/// <param name="text">The text to copy to the clipboard.</param>
|
||||
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||
public async Task CopyText2Clipboard(string text)
|
||||
{
|
||||
var message = TB("Successfully copied the text to your clipboard");
|
||||
var iconColor = Color.Error;
|
||||
var severity = Severity.Error;
|
||||
var succeeded = false;
|
||||
try
|
||||
{
|
||||
var encryptedText = await text.Encrypt(this.encryptor!);
|
||||
@@ -32,19 +35,16 @@ public sealed partial class RustService
|
||||
message = TB("Failed to copy the text to your clipboard.");
|
||||
return;
|
||||
}
|
||||
|
||||
iconColor = Color.Success;
|
||||
severity = Severity.Success;
|
||||
|
||||
succeeded = true;
|
||||
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
snackbar.Add(message, severity, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = iconColor;
|
||||
});
|
||||
if (succeeded)
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.ContentCopy, message));
|
||||
else
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ContentCopy, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates and optionally optimizes a local image in the Rust runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The runtime rejects files whose content does not match their extension, so the returned MIME
|
||||
/// type always describes the actual bytes.
|
||||
/// </remarks>
|
||||
/// <param name="path">The absolute path of a PNG, JPEG, or WebP image.</param>
|
||||
/// <param name="optimize">Whether the maximum-edge policy and re-encoding are applied.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The prepared image, dimensions, and stable MIME type.</returns>
|
||||
public async Task<ImagePrepareResponse> PrepareImageAsync(
|
||||
string path,
|
||||
bool optimize,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await this.http.PostAsJsonAsync("/image/prepare", new { path, optimize }, this.jsonRustSerializerOptions, token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ImagePrepareResponse>(this.jsonRustSerializerOptions, token)
|
||||
?? throw new InvalidDataException("The Rust image preparation returned an empty response.");
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(UpdateService).Namespace, nameof(UpdateService));
|
||||
|
||||
private static bool IS_INITIALIZED;
|
||||
private static ISnackbar? SNACKBAR;
|
||||
|
||||
|
||||
private readonly SettingsManager settingsManager;
|
||||
private readonly MessageBus messageBus;
|
||||
private readonly RustService rust;
|
||||
@@ -101,12 +100,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
|
||||
if (notifyUserWhenNoUpdate)
|
||||
{
|
||||
SNACKBAR!.Add(TB("Failed to check for updates. Please try again later."), Severity.Error, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Error;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = Color.Error;
|
||||
});
|
||||
await this.messageBus.SendError(new(Icons.Material.Filled.Error, TB("Failed to check for updates. Please try again later.")));
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -133,12 +127,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
SNACKBAR!.Add(TB("Failed to install update automatically. Please try again manually."), Severity.Error, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Error;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = Color.Error;
|
||||
});
|
||||
await this.messageBus.SendError(new(Icons.Material.Filled.Error, TB("Failed to install update automatically. Please try again manually.")));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -148,12 +137,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
{
|
||||
if (notifyUserWhenNoUpdate)
|
||||
{
|
||||
SNACKBAR!.Add(TB("No update found."), Severity.Normal, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Update;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = Color.Primary;
|
||||
});
|
||||
await this.messageBus.SendInfo(new(Icons.Material.Filled.Update, TB("No update found.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,9 +152,8 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
_ => Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
public static void SetBlazorDependencies(ISnackbar snackbar)
|
||||
{
|
||||
SNACKBAR = snackbar;
|
||||
IS_INITIALIZED = true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Signals that the Blazor UI is ready, so queued update notifications can be shown.
|
||||
/// </summary>
|
||||
public static void MarkBlazorReady() => IS_INITIALIZED = true;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
@@ -59,7 +58,6 @@ public static class FileExtensionValidation
|
||||
return false;
|
||||
}
|
||||
|
||||
var capabilities = provider?.GetModelCapabilities() ?? new();
|
||||
if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE))
|
||||
{
|
||||
switch (useCae)
|
||||
@@ -76,8 +74,7 @@ public static class FileExtensionValidation
|
||||
return true;
|
||||
|
||||
// In this use case, we can check the provider capabilities:
|
||||
case UseCase.ATTACHING_CONTENT when capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) ||
|
||||
capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT):
|
||||
case UseCase.ATTACHING_CONTENT when provider?.SupportsImageInput() is true:
|
||||
return true;
|
||||
|
||||
// We know that images are not supported:
|
||||
|
||||
Reference in new issue
Block a user