Compare commits

...
10 Commits
83 changed files with 2116 additions and 411 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ MindWork AI Studio is a desktop application available for macOS, Windows, and Li
**Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Independence**: Users are not tied to any single provider. Instead, they can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp) or [LM Studio](https://lmstudio.ai/). Support for Google Gemini, [ollama](https://github.com/ollama/ollama), [Replicate](https://replicate.com/), and [Fireworks](https://fireworks.ai/) is planned.
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), or [Fireworks](https://fireworks.ai/). Support for Google Gemini, and [Replicate](https://replicate.com/) is planned.
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API.
+2
View File
@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -1,7 +1,7 @@
@using AIStudio.Tools
@using MudBlazor
<MudCard Class="my-2 rounded-lg" Outlined="@true">
<MudCard Class="@this.CardClasses" Outlined="@true">
<MudCardHeader>
<CardHeaderAvatar>
<MudAvatar Color="@this.Role.ToColor()">
@@ -33,6 +33,12 @@ public partial class ContentBlockComponent : ComponentBase
[Parameter]
public DateTimeOffset Time { get; init; }
/// <summary>
/// Optional CSS classes.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;
[Inject]
private Rust Rust { get; init; } = null!;
@@ -107,4 +113,6 @@ public partial class ContentBlockComponent : ComponentBase
break;
}
}
private string CardClasses => $"my-2 rounded-lg {this.Class}";
}
@@ -0,0 +1,39 @@
@using AIStudio.Chat
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@this.Title
</MudText>
<InnerScrolling HeaderHeight="12.3em">
<ChildContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.inputIsValid" @bind-Errors="@this.inputIssues" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
@if (this.Body is not null)
{
@this.Body
}
</MudForm>
@if (this.inputIssues.Any())
{
<MudPaper Class="pr-2 mt-3" Outlined="@true">
<MudText Typo="Typo.h6">Issues</MudText>
<MudList T="string">
@foreach (var issue in this.inputIssues)
{
<MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
@issue
</MudListItem>
}
</MudList>
</MudPaper>
}
@if (this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
}
</ChildContent>
</InnerScrolling>
@@ -0,0 +1,122 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public abstract partial class AssistantBase : ComponentBase
{
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
protected Random RNG { get; set; } = null!;
protected abstract string Title { get; }
protected abstract string Description { get; }
protected abstract string SystemPrompt { get; }
private protected virtual RenderFragment? Body => null;
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form;
protected bool inputIsValid;
private ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnParametersSetAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if(firstRender)
this.form?.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
#endregion
protected string? ValidatingProvider(AIStudio.Settings.Provider provider)
{
if(provider.UsedProvider == Providers.NONE)
return "Please select a provider.";
return null;
}
protected void CreateChatThread()
{
this.chatThread = new()
{
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = this.SystemPrompt,
Blocks = [],
};
}
protected DateTimeOffset AddUserRequest(string request)
{
var time = DateTimeOffset.Now;
this.chatThread!.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = request,
},
});
return time;
}
protected async Task AddAIResponseAsync(DateTimeOffset time)
{
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
this.resultingContentBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
}
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace AIStudio.Components;
//
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
//
public abstract class AssistantBaseCore : AssistantBase
{
private protected sealed override RenderFragment Body => this.BuildRenderTree;
// Allow content to be provided by a .razor file but without
// overriding the content of the base class
protected new virtual void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
@@ -0,0 +1,26 @@
<MudItem xs="3">
<MudCard Outlined="@true" Style="border-width: 2px; border-color: #0d47a1; border-radius: 12px; border-style: solid;">
<MudCardHeader>
<CardHeaderContent>
<MudStack AlignItems="AlignItems.Center" Row="@true">
<MudIcon Icon="@this.Icon" Size="Size.Large" Color="Color.Primary"/>
<MudText Typo="Typo.h6">
@this.Name
</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack>
<MudText>
@this.Description
</MudText>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
@this.ButtonText
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
public partial class AssistantBlock : ComponentBase
{
[Parameter]
public string Name { get; set; } = string.Empty;
[Parameter]
public string Description { get; set; } = string.Empty;
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.DisabledByDefault;
[Parameter]
public string ButtonText { get; set; } = "Start";
[Parameter]
public string Link { get; set; } = string.Empty;
}
@@ -13,7 +13,12 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (161, "v0.7.1, build 161 (2024-07-13 12:44 UTC)", "v0.7.1.md"),
new (166, "v0.8.4, build 166 (2024-07-26 06:53 UTC)", "v0.8.4.md"),
new (165, "v0.8.3, build 165 (2024-07-25 13:25 UTC)", "v0.8.3.md"),
new (164, "v0.8.2, build 164 (2024-07-16 18:03 UTC)", "v0.8.2.md"),
new (163, "v0.8.1, build 163 (2024-07-16 08:32 UTC)", "v0.8.1.md"),
new (162, "v0.8.0, build 162 (2024-07-14 19:39 UTC)", "v0.8.0.md"),
new (161, "v0.7.1, build 161 (2024-07-13 11:42 UTC)", "v0.7.1.md"),
new (160, "v0.7.0, build 160 (2024-07-13 08:21 UTC)", "v0.7.0.md"),
new (159, "v0.6.3, build 159 (2024-07-03 18:26 UTC)", "v0.6.3.md"),
new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"),
@@ -1,4 +1,4 @@
<MudExpansionPanel Class="border-solid border rounded-lg" IsInitiallyExpanded="@this.IsExpanded">
<MudExpansionPanel Class="border-solid border rounded-lg" Expanded="@this.IsExpanded">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/>
@@ -1,4 +1,4 @@
@inherits AIStudio.Tools.MSGComponentBase
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
@@ -1,7 +1,7 @@
<MudList Clickable="@this.Clickable" Class="@this.Classes">
<MudList T="string" ReadOnly="@(!this.Clickable)" Class="@this.Classes">
@foreach(var item in this.Items)
{
<MudListItem Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
<MudListItem T="string" Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;"><b>@item.Header:</b> @item.Text</MudText>
</MudListItem>
}
@@ -18,5 +18,5 @@ public class TreeItemData : ITreeItem
public bool Expandable { get; init; } = true;
public HashSet<ITreeItem> Children { get; init; } = [];
public IReadOnlyCollection<TreeItemData<ITreeItem>> Children { get; init; } = [];
}
@@ -1,6 +1,6 @@
<MudTreeView T="ITreeItem" Items="@this.treeItems" MultiSelection="@false" Hover="@true" ExpandOnClick="@true">
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true">
<ItemTemplate Context="item">
@switch (item)
@switch (item.Value)
{
case TreeDivider:
<li style="min-height: 1em;">
@@ -11,7 +11,7 @@
case TreeItemData treeItem:
@if (treeItem.Type is TreeItemType.CHAT)
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="() => this.LoadChat(treeItem.Path, true)">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="() => this.LoadChat(treeItem.Path, true)">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">
@@ -44,7 +44,7 @@
}
else if (treeItem.Type is TreeItemType.WORKSPACE)
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
@@ -63,7 +63,7 @@
}
else
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
@@ -47,8 +47,8 @@ public partial class Workspaces : ComponentBase
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
}
};
private readonly HashSet<ITreeItem> treeItems = new();
private readonly List<TreeItemData<ITreeItem>> treeItems = new();
#region Overrides of ComponentBase
@@ -70,33 +70,46 @@ public partial class Workspaces : ComponentBase
private async Task LoadTreeItems()
{
this.treeItems.Clear();
this.treeItems.Add(new TreeItemData
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Depth = 0,
Branch = WorkspaceBranch.WORKSPACES,
Text = "Workspaces",
Icon = Icons.Material.Filled.Folder,
Expandable = true,
Path = "root",
Children = await this.LoadWorkspaces(),
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.WORKSPACES,
Text = "Workspaces",
Icon = Icons.Material.Filled.Folder,
Expandable = true,
Path = "root",
Children = await this.LoadWorkspaces(),
},
});
this.treeItems.Add(new TreeDivider());
this.treeItems.Add(new TreeItemData
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeDivider(),
});
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Depth = 0,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = "Temporary chats",
Icon = Icons.Material.Filled.Timer,
Expandable = true,
Path = "temp",
Children = await this.LoadTemporaryChats(),
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = "Temporary chats",
Icon = Icons.Material.Filled.Timer,
Expandable = true,
Path = "temp",
Children = await this.LoadTemporaryChats(),
},
});
}
private async Task<HashSet<ITreeItem>> LoadTemporaryChats()
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadTemporaryChats()
{
var tempChildren = new HashSet<ITreeItem>();
var tempChildren = new List<TreeItemData<ITreeItem>>();
//
// Search for workspace folders in the data directory:
@@ -115,15 +128,19 @@ public partial class Workspaces : ComponentBase
var chatNamePath = Path.Join(tempChatDirPath, "name");
var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
tempChildren.Add(new TreeItemData
tempChildren.Add(new TreeItemData<ITreeItem>
{
Type = TreeItemType.CHAT,
Depth = 1,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = chatName,
Icon = Icons.Material.Filled.Timer,
Expandable = false,
Path = tempChatDirPath,
Value = new TreeItemData
{
Type = TreeItemType.CHAT,
Depth = 1,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = chatName,
Icon = Icons.Material.Filled.Timer,
Expandable = false,
Path = tempChatDirPath,
},
});
}
@@ -140,9 +157,9 @@ public partial class Workspaces : ComponentBase
return await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
}
private async Task<HashSet<ITreeItem>> LoadWorkspaces()
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaces()
{
var workspaces = new HashSet<ITreeItem>();
var workspaces = new List<TreeItemData<ITreeItem>>();
//
// Search for workspace folders in the data directory:
@@ -161,26 +178,34 @@ public partial class Workspaces : ComponentBase
var workspaceNamePath = Path.Join(workspaceDirPath, "name");
var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
workspaces.Add(new TreeItemData
workspaces.Add(new TreeItemData<ITreeItem>
{
Type = TreeItemType.WORKSPACE,
Depth = 1,
Branch = WorkspaceBranch.WORKSPACES,
Text = workspaceName,
Icon = Icons.Material.Filled.Description,
Expandable = true,
Path = workspaceDirPath,
Children = await this.LoadWorkspaceChats(workspaceDirPath),
Value = new TreeItemData
{
Type = TreeItemType.WORKSPACE,
Depth = 1,
Branch = WorkspaceBranch.WORKSPACES,
Text = workspaceName,
Icon = Icons.Material.Filled.Description,
Expandable = true,
Path = workspaceDirPath,
Children = await this.LoadWorkspaceChats(workspaceDirPath),
},
});
}
workspaces.Add(new TreeButton(WorkspaceBranch.WORKSPACES, 1, "Add workspace",Icons.Material.Filled.LibraryAdd, this.AddWorkspace));
workspaces.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, "Add workspace",Icons.Material.Filled.LibraryAdd, this.AddWorkspace),
});
return workspaces;
}
private async Task<HashSet<ITreeItem>> LoadWorkspaceChats(string workspacePath)
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaceChats(string workspacePath)
{
var workspaceChats = new HashSet<ITreeItem>();
var workspaceChats = new List<TreeItemData<ITreeItem>>();
// Enumerate the workspace directory:
foreach (var chatPath in Directory.EnumerateDirectories(workspacePath))
@@ -189,19 +214,28 @@ public partial class Workspaces : ComponentBase
var chatNamePath = Path.Join(chatPath, "name");
var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
workspaceChats.Add(new TreeItemData
workspaceChats.Add(new TreeItemData<ITreeItem>
{
Type = TreeItemType.CHAT,
Depth = 2,
Branch = WorkspaceBranch.WORKSPACES,
Text = chatName,
Icon = Icons.Material.Filled.Chat,
Expandable = false,
Path = chatPath,
Value = new TreeItemData
{
Type = TreeItemType.CHAT,
Depth = 2,
Branch = WorkspaceBranch.WORKSPACES,
Text = chatName,
Icon = Icons.Material.Filled.Chat,
Expandable = false,
Path = chatPath,
},
});
}
workspaceChats.Add(new TreeButton(WorkspaceBranch.WORKSPACES, 2, "Add chat",Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)));
workspaceChats.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, "Add chat",Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)),
});
return workspaceChats;
}
@@ -247,7 +281,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Load Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return null;
}
@@ -294,7 +328,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
}
@@ -331,7 +365,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
chat.Name = (dialogResult.Data as string)!;
@@ -356,7 +390,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var alteredWorkspaceName = (dialogResult.Data as string)!;
@@ -377,7 +411,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Add Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = Guid.NewGuid();
@@ -408,7 +442,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
Directory.Delete(workspacePath, true);
@@ -430,7 +464,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = dialogResult.Data is Guid id ? id : default;
@@ -475,7 +509,7 @@ public partial class Workspaces : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Create Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
}
@@ -1,4 +1,5 @@
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -23,7 +24,12 @@ public partial class ConfigurationBase : ComponentBase
public string OptionHelp { get; set; } = string.Empty;
[Inject]
public SettingsManager SettingsManager { get; init; } = null!;
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
protected const string MARGIN_CLASS = "mb-6";
protected async Task InformAboutChange() => await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
@@ -35,5 +35,6 @@ public partial class ConfigurationOption : ConfigurationBase
{
this.StateUpdate(updatedState);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
}
@@ -30,6 +30,7 @@ public partial class ConfigurationSelect<T> : ConfigurationBase
{
this.SelectionUpdate(updatedValue);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
private static string GetClass => $"{MARGIN_CLASS} rounded-lg";
@@ -47,4 +47,12 @@ public static class ConfigurationSelectDataFactory
yield return new("Delete temporary chats older than 180 days", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_180_DAYS);
yield return new("Delete temporary chats older than 1 year", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_365_DAYS);
}
public static IEnumerable<ConfigurationSelectData<NavBehavior>> GetNavBehaviorData()
{
yield return new("Navigation expands on mouse hover", NavBehavior.EXPAND_ON_HOVER);
yield return new("Navigation never expands, but there are tooltips", NavBehavior.NEVER_EXPAND_USE_TOOLTIPS);
yield return new("Navigation never expands, no tooltips", NavBehavior.NEVER_EXPAND_NO_TOOLTIPS);
yield return new("Always expand navigation", NavBehavior.ALWAYS_EXPAND);
}
}
@@ -1,33 +1,32 @@
@inherits LayoutComponentBase
@using AIStudio.Settings
@inherits LayoutComponentBase
<MudPaper Height="calc(100vh);" Elevation="0">
<MudLayout>
@if (!this.performingUpdate)
{
<MudDrawerContainer Class="mud-height-full absolute">
<MudDrawer Elevation="0" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@true" Color="Color.Default">
<MudDrawer @bind-Open="@this.navBarOpen" MiniWidth="@NAVBAR_COLLAPSED_WIDTH" Width="@NAVBAR_EXPANDED_WIDTH" Elevation="1" Fixed="@true" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.EXPAND_ON_HOVER)" Color="Color.Default">
<MudNavMenu>
<MudTooltip Text="Home" Placement="Placement.Right">
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
</MudTooltip>
<MudTooltip Text="Chat" Placement="Placement.Right">
<MudNavLink Href="/chat" Icon="@Icons.Material.Filled.Chat">Chat</MudNavLink>
</MudTooltip>
<MudTooltip Text="Supporters" Placement="Placement.Right">
<MudNavLink Href="/supporters" Icon="@Icons.Material.Filled.Favorite" IconColor="Color.Error">Supporters</MudNavLink>
</MudTooltip>
<MudTooltip Text="About" Placement="Placement.Right">
<MudNavLink Href="/about" Icon="@Icons.Material.Filled.Info">About</MudNavLink>
</MudTooltip>
<MudTooltip Text="Settings" Placement="Placement.Right">
<MudNavLink Href="/settings" Icon="@Icons.Material.Filled.Settings">Settings</MudNavLink>
</MudTooltip>
@foreach (var navBarItem in NAV_ITEMS)
{
if (this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.NEVER_EXPAND_USE_TOOLTIPS)
{
<MudTooltip Text="@navBarItem.Name" Placement="Placement.Right">
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" IconColor="@navBarItem.IconColor">@navBarItem.Name</MudNavLink>
</MudTooltip>
}
else
{
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" IconColor="@navBarItem.IconColor">@navBarItem.Name</MudNavLink>
}
}
</MudNavMenu>
</MudDrawer>
</MudDrawerContainer>
}
<MudMainContent Class="mud-height-full pt-1">
<MudMainContent Class="mud-height-full pt-1" Style="@this.PaddingLeft">
<MudContainer Fixed="@true" Class="mud-height-full" Style="margin-left: 5em; width: calc(100% - 5em);">
@if (!this.performingUpdate && this.IsUpdateAlertVisible)
{
@@ -34,12 +34,30 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
public string AdditionalHeight { get; private set; } = "0em";
private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
private const int NAVBAR_COLLAPSED_WIDTH_INT = 4;
private const int NAVBAR_EXPANDED_WIDTH_INT = 10;
private static readonly string NAVBAR_COLLAPSED_WIDTH = $"{NAVBAR_COLLAPSED_WIDTH_INT}em";
private static readonly string NAVBAR_EXPANDED_WIDTH = $"{NAVBAR_EXPANDED_WIDTH_INT}em";
private bool navBarOpen;
private bool isUpdateAvailable;
private bool performingUpdate;
private bool userDismissedUpdate;
private string updateToVersion = string.Empty;
private UpdateResponse? currentUpdateResponse;
private static readonly IReadOnlyCollection<NavBarItem> NAV_ITEMS = new List<NavBarItem>
{
new("Home", Icons.Material.Filled.Home, Color.Default, "/", true),
new("Chat", Icons.Material.Filled.Chat, Color.Default, "/chat", false),
new("Assistants", Icons.Material.Filled.Apps, Color.Default ,"/assistants", false),
new("Supporters", Icons.Material.Filled.Favorite, Color.Error ,"/supporters", false),
new("About", Icons.Material.Filled.Info, Color.Default ,"/about", false),
new("Settings", Icons.Material.Filled.Settings, Color.Default ,"/settings", false),
};
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
@@ -63,12 +81,16 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
// Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE ]);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED ]);
// Set the js runtime for the update service:
UpdateService.SetBlazorDependencies(this.JsRuntime, this.Snackbar);
TemporaryChatService.Initialize();
// Should the navigation bar be open by default?
if(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
this.navBarOpen = true;
await base.OnInitializedAsync();
}
@@ -96,6 +118,15 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
}
break;
case Event.CONFIGURATION_CHANGED:
if(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
this.navBarOpen = true;
else
this.navBarOpen = false;
this.StateHasChanged();
break;
}
}
@@ -105,7 +136,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
}
#endregion
private async Task DismissUpdate()
{
this.userDismissedUpdate = true;
@@ -151,7 +182,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
var dialogReference = await this.DialogService.ShowAsync<UpdateDialog>("Update", dialogParameters, DialogOptions.FULLSCREEN_NO_HEADER);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
this.performingUpdate = true;
@@ -170,7 +201,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Leave Chat Page", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
{
context.PreventNavigation();
return;
@@ -0,0 +1,3 @@
namespace AIStudio.Components.Layout;
public record NavBarItem(string Name, string Icon, Color IconColor, string Path, bool MatchAll);
@@ -1,6 +1,8 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
namespace AIStudio.Components;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{
@@ -9,14 +9,14 @@
<MudText>
The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
</MudText>
<MudList Clickable="@true">
<MudListItem Icon="@Icons.Material.Outlined.Chat" Text="@VersionApp"/>
<MudListItem Icon="@Icons.Material.Outlined.Timer" Text="@BuildTime"/>
<MudListItem Icon="@Icons.Material.Outlined.Build" Text="@VersionDotnetSdk"/>
<MudListItem Icon="@Icons.Material.Outlined.Memory" Text="@VersionDotnetRuntime"/>
<MudListItem Icon="@Icons.Material.Outlined.Build" Text="@VersionRust"/>
<MudListItem Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
<MudListItem Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
<MudList T="string">
<MudListItem T="string" Icon="@Icons.Material.Outlined.Chat" Text="@VersionApp"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Timer" Text="@BuildTime"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@VersionDotnetSdk"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@VersionDotnetRuntime"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@VersionRust"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
</MudList>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="() => this.CheckForUpdate()">
Check for updates
@@ -0,0 +1,12 @@
@page "/assistants"
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
Assistants
</MudText>
<MudGrid>
<AssistantBlock Name="Icon Finder" Description="Using a LLM to find an icon for a given context." Icon="@Icons.Material.Filled.FindInPage" Link="/assistant/icons"/>
<AssistantBlock Name="Text Summarizer" Description="Using a LLM to summarize a given text." Icon="@Icons.Material.Filled.TextSnippet" Link="/assistant/summarizer"/>
<AssistantBlock Name="Translator" Description="Translate text into another language." Icon="@Icons.Material.Filled.Translate" Link="/assistant/translator"/>
<AssistantBlock Name="Coding" Description="Get coding and debugging support from a LLM." Icon="@Icons.Material.Filled.Code" Link="/assistant/coding"/>
</MudGrid>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages;
public partial class Assistants : ComponentBase;
@@ -2,7 +2,7 @@
@using AIStudio.Chat
@using AIStudio.Settings
@inherits AIStudio.Tools.MSGComponentBase
@inherits MSGComponentBase
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
@@ -15,7 +15,7 @@
}
</MudText>
<MudSelect T="Provider" @bind-Value="@this.selectedProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined">
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{
<MudSelectItem Value="@provider"/>
@@ -32,7 +32,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private AIStudio.Settings.Provider selectedProvider;
private AIStudio.Settings.Provider providerSettings;
private ChatThread? chatThread;
private bool hasUnsavedChanges;
private bool isStreaming;
@@ -61,11 +61,11 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
#endregion
private bool IsProviderSelected => this.selectedProvider.UsedProvider != Providers.NONE;
private bool IsProviderSelected => this.providerSettings.UsedProvider != Providers.NONE;
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.selectedProvider.InstanceName}', provider '{this.selectedProvider.UsedProvider.ToName()}')" : "Select a provider first";
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.providerSettings.InstanceName}', provider '{this.providerSettings.UsedProvider.ToName()}')" : "Select a provider first";
private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0;
@@ -151,7 +151,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat:
if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
@@ -235,7 +235,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
}
@@ -297,7 +297,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
var confirmationDialogResult = await confirmationDialogReference.Result;
if (confirmationDialogResult.Canceled)
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
return;
}
@@ -310,7 +310,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = dialogResult.Data is Guid id ? id : default;
@@ -0,0 +1,39 @@
@page "/assistant/coding"
@using AIStudio.Settings
@inherits AssistantBaseCore
<MudExpansionPanels Class="mb-3">
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
{
var codingContext = this.codingContexts[contextIndex];
<ExpansionPanel HeaderText="@codingContext.Id" HeaderIcon="@Icons.Material.Filled.Code">
<CodingContextItem @bind-CodingContext="@codingContext"/>
</ExpansionPanel>
}
</MudExpansionPanels>
<MudButton Variant="Variant.Filled" OnClick="() => this.AddCodingContext()" Class="mb-3">
Add context
</MudButton>
<MudStack Row="@false" Class="mb-3">
<MudSwitch T="bool" @bind-Value="@this.provideCompilerMessages">
@(this.provideCompilerMessages ? "Provide compiler messages" : "Provide no compiler messages")
</MudSwitch>
@if (this.provideCompilerMessages)
{
<MudTextField T="string" @bind-Text="@this.compilerMessages" Validation="@this.ValidatingCompilerMessages" AdornmentIcon="@Icons.Material.Filled.Error" Adornment="Adornment.Start" Label="Compiler messages" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudTextField T="string" @bind-Text="@this.questions" Validation="@this.ValidateQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionMark" Adornment="Adornment.Start" Label="Your question(s)" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{
<MudSelectItem Value="@provider"/>
}
</MudSelect>
<MudButton Variant="Variant.Filled" Color="Color.Info" OnClick="() => this.GetSupport()" Class="mb-3">
Get support
</MudButton>
@@ -0,0 +1,106 @@
using System.Text;
namespace AIStudio.Components.Pages.Coding;
public partial class AssistantCoding : AssistantBaseCore
{
protected override string Title => "Coding Assistant";
protected override string Description =>
"""
This coding assistant supports you in writing code. Provide some coding context by copying and pasting
your code into the input fields. You might assign an ID to your code snippet to easily reference it later.
When you have compiler messages, you can paste them into the input fields to get help with debugging as well.
""";
protected override string SystemPrompt =>
"""
You are a friendly, helpful senior software developer with extensive experience in various programming languages
and concepts. You are familiar with principles like DRY, KISS, YAGNI, and SOLID and can apply and explain them.
You know object-oriented programming, as well as functional programming and procedural programming. You are also
familiar with design patterns and can explain them. You are an expert of debugging and can help with compiler
messages. You can also help with code refactoring and optimization.
When the user asks in a different language than English, you answer in the same language!
""";
private readonly List<CodingContext> codingContexts = new();
private bool provideCompilerMessages;
private string compilerMessages = string.Empty;
private string questions = string.Empty;
private string? ValidatingCompilerMessages(string checkCompilerMessages)
{
if(!this.provideCompilerMessages)
return null;
if(string.IsNullOrWhiteSpace(checkCompilerMessages))
return "Please provide the compiler messages.";
return null;
}
private string? ValidateQuestions(string checkQuestions)
{
if(string.IsNullOrWhiteSpace(checkQuestions))
return "Please provide your questions.";
return null;
}
private void AddCodingContext()
{
this.codingContexts.Add(new()
{
Id = $"Context {this.codingContexts.Count + 1}",
});
}
private async Task GetSupport()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
var sbContext = new StringBuilder();
if (this.codingContexts.Count > 0)
{
sbContext.AppendLine("I have the following coding context:");
sbContext.AppendLine();
foreach (var codingContext in this.codingContexts)
{
sbContext.AppendLine($"ID: {codingContext.Id}");
sbContext.AppendLine($"Language: {codingContext.Language.Name()}");
sbContext.AppendLine($"Other Language: {codingContext.OtherLanguage}");
sbContext.AppendLine($"Content:");
sbContext.AppendLine("```");
sbContext.AppendLine(codingContext.Code);
sbContext.AppendLine("```");
sbContext.AppendLine();
}
}
var sbCompilerMessages = new StringBuilder();
if (this.provideCompilerMessages)
{
sbCompilerMessages.AppendLine("I have the following compiler messages:");
sbCompilerMessages.AppendLine();
sbCompilerMessages.AppendLine("```");
sbCompilerMessages.AppendLine(this.compilerMessages);
sbCompilerMessages.AppendLine("```");
sbCompilerMessages.AppendLine();
}
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{sbContext}
{sbCompilerMessages}
My questions are:
{this.questions}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,16 @@
namespace AIStudio.Components.Pages.Coding;
public sealed class CodingContext(string id, CommonCodingLanguages language, string otherLanguage, string code)
{
public CodingContext() : this(string.Empty, CommonCodingLanguages.NONE, string.Empty, string.Empty)
{
}
public string Id { get; set; } = id;
public CommonCodingLanguages Language { get; set; } = language;
public string OtherLanguage { get; set; } = otherLanguage;
public string Code { get; set; } = code;
}
@@ -0,0 +1,14 @@
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
{
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
}
</MudSelect>
@if (this.CodingContext.Language is CommonCodingLanguages.OTHER)
{
<MudTextField T="string" @bind-Text="@this.CodingContext.OtherLanguage" Validation="@this.ValidatingOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudTextField T="string" @bind-Text="@this.CodingContext.Code" Validation="@this.ValidatingCode" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your code" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" />
@@ -0,0 +1,50 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages.Coding;
public partial class CodingContextItem : ComponentBase
{
[Parameter]
public CodingContext CodingContext { get; set; } = new();
[Parameter]
public EventCallback<CodingContext> CodingContextChanged { get; set; }
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnParametersSetAsync();
}
#endregion
private string? ValidatingCode(string code)
{
if(string.IsNullOrWhiteSpace(code))
return $"{this.CodingContext.Id}: Please provide your input.";
return null;
}
private string? ValidatingOtherLanguage(string language)
{
if(this.CodingContext.Language != CommonCodingLanguages.OTHER)
return null;
if(string.IsNullOrWhiteSpace(language))
return "Please specify the language.";
return null;
}
}
@@ -0,0 +1,43 @@
namespace AIStudio.Components.Pages.Coding;
public static class CommonCodingLanguageExtensions
{
public static string Name(this CommonCodingLanguages language) => language switch
{
CommonCodingLanguages.NONE => "None",
CommonCodingLanguages.BASH => "Bash",
CommonCodingLanguages.BLAZOR => ".NET Blazor",
CommonCodingLanguages.C => "C",
CommonCodingLanguages.CPP => "C++",
CommonCodingLanguages.CSHARP => "C#",
CommonCodingLanguages.CSS => "CSS",
CommonCodingLanguages.FORTRAN => "Fortran",
CommonCodingLanguages.GDSCRIPT => "GDScript",
CommonCodingLanguages.GO => "Go",
CommonCodingLanguages.HTML => "HTML",
CommonCodingLanguages.JAVA => "Java",
CommonCodingLanguages.JAVASCRIPT => "JavaScript",
CommonCodingLanguages.JSON => "JSON",
CommonCodingLanguages.JULIA => "Julia",
CommonCodingLanguages.KOTLIN => "Kotlin",
CommonCodingLanguages.LUA => "Lua",
CommonCodingLanguages.MARKDOWN => "Markdown",
CommonCodingLanguages.MATHEMATICA => "Mathematica",
CommonCodingLanguages.MATLAB => "MATLAB",
CommonCodingLanguages.PHP => "PHP",
CommonCodingLanguages.POWERSHELL => "PowerShell",
CommonCodingLanguages.PROLOG => "Prolog",
CommonCodingLanguages.PYTHON => "Python",
CommonCodingLanguages.R => "R",
CommonCodingLanguages.RUBY => "Ruby",
CommonCodingLanguages.RUST => "Rust",
CommonCodingLanguages.SQL => "SQL",
CommonCodingLanguages.SWIFT => "Swift",
CommonCodingLanguages.TYPESCRIPT => "TypeScript",
CommonCodingLanguages.XML => "XML",
CommonCodingLanguages.OTHER => "Other",
_ => "Unknown"
};
}
@@ -0,0 +1,39 @@
namespace AIStudio.Components.Pages.Coding;
public enum CommonCodingLanguages
{
NONE,
BASH,
BLAZOR,
C,
CPP,
CSHARP,
CSS,
FORTRAN,
GDSCRIPT,
GO,
HTML,
JAVA,
JAVASCRIPT,
JSON,
JULIA,
KOTLIN,
LUA,
MARKDOWN,
MATHEMATICA,
MATLAB,
PHP,
POWERSHELL,
PROLOG,
PYTHON,
R,
RUBY,
RUST,
SQL,
SWIFT,
TYPESCRIPT,
XML,
OTHER,
}
@@ -31,7 +31,7 @@ public partial class Home : ComponentBase
private static readonly TextItem[] ITEMS_ADVANTAGES =
[
new TextItem("Free of charge", "The app is free to use, both for personal and commercial purposes."),
new TextItem("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using llama.cpp or LM Studio. Support for Google Gemini, ollama, Replicate, and Fireworks is planned."),
new TextItem("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using llama.cpp, ollama, LM Studio, or Fireworks. Support for Google Gemini and Replicate is planned."),
new TextItem("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
new TextItem("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
new TextItem("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
@@ -0,0 +1,29 @@
@page "/assistant/icons"
@using AIStudio.Settings
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var source in Enum.GetValues<IconSources>())
{
<MudSelectItem Value="@source">@source.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedIconSource is not IconSources.GENERIC)
{
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
}
</MudStack>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{
<MudSelectItem Value="@provider"/>
}
</MudSelect>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
Find icon
</MudButton>
@@ -0,0 +1,55 @@
namespace AIStudio.Components.Pages.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore
{
private string inputContext = string.Empty;
private IconSources selectedIconSource;
protected override string Title => "Icon Finder";
protected override string Description =>
"""
Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
You need to extract a concept from your context, such as from a text. Let's take an example where
your text contains statements about multiple departments. The sought-after concept could be "departments."
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
""";
protected override string SystemPrompt =>
"""
I can search for icons using US English keywords. Please help me come up with the right search queries.
I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords
that are likely to yield suitable icons. For example, I might ask for an icon about departments, but icons
related to the keyword "buildings" might be the best match. Provide your keywords in a Markdown list without
quotation marks.
""";
private string? ValidatingContext(string context)
{
if(string.IsNullOrWhiteSpace(context))
return "Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon.";
return null;
}
private async Task FindIcon()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.selectedIconSource.Prompt()} I search for an icon for the following context:
```
{this.inputContext}
```
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,40 @@
namespace AIStudio.Components.Pages.IconFinder;
public static class IconSourceExtensions
{
public static string Name(this IconSources iconSource) => iconSource switch
{
IconSources.FLAT_ICON => "Flaticon",
IconSources.FONT_AWESOME => "Font Awesome",
IconSources.MATERIAL_ICONS => "Material Icons",
IconSources.FEATHER_ICONS => "Feather Icons",
IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons",
IconSources.ICONS8 => "Icons8",
_ => "Generic",
};
public static string Prompt(this IconSources iconSource) => iconSource switch
{
IconSources.FLAT_ICON => "My icon source is Flaticon.",
IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.",
IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.",
IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.",
IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.",
IconSources.ICONS8 => "I look for an icon on Icons8.",
_ => string.Empty,
};
public static string URL(this IconSources iconSource) => iconSource switch
{
IconSources.FLAT_ICON => "https://www.flaticon.com/",
IconSources.FONT_AWESOME => "https://fontawesome.com/",
IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/",
IconSources.FEATHER_ICONS => "https://feathericons.com/",
IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/",
IconSources.ICONS8 => "https://icons8.com/",
_ => string.Empty,
};
}
@@ -0,0 +1,13 @@
namespace AIStudio.Components.Pages.IconFinder;
public enum IconSources
{
GENERIC,
ICONS8,
FLAT_ICON,
FONT_AWESOME,
MATERIAL_ICONS,
FEATHER_ICONS,
BOOTSTRAP_ICONS,
}
@@ -1,5 +1,6 @@
@page "/settings"
@using AIStudio.Provider
@using Host = AIStudio.Provider.SelfHosted.Host
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
@@ -26,13 +27,21 @@
<MudTd>@context.InstanceName</MudTd>
<MudTd>@context.UsedProvider</MudTd>
<MudTd>
@if(context.UsedProvider is not Providers.SELF_HOSTED)
@context.Model
@if (context.UsedProvider is not Providers.SELF_HOSTED)
{
@this.GetProviderModelName(context)
}
else if (context.UsedProvider is Providers.SELF_HOSTED && context.Host is not Host.LLAMACPP)
{
@this.GetProviderModelName(context)
}
else
{
@("as selected by provider")
}
</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@this.GetProviderDashboardURL(context.UsedProvider)" Target="_blank" Disabled="@(context.UsedProvider is Providers.NONE or Providers.SELF_HOSTED)">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@this.GetProviderDashboardURL(context.UsedProvider)" Target="_blank" Disabled="@(!this.HasDashboard(context.UsedProvider))">
Open Dashboard
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditProvider(context)">
@@ -64,5 +73,6 @@
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
<ConfigurationSelect OptionDescription="Workspace behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior = selectedValue)" OptionHelp="Should we store your chats?"/>
<ConfigurationSelect OptionDescription="Workspace maintenance" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageTemporaryMaintenancePolicyData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy = selectedValue)" OptionHelp="If and when should we delete your temporary chats?"/>
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
</MudPaper>
</InnerScrolling>
@@ -1,6 +1,8 @@
using AIStudio.Components.CommonDialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
@@ -19,6 +21,9 @@ public partial class Settings : ComponentBase
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
#region Provider related
@@ -31,14 +36,15 @@ public partial class Settings : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Add Provider", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var addedProvider = (AIStudio.Settings.Provider)dialogResult.Data;
var addedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
addedProvider = addedProvider with { Num = this.SettingsManager.ConfigurationData.NextProviderNum++ };
this.SettingsManager.ConfigurationData.Providers.Add(addedProvider);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditProvider(AIStudio.Settings.Provider provider)
@@ -53,14 +59,15 @@ public partial class Settings : ComponentBase
{ x => x.DataHostname, provider.Hostname },
{ x => x.IsSelfHosted, provider.IsSelfHosted },
{ x => x.IsEditing, true },
{ x => x.DataHost, provider.Host },
};
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit Provider", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data;
var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
// Set the provider number if it's not set. This is important for providers
// added before we started saving the provider number.
@@ -69,6 +76,7 @@ public partial class Settings : ComponentBase
this.SettingsManager.ConfigurationData.Providers[this.SettingsManager.ConfigurationData.Providers.IndexOf(provider)] = editedProvider;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task DeleteProvider(AIStudio.Settings.Provider provider)
@@ -80,26 +88,46 @@ public partial class Settings : ComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Provider", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult.Canceled)
if (dialogResult is null || dialogResult.Canceled)
return;
var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName, provider.Hostname);
var providerInstance = provider.CreateProvider();
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
await this.SettingsManager.StoreSettings();
}
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private bool HasDashboard(Providers provider) => provider switch
{
Providers.OPEN_AI => true,
Providers.MISTRAL => true,
Providers.ANTHROPIC => true,
Providers.FIREWORKS => true,
_ => false,
};
private string GetProviderDashboardURL(Providers provider) => provider switch
{
Providers.OPEN_AI => "https://platform.openai.com/usage",
Providers.MISTRAL => "https://console.mistral.ai/usage/",
Providers.ANTHROPIC => "https://console.anthropic.com/settings/plans",
Providers.FIREWORKS => "https://fireworks.ai/account/billing",
_ => string.Empty,
};
private string GetProviderModelName(AIStudio.Settings.Provider provider)
{
const int MAX_LENGTH = 36;
var modelName = provider.Model.ToString();
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
}
#endregion
}
@@ -0,0 +1,43 @@
@page "/assistant/summarizer"
@using AIStudio.Settings
@using AIStudio.Tools
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="CommonLanguages" @bind-Value="@this.selectedTargetLanguage" AdornmentIcon="@Icons.Material.Filled.Translate" Adornment="Adornment.Start" Label="Target language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var targetLanguage in Enum.GetValues<CommonLanguages>())
{
<MudSelectItem Value="@targetLanguage">@targetLanguage.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedTargetLanguage is CommonLanguages.OTHER)
{
<MudTextField T="string" @bind-Text="@this.customTargetLanguage" Validation="@this.ValidateCustomLanguage" Label="Custom target language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="Complexity" @bind-Value="@this.selectedComplexity" AdornmentIcon="@Icons.Material.Filled.Layers" Adornment="Adornment.Start" Label="Target complexity" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var targetComplexity in Enum.GetValues<Complexity>())
{
<MudSelectItem Value="@targetComplexity">@targetComplexity.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
{
<MudTextField T="string" @bind-Text="@this.expertInField" Validation="@this.ValidateExpertInField" Label="Your expertise" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{
<MudSelectItem Value="@provider"/>
}
</MudSelect>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SummarizeText()">
Summarize
</MudButton>
@@ -0,0 +1,77 @@
using AIStudio.Tools;
namespace AIStudio.Components.Pages.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore
{
protected override string Title => "Text Summarizer";
protected override string Description =>
"""
Summarize long text into a shorter version while retaining the main points.
You might want to change the language of the summary to make it more readable.
It is also possible to change the complexity of the summary to make it
easy to understand.
""";
protected override string SystemPrompt =>
"""
You get a long text as input. The user wants to get a summary of the text.
The user might want to change the language of the summary. In this case,
you should provide a summary in the requested language. Eventually, the user
want to change the complexity of the text. In this case, you should provide
a summary with the requested complexity. In any case, do not add any information.
""";
private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private Complexity selectedComplexity;
private string expertInField = string.Empty;
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string? ValidateExpertInField(string field)
{
if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field))
return "Please provide your field of expertise.";
return null;
}
private async Task SummarizeText()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
{this.selectedComplexity.Prompt(this.expertInField)}
Please summarize the following text:
```
{this.inputText}
```
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,14 @@
using AIStudio.Tools;
namespace AIStudio.Components.Pages.TextSummarizer;
public static class CommonLanguagePrompts
{
public static string Prompt(this CommonLanguages language, string customLanguage) => language switch
{
CommonLanguages.AS_IS => "Do not change the language of the text.",
CommonLanguages.OTHER => $"Output you summary in {customLanguage}.",
_ => $"Output your summary in {language.Name()} ({language}).",
};
}
@@ -0,0 +1,13 @@
namespace AIStudio.Components.Pages.TextSummarizer;
public enum Complexity
{
NO_CHANGE,
SIMPLE_LANGUAGE,
TEEN_LANGUAGE,
EVERYDAY_LANGUAGE,
POPULAR_SCIENCE_LANGUAGE,
SCIENTIFIC_LANGUAGE_FIELD_EXPERTS,
SCIENTIFIC_LANGUAGE_OTHER_EXPERTS,
}
@@ -0,0 +1,32 @@
namespace AIStudio.Components.Pages.TextSummarizer;
public static class ComplexityExtensions
{
public static string Name(this Complexity complexity) => complexity switch
{
Complexity.NO_CHANGE => "No change in complexity",
Complexity.SIMPLE_LANGUAGE => "Simple language, e.g., for children",
Complexity.TEEN_LANGUAGE => "Teen language, e.g., for teenagers",
Complexity.EVERYDAY_LANGUAGE => "Everyday language, e.g., for adults",
Complexity.POPULAR_SCIENCE_LANGUAGE => "Popular science language, e.g., for people interested in science",
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Scientific language for experts in this field",
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => "Scientific language for experts from other fields (interdisciplinary)",
_ => "No change in complexity",
};
public static string Prompt(this Complexity complexity, string expertInField) => complexity switch
{
Complexity.NO_CHANGE => "Do not change the complexity of the text.",
Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.",
Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.",
Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.",
Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.",
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.",
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.",
_ => "Do not change the complexity of the text.",
};
}
@@ -0,0 +1,50 @@
@page "/assistant/translator"
@using AIStudio.Settings
@using AIStudio.Tools
@inherits AssistantBaseCore
<MudField Label="Live translation" Variant="Variant.Outlined" Class="mb-3">
<MudSwitch T="bool" @bind-Value="@this.liveTranslation">
@(this.liveTranslation ? "Live translation" : "No live translation")
</MudSwitch>
</MudField>
@if (this.liveTranslation)
{
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="1_000" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
else
{
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="CommonLanguages" @bind-Value="@this.selectedTargetLanguage" Validation="@this.ValidatingTargetLanguage" AdornmentIcon="@Icons.Material.Filled.Translate" Adornment="Adornment.Start" Label="Target language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var targetLanguage in Enum.GetValues<CommonLanguages>())
{
if (targetLanguage is CommonLanguages.AS_IS)
{
<MudSelectItem Value="@targetLanguage">Please select the target language</MudSelectItem>
}
else
{
<MudSelectItem Value="@targetLanguage">@targetLanguage.Name()</MudSelectItem>
}
}
</MudSelect>
@if (this.selectedTargetLanguage is CommonLanguages.OTHER)
{
<MudTextField T="string" @bind-Text="@this.customTargetLanguage" Validation="@this.ValidateCustomLanguage" Label="Custom target language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{
<MudSelectItem Value="@provider"/>
}
</MudSelect>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.TranslateText(force: true)">
Translate
</MudButton>
@@ -0,0 +1,74 @@
using AIStudio.Tools;
namespace AIStudio.Components.Pages.Translator;
public partial class AssistantTranslator : AssistantBaseCore
{
protected override string Title => "Translator";
protected override string Description =>
"""
Translate text from one language to another.
""";
protected override string SystemPrompt =>
"""
You get text in a source language as input. The user wants to get the text translated into a target language.
Provide the translation in the requested language. Do not add any information. Correct any spelling or grammar mistakes.
Do not ask for additional information. Do not mirror the user's language. Do not mirror the task. When the target
language requires, e.g., shorter sentences, you should split the text into shorter sentences.
""";
private bool liveTranslation;
private string inputText = string.Empty;
private string inputTextLastTranslation = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidatingTargetLanguage(CommonLanguages language)
{
if(language == CommonLanguages.AS_IS)
return "Please select a target language.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private async Task TranslateText(bool force)
{
if (!this.inputIsValid)
return;
if(!force && this.inputText == this.inputTextLastTranslation)
return;
this.inputTextLastTranslation = this.inputText;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
The given text is:
---
{this.inputText}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,13 @@
using AIStudio.Tools;
namespace AIStudio.Components.Pages.Translator;
public static class CommonLanguageExtension
{
public static string Prompt(this CommonLanguages language, string customLanguage) => language switch
{
CommonLanguages.OTHER => $"Translate the text in {customLanguage}.",
_ => $"Translate the given text in {language.Name()} ({language}).",
};
}
@@ -7,4 +7,5 @@
<MudThemeProvider />
<MudDialogProvider />
<MudPopoverProvider />
<MudSnackbarProvider />
@@ -45,8 +45,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.6" />
<PackageReference Include="MudBlazor" Version="6.20.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.7" />
<PackageReference Include="MudBlazor" Version="7.4.0" />
<PackageReference Include="MudBlazor.Markdown" Version="1.0.2" />
</ItemGroup>
@@ -0,0 +1,13 @@
namespace AIStudio.Provider.Fireworks;
/// <summary>
/// The Fireworks chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream
);
@@ -0,0 +1,8 @@
namespace AIStudio.Provider.Fireworks;
/// <summary>
/// Chat message model.
/// </summary>
/// <param name="Content">The text content of the message.</param>
/// <param name="Role">The role of the message.</param>
public readonly record struct Message(string Content, string Role);
@@ -0,0 +1,160 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks;
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
#region Implementation of IProvider
/// <inheritdoc />
public string Id => "Fireworks.ai";
/// <inheritdoc />
public string InstanceName { get; set; } = "Fireworks.ai";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
if(!requestedSecret.Success)
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
{
Role = "system",
Content = chatThread.SystemPrompt,
};
// Prepare the Fireworks HTTP chat request:
var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
{
Role = n.Role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",
},
Content = n.Content switch
{
ContentText text => text.Text,
_ => string.Empty,
}
}).ToList()],
// Right now, we only support streaming completions:
Stream = true,
}, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret);
// Set the content:
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
// Open the response stream:
var fireworksStream = await response.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(fireworksStream);
// Read the stream, line by line:
while(!streamReader.EndOfStream)
{
// Check if the token is canceled:
if(token.IsCancellationRequested)
yield break;
// Read the next line:
var line = await streamReader.ReadLineAsync(token);
// Skip empty lines:
if(string.IsNullOrWhiteSpace(line))
continue;
// Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines:
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
continue;
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
yield break;
ResponseStreamLine fireworksResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
fireworksResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
}
catch
{
// Skip invalid JSON data:
continue;
}
// Skip empty responses:
if(fireworksResponse == default || fireworksResponse.Choices.Count == 0)
continue;
// Yield the response:
yield return fireworksResponse.Choices[0].Delta.Content;
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
#endregion
}
@@ -0,0 +1,24 @@
namespace AIStudio.Provider.Fireworks;
/// <summary>
/// Data model for a line in the response stream, for streaming completions.
/// </summary>
/// <param name="Id">The id of the response.</param>
/// <param name="Object">The object describing the response.</param>
/// <param name="Created">The timestamp of the response.</param>
/// <param name="Model">The model used for the response.</param>
/// <param name="Choices">The choices made by the AI.</param>
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, IList<Choice> Choices);
/// <summary>
/// Data model for a choice made by the AI.
/// </summary>
/// <param name="Index">The index of the choice.</param>
/// <param name="Delta">The delta text of the choice.</param>
public readonly record struct Choice(int Index, Delta Delta);
/// <summary>
/// The delta text of a choice.
/// </summary>
/// <param name="Content">The content of the delta text.</param>
public readonly record struct Delta(string Content);
@@ -97,7 +97,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
// Read the stream, line by line:
while(!streamReader.EndOfStream)
{
// Check if the token is cancelled:
// Check if the token is canceled:
if(token.IsCancellationRequested)
yield break;
+33 -17
View File
@@ -1,4 +1,5 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
@@ -10,13 +11,15 @@ namespace AIStudio.Provider;
/// </summary>
public enum Providers
{
NONE,
NONE = 0,
OPEN_AI,
ANTHROPIC,
MISTRAL,
OPEN_AI = 1,
ANTHROPIC = 2,
MISTRAL = 3,
SELF_HOSTED,
FIREWORKS = 5,
SELF_HOSTED = 4,
}
/// <summary>
@@ -37,6 +40,8 @@ public static class ExtensionsProvider
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
@@ -45,18 +50,29 @@ public static class ExtensionsProvider
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="provider">The provider value.</param>
/// <param name="instanceName">The used instance name.</param>
/// <param name="hostname">The hostname of the provider.</param>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Providers provider, string instanceName, string hostname = "http://localhost:1234") => provider switch
public static IProvider CreateProvider(this Settings.Provider providerSettings)
{
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName },
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName },
Providers.MISTRAL => new ProviderMistral { InstanceName = instanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(hostname) { InstanceName = instanceName },
_ => new NoProvider(),
};
try
{
return providerSettings.UsedProvider switch
{
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName },
Providers.FIREWORKS => new ProviderFireworks { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(),
};
}
catch (Exception e)
{
Console.WriteLine($"Failed to create provider: {e.Message}");
return new NoProvider();
}
}
}
@@ -0,0 +1,42 @@
namespace AIStudio.Provider.SelfHosted;
public enum Host
{
NONE,
LM_STUDIO,
LLAMACPP,
OLLAMA,
}
public static class HostExtensions
{
public static string Name(this Host host) => host switch
{
Host.NONE => "None",
Host.LM_STUDIO => "LM Studio",
Host.LLAMACPP => "llama.cpp",
Host.OLLAMA => "ollama",
_ => "Unknown",
};
public static string BaseURL(this Host host) => host switch
{
Host.LM_STUDIO => "/v1/",
Host.LLAMACPP => "/v1/",
Host.OLLAMA => "/v1/",
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
{
Host.LM_STUDIO => "chat/completions",
Host.LLAMACPP => "chat/completions",
Host.OLLAMA => "chat/completions",
_ => "chat/completions",
};
}
@@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted;
public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostname}/v1/"), IProvider
public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -33,7 +33,7 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna
// Prepare the OpenAI HTTP chat request:
var providerChatRequest = JsonSerializer.Serialize(new ChatRequest
{
Model = (await this.GetTextModels(jsRuntime, settings, token: token)).First().Id,
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
@@ -62,7 +62,7 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna
}, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
// Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
@@ -137,17 +137,33 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna
public async Task<IEnumerable<Provider.Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
{
var request = new HttpRequestMessage(HttpMethod.Get, "models");
var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
try
{
switch (provider.Host)
{
case Host.LLAMACPP:
// Right now, llama.cpp only supports one model.
// There is no API to list the model(s).
return [ new Provider.Model("as configured by llama.cpp") ];
case Host.LM_STUDIO:
case Host.OLLAMA:
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
if(!lmStudioResponse.IsSuccessStatusCode)
return [];
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
if (modelResponse.Data.Length > 1)
Console.WriteLine("Warning: multiple models found; using the first one.");
var firstModel = modelResponse.Data.First();
return [ new Provider.Model(firstModel.Id) ];
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
return lmStudioModelResponse.Data.Select(n => new Provider.Model(n.Id));
}
return [];
}
catch(Exception e)
{
Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}");
return [];
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+6 -1
View File
@@ -9,7 +9,7 @@ public sealed class Data
/// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V2;
public Version Version { get; init; } = Version.V3;
/// <summary>
/// List of configured providers.
@@ -51,4 +51,9 @@ public sealed class Data
/// The chat storage maintenance behavior.
/// </summary>
public WorkspaceStorageTemporaryMaintenancePolicy WorkspaceStorageTemporaryMaintenancePolicy { get; set; } = WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_90_DAYS;
/// <summary>
/// The navigation behavior.
/// </summary>
public NavBehavior NavigationBehavior { get; set; } = NavBehavior.EXPAND_ON_HOVER;
}
@@ -0,0 +1,9 @@
namespace AIStudio.Settings;
public enum NavBehavior
{
EXPAND_ON_HOVER,
NEVER_EXPAND_USE_TOOLTIPS,
NEVER_EXPAND_NO_TOOLTIPS,
ALWAYS_EXPAND,
}
+12 -2
View File
@@ -1,5 +1,7 @@
using AIStudio.Provider;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings;
/// <summary>
@@ -12,7 +14,15 @@ namespace AIStudio.Settings;
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
/// <param name="Model">The LLM model to use for chat.</param>
public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model, bool IsSelfHosted = false, string Hostname = "http://localhost:1234")
public readonly record struct Provider(
uint Num,
string Id,
string InstanceName,
Providers UsedProvider,
Model Model,
bool IsSelfHosted = false,
string Hostname = "http://localhost:1234",
Host Host = Host.NONE)
{
#region Overrides of ValueType
@@ -24,7 +34,7 @@ public readonly record struct Provider(uint Num, string Id, string InstanceName,
public override string ToString()
{
if(this.IsSelfHosted)
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Hostname}, {this.Model})";
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Host}, {this.Hostname}, {this.Model})";
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Model})";
}
@@ -1,4 +1,5 @@
@using AIStudio.Provider
@using AIStudio.Provider.SelfHosted
@using MudBlazor
<MudDialog>
@@ -12,7 +13,7 @@
<MudSelectItem Value="@provider">@provider</MudSelectItem>
}
</MudSelect>
<MudButton Disabled="@this.IsSelfHostedOrNone" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.GetProviderCreationURL()" Target="_blank">Create account</MudButton>
<MudButton Disabled="@(!this.ShowRegisterButton)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.GetProviderCreationURL()" Target="_blank">Create account</MudButton>
</MudStack>
@* ReSharper disable once CSharpWarnings::CS8974 *@
@@ -20,7 +21,7 @@
T="string"
@bind-Text="@this.dataAPIKey"
Label="API Key"
Disabled="@this.IsSelfHostedOrNone"
Disabled="@(!this.NeedAPIKey)"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.VpnKey"
@@ -33,22 +34,48 @@
T="string"
@bind-Text="@this.DataHostname"
Label="Hostname"
Disabled="@this.IsCloudProvider"
Disabled="@(!this.NeedHostname)"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Validation="@this.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudSelect Disabled="@(!this.NeedHost)" @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
<MudSelectItem Value="@host">@host.Name()</MudSelectItem>
}
</MudSelect>
<MudStack Row="@true" AlignItems="AlignItems.Center">
<MudButton Disabled="@(!this.CanLoadModels)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Load</MudButton>
<MudSelect Disabled="@this.IsSelfHostedOrNone" @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel">
@foreach (var model in this.availableModels)
{
<MudSelectItem Value="@model">@model</MudSelectItem>
}
</MudSelect>
@if (this.ProvideModelManually)
{
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.GetModelOverviewURL()" Target="_blank">Show available models</MudButton>
<MudTextField
T="string"
@bind-Text="@this.dataManuallyModel"
Label="Model"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
}
else
{
<MudButton Disabled="@(!this.CanLoadModels())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Load</MudButton>
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel">
@foreach (var model in this.availableModels)
{
<MudSelectItem Value="@model">@model</MudSelectItem>
}
</MudSelect>
}
</MudStack>
@* ReSharper disable once CSharpWarnings::CS8974 *@
@@ -61,7 +88,7 @@
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Validation="@this.ValidatingInstanceName"
UserAttributes="@INSTANCE_NAME_ATTRIBUTES"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
</MudForm>
@@ -70,10 +97,10 @@
{
<MudPaper Class="pa-2 mt-3">
<MudText Typo="Typo.h6">Issues</MudText>
<MudList Clickable="@true">
<MudList T="string">
@foreach (var issue in this.dataIssues)
{
<MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
<MudListItem T="string" Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
@issue
</MudListItem>
}
@@ -4,6 +4,8 @@ using AIStudio.Provider;
using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings;
/// <summary>
@@ -38,6 +40,12 @@ public partial class ProviderDialog : ComponentBase
[Parameter]
public string DataHostname { get; set; } = string.Empty;
/// <summary>
/// The local host to use, e.g., llama.cpp.
/// </summary>
[Parameter]
public Host DataHost { get; set; } = Host.NONE;
/// <summary>
/// Is this provider self-hosted?
/// </summary>
@@ -68,7 +76,7 @@ public partial class ProviderDialog : ComponentBase
[Inject]
private IJSRuntime JsRuntime { get; set; } = null!;
private static readonly Dictionary<string, object?> INSTANCE_NAME_ATTRIBUTES = new();
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used instance names. We need this to check for uniqueness.
@@ -78,6 +86,7 @@ public partial class ProviderDialog : ComponentBase
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@@ -85,13 +94,25 @@ public partial class ProviderDialog : ComponentBase
private MudForm form = null!;
private readonly List<Model> availableModels = new();
private Provider CreateProviderSettings() => new()
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedProvider = this.DataProvider,
Model = this.DataProvider is Providers.FIREWORKS ? new Model(this.dataManuallyModel) : this.DataModel,
IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED,
Hostname = this.DataHostname.EndsWith('/') ? this.DataHostname[..^1] : this.DataHostname,
Host = this.DataHost,
};
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(INSTANCE_NAME_ATTRIBUTES);
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
@@ -100,9 +121,24 @@ public partial class ProviderDialog : ComponentBase
if(this.IsEditing)
{
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
var provider = this.DataProvider.CreateProvider(this.DataInstanceName);
if(provider is NoProvider)
//
// We cannot load the API key for self-hosted providers:
//
if (this.DataProvider is Providers.SELF_HOSTED)
{
await this.ReloadModels();
await base.OnInitializedAsync();
return;
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider();
if(provider is NoProvider)
{
await base.OnInitializedAsync();
return;
}
// Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
@@ -111,8 +147,7 @@ public partial class ProviderDialog : ComponentBase
this.dataAPIKey = requestedSecret.Secret;
// Now, we try to load the list of available models:
if(this.DataProvider is not Providers.SELF_HOSTED)
await this.ReloadModels();
await this.ReloadModels();
}
else
{
@@ -148,30 +183,23 @@ public partial class ProviderDialog : ComponentBase
// Use the data model to store the provider.
// We just return this data to the parent component:
var addedProvider = new Provider
var addedProviderSettings = this.CreateProviderSettings();
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedProvider = this.DataProvider,
Model = this.DataModel,
IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED,
Hostname = this.DataHostname,
};
// We need to instantiate the provider to store the API key:
var provider = this.DataProvider.CreateProvider(this.DataInstanceName);
// We need to instantiate the provider to store the API key:
var provider = addedProviderSettings.CreateProvider();
// Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
if (!storeResponse.Success)
{
this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again.";
await this.form.Validate();
return;
// Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
if (!storeResponse.Success)
{
this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again.";
await this.form.Validate();
return;
}
}
this.MudDialog.Close(DialogResult.Ok(addedProvider));
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
}
private string? ValidatingProvider(Providers provider)
@@ -182,9 +210,28 @@ public partial class ProviderDialog : ComponentBase
return null;
}
private string? ValidatingHost(Host host)
{
if(this.DataProvider is not Providers.SELF_HOSTED)
return null;
if (host == Host.NONE)
return "Please select a host.";
return null;
}
private string? ValidateManuallyModel(string manuallyModel)
{
if (this.DataProvider is Providers.FIREWORKS && string.IsNullOrWhiteSpace(manuallyModel))
return "Please enter a model name.";
return null;
}
private string? ValidatingModel(Model model)
{
if(this.DataProvider is Providers.SELF_HOSTED)
if(this.DataProvider is Providers.SELF_HOSTED && this.DataHost == Host.LLAMACPP)
return null;
if (model == default)
@@ -196,7 +243,7 @@ public partial class ProviderDialog : ComponentBase
[GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")]
private static partial Regex InstanceNameRegex();
private static readonly string[] RESERVED_NAMES = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
private static readonly string[] RESERVED_NAMES = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"];
private string? ValidatingInstanceName(string instanceName)
{
@@ -270,10 +317,11 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels()
{
var provider = this.DataProvider.CreateProvider("temp");
var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider();
if(provider is NoProvider)
return;
var models = await provider.GetTextModels(this.JsRuntime, this.SettingsManager, this.dataAPIKey);
// Order descending by ID means that the newest models probably come first:
@@ -283,11 +331,84 @@ public partial class ProviderDialog : ComponentBase
this.availableModels.AddRange(orderedModels);
}
private bool CanLoadModels => !string.IsNullOrWhiteSpace(this.dataAPIKey) && this.DataProvider != Providers.NONE && this.DataProvider != Providers.SELF_HOSTED;
private bool CanLoadModels()
{
if (this.DataProvider is Providers.SELF_HOSTED)
{
switch (this.DataHost)
{
case Host.NONE:
return false;
case Host.LLAMACPP:
return false;
case Host.LM_STUDIO:
return true;
case Host.OLLAMA:
return true;
default:
return false;
}
}
if(this.DataProvider is Providers.NONE)
return false;
if(string.IsNullOrWhiteSpace(this.dataAPIKey))
return false;
return true;
}
private bool ShowRegisterButton => this.DataProvider switch
{
Providers.OPEN_AI => true,
Providers.MISTRAL => true,
Providers.ANTHROPIC => true,
Providers.FIREWORKS => true,
_ => false,
};
private bool NeedAPIKey => this.DataProvider switch
{
Providers.OPEN_AI => true,
Providers.MISTRAL => true,
Providers.ANTHROPIC => true,
Providers.FIREWORKS => true,
_ => false,
};
private bool NeedHostname => this.DataProvider switch
{
Providers.SELF_HOSTED => true,
_ => false,
};
private bool IsCloudProvider => this.DataProvider is not Providers.SELF_HOSTED;
private bool NeedHost => this.DataProvider switch
{
Providers.SELF_HOSTED => true,
_ => false,
};
private bool IsSelfHostedOrNone => this.DataProvider is Providers.SELF_HOSTED or Providers.NONE;
private bool ProvideModelManually => this.DataProvider switch
{
Providers.FIREWORKS => true,
_ => false,
};
private string GetModelOverviewURL() => this.DataProvider switch
{
Providers.FIREWORKS => "https://fireworks.ai/models?show=Serverless",
_ => string.Empty,
};
private string GetProviderCreationURL() => this.DataProvider switch
{
@@ -295,6 +416,10 @@ public partial class ProviderDialog : ComponentBase
Providers.MISTRAL => "https://console.mistral.ai/",
Providers.ANTHROPIC => "https://console.anthropic.com/dashboard",
Providers.FIREWORKS => "https://fireworks.ai/login",
_ => string.Empty,
};
private bool IsNoneProvider => this.DataProvider is Providers.NONE;
}
@@ -1,3 +1,5 @@
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings;
public static class SettingsMigrations
@@ -7,7 +9,11 @@ public static class SettingsMigrations
switch (previousData.Version)
{
case Version.V1:
return MigrateFromV1(previousData);
previousData = MigrateV1ToV2(previousData);
return MigrateV2ToV3(previousData);
case Version.V2:
return MigrateV2ToV3(previousData);
default:
Console.WriteLine("No migration needed.");
@@ -15,7 +21,7 @@ public static class SettingsMigrations
}
}
private static Data MigrateFromV1(Data previousData)
private static Data MigrateV1ToV2(Data previousData)
{
//
// Summary:
@@ -36,4 +42,33 @@ public static class SettingsMigrations
UpdateBehavior = previousData.UpdateBehavior,
};
}
private static Data MigrateV2ToV3(Data previousData)
{
//
// Summary:
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
//
Console.WriteLine("Migrating from v2 to v3...");
return new()
{
Version = Version.V3,
Providers = previousData.Providers.Select(provider =>
{
if(provider.IsSelfHosted)
return provider with { Host = Host.LM_STUDIO };
return provider with { Host = Host.NONE };
}).ToList(),
EnableSpellchecking = previousData.EnableSpellchecking,
IsSavingEnergy = previousData.IsSavingEnergy,
NextProviderNum = previousData.NextProviderNum,
ShortcutSendBehavior = previousData.ShortcutSendBehavior,
UpdateBehavior = previousData.UpdateBehavior,
WorkspaceStorageBehavior = previousData.WorkspaceStorageBehavior,
WorkspaceStorageTemporaryMaintenancePolicy = previousData.WorkspaceStorageTemporaryMaintenancePolicy,
};
}
}
@@ -10,4 +10,5 @@ public enum Version
V1,
V2,
V3,
}
@@ -0,0 +1,22 @@
namespace AIStudio.Tools;
public static class CommonLanguageExtensions
{
public static string Name(this CommonLanguages language) => language switch
{
CommonLanguages.AS_IS => "Do not change the language",
CommonLanguages.EN_US => "English (US)",
CommonLanguages.EN_GB => "English (UK)",
CommonLanguages.ZH_CN => "Chinese (Simplified)",
CommonLanguages.HI_IN => "Hindi (India)",
CommonLanguages.ES_ES => "Spanish (Spain)",
CommonLanguages.FR_FR => "French (France)",
CommonLanguages.DE_DE => "German (Germany)",
CommonLanguages.DE_AT => "German (Austria)",
CommonLanguages.DE_CH => "German (Switzerland)",
CommonLanguages.JA_JP => "Japanese (Japan)",
_ => "Other",
};
}
@@ -0,0 +1,19 @@
namespace AIStudio.Tools;
public enum CommonLanguages
{
AS_IS,
EN_US,
EN_GB,
ZH_CN,
HI_IN,
ES_ES,
FR_FR,
DE_DE,
DE_CH,
DE_AT,
JA_JP,
OTHER,
}
+1
View File
@@ -6,6 +6,7 @@ public enum Event
// Common events:
STATE_HAS_CHANGED,
CONFIGURATION_CHANGED,
// Update events:
USER_SEARCH_FOR_UPDATE,
+35 -35
View File
@@ -4,9 +4,9 @@
"net8.0": {
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
"requested": "[8.0.6, )",
"resolved": "8.0.6",
"contentHash": "hQlf5+YxiUbKdpaPBf/zdMGItnWF8ai9ToPjeZ6gnxT10V4RGDKvChl5MSdc838+2SRHLAC0cdJwG0+L7dqR0g==",
"requested": "[8.0.7, )",
"resolved": "8.0.7",
"contentHash": "ABsn0T09b5lzVNbOcuRc10+kNZkO+RGtZWfzqVay0Ah+/ouhEvG7JrXc+9+7zFgoPuH4E4N6+uWfTp+pJqMeGw==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0"
}
@@ -19,13 +19,13 @@
},
"MudBlazor": {
"type": "Direct",
"requested": "[6.20.0, )",
"resolved": "6.20.0",
"contentHash": "2MqW/E1OLszSqDhW06rpRTN4OpIRJU0iVzeiZLPzymckrKJH1Hg09St+O3kaEnsbXRcjcdM9iE5cSKzCgxvySQ==",
"requested": "[7.4.0, )",
"resolved": "7.4.0",
"contentHash": "dm8ZD6OhyI6icE3Si0V3BJvwPZp8fEspPFq4/iQXU8/i7L39/ny2bPgQqPnQmIojppd9cR/H4RdsaBlSLe6rIw==",
"dependencies": {
"Microsoft.AspNetCore.Components": "8.0.3",
"Microsoft.AspNetCore.Components.Web": "8.0.3",
"Microsoft.Extensions.Localization": "8.0.3"
"Microsoft.AspNetCore.Components": "8.0.6",
"Microsoft.AspNetCore.Components.Web": "8.0.6",
"Microsoft.Extensions.Localization": "8.0.6"
}
},
"MudBlazor.Markdown": {
@@ -45,53 +45,53 @@
},
"Microsoft.AspNetCore.Authorization": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "9Nic17acgZbysUlhGc+TEd9F8jI01kC6+V31sC7/xI5v2OSWGL8NhdYaB/Iu4KnDRoQEolg6qvepGsVfeYpIYA==",
"resolved": "8.0.6",
"contentHash": "H1CSbD7UeSPsrJSUpvbms6SqWMa5y8ch4Rw+gyHh2uztOEb20fTP2r0AUnStn1Q9WYghikiDO5wzkgV+n6KC2Q==",
"dependencies": {
"Microsoft.AspNetCore.Metadata": "8.0.3",
"Microsoft.AspNetCore.Metadata": "8.0.6",
"Microsoft.Extensions.Logging.Abstractions": "8.0.1",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.AspNetCore.Components": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "q1Da8sfxG+B+BSYpc/3RKNEdzGcLbDTXkTUqekY65kXMMVCTqTAQ0Zs4csmB7FNVTFSjwaw1dGMFD0bQ+erlBw==",
"resolved": "8.0.6",
"contentHash": "Je2l+rd5i8gB+ZWsN5wBHOsnyAh81h6+a5vNq6rjfQhM2ZYxH+BVsmanCYa+F8BFy6rmd+ZL61SEiNaAyAOgtg==",
"dependencies": {
"Microsoft.AspNetCore.Authorization": "8.0.3",
"Microsoft.AspNetCore.Components.Analyzers": "8.0.3"
"Microsoft.AspNetCore.Authorization": "8.0.6",
"Microsoft.AspNetCore.Components.Analyzers": "8.0.6"
}
},
"Microsoft.AspNetCore.Components.Analyzers": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "iERLuYM+YFI/K1jkinr1YeAkJYHUcijPiPCKgmgs2ZhJLqiIVJRT08vUtIsfhiFtGiI5MIzK0R1BZHyS3yAQng=="
"resolved": "8.0.6",
"contentHash": "SEL0CN1jJdJkCDwRox3kSY1ffMnahlOCDJZYrYWfQ6ftjqRuuiPtwyvI3VIeVHLcpT3VP1AXww8wzKDK3oeFxg=="
},
"Microsoft.AspNetCore.Components.Forms": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "OxY5NDmePnn6FMb+Fum57YL7LCHk3u2Wg0qSln3uZSayo+oIxYuoGnqH2dUMp1P5vOPfq17NKCIIEbxfU2dirQ==",
"resolved": "8.0.6",
"contentHash": "49wIE1ns3ZGDU4NPMcKCSTC716IxujUeaynsUyflM5Qmb2WlF6M6m4pMcJ6UET1Ixmn027nZjMEc8J7pU4BLBg==",
"dependencies": {
"Microsoft.AspNetCore.Components": "8.0.3"
"Microsoft.AspNetCore.Components": "8.0.6"
}
},
"Microsoft.AspNetCore.Components.Web": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "bHWJiz/JhjptK3iYzha0Rm73chjFcbMAOD9DdDq2tn1rp4rQa/K7O/zdnZpSYAT3nI33Q0aY6ts6t0PUVu5hCA==",
"resolved": "8.0.6",
"contentHash": "xxYlTpcTEWsxaWUscDLN32mM32ysslLFtNQBS6wJOxaKJ9LCvusXDyBVEKL2DPbB8PZdereNvyrVr0deHkuOEw==",
"dependencies": {
"Microsoft.AspNetCore.Components": "8.0.3",
"Microsoft.AspNetCore.Components.Forms": "8.0.3",
"Microsoft.AspNetCore.Components": "8.0.6",
"Microsoft.AspNetCore.Components.Forms": "8.0.6",
"Microsoft.Extensions.DependencyInjection": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0",
"Microsoft.JSInterop": "8.0.3",
"Microsoft.JSInterop": "8.0.6",
"System.IO.Pipelines": "8.0.0"
}
},
"Microsoft.AspNetCore.Metadata": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "MAdmICjtSckGDutLRMydRI0pvBcGw/WoC4UC+hZ999idVW96l5iJlhyrtakgYkF5Rp0ekNVNWA+onP2gJZUkpw=="
"resolved": "8.0.6",
"contentHash": "3e7S/kz1MGds8zDA8SfQiutqifyVcULs5P/8Bxpes0WIkJRMGkU/l+XlMllj9KrDfCGwHW8bgjRy/zxfoILgfg=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
@@ -116,19 +116,19 @@
},
"Microsoft.Extensions.Localization": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "EjHnIiEwNq9xl8S36hf0nj64IF3DTQ1l5PVK8Vo+QocYKDKpDPONHNJC8env0DPwod7T5oA1HYLLo2mdSTCgrQ==",
"resolved": "8.0.6",
"contentHash": "EXcSpQE6E80Qutej0ZShz1oRWBFocDzir/R3Gd1jAqZ5SnPpSr1Ep/tH5ha0cRXY22cWVuOb7I9rM32Ew+ju7g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1",
"Microsoft.Extensions.Localization.Abstractions": "8.0.3",
"Microsoft.Extensions.Localization.Abstractions": "8.0.6",
"Microsoft.Extensions.Logging.Abstractions": "8.0.1",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Localization.Abstractions": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "k/kUPm1FQBxcs9/vsM1eF4qIOg2Sovqh/+KUGHur5Mc0Y3OFGuoz9ktBX7LA0gPz53SZhW3W3oaSaMFFcjgM6Q=="
"resolved": "8.0.6",
"contentHash": "oQdKQ4xlb+Qa4t1gWGj58cocLdWZynP+wLExIAxozh2SrvffEzTvTGI2IbRx4mBNgg9jbbyoPE8nNOwJ1Thddg=="
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
@@ -154,8 +154,8 @@
},
"Microsoft.JSInterop": {
"type": "Transitive",
"resolved": "8.0.3",
"contentHash": "Oi21Fa7KubCzafwXb2IOdSGg24+/ylYGwrJgAYdWmgXBj04Oj/1b8vr9hrcoFKjQ6K18ryHYh35ZO/CCIEhuzg=="
"resolved": "8.0.6",
"contentHash": "E1djWS3d41fcd++7sGlbYhOHde5Pb0oBpOcNvUbn+1ga/yCvsjzUfbd/tDRg1qacNKS0iwKWYOIqTZxJnh99dQ=="
},
"System.IO.Pipelines": {
"type": "Transitive",
@@ -0,0 +1,5 @@
# v0.8.0, build 162 (2024-07-14 19:39 UTC)
- Added overview of assistants
- Added icon finder assistant
- Added text summarization assistant
- Added translation assistant
@@ -0,0 +1,4 @@
# v0.8.1, build 163 (2024-07-16 08:32 UTC)
- Added support for ollama as a self-hosted provider
- Added support for model selection of self-hosted providers
- Fixed a bug where the spellchecking setting was not applied to assistants
@@ -0,0 +1,3 @@
# v0.8.2, build 164 (2024-07-16 18:03 UTC)
- Added the coding assistant.
- Fixed the in-app description of possible self-hosted models.
@@ -0,0 +1,6 @@
# v0.8.3, build 165 (2024-07-25 13:25 UTC)
- Added an option to configure the behavior of the navigation bar in the settings
- Added support for Fireworks.ai as provider, where you can use e.g., the llama 3.1 405b model
- Improved the handling of self-hosted provider hostnames
- Improved the configured provider table: long model names are now truncated
- Migrated UI framework from MudBlazor v6.x.x to v7.x.x
@@ -0,0 +1,2 @@
# v0.8.4, build 166 (2024-07-26 06:53 UTC)
- Fixed issue #44: After upgrading to MudBlazor 7.x.x, text padding was off, and tooltips were not displayed correctly. This issue was shown only on releases, not on the development builds.
@@ -2,4 +2,4 @@
* MudBlazor (https://mudblazor.com/)
* Copyright (c) 2021 MudBlazor
* Licensed under MIT (https://github.com/MudBlazor/MudBlazor/blob/master/LICENSE)
*/.mud-primary{background-color:var(--mud-palette-primary) !important}.mud-primary-text{color:var(--mud-palette-primary) !important}.mud-primary-hover{background-color:var(--mud-palette-primary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-primary-hover:hover{background-color:var(--mud-palette-primary-hover) !important}}.hover\:mud-primary-hover:focus-visible,.hover\:mud-primary-hover:active{background-color:var(--mud-palette-primary-hover) !important}.mud-border-primary{border-color:var(--mud-palette-primary) !important}.mud-theme-primary{color:var(--mud-palette-primary-text) !important;background-color:var(--mud-palette-primary) !important}.mud-secondary{background-color:var(--mud-palette-secondary) !important}.mud-secondary-text{color:var(--mud-palette-secondary) !important}.mud-secondary-hover{background-color:var(--mud-palette-secondary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-secondary-hover:hover{background-color:var(--mud-palette-secondary-hover) !important}}.hover\:mud-secondary-hover:focus-visible,.hover\:mud-secondary-hover:active{background-color:var(--mud-palette-secondary-hover) !important}.mud-border-secondary{border-color:var(--mud-palette-secondary) !important}.mud-theme-secondary{color:var(--mud-palette-secondary-text) !important;background-color:var(--mud-palette-secondary) !important}.mud-tertiary{background-color:var(--mud-palette-tertiary) !important}.mud-tertiary-text{color:var(--mud-palette-tertiary) !important}.mud-tertiary-hover{background-color:var(--mud-palette-tertiary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-tertiary-hover:hover{background-color:var(--mud-palette-tertiary-hover) !important}}.hover\:mud-tertiary-hover:focus-visible,.hover\:mud-tertiary-hover:active{background-color:var(--mud-palette-tertiary-hover) !important}.mud-border-tertiary{border-color:var(--mud-palette-tertiary) !important}.mud-theme-tertiary{color:var(--mud-palette-tertiary-text) !important;background-color:var(--mud-palette-tertiary) !important}.mud-info{background-color:var(--mud-palette-info) !important}.mud-info-text{color:var(--mud-palette-info) !important}.mud-info-hover{background-color:var(--mud-palette-info-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-info-hover:hover{background-color:var(--mud-palette-info-hover) !important}}.hover\:mud-info-hover:focus-visible,.hover\:mud-info-hover:active{background-color:var(--mud-palette-info-hover) !important}.mud-border-info{border-color:var(--mud-palette-info) !important}.mud-theme-info{color:var(--mud-palette-info-text) !important;background-color:var(--mud-palette-info) !important}.mud-success{background-color:var(--mud-palette-success) !important}.mud-success-text{color:var(--mud-palette-success) !important}.mud-success-hover{background-color:var(--mud-palette-success-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-success-hover:hover{background-color:var(--mud-palette-success-hover) !important}}.hover\:mud-success-hover:focus-visible,.hover\:mud-success-hover:active{background-color:var(--mud-palette-success-hover) !important}.mud-border-success{border-color:var(--mud-palette-success) !important}.mud-theme-success{color:var(--mud-palette-success-text) !important;background-color:var(--mud-palette-success) !important}.mud-warning{background-color:var(--mud-palette-warning) !important}.mud-warning-text{color:var(--mud-palette-warning) !important}.mud-warning-hover{background-color:var(--mud-palette-warning-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-warning-hover:hover{background-color:var(--mud-palette-warning-hover) !important}}.hover\:mud-warning-hover:focus-visible,.hover\:mud-warning-hover:active{background-color:var(--mud-palette-warning-hover) !important}.mud-border-warning{border-color:var(--mud-palette-warning) !important}.mud-theme-warning{color:var(--mud-palette-warning-text) !important;background-color:var(--mud-palette-warning) !important}.mud-error{background-color:var(--mud-palette-error) !important}.mud-error-text{color:var(--mud-palette-error) !important}.mud-error-hover{background-color:var(--mud-palette-error-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-error-hover:hover{background-color:var(--mud-palette-error-hover) !important}}.hover\:mud-error-hover:focus-visible,.hover\:mud-error-hover:active{background-color:var(--mud-palette-error-hover) !important}.mud-border-error{border-color:var(--mud-palette-error) !important}.mud-theme-error{color:var(--mud-palette-error-text) !important;background-color:var(--mud-palette-error) !important}.mud-dark{background-color:var(--mud-palette-dark) !important}.mud-dark-text{color:var(--mud-palette-dark) !important}.mud-dark-hover{background-color:var(--mud-palette-dark-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-dark-hover:hover{background-color:var(--mud-palette-dark-hover) !important}}.hovLine truncated
*/.mud-primary{background-color:var(--mud-palette-primary) !important}.mud-primary-text{color:var(--mud-palette-primary) !important;--mud-ripple-color: var(--mud-palette-primary) !important}.mud-primary-hover{background-color:var(--mud-palette-primary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-primary-hover:hover{background-color:var(--mud-palette-primary-hover) !important}}.hover\:mud-primary-hover:focus-visible,.hover\:mud-primary-hover:active{background-color:var(--mud-palette-primary-hover) !important}.mud-border-primary{border-color:var(--mud-palette-primary) !important}.mud-theme-primary{color:var(--mud-palette-primary-text) !important;background-color:var(--mud-palette-primary) !important}.mud-secondary{background-color:var(--mud-palette-secondary) !important}.mud-secondary-text{color:var(--mud-palette-secondary) !important;--mud-ripple-color: var(--mud-palette-secondary) !important}.mud-secondary-hover{background-color:var(--mud-palette-secondary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-secondary-hover:hover{background-color:var(--mud-palette-secondary-hover) !important}}.hover\:mud-secondary-hover:focus-visible,.hover\:mud-secondary-hover:active{background-color:var(--mud-palette-secondary-hover) !important}.mud-border-secondary{border-color:var(--mud-palette-secondary) !important}.mud-theme-secondary{color:var(--mud-palette-secondary-text) !important;background-color:var(--mud-palette-secondary) !important}.mud-tertiary{background-color:var(--mud-palette-tertiary) !important}.mud-tertiary-text{color:var(--mud-palette-tertiary) !important;--mud-ripple-color: var(--mud-palette-tertiary) !important}.mud-tertiary-hover{background-color:var(--mud-palette-tertiary-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-tertiary-hover:hover{background-color:var(--mud-palette-tertiary-hover) !important}}.hover\:mud-tertiary-hover:focus-visible,.hover\:mud-tertiary-hover:active{background-color:var(--mud-palette-tertiary-hover) !important}.mud-border-tertiary{border-color:var(--mud-palette-tertiary) !important}.mud-theme-tertiary{color:var(--mud-palette-tertiary-text) !important;background-color:var(--mud-palette-tertiary) !important}.mud-info{background-color:var(--mud-palette-info) !important}.mud-info-text{color:var(--mud-palette-info) !important;--mud-ripple-color: var(--mud-palette-info) !important}.mud-info-hover{background-color:var(--mud-palette-info-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-info-hover:hover{background-color:var(--mud-palette-info-hover) !important}}.hover\:mud-info-hover:focus-visible,.hover\:mud-info-hover:active{background-color:var(--mud-palette-info-hover) !important}.mud-border-info{border-color:var(--mud-palette-info) !important}.mud-theme-info{color:var(--mud-palette-info-text) !important;background-color:var(--mud-palette-info) !important}.mud-success{background-color:var(--mud-palette-success) !important}.mud-success-text{color:var(--mud-palette-success) !important;--mud-ripple-color: var(--mud-palette-success) !important}.mud-success-hover{background-color:var(--mud-palette-success-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-success-hover:hover{background-color:var(--mud-palette-success-hover) !important}}.hover\:mud-success-hover:focus-visible,.hover\:mud-success-hover:active{background-color:var(--mud-palette-success-hover) !important}.mud-border-success{border-color:var(--mud-palette-success) !important}.mud-theme-success{color:var(--mud-palette-success-text) !important;background-color:var(--mud-palette-success) !important}.mud-warning{background-color:var(--mud-palette-warning) !important}.mud-warning-text{color:var(--mud-palette-warning) !important;--mud-ripple-color: var(--mud-palette-warning) !important}.mud-warning-hover{background-color:var(--mud-palette-warning-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-warning-hover:hover{background-color:var(--mud-palette-warning-hover) !important}}.hover\:mud-warning-hover:focus-visible,.hover\:mud-warning-hover:active{background-color:var(--mud-palette-warning-hover) !important}.mud-border-warning{border-color:var(--mud-palette-warning) !important}.mud-theme-warning{color:var(--mud-palette-warning-text) !important;background-color:var(--mud-palette-warning) !important}.mud-error{background-color:var(--mud-palette-error) !important}.mud-error-text{color:var(--mud-palette-error) !important;--mud-ripple-color: var(--mud-palette-error) !important}.mud-error-hover{background-color:var(--mud-palette-error-hover) !important}@media(hover: hover)and (pointer: fine){.hover\:mud-error-hover:hover{background-color:var(--mud-palette-error-hover) !important}}.hover\:mud-error-hover:focus-visible,.hover\:mud-error-hover:active{background-color:var(--mud-palette-error-hover) !important}.mud-border-error{border-color:var(--mud-palette-error) !important}.mud-theme-error{color:var(--mud-paleLine truncated
+172 -163
View File
@@ -1,11 +1,28 @@
window.mudDragAndDrop={initDropZone:(id)=>{const elem=document.getElementById('mud-drop-zone-'+id);elem.addEventListener('dragover',()=>event.preventDefault());elem.addEventListener('dragstart',()=>event.dataTransfer.setData('',event.target.id));},makeDropZonesNotRelative:()=>{var firstDropItems=Array.from(document.getElementsByClassName('mud-drop-item')).filter(x=>x.getAttribute('index')=="-1");for(let dropItem of firstDropItems){dropItem.style.position='static';}
const dropZones=document.getElementsByClassName('mud-drop-zone');for(let dropZone of dropZones){dropZone.style.position='unset';}},getDropZoneIdentifierOnPosition:(x,y)=>{const elems=document.elementsFromPoint(x,y);const dropZones=elems.filter(e=>e.classList.contains('mud-drop-zone'))
const dropZone=dropZones[0];if(dropZone){return dropZone.getAttribute('identifier')||"";}
return"";},getDropIndexOnPosition:(x,y,id)=>{const elems=document.elementsFromPoint(x,y);const dropItems=elems.filter(e=>e.classList.contains('mud-drop-item')&&e.id!=('mud-drop-item-'+id))
const dropItem=dropItems[0];if(dropItem){return dropItem.getAttribute('index')||"";}
return"";},makeDropZonesRelative:()=>{const dropZones=document.getElementsByClassName('mud-drop-zone');for(let dropZone of dropZones){dropZone.style.position='relative';}
var firstDropItems=Array.from(document.getElementsByClassName('mud-drop-item')).filter(x=>x.getAttribute('index')=="-1");for(let dropItem of firstDropItems){dropItem.style.position='relative';}},moveItemByDifference:(id,dx,dy)=>{const elem=document.getElementById('mud-drop-item-'+id);var tx=(parseFloat(elem.getAttribute('data-x'))||0)+dx;var ty=(parseFloat(elem.getAttribute('data-y'))||0)+dy;elem.style.webkitTransform=elem.style.transform='translate3d('+tx+'px, '+ty+'px, 10px)';elem.setAttribute('data-x',tx);elem.setAttribute('data-y',ty);},resetItem:(id)=>{const elem=document.getElementById('mud-drop-item-'+id);if(elem){elem.style.webkitTransform=elem.style.transform='';elem.setAttribute('data-x',0);elem.setAttribute('data-y',0);}}};class MudKeyInterceptorFactory{connect(dotNetRef,elementId,options){if(!elementId)
window.getTabbableElements=(element)=>{return element.querySelectorAll("a[href]:not([tabindex='-1']),"+"area[href]:not([tabindex='-1']),"+"button:not([disabled]):not([tabindex='-1']),"+"input:not([disabled]):not([tabindex='-1']):not([type='hidden']),"+"select:not([disabled]):not([tabindex='-1']),"+"textarea:not([disabled]):not([tabindex='-1']),"+"iframe:not([tabindex='-1']),"+"details:not([tabindex='-1']),"+"[tabindex]:not([tabindex='-1']),"+"[contentEditable=true]:not([tabindex='-1'])");};window.serializeParameter=(data,spec)=>{if(typeof data=="undefined"||data===null){return null;}
if(typeof data==="number"||typeof data==="string"||typeof data=="boolean"){return data;}
let res=(Array.isArray(data))?[]:{};if(!spec){spec="*";}
for(let i in data){let currentMember=data[i];if(typeof currentMember==='function'||currentMember===null){continue;}
let currentMemberSpec;if(spec!="*"){currentMemberSpec=Array.isArray(data)?spec:spec[i];if(!currentMemberSpec){continue;}}else{currentMemberSpec="*"}
if(typeof currentMember==='object'){if(Array.isArray(currentMember)||currentMember.length){res[i]=[];for(let j=0;j<currentMember.length;j++){const arrayItem=currentMember[j];if(typeof arrayItem==='object'){res[i].push(this.serializeParameter(arrayItem,currentMemberSpec));}else{res[i].push(arrayItem);}}}else{if(currentMember.length===0){res[i]=[];}else{res[i]=this.serializeParameter(currentMember,currentMemberSpec);}}}else{if(currentMember===Infinity){currentMember="Infinity";}
if(currentMember!==null){res[i]=currentMember;}}}
return res;};class MudResizeObserverFactory{constructor(){this._maps={};}
connect(id,dotNetRef,elements,elementIds,options){var existingEntry=this._maps[id];if(!existingEntry){var observer=new MudResizeObserver(dotNetRef,options);this._maps[id]=observer;}
var result=this._maps[id].connect(elements,elementIds);return result;}
disconnect(id,element){var existingEntry=this._maps[id];if(existingEntry){existingEntry.disconnect(element);}}
cancelListener(id){var existingEntry=this._maps[id];if(existingEntry){existingEntry.cancelListener();delete this._maps[id];}}}
class MudResizeObserver{constructor(dotNetRef,options){this.logger=options.enableLogging?console.log:(message)=>{};this.options=options;this._dotNetRef=dotNetRef
var delay=(this.options||{}).reportRate||200;this.throttleResizeHandlerId=-1;var observervedElements=[];this._observervedElements=observervedElements;this.logger('[MudBlazor | ResizeObserver] Observer initialized');this._resizeObserver=new ResizeObserver(entries=>{var changes=[];this.logger('[MudBlazor | ResizeObserver] changes detected');for(let entry of entries){var target=entry.target;var affectedObservedElement=observervedElements.find((x)=>x.element==target);if(affectedObservedElement){var size=entry.target.getBoundingClientRect();if(affectedObservedElement.isInitialized==true){changes.push({id:affectedObservedElement.id,size:size});}
else{affectedObservedElement.isInitialized=true;}}}
if(changes.length>0){if(this.throttleResizeHandlerId>=0){clearTimeout(this.throttleResizeHandlerId);}
this.throttleResizeHandlerId=window.setTimeout(this.resizeHandler.bind(this,changes),delay);}});}
resizeHandler(changes){try{this.logger("[MudBlazor | ResizeObserver] OnSizeChanged handler invoked");this._dotNetRef.invokeMethodAsync("OnSizeChanged",changes);}catch(error){this.logger("[MudBlazor | ResizeObserver] Error in OnSizeChanged handler:",{error});}}
connect(elements,ids){var result=[];this.logger('[MudBlazor | ResizeObserver] Start observing elements...');for(var i=0;i<elements.length;i++){var newEntry={element:elements[i],id:ids[i],isInitialized:false,};this.logger("[MudBlazor | ResizeObserver] Start observing element:",{newEntry});result.push(elements[i].getBoundingClientRect());this._observervedElements.push(newEntry);this._resizeObserver.observe(elements[i]);}
return result;}
disconnect(elementId){this.logger('[MudBlazor | ResizeObserver] Try to unobserve element with id',{elementId});var affectedObservedElement=this._observervedElements.find((x)=>x.id==elementId);if(affectedObservedElement){var element=affectedObservedElement.element;this._resizeObserver.unobserve(element);this.logger('[MudBlazor | ResizeObserver] Element found. Ubobserving size changes of element',{element});var index=this._observervedElements.indexOf(affectedObservedElement);this._observervedElements.splice(index,1);}}
cancelListener(){this.logger('[MudBlazor | ResizeObserver] Closing ResizeObserver. Detaching all observed elements');this._resizeObserver.disconnect();this._dotNetRef=undefined;}}
window.mudResizeObserver=new MudResizeObserverFactory();class MudInput{resetValue(id){const input=document.getElementById(id);if(input){input.value='';}}}
window.mudInput=new MudInput();class MudKeyInterceptorFactory{connect(dotNetRef,elementId,options){if(!elementId)
throw"elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"no element found for id: "+elementId;if(!element.mudKeyInterceptor)
element.mudKeyInterceptor=new MudKeyInterceptor(dotNetRef,options);element.mudKeyInterceptor.connect(element);}
@@ -59,69 +76,86 @@ processKeyUp(args,keyOptions){if(this.matchesKeyCombination(keyOptions.preventUp
args.preventDefault();if(this.matchesKeyCombination(keyOptions.stopUp,args))
args.stopPropagation();}
toKeyboardEventArgs(args){return{Key:args.key,Code:args.code,Location:args.location,Repeat:args.repeat,CtrlKey:args.ctrlKey,ShiftKey:args.shiftKey,AltKey:args.altKey,MetaKey:args.metaKey};}}
class MudThrottledEventManager{constructor(){this.mapper={};}
subscribe(eventName,elementId,projection,throotleInterval,key,properties,dotnetReference){const handlerRef=this.throttleEventHandler.bind(this,key);let elem=document.getElementById(elementId);if(elem){elem.addEventListener(eventName,handlerRef,false);let projector=null;if(projection){const parts=projection.split('.');let functionPointer=window;let functionReferenceFound=true;if(parts.length==0||parts.length==1){functionPointer=functionPointer[projection];}
else{for(let i=0;i<parts.length;i++){functionPointer=functionPointer[parts[i]];if(!functionPointer){functionReferenceFound=false;break;}}}
if(functionReferenceFound===true){projector=functionPointer;}}
this.mapper[key]={eventName:eventName,handler:handlerRef,delay:throotleInterval,timerId:-1,reference:dotnetReference,elementId:elementId,properties:properties,projection:projector,};}}
subscribeGlobal(eventName,throotleInterval,key,properties,dotnetReference){let handlerRef=throotleInterval>0?this.throttleEventHandler.bind(this,key):this.eventHandler.bind(this,key);document.addEventListener(eventName,handlerRef,false);this.mapper[key]={eventName:eventName,handler:handlerRef,delay:throotleInterval,timerId:-1,reference:dotnetReference,elementId:document,properties:properties,projection:null,};}
throttleEventHandler(key,event){const entry=this.mapper[key];if(!entry){return;}
clearTimeout(entry.timerId);entry.timerId=window.setTimeout(this.eventHandler.bind(this,key,event),entry.delay);}
eventHandler(key,event){const entry=this.mapper[key];if(!entry){return;}
var elem=document.getElementById(entry.elementId);if(elem!=event.srcElement&&entry.elementId!=document){return;}
const eventEntry={};for(var i=0;i<entry.properties.length;i++){eventEntry[entry.properties[i]]=event[entry.properties[i]];}
if(entry.projection){if(typeof entry.projection==="function"){entry.projection.apply(null,[eventEntry,event]);}}
entry.reference.invokeMethodAsync('OnEventOccur',key,JSON.stringify(eventEntry));}
unsubscribe(key){const entry=this.mapper[key];if(!entry){return;}
entry.reference=null;if(document==entry.elementId){document.removeEventListener(entry.eventName,entry.handler,false);}else{const elem=document.getElementById(entry.elementId);if(elem){elem.removeEventListener(entry.eventName,entry.handler,false);}}
delete this.mapper[key];}};window.mudThrottledEventManager=new MudThrottledEventManager();window.mudEventProjections={correctOffset:function(eventEntry,event){var target=event.target.getBoundingClientRect();eventEntry.offsetX=event.clientX-target.x;eventEntry.offsetY=event.clientY-target.y;}};class MudJsEventFactory{connect(dotNetRef,elementId,options){if(!elementId)
throw"[MudBlazor | JsEvent] elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"[MudBlazor | JsEvent] no element found for id: "+elementId;if(!element.mudJsEvent)
element.mudJsEvent=new MudJsEvent(dotNetRef,options);element.mudJsEvent.connect(element);}
disconnect(elementId){var element=document.getElementById(elementId);if(!element||!element.mudJsEvent)
return;element.mudJsEvent.disconnect();}
subscribe(elementId,eventName){if(!elementId)
throw"[MudBlazor | JsEvent] elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"[MudBlazor | JsEvent] no element found for id: "+elementId;if(!element.mudJsEvent)
throw"[MudBlazor | JsEvent] please connect before subscribing"
element.mudJsEvent.subscribe(eventName);}
unsubscribe(elementId,eventName){var element=document.getElementById(elementId);if(!element||!element.mudJsEvent)
return;element.mudJsEvent.unsubscribe(element,eventName);}}
window.mudJsEvent=new MudJsEventFactory();class MudJsEvent{constructor(dotNetRef,options){this._dotNetRef=dotNetRef;this._options=options||{};this.logger=options.enableLogging?console.log:(message)=>{};this.logger('[MudBlazor | JsEvent] Initialized',{options});this._subscribedEvents={};}
connect(element){if(!this._options)
return;if(!this._options.targetClass)
throw"_options.targetClass: css class name expected";if(this._observer){return;}
var targetClass=this._options.targetClass;this.logger('[MudBlazor | JsEvent] Start observing DOM of element for changes to child with class ',{element,targetClass});this._element=element;this._observer=new MutationObserver(this.onDomChanged);this._observer.mudJsEvent=this;this._observer.observe(this._element,{attributes:false,childList:true,subtree:true});this._observedChildren=[];}
disconnect(){if(!this._observer)
return;this.logger('[MudBlazor | JsEvent] disconnect mutation observer and event handler ');this._observer.disconnect();this._observer=null;for(const child of this._observedChildren)
this.detachHandlers(child);}
subscribe(eventName){if(this._subscribedEvents[eventName]){return;}
var element=this._element;var targetClass=this._options.targetClass;this._subscribedEvents[eventName]=true;for(const child of element.getElementsByClassName(targetClass)){this.attachHandlers(child);}}
unsubscribe(eventName){if(!this._observer)
return;this.logger('[MudBlazor | JsEvent] unsubscribe event handler '+eventName);this._observer.disconnect();this._observer=null;this._subscribedEvents[eventName]=false;for(const child of this._observedChildren){this.detachHandler(child,eventName);}}
attachHandlers(child){child.mudJsEvent=this;for(var eventName of Object.getOwnPropertyNames(this._subscribedEvents)){if(!this._subscribedEvents[eventName])
continue;this.logger('[MudBlazor | JsEvent] attaching event '+eventName,child);child.addEventListener(eventName,this.eventHandler);}
if(this._observedChildren.indexOf(child)<0)
this._observedChildren.push(child);}
detachHandler(child,eventName){this.logger('[MudBlazor | JsEvent] detaching handler '+eventName,child);child.removeEventListener(eventName,this.eventHandler);}
detachHandlers(child){this.logger('[MudBlazor | JsEvent] detaching handlers ',child);for(var eventName of Object.getOwnPropertyNames(this._subscribedEvents)){if(!this._subscribedEvents[eventName])
continue;child.removeEventListener(eventName,this.eventHandler);}
this._observedChildren=this._observedChildren.filter(x=>x!==child);}
onDomChanged(mutationsList,observer){var self=this.mudJsEvent;var targetClass=self._options.targetClass;for(const mutation of mutationsList){for(const element of mutation.addedNodes){if(element.classList&&element.classList.contains(targetClass)){if(!self._options.TagName||element.tagName==self._options.TagName)
self.attachHandlers(element);}}
for(const element of mutation.removedNodes){if(element.classList&&element.classList.contains(targetClass)){if(!self._options.tagName||element.tagName==self._options.tagName)
self.detachHandlers(element);}}}}
eventHandler(e){var self=this.mudJsEvent;var eventName=e.type;self.logger('[MudBlazor | JsEvent] "'+eventName+'"',e);self["on"+eventName](self,e);}
onkeyup(self,e){const caretPosition=e.target.selectionStart;const invoke=self._subscribedEvents["keyup"];if(invoke){self._dotNetRef.invokeMethodAsync('OnCaretPositionChanged',caretPosition);}}
onclick(self,e){const caretPosition=e.target.selectionStart;const invoke=self._subscribedEvents["click"];if(invoke){self._dotNetRef.invokeMethodAsync('OnCaretPositionChanged',caretPosition);}}
onpaste(self,e){const invoke=self._subscribedEvents["paste"];if(invoke){e.preventDefault();e.stopPropagation();const text=(e.originalEvent||e).clipboardData.getData('text/plain');self._dotNetRef.invokeMethodAsync('OnPaste',text);}}
onselect(self,e){const invoke=self._subscribedEvents["select"];if(invoke){const start=e.target.selectionStart;const end=e.target.selectionEnd;if(start===end)
return;self._dotNetRef.invokeMethodAsync('OnSelect',start,end);}}}
class MudResizeListener{constructor(id){this.logger=function(message){};this.options={};this.throttleResizeHandlerId=-1;this.dotnet=undefined;this.breakpoint=-1;this.id=id;this.handleResize=this.throttleResizeHandler.bind(this);}
class MudElementReference{constructor(){this.listenerId=0;this.eventListeners={};}
focus(element){if(element)
{element.focus();}}
blur(element){if(element){element.blur();}}
focusFirst(element,skip=0,min=0){if(element)
{let tabbables=getTabbableElements(element);if(tabbables.length<=min)
element.focus();else
tabbables[skip].focus();}}
focusLast(element,skip=0,min=0){if(element)
{let tabbables=getTabbableElements(element);if(tabbables.length<=min)
element.focus();else
tabbables[tabbables.length-skip-1].focus();}}
saveFocus(element){if(element)
{element['mudblazor_savedFocus']=document.activeElement;}}
restoreFocus(element){if(element)
{let previous=element['mudblazor_savedFocus'];delete element['mudblazor_savedFocus']
if(previous)
previous.focus();}}
selectRange(element,pos1,pos2){if(element)
{if(element.createTextRange){let selRange=element.createTextRange();selRange.collapse(true);selRange.moveStart('character',pos1);selRange.moveEnd('character',pos2);selRange.select();}else if(element.setSelectionRange){element.setSelectionRange(pos1,pos2);}else if(element.selectionStart){element.selectionStart=pos1;element.selectionEnd=pos2;}
element.focus();}}
select(element){if(element)
{element.select();}}
getBoundingClientRect(element){if(!element)return;var rect=JSON.parse(JSON.stringify(element.getBoundingClientRect()));rect.scrollY=window.scrollY||document.documentElement.scrollTop;rect.scrollX=window.scrollX||document.documentElement.scrollLeft;rect.windowHeight=window.innerHeight;rect.windowWidth=window.innerWidth;return rect;}
changeCss(element,css){if(element)
{element.className=css;}}
removeEventListener(element,event,eventId){element.removeEventListener(event,this.eventListeners[eventId]);delete this.eventListeners[eventId];}
addDefaultPreventingHandler(element,eventName){let listener=function(e){e.preventDefault();}
element.addEventListener(eventName,listener,{passive:false});this.eventListeners[++this.listenerId]=listener;return this.listenerId;}
removeDefaultPreventingHandler(element,eventName,listenerId){this.removeEventListener(element,eventName,listenerId);}
addDefaultPreventingHandlers(element,eventNames){let listeners=[];for(const eventName of eventNames){let listenerId=this.addDefaultPreventingHandler(element,eventName);listeners.push(listenerId);}
return listeners;}
removeDefaultPreventingHandlers(element,eventNames,listenerIds){for(let index=0;index<eventNames.length;++index){const eventName=eventNames[index];const listenerId=listenerIds[index];this.removeDefaultPreventingHandler(element,eventName,listenerId);}}};window.mudElementRef=new MudElementReference();const darkThemeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");window.darkModeChange=()=>{return darkThemeMediaQuery.matches;};function darkModeChangeListener(e){dotNetHelperTheme.invokeMethodAsync('SystemPreferenceChanged',e.matches);}
function watchDarkThemeMedia(dotNetHelper){dotNetHelperTheme=dotNetHelper;darkThemeMediaQuery.addEventListener('change',darkModeChangeListener);}
function stopWatchingDarkThemeMedia(){darkThemeMediaQuery.removeEventListener('change',darkModeChangeListener);}
class MudScrollSpy{constructor(){this.lastKnowElement=null;this.handlerRef=null;}
spying(dotnetReference,containerSelector,sectionClassSelector){this.lastKnowElement=null;this.handlerRef=this.handleScroll.bind(this,dotnetReference,containerSelector,sectionClassSelector);document.addEventListener('scroll',this.handlerRef,true);window.addEventListener('resize',this.handlerRef,true);}
handleScroll(dotnetReference,containerSelector,sectionClassSelector,event){const container=document.querySelector(containerSelector);if(container===null){return;}
const elements=document.getElementsByClassName(sectionClassSelector);if(elements.length===0){return;}
const containerTop=container.tagName==='HTML'?0:container.getBoundingClientRect().top;const containerHeight=container.clientHeight;const center=containerTop+containerHeight/2.0;let minDifference=Number.MAX_SAFE_INTEGER;let foundAbove=false;let elementId='';for(let i=0;i<elements.length;i++){const element=elements[i];const rect=element.getBoundingClientRect();const diff=Math.abs(rect.top-center);if(!foundAbove&&rect.top<center){foundAbove=true;minDifference=diff;elementId=element.id;continue;}
if(foundAbove&&rect.top>=center){continue;}
if(diff<minDifference){minDifference=diff;elementId=element.id;}}
if(elementId!==this.lastKnowElement){this.lastKnowElement=elementId;history.replaceState(null,'',window.location.pathname+"#"+elementId);dotnetReference.invokeMethodAsync('SectionChangeOccured',elementId);}}
activateSection(sectionId){const element=document.getElementById(sectionId);if(element){this.lastKnowElement=sectionId;history.replaceState(null,'',window.location.pathname+"#"+sectionId);}}
scrollToSection(sectionId){if(sectionId){let element=document.getElementById(sectionId);if(element){element.scrollIntoView({behavior:'smooth',block:'center',inline:'start'});}}
else{window.scrollTo({top:0,behavior:'smooth'});}}
unspy(){document.removeEventListener('scroll',this.handlerRef,true);window.removeEventListener('resize',this.handlerRef,true);}};window.mudScrollSpy=new MudScrollSpy();window.mudInputAutoGrow={initAutoGrow:(elem,maxLines)=>{const compStyle=getComputedStyle(elem);const lineHeight=parseFloat(compStyle.getPropertyValue('line-height'));let maxHeight=0;elem.updateParameters=function(newMaxLines){if(newMaxLines>0){maxHeight=lineHeight*newMaxLines;}else{maxHeight=0;}}
elem.adjustAutoGrowHeight=function(didReflow=false){const scrollTops=[];let curElem=elem;while(curElem&&curElem.parentNode&&curElem.parentNode instanceof Element){if(curElem.parentNode.scrollTop){scrollTops.push([curElem.parentNode,curElem.parentNode.scrollTop]);}
curElem=curElem.parentNode;}
elem.style.height=0;if(didReflow){elem.style.textAlign=null;}
let minHeight=lineHeight*elem.rows;let newHeight=Math.max(minHeight,elem.scrollHeight);let initialOverflowY=elem.style.overflowY;if(maxHeight>0&&newHeight>maxHeight){elem.style.overflowY='auto';newHeight=maxHeight;}else{elem.style.overflowY='hidden';}
elem.style.height=newHeight+"px";scrollTops.forEach(([node,scrollTop])=>{node.style.scrollBehavior='auto';node.scrollTop=scrollTop;node.style.scrollBehavior=null;});if(!didReflow&&initialOverflowY!==elem.style.overflowY&&elem.style.overflowY==='hidden'){elem.style.textAlign='end';elem.adjustAutoGrowHeight(true);}}
elem.restoreToInitialState=function(){elem.removeEventListener('input',elem.adjustAutoGrowHeight);elem.style.overflowY=null;elem.style.height=null;}
elem.addEventListener('input',elem.adjustAutoGrowHeight);window.addEventListener('resize',elem.adjustAutoGrowHeight);elem.updateParameters(maxLines);elem.adjustAutoGrowHeight();},adjustHeight:(elem)=>{if(typeof elem.adjustAutoGrowHeight==='function'){elem.adjustAutoGrowHeight();}},updateParams:(elem,maxLines)=>{if(typeof elem.updateParameters==='function'){elem.updateParameters(maxLines);}
if(typeof elem.adjustAutoGrowHeight==='function'){elem.adjustAutoGrowHeight();}},destroy:(elem)=>{window.removeEventListener('resize',elem.adjustAutoGrowHeight);if(typeof elem.restoreToInitialState==='function'){elem.restoreToInitialState();}}}
window.mudTimePicker={initPointerEvents:(clock,dotNetHelper)=>{let isPointerDown=false;const startHandler=(event)=>{if(event.button!==0){return;}
isPointerDown=true;event.target.releasePointerCapture(event.pointerId);if(event.target.classList.contains('mud-hour')||event.target.classList.contains('mud-minute')){let attributeValue=event.target.getAttribute('data-stick-value');let stickValue=attributeValue?parseInt(attributeValue):-1;dotNetHelper.invokeMethodAsync('SelectTimeFromStick',stickValue,false);}
event.preventDefault();};const endHandler=(event)=>{if(event.button!==0){return;}
isPointerDown=false;if(event.target.classList.contains('mud-hour')||event.target.classList.contains('mud-minute')){let attributeValue=event.target.getAttribute('data-stick-value');let stickValue=attributeValue?parseInt(attributeValue):-1;dotNetHelper.invokeMethodAsync('OnStickClick',stickValue);}
event.preventDefault();};const moveHandler=(event)=>{if(!isPointerDown||(!event.target.classList.contains('mud-hour')&&!event.target.classList.contains('mud-minute'))){return;}
let attributeValue=event.target.getAttribute('data-stick-value');let stickValue=attributeValue?parseInt(attributeValue):-1;dotNetHelper.invokeMethodAsync('SelectTimeFromStick',stickValue,true);event.preventDefault();};clock.addEventListener('pointerdown',startHandler);clock.addEventListener('pointerup',endHandler);clock.addEventListener('pointercancel',endHandler);clock.addEventListener('pointerover',moveHandler);clock.destroy=()=>{clock.removeEventListener('pointerdown',startHandler);clock.removeEventListener('pointerup',endHandler);clock.removeEventListener('pointercancel',endHandler);clock.removeEventListener('pointerover',moveHandler);};},destroyPointerEvents:(container)=>{if(typeof container.destroy==='function'){container.destroy();}}};class MudScrollListener{constructor(){this.throttleScrollHandlerId=-1;this.handlerRef=null;}
listenForScroll(dotnetReference,selector){let element=selector?document.querySelector(selector):document;this.handlerRef=this.throttleScrollHandler.bind(this,dotnetReference);element.addEventListener('scroll',this.handlerRef,false);}
throttleScrollHandler(dotnetReference,event){clearTimeout(this.throttleScrollHandlerId);this.throttleScrollHandlerId=window.setTimeout(this.scrollHandler.bind(this,dotnetReference,event),100);}
scrollHandler(dotnetReference,event){try{let element=event.target;let scrollTop=element.scrollTop;let scrollHeight=element.scrollHeight;let scrollWidth=element.scrollWidth;let scrollLeft=element.scrollLeft;let nodeName=element.nodeName;let firstChild=element.firstElementChild;let firstChildBoundingClientRect=firstChild.getBoundingClientRect();dotnetReference.invokeMethodAsync('RaiseOnScroll',{firstChildBoundingClientRect,scrollLeft,scrollTop,scrollHeight,scrollWidth,nodeName,});}catch(error){console.log('[MudBlazor] Error in scrollHandler:',{error});}}
cancelListener(selector){let element=selector?document.querySelector(selector):document;element.removeEventListener('scroll',this.handlerRef);}};window.mudScrollListener=new MudScrollListener();window.mudDragAndDrop={initDropZone:(id)=>{const elem=document.getElementById('mud-drop-zone-'+id);elem.addEventListener('dragover',()=>event.preventDefault());elem.addEventListener('dragstart',()=>event.dataTransfer.setData('',event.target.id));},makeDropZonesNotRelative:()=>{var firstDropItems=Array.from(document.getElementsByClassName('mud-drop-item')).filter(x=>x.getAttribute('index')=="-1");for(let dropItem of firstDropItems){dropItem.style.position='static';}
const dropZones=document.getElementsByClassName('mud-drop-zone');for(let dropZone of dropZones){dropZone.style.position='unset';}},getDropZoneIdentifierOnPosition:(x,y)=>{const elems=document.elementsFromPoint(x,y);const dropZones=elems.filter(e=>e.classList.contains('mud-drop-zone'))
const dropZone=dropZones[0];if(dropZone){return dropZone.getAttribute('identifier')||"";}
return"";},getDropIndexOnPosition:(x,y,id)=>{const elems=document.elementsFromPoint(x,y);const dropItems=elems.filter(e=>e.classList.contains('mud-drop-item')&&e.id!=('mud-drop-item-'+id))
const dropItem=dropItems[0];if(dropItem){return dropItem.getAttribute('index')||"";}
return"";},makeDropZonesRelative:()=>{const dropZones=document.getElementsByClassName('mud-drop-zone');for(let dropZone of dropZones){dropZone.style.position='relative';}
var firstDropItems=Array.from(document.getElementsByClassName('mud-drop-item')).filter(x=>x.getAttribute('index')=="-1");for(let dropItem of firstDropItems){dropItem.style.position='relative';}},moveItemByDifference:(id,dx,dy)=>{const elem=document.getElementById('mud-drop-item-'+id);var tx=(parseFloat(elem.getAttribute('data-x'))||0)+dx;var ty=(parseFloat(elem.getAttribute('data-y'))||0)+dy;elem.style.webkitTransform=elem.style.transform='translate3d('+tx+'px, '+ty+'px, 10px)';elem.setAttribute('data-x',tx);elem.setAttribute('data-y',ty);},resetItem:(id)=>{const elem=document.getElementById('mud-drop-item-'+id);if(elem){elem.style.webkitTransform=elem.style.transform='';elem.setAttribute('data-x',0);elem.setAttribute('data-y',0);}}};class MudWindow{copyToClipboard(text){navigator.clipboard.writeText(text);}
changeCssById(id,css){var element=document.getElementById(id);if(element){element.className=css;}}
updateStyleProperty(elementId,propertyName,value){const element=document.getElementById(elementId);if(element){element.style.setProperty(propertyName,value);}}
changeGlobalCssVariable(name,newValue){document.documentElement.style.setProperty(name,newValue);}
open(args){window.open(args);}}
window.mudWindow=new MudWindow();class MudResizeListener{constructor(id){this.logger=function(message){};this.options={};this.throttleResizeHandlerId=-1;this.dotnet=undefined;this.breakpoint=-1;this.id=id;this.handleResize=this.throttleResizeHandler.bind(this);}
listenForResize(dotnetRef,options){if(this.dotnet){this.options=options;return;}
this.options=options;this.dotnet=dotnetRef;this.logger=options.enableLogging?console.log:(message)=>{};this.logger(`[MudBlazor]Reporting resize events at rate of:${(this.options||{}).reportRate||100}ms`);window.addEventListener("resize",this.handleResize,false);if(!this.options.suppressInitEvent){this.resizeHandler();}
this.options=options;this.dotnet=dotnetRef;this.logger=options.enableLogging?console.log:(message)=>{};this.logger(`[MudBlazor]Reporting resize events at rate of:${this.options.reportRate}ms`);window.addEventListener("resize",this.handleResize,false);if(!this.options.suppressInitEvent){this.resizeHandler();}
this.breakpoint=this.getBreakpoint(window.innerWidth);}
throttleResizeHandler(){clearTimeout(this.throttleResizeHandlerId);this.throttleResizeHandlerId=window.setTimeout(this.resizeHandler.bind(this),((this.options||{}).reportRate||100));}
throttleResizeHandler(){clearTimeout(this.throttleResizeHandlerId);this.throttleResizeHandlerId=window.setTimeout(this.resizeHandler.bind(this),this.options.reportRate);}
resizeHandler(){if(this.options.notifyOnBreakpointOnly){let bp=this.getBreakpoint(window.innerWidth);if(bp==this.breakpoint){return;}
this.breakpoint=bp;}
try{if(this.id){this.dotnet.invokeMethodAsync('RaiseOnResized',{height:window.innerHeight,width:window.innerWidth},this.getBreakpoint(window.innerWidth),this.id);}
@@ -138,32 +172,7 @@ return 1;else
return 0;}};window.mudResizeListener=new MudResizeListener();window.mudResizeListenerFactory={mapping:{},listenForResize:(dotnetRef,options,id)=>{var map=window.mudResizeListenerFactory.mapping;if(map[id]){return;}
var listener=new MudResizeListener(id);listener.listenForResize(dotnetRef,options);map[id]=listener;},cancelListener:(id)=>{var map=window.mudResizeListenerFactory.mapping;if(!map[id]){return;}
var listener=map[id];listener.cancelListener();delete map[id];},cancelListeners:(ids)=>{for(let i=0;i<ids.length;i++){window.mudResizeListenerFactory.cancelListener(ids[i]);}},dispose(){var map=window.mudResizeListenerFactory.mapping;for(var id in map){window.mudResizeListenerFactory.cancelListener(id);}}}
const darkThemeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");window.darkModeChange=(dotNetHelper)=>{return darkThemeMediaQuery.matches;};function watchDarkThemeMedia(dotNetHelper){dotNetHelperTheme=dotNetHelper;window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',function(e){dotNetHelperTheme.invokeMethodAsync('SystemPreferenceChanged',window.matchMedia("(prefers-color-scheme: dark)").matches);});}function setRippleOffset(event,target){const rect=target.getBoundingClientRect();const x=event.clientX-rect.left-rect.width/2;const y=event.clientY-rect.top-rect.height/2;target.style.setProperty("--mud-ripple-offset-x",`${x}px`);target.style.setProperty("--mud-ripple-offset-y",`${y}px`);}
document.addEventListener("click",function(event){const target=event.target.closest(".mud-ripple");if(target){setRippleOffset(event,target);}});class MudResizeObserverFactory{constructor(){this._maps={};}
connect(id,dotNetRef,elements,elementIds,options){var existingEntry=this._maps[id];if(!existingEntry){var observer=new MudResizeObserver(dotNetRef,options);this._maps[id]=observer;}
var result=this._maps[id].connect(elements,elementIds);return result;}
disconnect(id,element){var existingEntry=this._maps[id];if(existingEntry){existingEntry.disconnect(element);}}
cancelListener(id){var existingEntry=this._maps[id];if(existingEntry){existingEntry.cancelListener();delete this._maps[id];}}}
class MudResizeObserver{constructor(dotNetRef,options){this.logger=options.enableLogging?console.log:(message)=>{};this.options=options;this._dotNetRef=dotNetRef
var delay=(this.options||{}).reportRate||200;this.throttleResizeHandlerId=-1;var observervedElements=[];this._observervedElements=observervedElements;this.logger('[MudBlazor | ResizeObserver] Observer initialized');this._resizeObserver=new ResizeObserver(entries=>{var changes=[];this.logger('[MudBlazor | ResizeObserver] changes detected');for(let entry of entries){var target=entry.target;var affectedObservedElement=observervedElements.find((x)=>x.element==target);if(affectedObservedElement){var size=entry.target.getBoundingClientRect();if(affectedObservedElement.isInitialized==true){changes.push({id:affectedObservedElement.id,size:size});}
else{affectedObservedElement.isInitialized=true;}}}
if(changes.length>0){if(this.throttleResizeHandlerId>=0){clearTimeout(this.throttleResizeHandlerId);}
this.throttleResizeHandlerId=window.setTimeout(this.resizeHandler.bind(this,changes),delay);}});}
resizeHandler(changes){try{this.logger("[MudBlazor | ResizeObserver] OnSizeChanged handler invoked");this._dotNetRef.invokeMethodAsync("OnSizeChanged",changes);}catch(error){this.logger("[MudBlazor | ResizeObserver] Error in OnSizeChanged handler:",{error});}}
connect(elements,ids){var result=[];this.logger('[MudBlazor | ResizeObserver] Start observing elements...');for(var i=0;i<elements.length;i++){var newEntry={element:elements[i],id:ids[i],isInitialized:false,};this.logger("[MudBlazor | ResizeObserver] Start observing element:",{newEntry});result.push(elements[i].getBoundingClientRect());this._observervedElements.push(newEntry);this._resizeObserver.observe(elements[i]);}
return result;}
disconnect(elementId){this.logger('[MudBlazor | ResizeObserver] Try to unobserve element with id',{elementId});var affectedObservedElement=this._observervedElements.find((x)=>x.id==elementId);if(affectedObservedElement){var element=affectedObservedElement.element;this._resizeObserver.unobserve(element);this.logger('[MudBlazor | ResizeObserver] Element found. Ubobserving size changes of element',{element});var index=this._observervedElements.indexOf(affectedObservedElement);this._observervedElements.splice(index,1);}}
cancelListener(){this.logger('[MudBlazor | ResizeObserver] Closing ResizeObserver. Detaching all observed elements');this._resizeObserver.disconnect();this._dotNetRef=undefined;}}
window.mudResizeObserver=new MudResizeObserverFactory();class MudScrollManager{scrollToYear(elementId,offset){let element=document.getElementById(elementId);if(element){element.parentNode.scrollTop=element.offsetTop-element.parentNode.offsetTop-element.scrollHeight*3;}}
scrollToListItem(elementId){let element=document.getElementById(elementId);if(element){let parent=element.parentElement;if(parent){parent.scrollTop=element.offsetTop;}}}
scrollTo(selector,left,top,behavior){let element=document.querySelector(selector)||document.documentElement;element.scrollTo({left,top,behavior});}
scrollIntoView(selector,behavior){let element=document.querySelector(selector)||document.documentElement;if(element)
element.scrollIntoView({behavior,block:'center',inline:'start'});}
scrollToBottom(selector,behavior){let element=document.querySelector(selector);if(element)
element.scrollTop=element.scrollHeight;else
window.scrollTo(0,document.body.scrollHeight);}
lockScroll(selector,lockclass){let element=document.querySelector(selector)||document.body;let hasScrollBar=window.innerWidth>document.body.clientWidth;if(hasScrollBar){element.classList.add(lockclass);}else{let lockClassNoPadding=lockclass+"-no-padding";element.classList.add(lockClassNoPadding);}}
unlockScroll(selector,lockclass){let element=document.querySelector(selector)||document.body;element.classList.remove(lockclass);element.classList.remove(lockclass+"-no-padding");}};window.mudScrollManager=new MudScrollManager();window.mudpopoverHelper={calculatePopoverPosition:function(list,boundingRect,selfRect){let top=0;let left=0;if(list.indexOf('mud-popover-anchor-top-left')>=0){left=boundingRect.left;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-top-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-top-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-center-left')>=0){left=boundingRect.left;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-center-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-center-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-bottom-left')>=0){left=boundingRect.left;top=boundingRect.top+boundingRect.height;}else if(list.indexOf('mud-popover-anchor-bottom-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top+boundingRect.height;}else if(list.indexOf('mud-popover-anchor-bottom-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top+boundingRect.height;}
window.mudpopoverHelper={calculatePopoverPosition:function(list,boundingRect,selfRect){let top=0;let left=0;if(list.indexOf('mud-popover-anchor-top-left')>=0){left=boundingRect.left;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-top-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-top-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top;}else if(list.indexOf('mud-popover-anchor-center-left')>=0){left=boundingRect.left;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-center-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-center-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top+boundingRect.height/2;}else if(list.indexOf('mud-popover-anchor-bottom-left')>=0){left=boundingRect.left;top=boundingRect.top+boundingRect.height;}else if(list.indexOf('mud-popover-anchor-bottom-center')>=0){left=boundingRect.left+boundingRect.width/2;top=boundingRect.top+boundingRect.height;}else if(list.indexOf('mud-popover-anchor-bottom-right')>=0){left=boundingRect.left+boundingRect.width;top=boundingRect.top+boundingRect.height;}
let offsetX=0;let offsetY=0;if(list.indexOf('mud-popover-top-left')>=0){offsetX=0;offsetY=0;}else if(list.indexOf('mud-popover-top-center')>=0){offsetX=-selfRect.width/2;offsetY=0;}else if(list.indexOf('mud-popover-top-right')>=0){offsetX=-selfRect.width;offsetY=0;}
else if(list.indexOf('mud-popover-center-left')>=0){offsetX=0;offsetY=-selfRect.height/2;}else if(list.indexOf('mud-popover-center-center')>=0){offsetX=-selfRect.width/2;offsetY=-selfRect.height/2;}else if(list.indexOf('mud-popover-center-right')>=0){offsetX=-selfRect.width;offsetY=-selfRect.height/2;}
else if(list.indexOf('mud-popover-bottom-left')>=0){offsetX=0;offsetY=-selfRect.height;}else if(list.indexOf('mud-popover-bottom-center')>=0){offsetX=-selfRect.width/2;offsetY=-selfRect.height;}else if(list.indexOf('mud-popover-bottom-right')>=0){offsetX=-selfRect.width;offsetY=-selfRect.height;}
@@ -207,72 +216,72 @@ dispose(){for(var i in this.map){disconnect(i);}
this.contentObserver.disconnect();this.contentObserver=null;}
getAllObservedContainers(){const result=[];for(var i in this.map){result.push(i);}
return result;}}
window.mudPopover=new MudPopover();window.addEventListener('scroll',()=>{window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed');window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always');});window.addEventListener('resize',()=>{window.mudpopoverHelper.placePopoverByClassSelector();});window.mudInputAutoGrow={initAutoGrow:(elem,maxLines)=>{const compStyle=getComputedStyle(elem);const lineHeight=parseFloat(compStyle.getPropertyValue('line-height'));let maxHeight=0;elem.updateParameters=function(newMaxLines){if(newMaxLines>0){maxHeight=lineHeight*newMaxLines;}else{maxHeight=0;}}
elem.adjustAutoGrowHeight=function(didReflow=false){const scrollTops=[];let curElem=elem;while(curElem&&curElem.parentNode&&curElem.parentNode instanceof Element){if(curElem.parentNode.scrollTop){scrollTops.push([curElem.parentNode,curElem.parentNode.scrollTop]);}
curElem=curElem.parentNode;}
elem.style.height=0;if(didReflow){elem.style.textAlign=null;}
let minHeight=lineHeight*elem.rows;let newHeight=Math.max(minHeight,elem.scrollHeight);let initialOverflowY=elem.style.overflowY;if(maxHeight>0&&newHeight>maxHeight){elem.style.overflowY='auto';newHeight=maxHeight;}else{elem.style.overflowY='hidden';}
elem.style.height=newHeight+"px";scrollTops.forEach(([node,scrollTop])=>{node.style.scrollBehavior='auto';node.scrollTop=scrollTop;node.style.scrollBehavior=null;});if(!didReflow&&initialOverflowY!==elem.style.overflowY&&elem.style.overflowY==='hidden'){elem.style.textAlign='end';elem.adjustAutoGrowHeight(true);}}
elem.restoreToInitialState=function(){elem.removeEventListener('input',elem.adjustAutoGrowHeight);elem.style.overflowY=null;elem.style.height=null;}
elem.addEventListener('input',elem.adjustAutoGrowHeight);window.addEventListener('resize',elem.adjustAutoGrowHeight);elem.updateParameters(maxLines);elem.adjustAutoGrowHeight();},adjustHeight:(elem)=>{if(typeof elem.adjustAutoGrowHeight==='function'){elem.adjustAutoGrowHeight();}},updateParams:(elem,maxLines)=>{if(typeof elem.updateParameters==='function'){elem.updateParameters(maxLines);}
if(typeof elem.adjustAutoGrowHeight==='function'){elem.adjustAutoGrowHeight();}},destroy:(elem)=>{window.removeEventListener('resize',elem.adjustAutoGrowHeight);if(typeof elem.restoreToInitialState==='function'){elem.restoreToInitialState();}}}
class MudWindow{copyToClipboard(text){navigator.clipboard.writeText(text);}
changeCssById(id,css){var element=document.getElementById(id);if(element){element.className=css;}}
updateStyleProperty(elementId,propertyName,value){const element=document.getElementById(elementId);if(element){element.style.setProperty(propertyName,value);}}
changeGlobalCssVariable(name,newValue){document.documentElement.style.setProperty(name,newValue);}
open(args){window.open(args);}};window.mudWindow=new MudWindow();class MudElementReference{constructor(){this.listenerId=0;this.eventListeners={};}
focus(element){if(element)
{element.focus();}}
blur(element){if(element){element.blur();}}
focusFirst(element,skip=0,min=0){if(element)
{let tabbables=getTabbableElements(element);if(tabbables.length<=min)
element.focus();else
tabbables[skip].focus();}}
focusLast(element,skip=0,min=0){if(element)
{let tabbables=getTabbableElements(element);if(tabbables.length<=min)
element.focus();else
tabbables[tabbables.length-skip-1].focus();}}
saveFocus(element){if(element)
{element['mudblazor_savedFocus']=document.activeElement;}}
restoreFocus(element){if(element)
{let previous=element['mudblazor_savedFocus'];delete element['mudblazor_savedFocus']
if(previous)
previous.focus();}}
selectRange(element,pos1,pos2){if(element)
{if(element.createTextRange){let selRange=element.createTextRange();selRange.collapse(true);selRange.moveStart('character',pos1);selRange.moveEnd('character',pos2);selRange.select();}else if(element.setSelectionRange){element.setSelectionRange(pos1,pos2);}else if(element.selectionStart){element.selectionStart=pos1;element.selectionEnd=pos2;}
element.focus();}}
select(element){if(element)
{element.select();}}
getBoundingClientRect(element){if(!element)return;var rect=JSON.parse(JSON.stringify(element.getBoundingClientRect()));rect.scrollY=window.scrollY||document.documentElement.scrollTop;rect.scrollX=window.scrollX||document.documentElement.scrollLeft;rect.windowHeight=window.innerHeight;rect.windowWidth=window.innerWidth;return rect;}
changeCss(element,css){if(element)
{element.className=css;}}
removeEventListener(element,event,eventId){element.removeEventListener(event,this.eventListeners[eventId]);delete this.eventListeners[eventId];}
addDefaultPreventingHandler(element,eventName){let listener=function(e){e.preventDefault();}
element.addEventListener(eventName,listener,{passive:false});this.eventListeners[++this.listenerId]=listener;return this.listenerId;}
removeDefaultPreventingHandler(element,eventName,listenerId){this.removeEventListener(element,eventName,listenerId);}
addDefaultPreventingHandlers(element,eventNames){let listeners=[];for(const eventName of eventNames){let listenerId=this.addDefaultPreventingHandler(element,eventName);listeners.push(listenerId);}
return listeners;}
removeDefaultPreventingHandlers(element,eventNames,listenerIds){for(let index=0;index<eventNames.length;++index){const eventName=eventNames[index];const listenerId=listenerIds[index];this.removeDefaultPreventingHandler(element,eventName,listenerId);}}};window.mudElementRef=new MudElementReference();class MudScrollSpy{constructor(){this.scrollToSectionRequested=null;this.lastKnowElement=null;this.handlerRef=null;}
spying(dotnetReference,selector){this.scrollToSectionRequested=null;this.lastKnowElement=null;this.handlerRef=this.handleScroll.bind(this,selector,dotnetReference);document.addEventListener('scroll',this.handlerRef,true);window.addEventListener('resize',this.handlerRef,true);}
handleScroll(dotnetReference,selector,event){const elements=document.getElementsByClassName(selector);if(elements.length===0){return;}
const center=window.innerHeight/2.0;let minDifference=Number.MAX_SAFE_INTEGER;let elementId='';for(let i=0;i<elements.length;i++){const element=elements[i];const rect=element.getBoundingClientRect();const diff=Math.abs(rect.top-center);if(diff<minDifference){minDifference=diff;elementId=element.id;}}
if(document.getElementById(elementId).getBoundingClientRect().top<window.innerHeight*0.8===false){return;}
if(this.scrollToSectionRequested!=null){if(this.scrollToSectionRequested==' '&&window.scrollY==0){this.scrollToSectionRequested=null;}
else{if(elementId===this.scrollToSectionRequested){this.scrollToSectionRequested=null;}}
return;}
if(elementId!=this.lastKnowElement){this.lastKnowElement=elementId;history.replaceState(null,'',window.location.pathname+"#"+elementId);dotnetReference.invokeMethodAsync('SectionChangeOccured',elementId);}}
activateSection(sectionId){const element=document.getElementById(sectionId);if(element){this.lastKnowElement=sectionId;history.replaceState(null,'',window.location.pathname+"#"+sectionId);}}
scrollToSection(sectionId){if(sectionId){let element=document.getElementById(sectionId);if(element){this.scrollToSectionRequested=sectionId;element.scrollIntoView({behavior:'smooth',block:'center',inline:'start'});}}
else{window.scrollTo({top:0,behavior:'smooth'});this.scrollToSectionRequested=' ';}}
unspy(){document.removeEventListener('scroll',this.handlerRef,true);window.removeEventListener('resize',this.handlerRef,true);}};window.mudScrollSpy=new MudScrollSpy();window.getTabbableElements=(element)=>{return element.querySelectorAll("a[href]:not([tabindex='-1']),"+"area[href]:not([tabindex='-1']),"+"button:not([disabled]):not([tabindex='-1']),"+"input:not([disabled]):not([tabindex='-1']):not([type='hidden']),"+"select:not([disabled]):not([tabindex='-1']),"+"textarea:not([disabled]):not([tabindex='-1']),"+"iframe:not([tabindex='-1']),"+"details:not([tabindex='-1']),"+"[tabindex]:not([tabindex='-1']),"+"[contentEditable=true]:not([tabindex='-1'])");};window.serializeParameter=(data,spec)=>{if(typeof data=="undefined"||data===null){return null;}
if(typeof data==="number"||typeof data==="string"||typeof data=="boolean"){return data;}
let res=(Array.isArray(data))?[]:{};if(!spec){spec="*";}
for(let i in data){let currentMember=data[i];if(typeof currentMember==='function'||currentMember===null){continue;}
let currentMemberSpec;if(spec!="*"){currentMemberSpec=Array.isArray(data)?spec:spec[i];if(!currentMemberSpec){continue;}}else{currentMemberSpec="*"}
if(typeof currentMember==='object'){if(Array.isArray(currentMember)||currentMember.length){res[i]=[];for(let j=0;j<currentMember.length;j++){const arrayItem=currentMember[j];if(typeof arrayItem==='object'){res[i].push(this.serializeParameter(arrayItem,currentMemberSpec));}else{res[i].push(arrayItem);}}}else{if(currentMember.length===0){res[i]=[];}else{res[i]=this.serializeParameter(currentMember,currentMemberSpec);}}}else{if(currentMember===Infinity){currentMember="Infinity";}
if(currentMember!==null){res[i]=currentMember;}}}
return res;};class MudScrollListener{constructor(){this.throttleScrollHandlerId=-1;this.handlerRef=null;}
listenForScroll(dotnetReference,selector){let element=selector?document.querySelector(selector):document;this.handlerRef=this.throttleScrollHandler.bind(this,dotnetReference);element.addEventListener('scroll',this.handlerRef,false);}
throttleScrollHandler(dotnetReference,event){clearTimeout(this.throttleScrollHandlerId);this.throttleScrollHandlerId=window.setTimeout(this.scrollHandler.bind(this,dotnetReference,event),100);}
scrollHandler(dotnetReference,event){try{let element=event.target;let scrollTop=element.scrollTop;let scrollHeight=element.scrollHeight;let scrollWidth=element.scrollWidth;let scrollLeft=element.scrollLeft;let nodeName=element.nodeName;let firstChild=element.firstElementChild;let firstChildBoundingClientRect=firstChild.getBoundingClientRect();dotnetReference.invokeMethodAsync('RaiseOnScroll',{firstChildBoundingClientRect,scrollLeft,scrollTop,scrollHeight,scrollWidth,nodeName,});}catch(error){console.log('[MudBlazor] Error in scrollHandler:',{error});}}
cancelListener(selector){let element=selector?document.querySelector(selector):document;element.removeEventListener('scroll',this.handlerRef);}};window.mudScrollListener=new MudScrollListener();
window.mudPopover=new MudPopover();window.addEventListener('scroll',()=>{window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed');window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always');});window.addEventListener('resize',()=>{window.mudpopoverHelper.placePopoverByClassSelector();});class MudFileUpload{openFilePicker(id){const element=document.getElementById(id);if(!element){return;}
try{element.showPicker();}catch(error){element.click();}}}
window.mudFileUpload=new MudFileUpload();function setRippleOffset(event,target){const rect=target.getBoundingClientRect();const x=event.clientX-rect.left-rect.width/2;const y=event.clientY-rect.top-rect.height/2;target.style.setProperty("--mud-ripple-offset-x",`${x}px`);target.style.setProperty("--mud-ripple-offset-y",`${y}px`);}
document.addEventListener("click",function(event){const target=event.target.closest(".mud-ripple");if(target){setRippleOffset(event,target);}});class MudScrollManager{scrollToYear(elementId,offset){let element=document.getElementById(elementId);if(element){element.parentNode.scrollTop=element.offsetTop-element.parentNode.offsetTop-element.scrollHeight*3;}}
scrollToListItem(elementId){let element=document.getElementById(elementId);if(element){let parent=element.parentElement;if(parent){parent.scrollTop=element.offsetTop;}}}
scrollTo(selector,left,top,behavior){let element=document.querySelector(selector)||document.documentElement;element.scrollTo({left,top,behavior});}
scrollIntoView(selector,behavior){let element=document.querySelector(selector)||document.documentElement;if(element)
element.scrollIntoView({behavior,block:'center',inline:'start'});}
scrollToBottom(selector,behavior){let element=document.querySelector(selector);if(element){element.scrollTo({top:element.scrollHeight,behavior:behavior});}else{window.scrollTo({top:document.body.scrollHeight,behavior:behavior});}}
lockScroll(selector,lockclass){let element=document.querySelector(selector)||document.body;let hasScrollBar=window.innerWidth>document.body.clientWidth;if(hasScrollBar){element.classList.add(lockclass);}else{let lockClassNoPadding=lockclass+"-no-padding";element.classList.add(lockClassNoPadding);}}
unlockScroll(selector,lockclass){let element=document.querySelector(selector)||document.body;element.classList.remove(lockclass);element.classList.remove(lockclass+"-no-padding");}};window.mudScrollManager=new MudScrollManager();class MudJsEventFactory{connect(dotNetRef,elementId,options){if(!elementId)
throw"[MudBlazor | JsEvent] elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"[MudBlazor | JsEvent] no element found for id: "+elementId;if(!element.mudJsEvent)
element.mudJsEvent=new MudJsEvent(dotNetRef,options);element.mudJsEvent.connect(element);}
disconnect(elementId){var element=document.getElementById(elementId);if(!element||!element.mudJsEvent)
return;element.mudJsEvent.disconnect();}
subscribe(elementId,eventName){if(!elementId)
throw"[MudBlazor | JsEvent] elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"[MudBlazor | JsEvent] no element found for id: "+elementId;if(!element.mudJsEvent)
throw"[MudBlazor | JsEvent] please connect before subscribing"
element.mudJsEvent.subscribe(eventName);}
unsubscribe(elementId,eventName){var element=document.getElementById(elementId);if(!element||!element.mudJsEvent)
return;element.mudJsEvent.unsubscribe(element,eventName);}}
window.mudJsEvent=new MudJsEventFactory();class MudJsEvent{constructor(dotNetRef,options){this._dotNetRef=dotNetRef;this._options=options||{};this.logger=options.enableLogging?console.log:(message)=>{};this.logger('[MudBlazor | JsEvent] Initialized',{options});this._subscribedEvents={};}
connect(element){if(!this._options)
return;if(!this._options.targetClass)
throw"_options.targetClass: css class name expected";if(this._observer){return;}
var targetClass=this._options.targetClass;this.logger('[MudBlazor | JsEvent] Start observing DOM of element for changes to child with class ',{element,targetClass});this._element=element;this._observer=new MutationObserver(this.onDomChanged);this._observer.mudJsEvent=this;this._observer.observe(this._element,{attributes:false,childList:true,subtree:true});this._observedChildren=[];}
disconnect(){if(!this._observer)
return;this.logger('[MudBlazor | JsEvent] disconnect mutation observer and event handler ');this._observer.disconnect();this._observer=null;for(const child of this._observedChildren)
this.detachHandlers(child);}
subscribe(eventName){if(this._subscribedEvents[eventName]){return;}
var element=this._element;var targetClass=this._options.targetClass;this._subscribedEvents[eventName]=true;for(const child of element.getElementsByClassName(targetClass)){this.attachHandlers(child);}}
unsubscribe(eventName){if(!this._observer)
return;this.logger('[MudBlazor | JsEvent] unsubscribe event handler '+eventName);this._observer.disconnect();this._observer=null;this._subscribedEvents[eventName]=false;for(const child of this._observedChildren){this.detachHandler(child,eventName);}}
attachHandlers(child){child.mudJsEvent=this;for(var eventName of Object.getOwnPropertyNames(this._subscribedEvents)){if(!this._subscribedEvents[eventName])
continue;this.logger('[MudBlazor | JsEvent] attaching event '+eventName,child);child.addEventListener(eventName,this.eventHandler);}
if(this._observedChildren.indexOf(child)<0)
this._observedChildren.push(child);}
detachHandler(child,eventName){this.logger('[MudBlazor | JsEvent] detaching handler '+eventName,child);child.removeEventListener(eventName,this.eventHandler);}
detachHandlers(child){this.logger('[MudBlazor | JsEvent] detaching handlers ',child);for(var eventName of Object.getOwnPropertyNames(this._subscribedEvents)){if(!this._subscribedEvents[eventName])
continue;child.removeEventListener(eventName,this.eventHandler);}
this._observedChildren=this._observedChildren.filter(x=>x!==child);}
onDomChanged(mutationsList,observer){var self=this.mudJsEvent;var targetClass=self._options.targetClass;for(const mutation of mutationsList){for(const element of mutation.addedNodes){if(element.classList&&element.classList.contains(targetClass)){if(!self._options.TagName||element.tagName==self._options.TagName)
self.attachHandlers(element);}}
for(const element of mutation.removedNodes){if(element.classList&&element.classList.contains(targetClass)){if(!self._options.tagName||element.tagName==self._options.tagName)
self.detachHandlers(element);}}}}
eventHandler(e){var self=this.mudJsEvent;var eventName=e.type;self.logger('[MudBlazor | JsEvent] "'+eventName+'"',e);self["on"+eventName](self,e);}
onkeyup(self,e){const caretPosition=e.target.selectionStart;const invoke=self._subscribedEvents["keyup"];if(invoke){self._dotNetRef.invokeMethodAsync('OnCaretPositionChanged',caretPosition);}}
onclick(self,e){const caretPosition=e.target.selectionStart;const invoke=self._subscribedEvents["click"];if(invoke){self._dotNetRef.invokeMethodAsync('OnCaretPositionChanged',caretPosition);}}
onpaste(self,e){const invoke=self._subscribedEvents["paste"];if(invoke){e.preventDefault();e.stopPropagation();const text=(e.originalEvent||e).clipboardData.getData('text/plain');self._dotNetRef.invokeMethodAsync('OnPaste',text);}}
onselect(self,e){const invoke=self._subscribedEvents["select"];if(invoke){const start=e.target.selectionStart;const end=e.target.selectionEnd;if(start===end)
return;self._dotNetRef.invokeMethodAsync('OnSelect',start,end);}}}
class MudThrottledEventManager{constructor(){this.mapper={};}
subscribe(eventName,elementId,projection,throotleInterval,key,properties,dotnetReference){const handlerRef=this.throttleEventHandler.bind(this,key);let elem=document.getElementById(elementId);if(elem){elem.addEventListener(eventName,handlerRef,false);let projector=null;if(projection){const parts=projection.split('.');let functionPointer=window;let functionReferenceFound=true;if(parts.length==0||parts.length==1){functionPointer=functionPointer[projection];}
else{for(let i=0;i<parts.length;i++){functionPointer=functionPointer[parts[i]];if(!functionPointer){functionReferenceFound=false;break;}}}
if(functionReferenceFound===true){projector=functionPointer;}}
this.mapper[key]={eventName:eventName,handler:handlerRef,delay:throotleInterval,timerId:-1,reference:dotnetReference,elementId:elementId,properties:properties,projection:projector,};}}
subscribeGlobal(eventName,throotleInterval,key,properties,dotnetReference){let handlerRef=throotleInterval>0?this.throttleEventHandler.bind(this,key):this.eventHandler.bind(this,key);document.addEventListener(eventName,handlerRef,false);this.mapper[key]={eventName:eventName,handler:handlerRef,delay:throotleInterval,timerId:-1,reference:dotnetReference,elementId:document,properties:properties,projection:null,};}
throttleEventHandler(key,event){const entry=this.mapper[key];if(!entry){return;}
clearTimeout(entry.timerId);entry.timerId=window.setTimeout(this.eventHandler.bind(this,key,event),entry.delay);}
eventHandler(key,event){const entry=this.mapper[key];if(!entry){return;}
var elem=document.getElementById(entry.elementId);if(elem!=event.srcElement&&entry.elementId!=document){return;}
const eventEntry={};for(var i=0;i<entry.properties.length;i++){eventEntry[entry.properties[i]]=event[entry.properties[i]];}
if(entry.projection){if(typeof entry.projection==="function"){entry.projection.apply(null,[eventEntry,event]);}}
entry.reference.invokeMethodAsync('OnEventOccur',key,JSON.stringify(eventEntry));}
unsubscribe(key){const entry=this.mapper[key];if(!entry){return;}
entry.reference=null;if(document==entry.elementId){document.removeEventListener(entry.eventName,entry.handler,false);}else{const elem=document.getElementById(entry.elementId);if(elem){elem.removeEventListener(entry.eventName,entry.handler,false);}}
delete this.mapper[key];}};window.mudThrottledEventManager=new MudThrottledEventManager();window.mudEventProjections={correctOffset:function(eventEntry,event){var target=event.target.getBoundingClientRect();eventEntry.offsetX=event.clientX-target.x;eventEntry.offsetY=event.clientY-target.y;}};
+5 -5
View File
@@ -1,9 +1,9 @@
0.7.1
2024-07-13 11:42:46 UTC
161
0.8.4
2024-07-26 06:53:46 UTC
166
8.0.107 (commit 1bdaef7265)
8.0.7 (commit 2aade6beb0)
1.79.0 (commit 129f3b996)
6.20.0
7.4.0
1.6.1
d781e48ae66, release
bd7eecdc80c, release
+1 -1
View File
@@ -2313,7 +2313,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mindwork-ai-studio"
version = "0.7.1"
version = "0.8.4"
dependencies = [
"arboard",
"flexi_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mindwork-ai-studio"
version = "0.7.1"
version = "0.8.4"
edition = "2021"
description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]
+1 -1
View File
@@ -6,7 +6,7 @@
},
"package": {
"productName": "MindWork AI Studio",
"version": "0.7.1"
"version": "0.8.4"
},
"tauri": {
"allowlist": {