diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor index 59845188..1c6a24d1 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -423,8 +423,8 @@ else var format = datePicker.GetDateFormat(); - - - private readonly HashSet executingButtonActions = []; private readonly HashSet executingSwitchActions = []; private string pluginPath = string.Empty; - private const string PLUGIN_SCHEME = "plugin://"; private const string ASSISTANT_QUERY_KEY = "assistantId"; - private static readonly CultureInfo INVARIANT_CULTURE = CultureInfo.InvariantCulture; - private static readonly string[] FALLBACK_DATE_FORMATS = ["yyyy-MM-dd", "dd.MM.yyyy", "MM/dd/yyyy"]; - private static readonly string[] FALLBACK_TIME_FORMATS = ["HH:mm", "HH:mm:ss", "hh:mm tt", "h:mm tt"]; - + + #region Implementation of AssistantBase + protected override void OnInitialized() { - var assistantPlugin = this.ResolveAssistantPlugin(); - if (assistantPlugin is null) + var pluginAssistant = this.ResolveAssistantPlugin(); + if (pluginAssistant is null) { this.Logger.LogWarning("AssistantDynamic could not resolve a registered assistant plugin."); base.OnInitialized(); return; } - this.assistantPlugin = assistantPlugin; - this.RootComponent = assistantPlugin.RootComponent; - this.title = assistantPlugin.AssistantTitle; - this.description = assistantPlugin.AssistantDescription; - this.systemPrompt = assistantPlugin.SystemPrompt; - this.submitText = assistantPlugin.SubmitText; - this.allowProfiles = assistantPlugin.AllowProfiles; - this.showFooterProfileSelection = !assistantPlugin.HasEmbeddedProfileSelection; - this.pluginPath = assistantPlugin.PluginPath; + this.assistantPlugin = pluginAssistant; + this.RootComponent = pluginAssistant.RootComponent; + this.title = pluginAssistant.AssistantTitle; + this.description = pluginAssistant.AssistantDescription; + this.systemPrompt = pluginAssistant.SystemPrompt; + this.submitText = pluginAssistant.SubmitText; + this.allowProfiles = pluginAssistant.AllowProfiles; + this.showFooterProfileSelection = !pluginAssistant.HasEmbeddedProfileSelection; + this.pluginPath = pluginAssistant.PluginPath; var rootComponent = this.RootComponent; if (rootComponent is not null) @@ -74,20 +71,41 @@ public partial class AssistantDynamic : AssistantBaseCore base.OnInitialized(); } + + protected override void ResetForm() + { + this.assistantState.Clear(); + + var rootComponent = this.RootComponent; + if (rootComponent is not null) + this.InitializeComponentState(rootComponent.Children); + } + + protected override bool MightPreselectValues() + { + // Dynamic assistants have arbitrary fields supplied via plugins, so there + // isn't a built-in settings section to prefill values. Always return + // false to keep the plugin-specified defaults. + return false; + } + + #endregion + + #region Implementation of dynamic plugin init private PluginAssistants? ResolveAssistantPlugin() { - var assistantPlugins = PluginFactory.RunningPlugins.OfType() + var pluginAssistants = PluginFactory.RunningPlugins.OfType() .Where(plugin => this.SettingsManager.IsPluginEnabled(plugin)) .ToList(); - if (assistantPlugins.Count == 0) + if (pluginAssistants.Count == 0) return null; var requestedPluginId = this.TryGetAssistantIdFromQuery(); - if (requestedPluginId is not { } id) return assistantPlugins.First(); + if (requestedPluginId is not { } id) return pluginAssistants.First(); - var requestedPlugin = assistantPlugins.FirstOrDefault(p => p.Id == id); - return requestedPlugin ?? assistantPlugins.First(); + var requestedPlugin = pluginAssistants.FirstOrDefault(p => p.Id == id); + return requestedPlugin ?? pluginAssistants.First(); } private Guid? TryGetAssistantIdFromQuery() @@ -111,22 +129,7 @@ public partial class AssistantDynamic : AssistantBaseCore return null; } - protected override void ResetForm() - { - this.assistantState.Clear(); - - var rootComponent = this.RootComponent; - if (rootComponent is not null) - this.InitializeComponentState(rootComponent.Children); - } - - protected override bool MightPreselectValues() - { - // Dynamic assistants have arbitrary fields supplied via plugins, so there - // isn't a built-in settings section to prefill values. Always return - // false to keep the plugin-specified defaults. - return false; - } + #endregion private string ResolveImageSource(AssistantImage image) { @@ -136,53 +139,11 @@ public partial class AssistantDynamic : AssistantBaseCore if (this.imageCache.TryGetValue(image.Src, out var cached) && !string.IsNullOrWhiteSpace(cached)) return cached; - var resolved = image.Src; - - if (resolved.StartsWith(PLUGIN_SCHEME, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(this.pluginPath)) - { - var relative = resolved[PLUGIN_SCHEME.Length..].TrimStart('/', '\\').Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); - var filePath = Path.Join(this.pluginPath, relative); - if (File.Exists(filePath)) - { - var mime = GetImageMimeType(filePath); - var data = Convert.ToBase64String(File.ReadAllBytes(filePath)); - resolved = $"data:{mime};base64,{data}"; - } - else - { - resolved = string.Empty; - } - } - else if (Uri.TryCreate(resolved, UriKind.Absolute, out var uri)) - { - if (uri.Scheme is not ("http" or "https" or "data")) - resolved = string.Empty; - } - else - { - resolved = string.Empty; - } - + var resolved = image.ResolveSource(this.pluginPath); this.imageCache[image.Src] = resolved; return resolved; } - private static string GetImageMimeType(string path) - { - var extension = Path.GetExtension(path).TrimStart('.').ToLowerInvariant(); - return extension switch - { - "svg" => "image/svg+xml", - "png" => "image/png", - "jpg" => "image/jpeg", - "jpeg" => "image/jpeg", - "gif" => "image/gif", - "webp" => "image/webp", - "bmp" => "image/bmp", - _ => "image/png", - }; - } - private async Task CollectUserPromptAsync() { if (this.assistantPlugin?.HasCustomPromptBuilder != true) return this.CollectUserPromptFallback(); @@ -216,10 +177,7 @@ public partial class AssistantDynamic : AssistantBaseCore { var prompt = string.Empty; var rootComponent = this.RootComponent; - if (rootComponent is null) - return prompt; - - return this.CollectUserPromptFallback(rootComponent.Children); + return rootComponent is null ? prompt : this.CollectUserPromptFallback(rootComponent.Children); } private void InitializeComponentState(IEnumerable components) @@ -236,15 +194,12 @@ public partial class AssistantDynamic : AssistantBaseCore private static string MergeClass(string customClass, string fallback) { - var trimmedCustom = customClass?.Trim() ?? string.Empty; - var trimmedFallback = fallback?.Trim() ?? string.Empty; + var trimmedCustom = customClass.Trim(); + var trimmedFallback = fallback.Trim(); if (string.IsNullOrEmpty(trimmedCustom)) return trimmedFallback; - if (string.IsNullOrEmpty(trimmedFallback)) - return trimmedCustom; - - return $"{trimmedCustom} {trimmedFallback}"; + return string.IsNullOrEmpty(trimmedFallback) ? trimmedCustom : $"{trimmedCustom} {trimmedFallback}"; } private string? GetOptionalStyle(string? style) => string.IsNullOrWhiteSpace(style) ? null : style; @@ -423,7 +378,7 @@ public partial class AssistantDynamic : AssistantBaseCore private string? ValidateProfileSelection(AssistantProfileSelection profileSelection, Profile? profile) { - if (profile != default && profile != Profile.NO_PROFILE) return null; + if (profile != null && profile != Profile.NO_PROFILE) return null; return !string.IsNullOrWhiteSpace(profileSelection.ValidationMessage) ? profileSelection.ValidationMessage : this.T("Please select one of your profiles."); } @@ -451,124 +406,4 @@ public partial class AssistantDynamic : AssistantBaseCore return prompt.ToString(); } - - private DateTime? ParseDatePickerValue(string? value, string? format) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - if (TryParseDate(value, format, out var parsedDate)) - return parsedDate; - - return null; - } - - private void SetDatePickerValue(string fieldName, DateTime? value, string? format) - { - this.assistantState.Dates[fieldName] = value.HasValue ? FormatDate(value.Value, format) : string.Empty; - } - - private DateRange? ParseDateRangePickerValue(string? value, string? format) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - var parts = value.Split(" - ", 2, StringSplitOptions.TrimEntries); - if (parts.Length != 2) - return null; - - if (!TryParseDate(parts[0], format, out var start) || !TryParseDate(parts[1], format, out var end)) - return null; - - return new DateRange(start, end); - } - - private void SetDateRangePickerValue(string fieldName, DateRange? value, string? format) - { - if (value?.Start is null || value.End is null) - { - this.assistantState.DateRanges[fieldName] = string.Empty; - return; - } - - this.assistantState.DateRanges[fieldName] = $"{FormatDate(value.Start.Value, format)} - {FormatDate(value.End.Value, format)}"; - } - - private TimeSpan? ParseTimePickerValue(string? value, string? format) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - if (TryParseTime(value, format, out var parsedTime)) - return parsedTime; - - return null; - } - - private void SetTimePickerValue(string fieldName, TimeSpan? value, string? format) - { - this.assistantState.Times[fieldName] = value.HasValue ? FormatTime(value.Value, format) : string.Empty; - } - - private static bool TryParseDate(string value, string? format, out DateTime parsedDate) - { - if (!string.IsNullOrWhiteSpace(format) && - DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) - { - return true; - } - - if (DateTime.TryParseExact(value, FALLBACK_DATE_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) - return true; - - return DateTime.TryParse(value, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate); - } - - private static bool TryParseTime(string value, string? format, out TimeSpan parsedTime) - { - if (!string.IsNullOrWhiteSpace(format) && - DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out var dateTime)) - { - parsedTime = dateTime.TimeOfDay; - return true; - } - - if (DateTime.TryParseExact(value, FALLBACK_TIME_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out dateTime)) - { - parsedTime = dateTime.TimeOfDay; - return true; - } - - if (TimeSpan.TryParse(value, INVARIANT_CULTURE, out parsedTime)) - return true; - - parsedTime = default; - return false; - } - - private static string FormatDate(DateTime value, string? format) - { - try - { - return value.ToString(string.IsNullOrWhiteSpace(format) ? "yyyy-MM-dd" : format, INVARIANT_CULTURE); - } - catch (FormatException) - { - return value.ToString("yyyy-MM-dd", INVARIANT_CULTURE); - } - } - - private static string FormatTime(TimeSpan value, string? format) - { - var dateTime = DateTime.Today.Add(value); - - try - { - return dateTime.ToString(string.IsNullOrWhiteSpace(format) ? "HH:mm" : format, INVARIANT_CULTURE); - } - catch (FormatException) - { - return dateTime.ToString("HH:mm", INVARIANT_CULTURE); - } - } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs index 910ac218..33bc81a4 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs @@ -70,10 +70,8 @@ internal sealed class AssistantColorPicker : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - if (state.Colors.TryGetValue(this.Name, out userInput) && !string.IsNullOrWhiteSpace(userInput)) + if (state.Colors.TryGetValue(this.Name, out var userInput) && !string.IsNullOrWhiteSpace(userInput)) promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; return promptFragment; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs index 003392c6..0f4e4a79 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs @@ -1,7 +1,12 @@ +using System.Globalization; + namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; internal sealed class AssistantDatePicker : StatefulAssistantComponentBase { + private static readonly CultureInfo INVARIANT_CULTURE = CultureInfo.InvariantCulture; + private static readonly string[] FALLBACK_DATE_FORMATS = ["dd.MM.yyyy", "yyyy-MM-dd", "MM/dd/yyyy"]; + public override AssistantComponentType Type => AssistantComponentType.DATE_PICKER; public override Dictionary Props { get; set; } = new(); public override List Children { get; set; } = new(); @@ -76,10 +81,8 @@ internal sealed class AssistantDatePicker : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - if (state.Dates.TryGetValue(this.Name, out userInput) && !string.IsNullOrWhiteSpace(userInput)) + if (state.Dates.TryGetValue(this.Name, out var userInput) && !string.IsNullOrWhiteSpace(userInput)) promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; return promptFragment; @@ -88,4 +91,38 @@ internal sealed class AssistantDatePicker : StatefulAssistantComponentBase #endregion public string GetDateFormat() => string.IsNullOrWhiteSpace(this.DateFormat) ? "yyyy-MM-dd" : this.DateFormat; + + public DateTime? ParseValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + return TryParseDate(value, this.GetDateFormat(), out var parsedDate) ? parsedDate : null; + } + + public string FormatValue(DateTime? value) => value.HasValue ? FormatDate(value.Value, this.GetDateFormat()) : string.Empty; + + private static bool TryParseDate(string value, string? format, out DateTime parsedDate) + { + if (!string.IsNullOrWhiteSpace(format) && + DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) + { + return true; + } + + return DateTime.TryParseExact(value, FALLBACK_DATE_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate) || + DateTime.TryParse(value, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate); + } + + private static string FormatDate(DateTime value, string? format) + { + try + { + return value.ToString(string.IsNullOrWhiteSpace(format) ? FALLBACK_DATE_FORMATS[0] : format, INVARIANT_CULTURE); + } + catch (FormatException) + { + return value.ToString(FALLBACK_DATE_FORMATS[0], INVARIANT_CULTURE); + } + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs index d646584f..af070ec5 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs @@ -1,7 +1,13 @@ +using System.Globalization; +using MudBlazor; + namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; internal sealed class AssistantDateRangePicker : StatefulAssistantComponentBase { + private static readonly CultureInfo INVARIANT_CULTURE = CultureInfo.InvariantCulture; + private static readonly string[] FALLBACK_DATE_FORMATS = ["dd.MM.yyyy", "yyyy-MM-dd" , "MM/dd/yyyy"]; + public override AssistantComponentType Type => AssistantComponentType.DATE_RANGE_PICKER; public override Dictionary Props { get; set; } = new(); public override List Children { get; set; } = new(); @@ -82,10 +88,8 @@ internal sealed class AssistantDateRangePicker : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - if (state.DateRanges.TryGetValue(this.Name, out userInput) && !string.IsNullOrWhiteSpace(userInput)) + if (state.DateRanges.TryGetValue(this.Name, out var userInput) && !string.IsNullOrWhiteSpace(userInput)) promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; return promptFragment; @@ -94,4 +98,53 @@ internal sealed class AssistantDateRangePicker : StatefulAssistantComponentBase #endregion public string GetDateFormat() => string.IsNullOrWhiteSpace(this.DateFormat) ? "yyyy-MM-dd" : this.DateFormat; + + public DateRange? ParseValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var format = this.GetDateFormat(); + var parts = value.Split(" - ", 2, StringSplitOptions.TrimEntries); + if (parts.Length != 2) + return null; + + if (!TryParseDate(parts[0], format, out var start) || !TryParseDate(parts[1], format, out var end)) + return null; + + return new DateRange(start, end); + } + + public string FormatValue(DateRange? value) + { + if (value?.Start is null || value.End is null) + return string.Empty; + + var format = this.GetDateFormat(); + return $"{FormatDate(value.Start.Value, format)} - {FormatDate(value.End.Value, format)}"; + } + + private static bool TryParseDate(string value, string? format, out DateTime parsedDate) + { + if (!string.IsNullOrWhiteSpace(format) && + DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) + { + return true; + } + + return DateTime.TryParseExact(value, FALLBACK_DATE_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate) || + DateTime.TryParse(value, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate); + } + + private static string FormatDate(DateTime value, string? format) + { + try + { + return value.ToString(string.IsNullOrWhiteSpace(format) ? FALLBACK_DATE_FORMATS[0] : format, INVARIANT_CULTURE); + } + catch (FormatException) + { + return value.ToString(FALLBACK_DATE_FORMATS[0], INVARIANT_CULTURE); + } + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs index 4c9e35b2..81e713a9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs @@ -112,14 +112,12 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"{Environment.NewLine}context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; if (this.IsMultiselect && state.MultiSelect.TryGetValue(this.Name, out var selections)) { promptFragment += $"user prompt:{Environment.NewLine}{string.Join(Environment.NewLine, selections.OrderBy(static value => value, StringComparer.Ordinal))}"; } - else if (state.SingleSelect.TryGetValue(this.Name, out userInput)) + else if (state.SingleSelect.TryGetValue(this.Name, out var userInput)) { promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs index 3ec9fdf1..793e5074 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs @@ -1,3 +1,4 @@ +using System.Text; using AIStudio.Assistants.Dynamic; namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; @@ -30,15 +31,15 @@ internal sealed class AssistantFileContentReader : StatefulAssistantComponentBas public override string UserPromptFallback(AssistantState state) { - var promptFragment = string.Empty; + var promptFragment = new StringBuilder(); if (state.FileContent.TryGetValue(this.Name, out var fileState)) - promptFragment += $"{Environment.NewLine}context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + promptFragment.Append($"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"); if (!string.IsNullOrWhiteSpace(fileState?.Content)) - promptFragment += $"user prompt:{Environment.NewLine}{fileState.Content}"; + promptFragment.Append($"user prompt:{Environment.NewLine}{fileState.Content}"); - return promptFragment; + return promptFragment.ToString(); } #endregion diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs index 885f4ea0..e07e5376 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs @@ -2,6 +2,8 @@ namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; internal sealed class AssistantImage : AssistantComponentBase { + private const string PLUGIN_SCHEME = "plugin://"; + public override AssistantComponentType Type => AssistantComponentType.IMAGE; public override Dictionary Props { get; set; } = new(); public override List Children { get; set; } = new(); @@ -35,4 +37,48 @@ internal sealed class AssistantImage : AssistantComponentBase get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); } + + public string ResolveSource(string pluginPath) + { + if (string.IsNullOrWhiteSpace(this.Src)) + return string.Empty; + + var resolved = this.Src; + + if (resolved.StartsWith(PLUGIN_SCHEME, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(pluginPath)) + { + var relative = resolved[PLUGIN_SCHEME.Length..] + .TrimStart('/', '\\') + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + var filePath = Path.Join(pluginPath, relative); + if (!File.Exists(filePath)) + return string.Empty; + + var mime = GetImageMimeType(filePath); + var data = Convert.ToBase64String(File.ReadAllBytes(filePath)); + return $"data:{mime};base64,{data}"; + } + + if (!Uri.TryCreate(resolved, UriKind.Absolute, out var uri)) + return string.Empty; + + return uri.Scheme is "http" or "https" or "data" ? resolved : string.Empty; + } + + private static string GetImageMimeType(string path) + { + var extension = Path.GetExtension(path).TrimStart('.').ToLowerInvariant(); + return extension switch + { + "svg" => "image/svg+xml", + "png" => "image/png", + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "gif" => "image/gif", + "webp" => "image/webp", + "bmp" => "image/bmp", + _ => "image/png", + }; + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs index 8b0889de..d20110e1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs @@ -97,10 +97,8 @@ public sealed class AssistantSwitch : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userDecision = false; - var promptFragment = $"{Environment.NewLine}context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - state.Bools.TryGetValue(this.Name, out userDecision); + state.Bools.TryGetValue(this.Name, out var userDecision); promptFragment += $"user decision: {userDecision}"; return promptFragment; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs index 33a1611b..1038d7aa 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs @@ -108,10 +108,8 @@ internal sealed class AssistantTextArea : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - if (state.Text.TryGetValue(this.Name, out userInput) && !string.IsNullOrWhiteSpace(userInput)) + if (state.Text.TryGetValue(this.Name, out var userInput) && !string.IsNullOrWhiteSpace(userInput)) promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; return promptFragment; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs index c65d53ad..f9fe4660 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs @@ -1,7 +1,12 @@ +using System.Globalization; + namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; internal sealed class AssistantTimePicker : StatefulAssistantComponentBase { + private static readonly CultureInfo INVARIANT_CULTURE = CultureInfo.InvariantCulture; + private static readonly string[] FALLBACK_TIME_FORMATS = ["HH:mm", "HH:mm:ss", "hh:mm tt", "h:mm tt"]; + public override AssistantComponentType Type => AssistantComponentType.TIME_PICKER; public override Dictionary Props { get; set; } = new(); public override List Children { get; set; } = new(); @@ -82,10 +87,8 @@ internal sealed class AssistantTimePicker : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var userInput = string.Empty; - var promptFragment = $"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; - if (state.Times.TryGetValue(this.Name, out userInput) && !string.IsNullOrWhiteSpace(userInput)) + if (state.Times.TryGetValue(this.Name, out var userInput) && !string.IsNullOrWhiteSpace(userInput)) promptFragment += $"user prompt:{Environment.NewLine}{userInput}"; return promptFragment; @@ -100,4 +103,45 @@ internal sealed class AssistantTimePicker : StatefulAssistantComponentBase return this.AmPm ? "hh:mm tt" : "HH:mm"; } + + public TimeSpan? ParseValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + return TryParseTime(value, this.GetTimeFormat(), out var parsedTime) ? parsedTime : null; + } + + public string FormatValue(TimeSpan? value) => value.HasValue ? FormatTime(value.Value, this.GetTimeFormat()) : string.Empty; + + private static bool TryParseTime(string value, string? format, out TimeSpan parsedTime) + { + if ((!string.IsNullOrWhiteSpace(format) && + DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out var dateTime)) || + DateTime.TryParseExact(value, FALLBACK_TIME_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out dateTime)) + { + parsedTime = dateTime.TimeOfDay; + return true; + } + + if (TimeSpan.TryParse(value, INVARIANT_CULTURE, out parsedTime)) + return true; + + parsedTime = TimeSpan.Zero; + return false; + } + + private static string FormatTime(TimeSpan value, string? format) + { + var dateTime = DateTime.Today.Add(value); + + try + { + return dateTime.ToString(string.IsNullOrWhiteSpace(format) ? FALLBACK_TIME_FORMATS[0] : format, INVARIANT_CULTURE); + } + catch (FormatException) + { + return dateTime.ToString(FALLBACK_TIME_FORMATS[0], INVARIANT_CULTURE); + } + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs index 478c3a47..11523b33 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs @@ -1,3 +1,4 @@ +using System.Text; using AIStudio.Assistants.Dynamic; namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; @@ -48,17 +49,18 @@ internal sealed class AssistantWebContentReader : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { - var promptFragment = string.Empty; + var promptFragment = new StringBuilder(); + if (state.WebContent.TryGetValue(this.Name, out var webState)) { if (!string.IsNullOrWhiteSpace(this.UserPrompt)) - promptFragment = $"{Environment.NewLine}context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + promptFragment.Append($"context:{Environment.NewLine}{this.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"); if (!string.IsNullOrWhiteSpace(webState.Content)) - promptFragment = $"user prompt:{Environment.NewLine}{webState.Content}"; + promptFragment.Append($"user prompt:{Environment.NewLine}{webState.Content}"); } - return promptFragment; + return promptFragment.ToString(); } #endregion