Compare commits

...
19 Commits
Author SHA1 Message Date
Thorsten Sommer 4ea767cb45 Revert MudBlazor to v7.6.0 (#123) 2024-09-06 13:14:51 +02:00
Thorsten Sommer 11846e279c Prepare release v0.9.3 (#121) 2024-09-05 20:53:51 +02:00
Thorsten Sommer c861d71383 Fix the send to chat scroll issue (#120) 2024-09-05 20:09:03 +02:00
Thorsten Sommer 6fbf32d8a2 Fix scrolling issue for long chats (#119) 2024-09-05 19:37:54 +02:00
Thorsten Sommer 4216a29325 Prepared release v0.9.2 (#117) 2024-09-05 11:26:05 +02:00
Thorsten Sommer 45770c2cd7 Fixed #115 (#116) 2024-09-05 11:13:40 +02:00
Thorsten Sommer 57905d5510 Fix the linux-arm build (#114) 2024-09-04 20:09:19 +02:00
Thorsten Sommer e999aab7c0 Prepared release v0.9.1 (#113) 2024-09-04 15:57:42 +02:00
Thorsten Sommer c47d3893ff Improve provider selection (#112) 2024-09-04 15:44:23 +02:00
Thorsten Sommer 09c1b77414 Cache the Rust build artifacts (#111) 2024-09-04 11:19:14 +02:00
Thorsten Sommer ea083fdec2 Bind mount the repo for linux-arm (#110) 2024-09-04 10:23:18 +02:00
Thorsten Sommer ceabcdb26e Prepared release v0.9.0 (#109) 2024-09-03 18:06:06 +02:00
Thorsten Sommer a25793ff11 Prepared release v0.9.0 (#108) 2024-09-03 18:03:13 +02:00
Thorsten Sommer 8d8a47b43b Update build instructions (#107) 2024-09-03 17:40:11 +02:00
Thorsten Sommer 2889581702 Changed release settings (#106) 2024-09-03 17:03:35 +02:00
Thorsten Sommer 12602ba8dd Improve initial log file (#105) 2024-09-03 16:13:57 +02:00
Thorsten Sommer e4b8487945 Fixed the handling of the signals field of the runtime API server (#104) 2024-09-01 21:54:23 +02:00
Thorsten Sommer fc64c0b505 Improve ipc (#102) 2024-09-01 20:10:03 +02:00
Thorsten Sommer 47f5756e5a Migrated to keyring v3.2.0 (#101) 2024-08-24 20:05:39 +02:00
96 changed files with 2699 additions and 1642 deletions

No files matched your search

+15 -4
View File
@@ -489,7 +489,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
id: linux_arm_cache id: linux_arm_cache
with: with:
path: $RUNNER_TEMP/linux_arm_qemu_cache.img path: ${{ runner.temp }}/linux_arm_qemu_cache.img
# When the entire key matches, Rust might just create the bundles using the current .NET build: # When the entire key matches, Rust might just create the bundles using the current .NET build:
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}-dependencies-${{ env.CARGO_LOCK_HASH }} key: target-linux-arm64-rust-${{ env.RUST_VERSION }}-dependencies-${{ env.CARGO_LOCK_HASH }}
@@ -544,7 +544,7 @@ jobs:
- name: Add the built runner image to the cache - name: Add the built runner image to the cache
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }} if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
run: | run: |
mv ${{ steps.build-linux-arm-runner.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img mv ${{ steps.build-linux-arm-runner.outputs.image }} ${{ runner.temp }}/linux_arm_qemu_cache.img
- name: Build Tauri project - name: Build Tauri project
if: ${{ env.SKIP != 'true' }} if: ${{ env.SKIP != 'true' }}
@@ -552,11 +552,12 @@ jobs:
id: build-linux-arm id: build-linux-arm
with: with:
base_image: file://$RUNNER_TEMP/linux_arm_qemu_cache.img base_image: file://${{ runner.temp }}/linux_arm_qemu_cache.img
cpu: cortex-a53 cpu: cortex-a53
optimize_image: false optimize_image: false
copy_artifact_path: runtime copy_artifact_path: runtime
copy_artifact_dest: result copy_artifact_dest: result
bind_mount_repository: true
# #
# We do not need to set the PRIVATE_PUBLISH_KEY and PRIVATE_PUBLISH_KEY_PASSWORD here, # We do not need to set the PRIVATE_PUBLISH_KEY and PRIVATE_PUBLISH_KEY_PASSWORD here,
@@ -573,8 +574,18 @@ jobs:
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
source "$HOME/.cargo/env" source "$HOME/.cargo/env"
cd runtime cd runtime
# Try to restore the Rust cache from previous build:
mkdir -p /rust-cache/target
rm -fr target
cp -Rp /rust-cache/target target
cargo tauri build --target aarch64-unknown-linux-gnu --bundles deb cargo tauri build --target aarch64-unknown-linux-gnu --bundles deb
# Save the built libraries for the next job:
rm -fr /rust-cache/target
cp -Rp target /rust-cache
- name: Debug - name: Debug
if: ${{ env.SKIP != 'true' }} if: ${{ env.SKIP != 'true' }}
run: | run: |
@@ -845,7 +856,7 @@ jobs:
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
prerelease: false prerelease: true
draft: false draft: false
make_latest: true make_latest: true
body: ${{ env.CHANGELOG }} body: ${{ env.CHANGELOG }}
+4 -5
View File
@@ -1,20 +1,19 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Agents; namespace AIStudio.Agents;
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : IAgent
{ {
protected SettingsManager SettingsManager { get; init; } = settingsManager; protected SettingsManager SettingsManager { get; init; } = settingsManager;
protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
protected ThreadSafeRandom RNG { get; init; } = rng; protected ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary> /// <summary>
/// Represents the type or category of this agent. /// Represents the type or category of this agent.
/// </summary> /// </summary>
@@ -104,6 +103,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.SettingsManager, providerSettings.Model, thread);
} }
} }
@@ -1,10 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
namespace AIStudio.Agents; namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng) public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, rng)
{ {
private static readonly ContentBlock EMPTY_BLOCK = new() private static readonly ContentBlock EMPTY_BLOCK = new()
{ {
@@ -1,12 +1,13 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Agenda; namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore public partial class AssistantAgenda : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
protected override string Title => "Agenda Planner"; protected override string Title => "Agenda Planner";
protected override string Description => protected override string Description =>
@@ -94,13 +95,7 @@ public partial class AssistantAgenda : AssistantBaseCore
- Mary Jane: Work package 3 - Mary Jane: Work package 3
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.AGENDA_ASSISTANT,
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{ {
@@ -159,7 +154,6 @@ public partial class AssistantAgenda : AssistantBaseCore
this.durationBreaks = this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime; this.durationBreaks = this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime;
this.activeParticipation = this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation; this.activeParticipation = this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation;
this.numberParticipants = this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants; this.numberParticipants = this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider);
return true; return true;
} }
@@ -195,7 +189,6 @@ public partial class AssistantAgenda : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_AGENDA_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_AGENDA_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputContent = deferredContent; this.inputContent = deferredContent;
@@ -35,49 +35,64 @@
</div> </div>
</ChildContent> </ChildContent>
<FooterContent> <FooterContent>
@if (this.FooterButtons.Count > 0) <MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
{
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@foreach (var button in this.FooterButtons)
{
switch (button)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
case ButtonData buttonData: @if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
@foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
{
if (assistant is Components.NONE || this.Component == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
@foreach (var button in this.FooterButtons)
{
switch (button)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()"> <MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text @buttonData.Text
</MudButton> </MudButton>
break; </MudTooltip>
break;
case SendToButton sendToButton: case ButtonData buttonData:
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info"> <MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@foreach (var assistant in Enum.GetValues<SendTo>().OrderBy(n => n.Name().Length)) @buttonData.Text
{ </MudButton>
if(assistant is SendTo.NONE || sendToButton.Self == assistant) break;
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)"> case SendToButton sendToButton:
@assistant.Name() <MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
</MudMenuItem> @foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
} {
</MudMenu> if(assistant is Components.NONE || sendToButton.Self == assistant)
break; continue;
}
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
} }
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()"> }
Copy result
</MudButton> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
<MudButton Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()"> Copy result
Reset </MudButton>
</MudButton>
</MudStack> <MudButton Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
} Reset
</MudButton>
</MudStack>
</FooterContent> </FooterContent>
</InnerScrolling> </InnerScrolling>
@@ -1,16 +1,17 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Assistants; namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase public abstract partial class AssistantBase : ComponentBase
{ {
[Inject] [Inject]
protected SettingsManager SettingsManager { get; set; } = null!; protected SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
protected IJSRuntime JsRuntime { get; init; } = null!; protected IJSRuntime JsRuntime { get; init; } = null!;
@@ -22,11 +23,14 @@ public abstract partial class AssistantBase : ComponentBase
protected ISnackbar Snackbar { get; init; } = null!; protected ISnackbar Snackbar { get; init; } = null!;
[Inject] [Inject]
protected Rust Rust { get; init; } = null!; protected RustService RustService { get; init; } = null!;
[Inject] [Inject]
protected NavigationManager NavigationManager { get; init; } = null!; protected NavigationManager NavigationManager { get; init; } = null!;
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult"; internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
internal const string RESULT_DIV_ID = "assistantResult"; internal const string RESULT_DIV_ID = "assistantResult";
@@ -36,6 +40,8 @@ public abstract partial class AssistantBase : ComponentBase
protected abstract string SystemPrompt { get; } protected abstract string SystemPrompt { get; }
protected abstract Tools.Components Component { get; }
protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch
{ {
ContentText textBlock => textBlock.Text, ContentText textBlock => textBlock.Text,
@@ -69,6 +75,13 @@ public abstract partial class AssistantBase : ComponentBase
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
// Configure the spellchecking for the user input: // Configure the spellchecking for the user input:
@@ -151,7 +164,7 @@ public abstract partial class AssistantBase : ComponentBase
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false; this.isProcessing = false;
this.StateHasChanged(); this.StateHasChanged();
@@ -162,7 +175,7 @@ public abstract partial class AssistantBase : ComponentBase
protected async Task CopyToClipboard() protected async Task CopyToClipboard()
{ {
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, this.Result2Copy()); await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
} }
private static string? GetButtonIcon(string icon) private static string? GetButtonIcon(string icon)
@@ -173,7 +186,7 @@ public abstract partial class AssistantBase : ComponentBase
return icon; return icon;
} }
private Task SendToAssistant(SendTo destination, SendToButton sendToButton) private Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{ {
var contentToSend = sendToButton.UseResultingContentBlockData switch var contentToSend = sendToButton.UseResultingContentBlockData switch
{ {
@@ -188,7 +201,7 @@ public abstract partial class AssistantBase : ComponentBase
var sendToData = destination.GetData(); var sendToData = destination.GetData();
switch (destination) switch (destination)
{ {
case SendTo.CHAT: case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread); MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
break; break;
@@ -210,6 +223,7 @@ public abstract partial class AssistantBase : ComponentBase
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID); await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom(); this.ResetFrom();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false; this.inputIsValid = false;
this.inputIssues = []; this.inputIssues = [];
@@ -1,11 +1,11 @@
using System.Text; using System.Text;
using AIStudio.Tools;
namespace AIStudio.Assistants.Coding; namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore public partial class AssistantCoding : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
protected override string Title => "Coding Assistant"; protected override string Title => "Coding Assistant";
protected override string Description => protected override string Description =>
@@ -26,13 +26,7 @@ public partial class AssistantCoding : AssistantBaseCore
When the user asks in a different language than English, you answer in the same language! When the user asks in a different language than English, you answer in the same language!
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.CODING_ASSISTANT,
},
];
protected override void ResetFrom() protected override void ResetFrom()
{ {
@@ -50,7 +44,6 @@ public partial class AssistantCoding : AssistantBaseCore
if (this.SettingsManager.ConfigurationData.Coding.PreselectOptions) if (this.SettingsManager.ConfigurationData.Coding.PreselectOptions)
{ {
this.provideCompilerMessages = this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages; this.provideCompilerMessages = this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Coding.PreselectedProvider);
return true; return true;
} }
@@ -66,7 +59,6 @@ public partial class AssistantCoding : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CODING_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CODING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.questions = deferredContent; this.questions = deferredContent;
@@ -1,12 +1,13 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.EMail; namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore public partial class AssistantEMail : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
protected override string Title => "E-Mail"; protected override string Title => "E-Mail";
protected override string Description => protected override string Description =>
@@ -21,13 +22,7 @@ public partial class AssistantEMail : AssistantBaseCore
{this.SystemPromptGreeting()} {this.SystemPromptName()} You write the email in the following language: {this.SystemPromptLanguage()}. {this.SystemPromptGreeting()} {this.SystemPromptName()} You write the email in the following language: {this.SystemPromptLanguage()}.
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.EMAIL_ASSISTANT,
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{ {
@@ -60,7 +55,6 @@ public partial class AssistantEMail : AssistantBaseCore
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle; this.selectedWritingStyle = this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage; this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage; this.customTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.EMail.PreselectedProvider);
return true; return true;
} }
@@ -89,7 +83,6 @@ public partial class AssistantEMail : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_EMAIL_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_EMAIL_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputBulletPoints = deferredContent; this.inputBulletPoints = deferredContent;
@@ -1,10 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.GrammarSpelling; namespace AIStudio.Assistants.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore public partial class AssistantGrammarSpelling : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
protected override string Title => "Grammar & Spelling Checker"; protected override string Title => "Grammar & Spelling Checker";
protected override string Description => protected override string Description =>
@@ -32,7 +33,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
[ [
new SendToButton new SendToButton
{ {
Self = SendTo.GRAMMAR_SPELLING_ASSISTANT, Self = Tools.Components.GRAMMAR_SPELLING_ASSISTANT,
UseResultingContentBlockData = false, UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.correctedText) ? this.inputText : this.correctedText GetText = () => string.IsNullOrWhiteSpace(this.correctedText) ? this.inputText : this.correctedText
}, },
@@ -60,7 +61,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
{ {
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage; this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage; this.customTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider);
return true; return true;
} }
@@ -71,7 +71,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputText = deferredContent; this.inputText = deferredContent;
@@ -1,11 +1,8 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.IconFinder; namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore public partial class AssistantIconFinder : AssistantBaseCore
{ {
private string inputContext = string.Empty; protected override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
private IconSources selectedIconSource;
protected override string Title => "Icon Finder"; protected override string Title => "Icon Finder";
@@ -28,13 +25,7 @@ public partial class AssistantIconFinder : AssistantBaseCore
quotation marks. quotation marks.
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.ICON_FINDER_ASSISTANT,
},
];
protected override void ResetFrom() protected override void ResetFrom()
{ {
@@ -50,18 +41,19 @@ public partial class AssistantIconFinder : AssistantBaseCore
if (this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions) if (this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)
{ {
this.selectedIconSource = this.SettingsManager.ConfigurationData.IconFinder.PreselectedSource; this.selectedIconSource = this.SettingsManager.ConfigurationData.IconFinder.PreselectedSource;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider);
return true; return true;
} }
return false; return false;
} }
private string inputContext = string.Empty;
private IconSources selectedIconSource;
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_ICON_FINDER_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_ICON_FINDER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputContext = deferredContent; this.inputContext = deferredContent;
@@ -1,9 +1,9 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.LegalCheck; namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore public partial class AssistantLegalCheck : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
protected override string Title => "Legal Check"; protected override string Title => "Legal Check";
protected override string Description => protected override string Description =>
@@ -22,13 +22,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
Never invent facts! Never invent facts!
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.LEGAL_CHECK_ASSISTANT,
},
];
protected override void ResetFrom() protected override void ResetFrom()
{ {
@@ -37,16 +31,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
this.MightPreselectValues(); this.MightPreselectValues();
} }
protected override bool MightPreselectValues() protected override bool MightPreselectValues() => false;
{
if (this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)
{
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider);
return true;
}
return false;
}
private bool isAgentRunning; private bool isAgentRunning;
private string inputLegalDocument = string.Empty; private string inputLegalDocument = string.Empty;
@@ -56,7 +41,6 @@ public partial class AssistantLegalCheck : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_LEGAL_CHECK_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_LEGAL_CHECK_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputQuestions = deferredContent; this.inputQuestions = deferredContent;
@@ -1,10 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.RewriteImprove; namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore public partial class AssistantRewriteImprove : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
protected override string Title => "Rewrite & Improve Text"; protected override string Title => "Rewrite & Improve Text";
protected override string Description => protected override string Description =>
@@ -33,7 +34,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
[ [
new SendToButton new SendToButton
{ {
Self = SendTo.REWRITE_ASSISTANT, Self = Tools.Components.REWRITE_ASSISTANT,
UseResultingContentBlockData = false, UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.rewrittenText) ? this.inputText : this.rewrittenText, GetText = () => string.IsNullOrWhiteSpace(this.rewrittenText) ? this.inputText : this.rewrittenText,
}, },
@@ -63,7 +64,6 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
{ {
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage; this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedOtherLanguage; this.customTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider);
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle; this.selectedWritingStyle = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle;
this.selectedSentenceStructure = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure; this.selectedSentenceStructure = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure;
return true; return true;
@@ -76,7 +76,6 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_REWRITE_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_REWRITE_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputText = deferredContent; this.inputText = deferredContent;
@@ -1,10 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.TextSummarizer; namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore public partial class AssistantTextSummarizer : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
protected override string Title => "Text Summarizer"; protected override string Title => "Text Summarizer";
protected override string Description => protected override string Description =>
@@ -24,13 +25,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
a summary with the requested complexity. In any case, do not add any information. a summary with the requested complexity. In any case, do not add any information.
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.TEXT_SUMMARIZER_ASSISTANT,
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{ {
@@ -57,7 +52,6 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
this.customTargetLanguage = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage; this.customTargetLanguage = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage;
this.selectedComplexity = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity; this.selectedComplexity = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity;
this.expertInField = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField; this.expertInField = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider);
return true; return true;
} }
@@ -75,7 +69,6 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputText = deferredContent; this.inputText = deferredContent;
@@ -1,10 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Translation; namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore public partial class AssistantTranslation : AssistantBaseCore
{ {
protected override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
protected override string Title => "Translation"; protected override string Title => "Translation";
protected override string Description => protected override string Description =>
@@ -20,13 +21,7 @@ public partial class AssistantTranslation : AssistantBaseCore
language requires, e.g., shorter sentences, you should split the text into shorter sentences. language requires, e.g., shorter sentences, you should split the text into shorter sentences.
"""; """;
protected override IReadOnlyList<IButtonData> FooterButtons => protected override IReadOnlyList<IButtonData> FooterButtons => [];
[
new SendToButton
{
Self = SendTo.TRANSLATION_ASSISTANT,
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{ {
@@ -52,7 +47,6 @@ public partial class AssistantTranslation : AssistantBaseCore
this.liveTranslation = this.SettingsManager.ConfigurationData.Translation.PreselectLiveTranslation; this.liveTranslation = this.SettingsManager.ConfigurationData.Translation.PreselectLiveTranslation;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage; this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage; this.customTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Translation.PreselectedProvider);
return true; return true;
} }
@@ -70,7 +64,6 @@ public partial class AssistantTranslation : AssistantBaseCore
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TRANSLATION_ASSISTANT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TRANSLATION_ASSISTANT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
this.inputText = deferredContent; this.inputText = deferredContent;
@@ -1,7 +1,7 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Chat; namespace AIStudio.Chat;
/// <summary> /// <summary>
@@ -40,10 +40,7 @@ public partial class ContentBlockComponent : ComponentBase
public string Class { get; set; } = string.Empty; public string Class { get; set; } = string.Empty;
[Inject] [Inject]
private Rust Rust { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
@@ -100,7 +97,7 @@ public partial class ContentBlockComponent : ComponentBase
{ {
case ContentType.TEXT: case ContentType.TEXT:
var textContent = (ContentText) this.Content; var textContent = (ContentText) this.Content;
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, textContent.Text); await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
break; break;
default: default:
+1 -1
View File
@@ -29,7 +29,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc /> /// <inheritdoc />
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default) public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
+5 -5
View File
@@ -35,18 +35,18 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc /> /// <inheritdoc />
public async Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default) public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
{ {
if(chatThread is null) if(chatThread is null)
return; return;
// Store the last time we got a response. We use this later, // Store the last time we got a response. We use this ater
// to determine whether we should notify the UI about the // to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode // new content or not. Depends on the energy saving mode
// the user chose. // the user chose.
var last = DateTimeOffset.Now; var last = DateTimeOffset.Now;
// Start another thread by using a task, to uncouple // Start another thread by using a task to uncouple
// the UI thread from the AI processing: // the UI thread from the AI processing:
await Task.Run(async () => await Task.Run(async () =>
{ {
@@ -54,7 +54,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true; this.InitialRemoteWait = true;
// Iterate over the responses from the AI: // Iterate over the responses from the AI:
await foreach (var deltaText in provider.StreamChatCompletion(jsRuntime, settings, chatModel, chatThread, token)) await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
{ {
// When the user cancels the request, we stop the loop: // When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@@ -89,7 +89,7 @@ public sealed class ContentText : IContent
} }
// Stop the waiting animation (in case the loop // Stop the waiting animation (in case the loop
// was stopped or no content was received): // was stopped, or no content was received):
this.InitialRemoteWait = false; this.InitialRemoteWait = false;
this.IsStreaming = false; this.IsStreaming = false;
}, token); }, token);
+1 -1
View File
@@ -42,5 +42,5 @@ public interface IContent
/// <summary> /// <summary>
/// Uses the provider to create the content. /// Uses the provider to create the content.
/// </summary> /// </summary>
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default); public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
} }
@@ -13,6 +13,10 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (178, "v0.9.3, build 178 (2024-09-06 11:06 UTC)", "v0.9.3.md"),
new (177, "v0.9.2, build 177 (2024-09-05 09:19 UTC)", "v0.9.2.md"),
new (176, "v0.9.1, build 176 (2024-09-04 13:48 UTC)", "v0.9.1.md"),
new (175, "v0.9.0, build 175 (2024-09-03 16:00 UTC)", "v0.9.0.md"),
new (174, "v0.8.12, build 174 (2024-08-24 08:30 UTC)", "v0.8.12.md"), new (174, "v0.8.12, build 174 (2024-08-24 08:30 UTC)", "v0.8.12.md"),
new (173, "v0.8.11, build 173 (2024-08-21 07:03 UTC)", "v0.8.11.md"), new (173, "v0.8.11, build 173 (2024-08-21 07:03 UTC)", "v0.8.11.md"),
new (172, "v0.8.10, build 172 (2024-08-18 19:44 UTC)", "v0.8.10.md"), new (172, "v0.8.10, build 172 (2024-08-18 19:44 UTC)", "v0.8.10.md"),
@@ -1,5 +1,4 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -1 +1 @@
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="Select a provider that is preselected." Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/> <ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
@@ -1,5 +1,4 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -22,6 +21,9 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
[Parameter] [Parameter]
public Func<bool> Disabled { get; set; } = () => false; public Func<bool> Disabled { get; set; } = () => false;
[Parameter]
public Func<string> HelpText { get; set; } = () => "Select a provider that is preselected.";
[Inject] [Inject]
private SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
@@ -1,5 +1,4 @@
using AIStudio.Layout; using AIStudio.Layout;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
@@ -1,7 +1,6 @@
using AIStudio.Agents; using AIStudio.Agents;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -5,7 +5,6 @@ using System.Text.Json.Serialization;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -24,6 +23,9 @@ public partial class Workspaces : ComponentBase
[Inject] [Inject]
private ThreadSafeRandom RNG { get; init; } = null!; private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private ILogger<Workspaces> Logger { get; init; } = null!;
[Parameter] [Parameter]
public ChatThread? CurrentChatThread { get; set; } public ChatThread? CurrentChatThread { get; set; }
@@ -309,7 +311,7 @@ public partial class Workspaces : ComponentBase
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
} }
return null; return null;
@@ -1,7 +1,5 @@
@using AIStudio.Components
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Provider.SelfHosted @using AIStudio.Provider.SelfHosted
@using MudBlazor
<MudDialog> <MudDialog>
<DialogContent> <DialogContent>
@@ -1,12 +1,14 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host; using Host = AIStudio.Provider.SelfHosted.Host;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Settings; namespace AIStudio.Dialogs;
/// <summary> /// <summary>
/// The provider settings dialog. /// The provider settings dialog.
@@ -71,10 +73,13 @@ public partial class ProviderDialog : ComponentBase
public bool IsEditing { get; init; } public bool IsEditing { get; init; }
[Inject] [Inject]
private SettingsManager SettingsManager { get; set; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
private IJSRuntime JsRuntime { get; set; } = null!; private ILogger<ProviderDialog> Logger { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
@@ -94,8 +99,9 @@ public partial class ProviderDialog : ComponentBase
private MudForm form = null!; private MudForm form = null!;
private readonly List<Model> availableModels = new(); private readonly List<Model> availableModels = new();
private readonly Encryption encryption = Program.ENCRYPTION;
private Provider CreateProviderSettings() => new() private Settings.Provider CreateProviderSettings() => new()
{ {
Num = this.DataNum, Num = this.DataNum,
Id = this.DataId, Id = this.DataId,
@@ -133,7 +139,7 @@ public partial class ProviderDialog : ComponentBase
} }
var loadedProviderSettings = this.CreateProviderSettings(); var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider(); var provider = loadedProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider) if(provider is NoProvider)
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
@@ -141,10 +147,10 @@ public partial class ProviderDialog : ComponentBase
} }
// Load the API key: // Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider); var requestedSecret = await this.RustService.GetAPIKey(provider);
if(requestedSecret.Success) if(requestedSecret.Success)
{ {
this.dataAPIKey = requestedSecret.Secret; this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
// Now, we try to load the list of available models: // Now, we try to load the list of available models:
await this.ReloadModels(); await this.ReloadModels();
@@ -187,10 +193,10 @@ public partial class ProviderDialog : ComponentBase
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED) if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
{ {
// We need to instantiate the provider to store the API key: // We need to instantiate the provider to store the API key:
var provider = addedProviderSettings.CreateProvider(); var provider = addedProviderSettings.CreateProvider(this.Logger);
// Store the API key in the OS secure storage: // Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); var storeResponse = await this.RustService.SetAPIKey(provider, this.dataAPIKey);
if (!storeResponse.Success) if (!storeResponse.Success)
{ {
this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again."; this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again.";
@@ -318,11 +324,11 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels() private async Task ReloadModels()
{ {
var currentProviderSettings = this.CreateProviderSettings(); var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider(); var provider = currentProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider) if(provider is NoProvider)
return; return;
var models = await provider.GetTextModels(this.JsRuntime, this.SettingsManager, this.dataAPIKey); var models = await provider.GetTextModels(this.dataAPIKey);
// Order descending by ID means that the newest models probably come first: // Order descending by ID means that the newest models probably come first:
var orderedModels = models.OrderByDescending(n => n.Id); var orderedModels = models.OrderByDescending(n => n.Id);
@@ -1,6 +1,6 @@
using System.Reflection; using System.Reflection;
using AIStudio.Tools; using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
+2
View File
@@ -1,5 +1,7 @@
// Global using directives // Global using directives
global using AIStudio.Tools;
global using Microsoft.JSInterop; global using Microsoft.JSInterop;
global using MudBlazor; global using MudBlazor;
@@ -1,21 +1,19 @@
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools; using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using DialogOptions = AIStudio.Dialogs.DialogOptions; using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Layout; namespace AIStudio.Layout;
public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDisposable public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDisposable
{ {
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject] [Inject]
private SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
@@ -26,7 +24,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject] [Inject]
private Rust Rust { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
@@ -34,6 +32,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
[Inject] [Inject]
private NavigationManager NavigationManager { get; init; } = null!; private NavigationManager NavigationManager { get; init; } = null!;
[Inject]
private ILogger<MainLayout> Logger { get; init; } = null!;
public string AdditionalHeight { get; private set; } = "0em"; 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 string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
@@ -70,12 +71,15 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
// We use the Tauri API (Rust) to get the data and config directories // We use the Tauri API (Rust) to get the data and config directories
// for this app. // for this app.
// //
var dataDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appLocalDataDir"); var dataDir = await this.RustService.GetDataDirectory();
var configDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appConfigDir"); var configDir = await this.RustService.GetConfigDirectory();
this.Logger.LogInformation($"The data directory is: '{dataDir}'");
this.Logger.LogInformation($"The config directory is: '{configDir}'");
// Store the directories in the settings manager: // Store the directories in the settings manager:
SettingsManager.ConfigDirectory = configDir; SettingsManager.ConfigDirectory = configDir;
SettingsManager.DataDirectory = Path.Join(dataDir, "data"); SettingsManager.DataDirectory = dataDir;
Directory.CreateDirectory(SettingsManager.DataDirectory); Directory.CreateDirectory(SettingsManager.DataDirectory);
// Ensure that all settings are loaded: // Ensure that all settings are loaded:
@@ -85,8 +89,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
this.MessageBus.RegisterComponent(this); this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED ]); this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED ]);
// Set the js runtime for the update service: // Set the snackbar for the update service:
UpdateService.SetBlazorDependencies(this.JsRuntime, this.Snackbar); UpdateService.SetBlazorDependencies(this.Snackbar);
TemporaryChatService.Initialize(); TemporaryChatService.Initialize();
// Should the navigation bar be open by default? // Should the navigation bar be open by default?
@@ -189,7 +193,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
this.performingUpdate = true; this.performingUpdate = true;
this.StateHasChanged(); this.StateHasChanged();
await this.Rust.InstallUpdate(this.JsRuntime); await this.RustService.InstallUpdate();
} }
private async ValueTask OnLocationChanging(LocationChangingContext context) private async ValueTask OnLocationChanging(LocationChangingContext context)
@@ -45,7 +45,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.65" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
<PackageReference Include="MudBlazor" Version="7.6.0" /> <PackageReference Include="MudBlazor" Version="7.6.0" />
<PackageReference Include="MudBlazor.Markdown" Version="1.0.2" /> <PackageReference Include="MudBlazor.Markdown" Version="1.0.2" />
+11 -6
View File
@@ -39,13 +39,18 @@
<ThirdPartyComponent Name=".NET" Developer="Microsoft & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/dotnet/runtime/blob/main/LICENSE.TXT" RepositoryUrl="https://github.com/dotnet" UseCase="The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."/> <ThirdPartyComponent Name=".NET" Developer="Microsoft & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/dotnet/runtime/blob/main/LICENSE.TXT" RepositoryUrl="https://github.com/dotnet" UseCase="The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."/>
<ThirdPartyComponent Name="MudBlazor" Developer="Jonny Larsson, Meinrad Recheis & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/MudBlazor/MudBlazor/blob/dev/LICENSE" RepositoryUrl="https://github.com/MudBlazor/MudBlazor/" UseCase="Building on .NET, ASP.NET Core, and Blazor, MudBlazor is used as a library for designing and developing the user interface. It is a great project that significantly accelerates the development of advanced user interfaces with Blazor."/> <ThirdPartyComponent Name="MudBlazor" Developer="Jonny Larsson, Meinrad Recheis & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/MudBlazor/MudBlazor/blob/dev/LICENSE" RepositoryUrl="https://github.com/MudBlazor/MudBlazor/" UseCase="Building on .NET, ASP.NET Core, and Blazor, MudBlazor is used as a library for designing and developing the user interface. It is a great project that significantly accelerates the development of advanced user interfaces with Blazor."/>
<ThirdPartyComponent Name="MudBlazor.Markdown" Developer="My Nihongo & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/MyNihongo/MudBlazor.Markdown/blob/main/LICENSE" RepositoryUrl="https://github.com/MyNihongo/MudBlazor.Markdown" UseCase="This component is used to render Markdown text. This is important because the LLM often responds with Markdown-formatted text, allowing us to present it in a way that is easier to read."/> <ThirdPartyComponent Name="MudBlazor.Markdown" Developer="My Nihongo & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/MyNihongo/MudBlazor.Markdown/blob/main/LICENSE" RepositoryUrl="https://github.com/MyNihongo/MudBlazor.Markdown" UseCase="This component is used to render Markdown text. This is important because the LLM often responds with Markdown-formatted text, allowing us to present it in a way that is easier to read."/>
<ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/rust-lang/rust?tab=readme-ov-file#license" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."/> <ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."/>
<ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/tauri-apps/tauri?tab=readme-ov-file#licenses" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"/> <ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"/>
<ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/serde-rs/serde?tab=readme-ov-file#license" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."/> <ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."/>
<ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/hwchen/keyring-rs?tab=readme-ov-file#license" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library."/> <ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."/>
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/1Password/arboard" RepositoryUrl="https://github.com/1Password/arboard" UseCase="To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system."/> <ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library."/>
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system."/>
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor."/> <ThirdPartyComponent Name="tokio" Developer="Alex Crichton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor."/>
<ThirdPartyComponent Name="flexi_logger" Developer="emabee & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/emabee/flexi_logger" RepositoryUrl="https://github.com/emabee/flexi_logger" UseCase="This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."/> <ThirdPartyComponent Name="flexi_logger" Developer="emabee & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/emabee/flexi_logger/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/emabee/flexi_logger" UseCase="This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."/>
<ThirdPartyComponent Name="rand" Developer="Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-random/rand/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-random/rand" UseCase="We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose."/>
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."/>
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project."/>
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose."/>
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/> <ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/> <ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."/> <ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."/>
@@ -1,7 +1,5 @@
using System.Reflection; using System.Reflection;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages; namespace AIStudio.Pages;
+25 -14
View File
@@ -4,7 +4,6 @@ using AIStudio.Dialogs;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
@@ -19,16 +18,16 @@ namespace AIStudio.Pages;
public partial class Chat : MSGComponentBase, IAsyncDisposable public partial class Chat : MSGComponentBase, IAsyncDisposable
{ {
[Inject] [Inject]
private SettingsManager SettingsManager { get; set; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
[Inject] [Inject]
private ThreadSafeRandom RNG { get; init; } = null!; private ThreadSafeRandom RNG { get; init; } = null!;
[Inject] [Inject]
public IDialogService DialogService { get; set; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject]
private ILogger<Chat> Logger { get; init; } = null!;
private InnerScrolling scrollingArea = null!; private InnerScrolling scrollingArea = null!;
@@ -45,6 +44,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private bool workspacesVisible; private bool workspacesVisible;
private Workspaces? workspaces; private Workspaces? workspaces;
private bool mustScrollToBottomAfterRender; private bool mustScrollToBottomAfterRender;
private byte scrollRenderCountdown;
// Unfortunately, we need the input field reference to clear it after sending a message. // Unfortunately, we need the input field reference to clear it after sending a message.
// This is necessary because we have to handle the key events ourselves. Otherwise, // This is necessary because we have to handle the key events ourselves. Otherwise,
@@ -60,11 +60,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Configure the spellchecking for the user input: // Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
if (this.SettingsManager.ConfigurationData.Chat.PreselectOptions) this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
{
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Chat.PreselectedProvider);
}
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
{ {
@@ -81,6 +77,13 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
}; };
} }
} }
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
this.StateHasChanged();
}
} }
await base.OnInitializedAsync(); await base.OnInitializedAsync();
@@ -90,8 +93,15 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
{ {
if(this.mustScrollToBottomAfterRender) if(this.mustScrollToBottomAfterRender)
{ {
await this.scrollingArea.ScrollToBottom(); if (--this.scrollRenderCountdown == 0)
this.mustScrollToBottomAfterRender = false; {
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
}
else
{
this.StateHasChanged();
}
} }
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
@@ -189,7 +199,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat: // Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
@@ -389,6 +399,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{ {
this.mustScrollToBottomAfterRender = true; this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
this.StateHasChanged(); this.StateHasChanged();
} }
} }
@@ -73,6 +73,7 @@
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." /> <ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." />
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/> <ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/> <ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
<ConfigurationProviderSelection Data="@this.availableProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Chat" HeaderText="Chat Options"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Chat" HeaderText="Chat Options">
+10 -7
View File
@@ -1,11 +1,11 @@
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions; using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
// ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ClassNeverInstantiated.Global
@@ -14,16 +14,19 @@ namespace AIStudio.Pages;
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Inject] [Inject]
public SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
public IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject] [Inject]
public IJSRuntime JsRuntime { get; init; } = null!; private MessageBus MessageBus { get; init; } = null!;
[Inject] [Inject]
protected MessageBus MessageBus { get; init; } = null!; private ILogger<Settings> Logger { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private readonly List<ConfigurationSelectData<string>> availableProviders = new(); private readonly List<ConfigurationSelectData<string>> availableProviders = new();
@@ -111,8 +114,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var providerInstance = provider.CreateProvider(); var providerInstance = provider.CreateProvider(this.Logger);
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); var deleteSecretResponse = await this.RustService.DeleteAPIKey(providerInstance);
if(deleteSecretResponse.Success) if(deleteSecretResponse.Success)
{ {
this.SettingsManager.ConfigurationData.Providers.Remove(provider); this.SettingsManager.ConfigurationData.Providers.Remove(provider);
+157 -61
View File
@@ -1,9 +1,10 @@
using AIStudio;
using AIStudio.Agents; using AIStudio.Agents;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Logging.Console;
using MudBlazor.Services; using MudBlazor.Services;
#if !DEBUG #if !DEBUG
@@ -11,73 +12,168 @@ using System.Reflection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
#endif #endif
var port = args.Length > 0 ? args[0] : "5000"; namespace AIStudio;
var builder = WebApplication.CreateBuilder();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined;
});
builder.Services.AddMudMarkdownServices(); internal sealed class Program
builder.Services.AddSingleton(MessageBus.INSTANCE); {
builder.Services.AddSingleton<Rust>(); public static RustService RUST_SERVICE = null!;
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>(); public static Encryption ENCRYPTION = null!;
builder.Services.AddSingleton<SettingsManager>(); public static string API_TOKEN = null!;
builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddTransient<HTMLParser>(); public static async Task Main(string[] args)
builder.Services.AddTransient<AgentTextContentCleaner>();
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
{ {
options.MaximumReceiveMessageSize = null; if(args.Length == 0)
options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200); {
options.HandshakeTimeout = TimeSpan.FromSeconds(30); Console.WriteLine("Error: Please provide the port of the runtime API.");
}); return;
}
builder.Services.AddSingleton(new HttpClient // Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
{ var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
BaseAddress = new Uri($"http://localhost:{port}") if(string.IsNullOrWhiteSpace(secretPasswordEncoded))
}); {
Console.WriteLine("Error: The AI_STUDIO_SECRET_PASSWORD environment variable is not set.");
return;
}
builder.WebHost.UseUrls($"http://localhost:{port}"); var secretPassword = Convert.FromBase64String(secretPasswordEncoded);
var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT");
if(string.IsNullOrWhiteSpace(secretKeySaltEncoded))
{
Console.WriteLine("Error: The AI_STUDIO_SECRET_KEY_SALT environment variable is not set.");
return;
}
var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded);
var certificateFingerprint = Environment.GetEnvironmentVariable("AI_STUDIO_CERTIFICATE_FINGERPRINT");
if(string.IsNullOrWhiteSpace(certificateFingerprint))
{
Console.WriteLine("Error: The AI_STUDIO_CERTIFICATE_FINGERPRINT environment variable is not set.");
return;
}
var apiToken = Environment.GetEnvironmentVariable("AI_STUDIO_API_TOKEN");
if(string.IsNullOrWhiteSpace(apiToken))
{
Console.WriteLine("Error: The AI_STUDIO_API_TOKEN environment variable is not set.");
return;
}
API_TOKEN = apiToken;
var rustApiPort = args[0];
using var rust = new RustService(rustApiPort, certificateFingerprint);
var appPort = await rust.GetAppPort();
if(appPort == 0)
{
Console.WriteLine("Error: Failed to get the app port from Rust.");
return;
}
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
{
kestrelServerOptions.ConfigureEndpointDefaults(listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
});
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
builder.Logging.AddFilter("Microsoft", LogLevel.Information);
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning);
builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning);
builder.Logging.AddFilter("MudBlazor", LogLevel.Information);
builder.Logging.AddConsole(options =>
{
options.FormatterName = TerminalLogger.FORMATTER_NAME;
}).AddConsoleFormatter<TerminalLogger, ConsoleFormatterOptions>();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined;
});
builder.Services.AddMudMarkdownServices();
builder.Services.AddSingleton(MessageBus.INSTANCE);
builder.Services.AddSingleton(rust);
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddTransient<HTMLParser>();
builder.Services.AddTransient<AgentTextContentCleaner>();
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = null;
options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton(new HttpClient
{
BaseAddress = new Uri($"http://localhost:{appPort}")
});
builder.WebHost.UseUrls($"http://localhost:{appPort}");
#if DEBUG
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
#endif
// Execute the builder to get the app:
var app = builder.Build();
// Initialize the encryption service:
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
var encryptionInitializer = encryption.Initialize();
// Set the logger for the Rust service:
var rustLogger = app.Services.GetRequiredService<ILogger<RustService>>();
rust.SetLogger(rustLogger);
rust.SetEncryptor(encryption);
RUST_SERVICE = rust;
ENCRYPTION = encryption;
app.Use(Redirect.HandlerContentAsync);
#if DEBUG #if DEBUG
builder.WebHost.UseWebRoot("wwwroot"); app.UseStaticFiles();
builder.WebHost.UseStaticWebAssets(); app.UseDeveloperExceptionPage();
#endif
var app = builder.Build();
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
app.UseStaticFiles();
app.UseDeveloperExceptionPage();
#else #else
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot"); app.UseStaticFiles(new StaticFileOptions
app.UseStaticFiles(new StaticFileOptions {
{ FileProvider = fileProvider,
FileProvider = fileProvider, RequestPath = string.Empty,
RequestPath = string.Empty, });
});
#endif #endif
app.UseAntiforgery(); app.UseAntiforgery();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
var serverTask = app.RunAsync(); var serverTask = app.RunAsync();
Console.WriteLine("RUST/TAURI SERVER STARTED"); await encryptionInitializer;
await serverTask; await rust.AppIsReady();
await serverTask;
}
}
@@ -4,11 +4,10 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic; namespace AIStudio.Provider.Anthropic;
public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@@ -22,10 +21,10 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
public string InstanceName { get; set; } = "Anthropic"; public string InstanceName { get; set; } = "Anthropic";
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Get the API key: // Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this); var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success) if(!requestedSecret.Success)
yield break; yield break;
@@ -64,7 +63,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
var request = new HttpRequestMessage(HttpMethod.Post, "messages"); var request = new HttpRequestMessage(HttpMethod.Post, "messages");
// Set the authorization header: // Set the authorization header:
request.Headers.Add("x-api-key", requestedSecret.Secret); request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the Anthropic version: // Set the Anthropic version:
request.Headers.Add("anthropic-version", "2023-06-01"); request.Headers.Add("anthropic-version", "2023-06-01");
@@ -137,14 +136,14 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <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) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(new[] return Task.FromResult(new[]
{ {
@@ -157,7 +156,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Model>()); return Task.FromResult(Enumerable.Empty<Model>());
} }
@@ -1,3 +1,5 @@
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Provider; namespace AIStudio.Provider;
/// <summary> /// <summary>
@@ -10,12 +12,30 @@ public abstract class BaseProvider
/// </summary> /// </summary>
protected readonly HttpClient httpClient = new(); protected readonly HttpClient httpClient = new();
/// <summary>
/// The logger to use.
/// </summary>
protected readonly ILogger logger;
static BaseProvider()
{
RUST_SERVICE = Program.RUST_SERVICE;
ENCRYPTION = Program.ENCRYPTION;
}
protected static readonly RustService RUST_SERVICE;
protected static readonly Encryption ENCRYPTION;
/// <summary> /// <summary>
/// Constructor for the base provider. /// Constructor for the base provider.
/// </summary> /// </summary>
/// <param name="url">The base URL for the provider.</param> /// <param name="url">The base URL for the provider.</param>
protected BaseProvider(string url) /// <param name="loggerService">The logger service to use.</param>
protected BaseProvider(string url, ILogger loggerService)
{ {
this.logger = loggerService;
// Set the base URL: // Set the base URL:
this.httpClient.BaseAddress = new(url); this.httpClient.BaseAddress = new(url);
} }
@@ -4,11 +4,10 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks; namespace AIStudio.Provider.Fireworks;
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@@ -24,10 +23,10 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
public string InstanceName { get; set; } = "Fireworks.ai"; public string InstanceName { get; set; } = "Fireworks.ai";
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Get the API key: // Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this); var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success) if(!requestedSecret.Success)
yield break; yield break;
@@ -73,7 +72,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header: // Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content: // Set the content:
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
@@ -139,20 +138,20 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <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) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Model>()); return Task.FromResult(Enumerable.Empty<Model>());
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Model>()); return Task.FromResult(Enumerable.Empty<Model>());
} }
+4 -13
View File
@@ -1,5 +1,4 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider; namespace AIStudio.Provider;
@@ -22,44 +21,36 @@ public interface IProvider
/// <summary> /// <summary>
/// Starts a chat completion stream. /// Starts a chat completion stream.
/// </summary> /// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="chatModel">The model to use for chat completion.</param> /// <param name="chatModel">The model to use for chat completion.</param>
/// <param name="chatThread">The chat thread to continue.</param> /// <param name="chatThread">The chat thread to continue.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The chat completion stream.</returns> /// <returns>The chat completion stream.</returns>
public IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, CancellationToken token = default); public IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, CancellationToken token = default);
/// <summary> /// <summary>
/// Starts an image completion stream. /// Starts an image completion stream.
/// </summary> /// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="imageModel">The model to use for image completion.</param> /// <param name="imageModel">The model to use for image completion.</param>
/// <param name="promptPositive">The positive prompt.</param> /// <param name="promptPositive">The positive prompt.</param>
/// <param name="promptNegative">The negative prompt.</param> /// <param name="promptNegative">The negative prompt.</param>
/// <param name="referenceImageURL">The reference image URL.</param> /// <param name="referenceImageURL">The reference image URL.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The image completion stream.</returns> /// <returns>The image completion stream.</returns>
public IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default); public IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
/// <summary> /// <summary>
/// Load all possible text models that can be used with this provider. /// Load all possible text models that can be used with this provider.
/// </summary> /// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param> /// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The list of text models.</returns> /// <returns>The list of text models.</returns>
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default); public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
/// <summary> /// <summary>
/// Load all possible image models that can be used with this provider. /// Load all possible image models that can be used with this provider.
/// </summary> /// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param> /// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The list of image models.</returns> /// <returns>The list of image models.</returns>
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default); public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
} }
@@ -5,11 +5,10 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Mistral; namespace AIStudio.Provider.Mistral;
public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/"), IProvider public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@@ -23,10 +22,10 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
public string InstanceName { get; set; } = "Mistral"; public string InstanceName { get; set; } = "Mistral";
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Get the API key: // Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this); var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success) if(!requestedSecret.Success)
yield break; yield break;
@@ -75,7 +74,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header: // Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content: // Set the content:
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
@@ -141,21 +140,21 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public async Task<IEnumerable<Provider.Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
var secretKey = apiKeyProvisional switch var secretKey = apiKeyProvisional switch
{ {
not null => apiKeyProvisional, not null => apiKeyProvisional,
_ => await settings.GetAPIKey(jsRuntime, this) switch _ => await RUST_SERVICE.GetAPIKey(this) switch
{ {
{ Success: true } result => result.Secret, { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null, _ => null,
} }
}; };
@@ -179,7 +178,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Provider.Model>()); return Task.FromResult(Enumerable.Empty<Provider.Model>());
} }
@@ -1,7 +1,6 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider; namespace AIStudio.Provider;
@@ -13,17 +12,17 @@ public class NoProvider : IProvider
public string InstanceName { get; set; } = "None"; public string InstanceName { get; set; } = "None";
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]); public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]); public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
await Task.FromResult(0); await Task.FromResult(0);
yield break; yield break;
} }
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) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
await Task.FromResult(0); await Task.FromResult(0);
yield break; yield break;
@@ -4,14 +4,13 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
/// <summary> /// <summary>
/// The OpenAI provider. /// The OpenAI provider.
/// </summary> /// </summary>
public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"), IProvider public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@@ -27,10 +26,10 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
public string InstanceName { get; set; } = "OpenAI"; public string InstanceName { get; set; } = "OpenAI";
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Get the API key: // Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this); var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success) if(!requestedSecret.Success)
yield break; yield break;
@@ -79,7 +78,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header: // Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content: // Set the content:
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
@@ -145,34 +144,34 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <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) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return this.LoadModels(jsRuntime, settings, "gpt-", token, apiKeyProvisional); return this.LoadModels("gpt-", token, apiKeyProvisional);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return this.LoadModels(jsRuntime, settings, "dall-e-", token, apiKeyProvisional); return this.LoadModels("dall-e-", token, apiKeyProvisional);
} }
#endregion #endregion
private async Task<IEnumerable<Model>> LoadModels(IJSRuntime jsRuntime, SettingsManager settings, string prefix, CancellationToken token, string? apiKeyProvisional = null) private async Task<IEnumerable<Model>> LoadModels(string prefix, CancellationToken token, string? apiKeyProvisional = null)
{ {
var secretKey = apiKeyProvisional switch var secretKey = apiKeyProvisional switch
{ {
not null => apiKeyProvisional, not null => apiKeyProvisional,
_ => await settings.GetAPIKey(jsRuntime, this) switch _ => await RUST_SERVICE.GetAPIKey(this) switch
{ {
{ Success: true } result => result.Secret, { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null, _ => null,
} }
}; };
@@ -1,9 +1,3 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Provider; namespace AIStudio.Provider;
/// <summary> /// <summary>
@@ -21,58 +15,3 @@ public enum Providers
SELF_HOSTED = 4, SELF_HOSTED = 4,
} }
/// <summary>
/// Extension methods for the provider enum.
/// </summary>
public static class ExtensionsProvider
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.NONE => "No provider selected",
Providers.OPEN_AI => "OpenAI",
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings)
{
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,60 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Provider;
public static class ProvidersExtensions
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.NONE => "No provider selected",
Providers.OPEN_AI => "OpenAI",
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <param name="logger">The logger to use.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
{
try
{
return providerSettings.UsedProvider switch
{
Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(),
};
}
catch (Exception e)
{
logger.LogError($"Failed to create provider: {e.Message}");
return new NoProvider();
}
}
}
@@ -4,11 +4,10 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted; namespace AIStudio.Provider.SelfHosted;
public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}", logger), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@@ -21,7 +20,8 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
public string InstanceName { get; set; } = "Self-hosted"; public string InstanceName { get; set; } = "Self-hosted";
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) /// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Prepare the system prompt: // Prepare the system prompt:
var systemPrompt = new Message var systemPrompt = new Message
@@ -129,14 +129,14 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task<IEnumerable<Provider.Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
try try
{ {
@@ -162,14 +162,14 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
} }
catch(Exception e) catch(Exception e)
{ {
Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}"); this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
return []; return [];
} }
} }
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) public Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Provider.Model>()); return Task.FromResult(Enumerable.Empty<Provider.Model>());
} }
@@ -5,7 +5,6 @@ using AIStudio.Assistants.RewriteImprove;
using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.TextSummarizer;
using AIStudio.Assistants.EMail; using AIStudio.Assistants.EMail;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles; using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles;
using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles; using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles;
@@ -1,5 +1,4 @@
using AIStudio.Assistants.Agenda; using AIStudio.Assistants.Agenda;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
@@ -22,4 +22,9 @@ public sealed class DataApp
/// The navigation behavior. /// The navigation behavior.
/// </summary> /// </summary>
public NavBehavior NavigationBehavior { get; set; } = NavBehavior.NEVER_EXPAND_USE_TOOLTIPS; public NavBehavior NavigationBehavior { get; set; } = NavBehavior.NEVER_EXPAND_USE_TOOLTIPS;
/// <summary>
/// Should we preselect a provider for the entire app?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
} }
@@ -1,5 +1,4 @@
using AIStudio.Assistants.EMail; using AIStudio.Assistants.EMail;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
public sealed class DataGrammarSpelling public sealed class DataGrammarSpelling
@@ -1,5 +1,4 @@
using AIStudio.Assistants.RewriteImprove; using AIStudio.Assistants.RewriteImprove;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
@@ -1,5 +1,4 @@
using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
public sealed class DataTranslation public sealed class DataTranslation
@@ -1,7 +1,6 @@
using AIStudio.Assistants.Coding; using AIStudio.Assistants.Coding;
using AIStudio.Assistants.IconFinder; using AIStudio.Assistants.IconFinder;
using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel.PreviousModels; namespace AIStudio.Settings.DataModel.PreviousModels;
@@ -1,7 +1,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
// ReSharper disable NotAccessedPositionalProperty.Local // ReSharper disable NotAccessedPositionalProperty.Local
@@ -11,7 +10,7 @@ namespace AIStudio.Settings;
/// <summary> /// <summary>
/// The settings manager. /// The settings manager.
/// </summary> /// </summary>
public sealed class SettingsManager public sealed class SettingsManager(ILogger<SettingsManager> logger)
{ {
private const string SETTINGS_FILENAME = "settings.json"; private const string SETTINGS_FILENAME = "settings.json";
@@ -21,6 +20,8 @@ public sealed class SettingsManager
Converters = { new JsonStringEnumConverter() }, Converters = { new JsonStringEnumConverter() },
}; };
private ILogger<SettingsManager> logger = logger;
/// <summary> /// <summary>
/// The directory where the configuration files are stored. /// The directory where the configuration files are stored.
/// </summary> /// </summary>
@@ -38,75 +39,23 @@ public sealed class SettingsManager
private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory); private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory);
#region API Key Handling
private readonly record struct GetSecretRequest(string Destination, string UserName);
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, string Secret, string Issue);
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<RequestedSecret>("window.__TAURI__.invoke", "get_secret", new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
private readonly record struct StoreSecretRequest(string Destination, string UserName, string Secret);
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IJSRuntime jsRuntime, IProvider provider, string key) => await jsRuntime.InvokeAsync<StoreSecretResponse>("window.__TAURI__.invoke", "store_secret", new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, key));
private readonly record struct DeleteSecretRequest(string Destination, string UserName);
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
/// <summary>
/// Tries to delete the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<DeleteSecretResponse>("window.__TAURI__.invoke", "delete_secret", new DeleteSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
#endregion
/// <summary> /// <summary>
/// Loads the settings from the file system. /// Loads the settings from the file system.
/// </summary> /// </summary>
public async Task LoadSettings() public async Task LoadSettings()
{ {
if(!this.IsSetUp) if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
return; return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!File.Exists(settingsPath)) if(!File.Exists(settingsPath))
{
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
return; return;
}
// We read the `"Version": "V3"` line to determine the version of the settings file: // We read the `"Version": "V3"` line to determine the version of the settings file:
await foreach (var line in File.ReadLinesAsync(settingsPath)) await foreach (var line in File.ReadLinesAsync(settingsPath))
@@ -123,16 +72,16 @@ public sealed class SettingsManager
Enum.TryParse(settingsVersionText, out Version settingsVersion); Enum.TryParse(settingsVersionText, out Version settingsVersion);
if(settingsVersion is Version.UNKNOWN) if(settingsVersion is Version.UNKNOWN)
{ {
Console.WriteLine("Error: Unknown version of the settings file."); this.logger.LogError("Unknown version of the settings file found.");
this.ConfigurationData = new(); this.ConfigurationData = new();
return; return;
} }
this.ConfigurationData = SettingsMigrations.Migrate(settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
return; return;
} }
Console.WriteLine("Error: Failed to read the version of the settings file."); this.logger.LogError("Failed to read the version of the settings file.");
this.ConfigurationData = new(); this.ConfigurationData = new();
} }
@@ -142,15 +91,50 @@ public sealed class SettingsManager
public async Task StoreSettings() public async Task StoreSettings()
{ {
if(!this.IsSetUp) if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return; return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!Directory.Exists(ConfigDirectory)) if(!Directory.Exists(ConfigDirectory))
{
this.logger.LogInformation("Creating the configuration directory.");
Directory.CreateDirectory(ConfigDirectory!); Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS); var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
await File.WriteAllTextAsync(settingsPath, settingsJson); await File.WriteAllTextAsync(settingsPath, settingsJson);
this.logger.LogInformation("Stored the settings to the file system.");
} }
public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false"; public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";
public Provider GetPreselectedProvider(Tools.Components component)
{
if(this.ConfigurationData.Providers.Count == 1)
return this.ConfigurationData.Providers[0];
var preselection = component switch
{
Tools.Components.CHAT => this.ConfigurationData.Chat.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.Chat.PreselectedProvider) : default,
Tools.Components.GRAMMAR_SPELLING_ASSISTANT => this.ConfigurationData.GrammarSpelling.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.GrammarSpelling.PreselectedProvider) : default,
Tools.Components.ICON_FINDER_ASSISTANT => this.ConfigurationData.IconFinder.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.IconFinder.PreselectedProvider) : default,
Tools.Components.REWRITE_ASSISTANT => this.ConfigurationData.RewriteImprove.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.RewriteImprove.PreselectedProvider) : default,
Tools.Components.TRANSLATION_ASSISTANT => this.ConfigurationData.Translation.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.Translation.PreselectedProvider) : default,
Tools.Components.AGENDA_ASSISTANT => this.ConfigurationData.Agenda.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.Agenda.PreselectedProvider) : default,
Tools.Components.CODING_ASSISTANT => this.ConfigurationData.Coding.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.Coding.PreselectedProvider) : default,
Tools.Components.TEXT_SUMMARIZER_ASSISTANT => this.ConfigurationData.TextSummarizer.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.TextSummarizer.PreselectedProvider) : default,
Tools.Components.EMAIL_ASSISTANT => this.ConfigurationData.EMail.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.EMail.PreselectedProvider) : default,
Tools.Components.LEGAL_CHECK_ASSISTANT => this.ConfigurationData.LegalCheck.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.LegalCheck.PreselectedProvider) : default,
_ => default,
};
if (preselection != default)
return preselection;
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider);
}
} }
@@ -9,7 +9,7 @@ namespace AIStudio.Settings;
public static class SettingsMigrations public static class SettingsMigrations
{ {
public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions) public static Data Migrate(ILogger<SettingsManager> logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
{ {
switch (previousVersion) switch (previousVersion)
{ {
@@ -17,41 +17,41 @@ public static class SettingsMigrations
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV1 is null) if (configV1 is null)
{ {
Console.WriteLine("Error: failed to parse the configuration. Using default values."); logger.LogError("Failed to parse the v1 configuration. Using default values.");
return new(); return new();
} }
configV1 = MigrateV1ToV2(configV1); configV1 = MigrateV1ToV2(logger, configV1);
configV1 = MigrateV2ToV3(configV1); configV1 = MigrateV2ToV3(logger, configV1);
return MigrateV3ToV4(configV1); return MigrateV3ToV4(logger, configV1);
case Version.V2: case Version.V2:
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV2 is null) if (configV2 is null)
{ {
Console.WriteLine("Error: failed to parse the configuration. Using default values."); logger.LogError("Failed to parse the v2 configuration. Using default values.");
return new(); return new();
} }
configV2 = MigrateV2ToV3(configV2); configV2 = MigrateV2ToV3(logger, configV2);
return MigrateV3ToV4(configV2); return MigrateV3ToV4(logger, configV2);
case Version.V3: case Version.V3:
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV3 is null) if (configV3 is null)
{ {
Console.WriteLine("Error: failed to parse the configuration. Using default values."); logger.LogError("Failed to parse the v3 configuration. Using default values.");
return new(); return new();
} }
return MigrateV3ToV4(configV3); return MigrateV3ToV4(logger, configV3);
default: default:
Console.WriteLine("No configuration migration needed."); logger.LogInformation("No configuration migration is needed.");
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions); var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV4 is null) if (configV4 is null)
{ {
Console.WriteLine("Error: failed to parse the configuration. Using default values."); logger.LogError("Failed to parse the v4 configuration. Using default values.");
return new(); return new();
} }
@@ -59,14 +59,14 @@ public static class SettingsMigrations
} }
} }
private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData) private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
{ {
// //
// Summary: // Summary:
// In v1 we had no self-hosted providers. Thus, we had no hostnames. // In v1 we had no self-hosted providers. Thus, we had no hostnames.
// //
Console.WriteLine("Migrating from v1 to v2..."); logger.LogInformation("Migrating from v1 to v2...");
return new() return new()
{ {
Version = Version.V2, Version = Version.V2,
@@ -81,14 +81,14 @@ public static class SettingsMigrations
}; };
} }
private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData) private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
{ {
// //
// Summary: // Summary:
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.) // In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
// //
Console.WriteLine("Migrating from v2 to v3..."); logger.LogInformation("Migrating from v2 to v3...");
return new() return new()
{ {
Version = Version.V3, Version = Version.V3,
@@ -110,14 +110,14 @@ public static class SettingsMigrations
}; };
} }
private static Data MigrateV3ToV4(DataV1V3 previousConfig) private static Data MigrateV3ToV4(ILogger<SettingsManager> logger, DataV1V3 previousConfig)
{ {
// //
// Summary: // Summary:
// We grouped the settings into different categories. // We grouped the settings into different categories.
// //
Console.WriteLine("Migrating from v3 to v4..."); logger.LogInformation("Migrating from v3 to v4...");
return new() return new()
{ {
Version = Version.V4, Version = Version.V4,
@@ -1,6 +1,6 @@
namespace AIStudio.Tools; namespace AIStudio.Tools;
public enum SendTo public enum Components
{ {
NONE = 0, NONE = 0,
@@ -0,0 +1,13 @@
using System.Security;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
[JsonConverter(typeof(EncryptedTextJsonConverter))]
public readonly record struct EncryptedText(string EncryptedData)
{
public EncryptedText() : this(string.Empty)
{
throw new SecurityException("Please provide the encrypted data.");
}
}
@@ -0,0 +1,8 @@
namespace AIStudio.Tools;
public static class EncryptedTextExtensions
{
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
}
@@ -0,0 +1,27 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
public sealed class EncryptedTextJsonConverter : JsonConverter<EncryptedText>
{
#region Overrides of JsonConverter<EncryptedText>
public override EncryptedText Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.String)
{
var value = reader.GetString()!;
return new EncryptedText(value);
}
throw new JsonException($"Unexpected token type when parsing EncryptedText. Expected {JsonTokenType.String}, but got {reader.TokenType}.");
}
public override void Write(Utf8JsonWriter writer, EncryptedText value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.EncryptedData);
}
#endregion
}
+142
View File
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace AIStudio.Tools;
public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword, byte[] secretKeySalt)
{
/// <summary>
/// The number of iterations to derive the key and IV from the password. For a password manager
/// where the user has to enter their primary password, 100 iterations would be too few and
/// insecure. Here, the use case is different: We generate a 512-byte long and cryptographically
/// secure password at every start. This password already contains enough entropy. In our case,
/// we need key and IV primarily because AES, with the algorithms we chose, requires a fixed key
/// length, and our password is too long.
/// </summary>
private const int ITERATIONS = 100;
private readonly byte[] key = new byte[32];
private readonly byte[] iv = new byte[16];
public async Task Initialize()
{
logger.LogInformation("Initializing encryption service...");
var stopwatch = Stopwatch.StartNew();
if (secretPassword.Length != 512)
{
logger.LogError($"The secret password must be 512 bytes long. It was {secretPassword.Length} bytes long.");
throw new CryptographicException("The secret password must be 512 bytes long.");
}
if(secretKeySalt.Length != 16)
{
logger.LogError($"The salt data must be 16 bytes long. It was {secretKeySalt.Length} bytes long.");
throw new CryptographicException("The salt data must be 16 bytes long.");
}
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512);
var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
Array.Copy(keyBytes, this.key, this.key.Length);
Array.Copy(ivBytes, this.iv, this.iv.Length);
});
var initDuration = stopwatch.Elapsed;
stopwatch.Stop();
logger.LogInformation($"Encryption service initialized in {initDuration.TotalMilliseconds} milliseconds.");
}
public async Task<EncryptedText> Encrypt(string data)
{
// Create AES encryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
aes.Mode = CipherMode.CBC;
using var encryption = aes.CreateEncryptor();
// Copy the given string data into a memory stream:
await using var plainDataStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
// A memory stream for the final, encrypted data:
await using var encryptedAndEncodedData = new MemoryStream();
// A base64 stream for the encoding:
await using var base64Stream = new CryptoStream(encryptedAndEncodedData, new ToBase64Transform(), CryptoStreamMode.Write);
// Write the salt into the base64 stream:
await base64Stream.WriteAsync(secretKeySalt);
// Create the encryption stream:
await using var cryptoStream = new CryptoStream(base64Stream, encryption, CryptoStreamMode.Write);
// Write the payload into the encryption stream:
await plainDataStream.CopyToAsync(cryptoStream);
// Flush the final block. Please note that it is not enough to call the regular flush method.
await cryptoStream.FlushFinalBlockAsync();
// Convert the base64 encoded data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return new EncryptedText(Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]));
}
public async Task<string> Decrypt(EncryptedText encryptedData)
{
// Build a memory stream to access the given base64 encoded data:
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData.EncryptedData));
// Wrap around the base64 decoder stream:
await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read);
// A buffer for the salt's bytes:
var readSaltBytes = new byte[16]; // 16 bytes = Guid
// Read the salt's bytes out of the stream:
var readBytes = 0;
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
while(readBytes < readSaltBytes.Length && !cts.Token.IsCancellationRequested)
{
readBytes += await base64Stream.ReadAsync(readSaltBytes, readBytes, readSaltBytes.Length - readBytes, cts.Token);
await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token);
}
// Check the salt bytes:
if(!readSaltBytes.SequenceEqual(secretKeySalt))
{
logger.LogError("The salt bytes do not match. The data is corrupted or tampered.");
throw new CryptographicException("The salt bytes do not match. The data is corrupted or tampered.");
}
// Create AES decryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
using var decryption = aes.CreateDecryptor();
// A memory stream for the final, decrypted data:
await using var decryptedData = new MemoryStream();
// The crypto stream:
await using var cryptoStream = new CryptoStream(base64Stream, decryption, CryptoStreamMode.Read);
// Reads all remaining data through the decrypt stream. Note that this operation
// starts at the current position, i.e., after the salt bytes:
await cryptoStream.CopyToAsync(decryptedData);
// Convert the decrypted data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]);
}
}
@@ -0,0 +1,18 @@
using System.Net.Http.Headers;
namespace AIStudio.Tools;
public static class HttpRequestHeadersExtensions
{
private static readonly string API_TOKEN;
static HttpRequestHeadersExtensions()
{
API_TOKEN = Program.API_TOKEN;
}
public static void AddApiToken(this HttpRequestHeaders headers)
{
headers.Add("token", API_TOKEN);
}
}
-54
View File
@@ -1,54 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class Rust
{
/// <summary>
/// Tries to copy the given text to the clipboard.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="snackbar">The snackbar to show the result.</param>
/// <param name="text">The text to copy to the clipboard.</param>
public async Task CopyText2Clipboard(IJSRuntime jsRuntime, ISnackbar snackbar, string text)
{
var response = await jsRuntime.InvokeAsync<SetClipboardResponse>("window.__TAURI__.invoke", "set_clipboard", new SetClipboardText(text));
var msg = response.Success switch
{
true => "Successfully copied text to clipboard!",
false => $"Failed to copy text to clipboard: {response.Issue}",
};
var severity = response.Success switch
{
true => Severity.Success,
false => Severity.Error,
};
snackbar.Add(msg, severity, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = severity switch
{
Severity.Success => Color.Success,
Severity.Error => Color.Error,
_ => Color.Default,
};
});
}
public async Task<UpdateResponse> CheckForUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
return await jsRuntime.InvokeAsync<UpdateResponse>("window.__TAURI__.invoke", cts.Token, "check_for_update");
}
public async Task InstallUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource();
await jsRuntime.InvokeVoidAsync("window.__TAURI__.invoke", cts.Token, "install_update");
}
}
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue);
@@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct SelectSecretRequest(string Destination, string UserName);
@@ -1,4 +1,4 @@
namespace AIStudio.Tools; namespace AIStudio.Tools.Rust;
/// <summary> /// <summary>
/// The response from the set clipboard operation. /// The response from the set clipboard operation.
@@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct StoreSecretRequest(string Destination, string UserName, EncryptedText Secret);
@@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
@@ -1,6 +1,4 @@
using System.Text.Json.Serialization; namespace AIStudio.Tools.Rust;
namespace AIStudio.Tools;
/// <summary> /// <summary>
/// The response of the update check. /// The response of the update check.
@@ -9,8 +7,8 @@ namespace AIStudio.Tools;
/// <param name="NewVersion">The new version, when available.</param> /// <param name="NewVersion">The new version, when available.</param>
/// <param name="Changelog">The changelog of the new version, when available.</param> /// <param name="Changelog">The changelog of the new version, when available.</param>
public readonly record struct UpdateResponse( public readonly record struct UpdateResponse(
[property:JsonPropertyName("update_is_available")] bool UpdateIsAvailable, bool UpdateIsAvailable,
[property:JsonPropertyName("error")] bool Error, bool Error,
[property:JsonPropertyName("new_version")] string NewVersion, string NewVersion,
string Changelog string Changelog
); );
+333
View File
@@ -0,0 +1,333 @@
using System.Security.Cryptography;
using System.Text.Json;
using AIStudio.Provider;
using AIStudio.Tools.Rust;
// ReSharper disable NotAccessedPositionalProperty.Local
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class RustService : IDisposable
{
private readonly HttpClient http;
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
private ILogger<RustService>? logger;
private Encryption? encryptor;
private readonly string apiPort;
private readonly string certificateFingerprint;
public RustService(string apiPort, string certificateFingerprint)
{
this.apiPort = apiPort;
this.certificateFingerprint = certificateFingerprint;
var certificateValidationHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
{
if(certificate is null)
return false;
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
return currentCertificateFingerprint == certificateFingerprint;
},
};
this.http = new HttpClient(certificateValidationHandler)
{
BaseAddress = new Uri($"https://127.0.0.1:{apiPort}"),
DefaultRequestVersion = Version.Parse("2.0"),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
};
this.http.DefaultRequestHeaders.AddApiToken();
}
public void SetLogger(ILogger<RustService> logService)
{
this.logger = logService;
}
public void SetEncryptor(Encryption encryptionService)
{
this.encryptor = encryptionService;
}
public async Task<int> GetAppPort()
{
Console.WriteLine("Trying to get app port from Rust runtime...");
//
// Note I: In the production environment, the Rust runtime is already running
// and listening on the given port. In the development environment, the IDE
// starts the Rust runtime in parallel with the .NET runtime. Since the
// Rust runtime needs some time to start, we have to wait for it to be ready.
//
const int MAX_TRIES = 160;
var tris = 0;
var wait4Try = TimeSpan.FromMilliseconds(250);
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
while (tris++ < MAX_TRIES)
{
//
// Note II: We use a new HttpClient instance for each try to avoid
// .NET is caching the result. When we use the same HttpClient
// instance, we would always get the same result (403 forbidden),
// without even trying to connect to the Rust server.
//
using var initialHttp = new HttpClient(new HttpClientHandler
{
//
// Note III: We have to create also a new HttpClientHandler instance
// for each try to avoid .NET is caching the result. This is necessary
// because it gets disposed when the HttpClient instance gets disposed.
//
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
{
if(certificate is null)
return false;
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
return currentCertificateFingerprint == this.certificateFingerprint;
}
});
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
initialHttp.DefaultRequestHeaders.AddApiToken();
try
{
var response = await initialHttp.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
await Task.Delay(wait4Try);
continue;
}
var appPortContent = await response.Content.ReadAsStringAsync();
var appPort = int.Parse(appPortContent);
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
return appPort;
}
catch (Exception e)
{
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
Console.WriteLine(e.InnerException);
throw;
}
}
Console.WriteLine("Failed to receive the app port from Rust runtime.");
return 0;
}
public async Task AppIsReady()
{
const string URL = "/system/dotnet/ready";
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
try
{
var response = await this.http.GetAsync(URL);
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
}
}
catch (Exception e)
{
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
throw;
}
}
public async Task<string> GetConfigDirectory()
{
var response = await this.http.GetAsync("/system/directories/config");
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
return string.Empty;
}
return await response.Content.ReadAsStringAsync();
}
public async Task<string> GetDataDirectory()
{
var response = await this.http.GetAsync("/system/directories/data");
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
return string.Empty;
}
return await response.Content.ReadAsStringAsync();
}
/// <summary>
/// Tries to copy the given text to the clipboard.
/// </summary>
/// <param name="snackbar">The snackbar to show the result.</param>
/// <param name="text">The text to copy to the clipboard.</param>
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
{
var message = "Successfully copied the text to your clipboard";
var iconColor = Color.Error;
var severity = Severity.Error;
try
{
var encryptedText = await text.Encrypt(this.encryptor!);
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
message = "Failed to copy the text to your clipboard.";
return;
}
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
if (!state.Success)
{
this.logger!.LogError("Failed to copy the text to the clipboard.");
message = "Failed to copy the text to your clipboard.";
return;
}
iconColor = Color.Success;
severity = Severity.Success;
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
}
finally
{
snackbar.Add(message, severity, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = iconColor;
});
}
}
public async Task<UpdateResponse> CheckForUpdate()
{
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
return response;
}
catch (Exception e)
{
this.logger!.LogError(e, "Failed to check for an update.");
return new UpdateResponse
{
Error = true,
UpdateIsAvailable = false,
};
}
}
public async Task InstallUpdate()
{
try
{
var cts = new CancellationTokenSource();
await this.http.GetAsync("/updates/install", cts.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IProvider provider)
{
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
}
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
if (!secret.Success)
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
return secret;
}
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IProvider provider, string key)
{
var encryptedKey = await this.encryptor!.Encrypt(key);
var request = new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, encryptedKey);
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
}
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success)
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
/// <summary>
/// Tries to delete the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
{
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
}
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success)
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
#region IDisposable
public void Dispose()
{
this.http.Dispose();
}
#endregion
}
+1 -1
View File
@@ -8,6 +8,6 @@ public readonly record struct SendToButton() : IButtonData
public bool UseResultingContentBlockData { get; init; } = true; public bool UseResultingContentBlockData { get; init; } = true;
public SendTo Self { get; init; } = SendTo.NONE; public Components Self { get; init; } = Components.NONE;
} }
@@ -2,36 +2,36 @@ namespace AIStudio.Tools;
public static class SendToExtensions public static class SendToExtensions
{ {
public static string Name(this SendTo assistant) => assistant switch public static string Name(this Components assistant) => assistant switch
{ {
SendTo.GRAMMAR_SPELLING_ASSISTANT => "Grammar & Spelling Assistant", Components.GRAMMAR_SPELLING_ASSISTANT => "Grammar & Spelling Assistant",
SendTo.TEXT_SUMMARIZER_ASSISTANT => "Text Summarizer Assistant", Components.TEXT_SUMMARIZER_ASSISTANT => "Text Summarizer Assistant",
SendTo.ICON_FINDER_ASSISTANT => "Icon Finder Assistant", Components.ICON_FINDER_ASSISTANT => "Icon Finder Assistant",
SendTo.TRANSLATION_ASSISTANT => "Translation Assistant", Components.TRANSLATION_ASSISTANT => "Translation Assistant",
SendTo.REWRITE_ASSISTANT => "Rewrite Assistant", Components.REWRITE_ASSISTANT => "Rewrite Assistant",
SendTo.AGENDA_ASSISTANT => "Agenda Assistant", Components.AGENDA_ASSISTANT => "Agenda Assistant",
SendTo.CODING_ASSISTANT => "Coding Assistant", Components.CODING_ASSISTANT => "Coding Assistant",
SendTo.EMAIL_ASSISTANT => "E-Mail Assistant", Components.EMAIL_ASSISTANT => "E-Mail Assistant",
SendTo.LEGAL_CHECK_ASSISTANT => "Legal Check Assistant", Components.LEGAL_CHECK_ASSISTANT => "Legal Check Assistant",
SendTo.CHAT => "New Chat", Components.CHAT => "New Chat",
_ => "Send to ...", _ => "Send to ...",
}; };
public static SendToData GetData(this SendTo destination) => destination switch public static SendToData GetData(this Components destination) => destination switch
{ {
SendTo.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA), Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
SendTo.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING), Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
SendTo.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE), Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
SendTo.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL), Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
SendTo.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION), Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
SendTo.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER), Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
SendTo.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING), Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
SendTo.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER), Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
SendTo.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK), Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
SendTo.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT), Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
_ => new(Event.NONE, Routes.ASSISTANTS), _ => new(Event.NONE, Routes.ASSISTANTS),
}; };
@@ -6,17 +6,15 @@ namespace AIStudio.Tools.Services;
/// Wire up the clipboard service to copy Markdown to the clipboard. /// Wire up the clipboard service to copy Markdown to the clipboard.
/// We use our own Rust-based clipboard service for this. /// We use our own Rust-based clipboard service for this.
/// </summary> /// </summary>
public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, ISnackbar snackbar) : IMudMarkdownClipboardService public sealed class MarkdownClipboardService(RustService rust, ISnackbar snackbar) : IMudMarkdownClipboardService
{ {
private IJSRuntime JsRuntime { get; } = jsRuntime;
private ISnackbar Snackbar { get; } = snackbar; private ISnackbar Snackbar { get; } = snackbar;
private Rust Rust { get; } = rust; private RustService Rust { get; } = rust;
/// <summary> /// <summary>
/// Gets called when the user wants to copy the markdown to the clipboard. /// Gets called when the user wants to copy the Markdown to the clipboard.
/// </summary> /// </summary>
/// <param name="text">The Markdown text to copy.</param> /// <param name="text">The Markdown text to copy.</param>
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text); public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.Snackbar, text);
} }
@@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services; namespace AIStudio.Tools.Services;
public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
{ {
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
private static bool IS_INITIALIZED; private static bool IS_INITIALIZED;
private readonly ILogger<TemporaryChatService> logger = logger;
#region Overrides of BackgroundService #region Overrides of BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
this.logger.LogInformation("The temporary chat maintenance service was initialized.");
await settingsManager.LoadSettings(); await settingsManager.LoadSettings();
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE)
{ {
Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
return; return;
} }
@@ -34,9 +38,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
private Task StartMaintenance() private Task StartMaintenance()
{ {
this.logger.LogInformation("Starting maintenance of temporary chat storage.");
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
if(!Directory.Exists(temporaryDirectories)) if(!Directory.Exists(temporaryDirectories))
{
this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
return Task.CompletedTask; return Task.CompletedTask;
}
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
{ {
@@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
}; };
if(deleteChat) if(deleteChat)
{
this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
Directory.Delete(tempChatDirPath, true); Directory.Delete(tempChatDirPath, true);
}
} }
this.logger.LogInformation("Finished maintenance of temporary chat storage.");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -7,20 +7,16 @@ namespace AIStudio.Tools.Services;
public sealed class UpdateService : BackgroundService, IMessageBusReceiver public sealed class UpdateService : BackgroundService, IMessageBusReceiver
{ {
// We cannot inject IJSRuntime into our service. This is because
// the service is not a Blazor component. We need to pass the IJSRuntime from
// the MainLayout component to the service.
private static IJSRuntime? JS_RUNTIME;
private static bool IS_INITIALIZED; private static bool IS_INITIALIZED;
private static ISnackbar? SNACKBAR; private static ISnackbar? SNACKBAR;
private readonly SettingsManager settingsManager; private readonly SettingsManager settingsManager;
private readonly MessageBus messageBus; private readonly MessageBus messageBus;
private readonly Rust rust; private readonly RustService rust;
private TimeSpan updateInterval; private TimeSpan updateInterval;
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust) public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.messageBus = messageBus; this.messageBus = messageBus;
@@ -96,7 +92,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
if(!IS_INITIALIZED) if(!IS_INITIALIZED)
return; return;
var response = await this.rust.CheckForUpdate(JS_RUNTIME!); var response = await this.rust.CheckForUpdate();
if (response.UpdateIsAvailable) if (response.UpdateIsAvailable)
{ {
await this.messageBus.SendMessage(null, Event.UPDATE_AVAILABLE, response); await this.messageBus.SendMessage(null, Event.UPDATE_AVAILABLE, response);
@@ -115,10 +111,9 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
} }
} }
public static void SetBlazorDependencies(IJSRuntime jsRuntime, ISnackbar snackbar) public static void SetBlazorDependencies(ISnackbar snackbar)
{ {
SNACKBAR = snackbar; SNACKBAR = snackbar;
JS_RUNTIME = jsRuntime;
IS_INITIALIZED = true; IS_INITIALIZED = true;
} }
} }
@@ -1,7 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Model for setting clipboard text.
/// </summary>
/// <param name="Text">The text to set to the clipboard.</param>
public record SetClipboardText(string Text);
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace AIStudio.Tools;
public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
{
public const string FORMATTER_NAME = "AI Studio Terminal Logger";
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
var logLevel = logEntry.LogLevel.ToString();
var category = logEntry.Category;
textWriter.Write($"=> {timestamp} [{logLevel}] {category}: {message}");
if (logEntry.Exception is not null)
textWriter.Write($" Exception was = {logEntry.Exception}");
textWriter.WriteLine();
}
}
+4 -4
View File
@@ -4,9 +4,9 @@
"net8.0": { "net8.0": {
"HtmlAgilityPack": { "HtmlAgilityPack": {
"type": "Direct", "type": "Direct",
"requested": "[1.11.62, )", "requested": "[1.11.65, )",
"resolved": "1.11.62", "resolved": "1.11.65",
"contentHash": "KS4h7ZjWsO6YixRfQgYdR+PZMbTaZod1LBPi+1Ph7dJCARzQm7DOKe5HPhP/o0EWX5l7QCgAZHa4VOa4pQa8JQ==" "contentHash": "Skse4MVlRcHHyP88x+v/uQrMxTPaBIwQodiQ/y59jxs8vHlqTR5M3jt5Wk+csYFnR4rX6I4ndqDji2M8p9kAzw=="
}, },
"Microsoft.Extensions.FileProviders.Embedded": { "Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct", "type": "Direct",
@@ -178,6 +178,6 @@
"contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==" "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
} }
}, },
"net8.0/osx-arm64": {} "net8.0/osx-x64": {}
} }
} }
+1 -1
View File
@@ -24,5 +24,5 @@ window.clearDiv = function (divName) {
} }
window.scrollToBottom = function(element) { window.scrollToBottom = function(element) {
element.scrollIntoView(); element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
} }
@@ -0,0 +1,4 @@
# v0.9.0, build 175 (2024-09-03 16:00 UTC)
- Upgraded `keyring` dependency to v3.2.0.
- Refactored the interprocess communication (IPC) to use a new runtime API for better performance, stability, extensibility, maintainability, and security. This is the foundation for upcoming features and improvements, such as performant RAG and scripting support.
- Refactored the logging system. Now, the logging of all systems is unified.
@@ -0,0 +1,4 @@
# v0.9.1, build 176 (2024-09-04 13:48 UTC)
- Added app option to specify a default provider for the entire app.
- Improved provider selection: when there is only one provider available, it is now preselected everywhere.
- Fixed preselection of e-mail assistant options: under some circumstances, the e-mail assistant options were not preselected correctly.
@@ -0,0 +1,2 @@
# v0.9.2, build 177 (2024-09-05 09:19 UTC)
- Fixed the broken auto. update of v0.9.0 and v0.9.1. Unfortunately, an issue crept into the IPC conversion in v0.9.0, which caused the automatic app update to stop working.
@@ -0,0 +1,5 @@
# v0.9.3, build 178 (2024-09-06 11:06 UTC)
- Fixed the scrolling issue for long chat histories.
- Fixed the scrolling issue when you send a lot of text from any assistant to a new chat.
- Upgraded Rust to v1.81.0.
- Upgraded HtmlAgilityPack to v1.11.65.
@@ -2,4 +2,4 @@
* MudBlazor (https://mudblazor.com/) * MudBlazor (https://mudblazor.com/)
* Copyright (c) 2021 MudBlazor * Copyright (c) 2021 MudBlazor
* Licensed under MIT (https://github.com/MudBlazor/MudBlazor/blob/master/LICENSE) * 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-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 */.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
+105 -103
View File
@@ -1,40 +1,6 @@
class MudFileUpload{openFilePicker(id){const element=document.getElementById(id);if(!element){return;} class MudInput{resetValue(id){const input=document.getElementById(id);if(input){input.value='';}}}
try{element.showPicker();}catch(error){element.click();}}} window.mudInput=new MudInput();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;}}
window.mudFileUpload=new MudFileUpload();class MudInput{resetValue(id){const input=document.getElementById(id);if(input){input.value='';}}}
window.mudInput=new MudInput();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.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 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 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();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);}
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]);} 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;} curElem=curElem.parentNode;}
elem.style.height=0;if(didReflow){elem.style.textAlign=null;} elem.style.height=0;if(didReflow){elem.style.textAlign=null;}
@@ -42,8 +8,36 @@ let minHeight=lineHeight*elem.rows;let newHeight=Math.max(minHeight,elem.scrollH
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.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.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);} 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();}}};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`);} if(typeof elem.adjustAutoGrowHeight==='function'){elem.adjustAutoGrowHeight();}},destroy:(elem)=>{if(elem==null){return;}
document.addEventListener("click",function(event){const target=event.target.closest(".mud-ripple");if(target){setRippleOffset(event,target);}});class MudJsEventFactory{connect(dotNetRef,elementId,options){if(!elementId) window.removeEventListener('resize',elem.adjustAutoGrowHeight);if(typeof elem.restoreToInitialState==='function'){elem.restoreToInitialState();}}};class MudFileUpload{openFilePicker(id){const element=document.getElementById(id);if(!element){return;}
try{element.showPicker();}catch(error){element.click();}}}
window.mudFileUpload=new MudFileUpload();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 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();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(container==null){return;}
if(typeof container.destroy==='function'){container.destroy();}}};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] 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] no element found for id: "+elementId;if(!element.mudJsEvent)
element.mudJsEvent=new MudJsEvent(dotNetRef,options);element.mudJsEvent.connect(element);} element.mudJsEvent=new MudJsEvent(dotNetRef,options);element.mudJsEvent.connect(element);}
@@ -86,6 +80,35 @@ onclick(self,e){const caretPosition=e.target.selectionStart;const invoke=self._s
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);}} 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) 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);}}} return;self._dotNetRef.invokeMethodAsync('OnSelect',start,end);}}}
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;};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 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}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);}
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);}
else{this.dotnet.invokeMethodAsync('RaiseOnResized',{height:window.innerHeight,width:window.innerWidth},this.getBreakpoint(window.innerWidth));}}catch(error){this.logger("[MudBlazor] Error in resizeHandler:",{error});}}
cancelListener(){this.dotnet=undefined;window.removeEventListener("resize",this.handleResize);}
matchMedia(query){let m=window.matchMedia(query).matches;return m;}
getBrowserWindowSize(){return{height:window.innerHeight,width:window.innerWidth};}
getBreakpoint(width){if(width>=this.options.breakpointDefinitions["Xxl"])
return 5;else if(width>=this.options.breakpointDefinitions["Xl"])
return 4;else if(width>=this.options.breakpointDefinitions["Lg"])
return 3;else if(width>=this.options.breakpointDefinitions["Md"])
return 2;else if(width>=this.options.breakpointDefinitions["Sm"])
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);}}}
class MudScrollListener{constructor(){this.throttleScrollHandlerId=-1;this.handlerRef=null;} 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);} 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);} throttleScrollHandler(dotnetReference,event){clearTimeout(this.throttleScrollHandlerId);this.throttleScrollHandlerId=window.setTimeout(this.scrollHandler.bind(this,dotnetReference,event),100);}
@@ -122,48 +145,7 @@ element.addEventListener(eventName,listener,{passive:false});this.eventListeners
removeDefaultPreventingHandler(element,eventName,listenerId){this.removeEventListener(element,eventName,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);} addDefaultPreventingHandlers(element,eventNames){let listeners=[];for(const eventName of eventNames){let listenerId=this.addDefaultPreventingHandler(element,eventName);listeners.push(listenerId);}
return listeners;} 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();window.mudTimePicker={initPointerEvents:(clock,dotNetHelper)=>{let isPointerDown=false;const startHandler=(event)=>{if(event.button!==0){return;} 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 MudKeyInterceptorFactory{connect(dotNetRef,elementId,options){if(!elementId)
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 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}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);}
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);}
else{this.dotnet.invokeMethodAsync('RaiseOnResized',{height:window.innerHeight,width:window.innerWidth},this.getBreakpoint(window.innerWidth));}}catch(error){this.logger("[MudBlazor] Error in resizeHandler:",{error});}}
cancelListener(){this.dotnet=undefined;window.removeEventListener("resize",this.handleResize);}
matchMedia(query){let m=window.matchMedia(query).matches;return m;}
getBrowserWindowSize(){return{height:window.innerHeight,width:window.innerWidth};}
getBreakpoint(width){if(width>=this.options.breakpointDefinitions["Xxl"])
return 5;else if(width>=this.options.breakpointDefinitions["Xl"])
return 4;else if(width>=this.options.breakpointDefinitions["Lg"])
return 3;else if(width>=this.options.breakpointDefinitions["Md"])
return 2;else if(width>=this.options.breakpointDefinitions["Sm"])
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);}}}
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 MudKeyInterceptorFactory{connect(dotNetRef,elementId,options){if(!elementId)
throw"elementId: expected element id!";var element=document.getElementById(elementId);if(!element) throw"elementId: expected element id!";var element=document.getElementById(elementId);if(!element)
throw"no element found for id: "+elementId;if(!element.mudKeyInterceptor) throw"no element found for id: "+elementId;if(!element.mudKeyInterceptor)
element.mudKeyInterceptor=new MudKeyInterceptor(dotNetRef,options);element.mudKeyInterceptor.connect(element);} element.mudKeyInterceptor=new MudKeyInterceptor(dotNetRef,options);element.mudKeyInterceptor.connect(element);}
@@ -217,6 +199,35 @@ processKeyUp(args,keyOptions){if(this.matchesKeyCombination(keyOptions.preventUp
args.preventDefault();if(this.matchesKeyCombination(keyOptions.stopUp,args)) args.preventDefault();if(this.matchesKeyCombination(keyOptions.stopUp,args))
args.stopPropagation();} 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};}} 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 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();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;}};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);}
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;} 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-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;}
@@ -261,26 +272,17 @@ dispose(){for(var i in this.map){disconnect(i);}
this.contentObserver.disconnect();this.contentObserver=null;} this.contentObserver.disconnect();this.contentObserver=null;}
getAllObservedContainers(){const result=[];for(var i in this.map){result.push(i);} getAllObservedContainers(){const result=[];for(var i in this.map){result.push(i);}
return result;}} 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();});class MudThrottledEventManager{constructor(){this.mapper={};} 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 MudScrollManager{scrollToYear(elementId,offset){let element=document.getElementById(elementId);if(element){element.parentNode.scrollTop=element.offsetTop-element.parentNode.offsetTop-element.scrollHeight*3;}}
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];} scrollToListItem(elementId){let element=document.getElementById(elementId);if(element){let parent=element.parentElement;if(parent){parent.scrollTop=element.offsetTop;}}}
else{for(let i=0;i<parts.length;i++){functionPointer=functionPointer[parts[i]];if(!functionPointer){functionReferenceFound=false;break;}}} scrollTo(selector,left,top,behavior){let element=document.querySelector(selector)||document.documentElement;element.scrollTo({left,top,behavior});}
if(functionReferenceFound===true){projector=functionPointer;}} scrollIntoView(selector,behavior){let element=document.querySelector(selector)||document.documentElement;if(element)
this.mapper[key]={eventName:eventName,handler:handlerRef,delay:throotleInterval,timerId:-1,reference:dotnetReference,elementId:elementId,properties:properties,projection:projector,};}} element.scrollIntoView({behavior,block:'center',inline:'start'});}
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,};} 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});}}
throttleEventHandler(key,event){const entry=this.mapper[key];if(!entry){return;} 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);}}
clearTimeout(entry.timerId);entry.timerId=window.setTimeout(this.eventHandler.bind(this,key,event),entry.delay);} 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.mudDragAndDrop={initDropZone:(id)=>{const elem=document.getElementById(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';}
eventHandler(key,event){const entry=this.mapper[key];if(!entry){return;} 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'))
var elem=document.getElementById(entry.elementId);if(elem!=event.srcElement&&entry.elementId!=document){return;} const dropZone=dropZones[0];if(dropZone){return dropZone.getAttribute('identifier')||"";}
const eventEntry={};for(var i=0;i<entry.properties.length;i++){eventEntry[entry.properties[i]]=event[entry.properties[i]];} return"";},getDropIndexOnPosition:(x,y,id)=>{const elems=document.elementsFromPoint(x,y);const dropItems=elems.filter(e=>e.classList.contains('mud-drop-item')&&e.id!=id)
if(entry.projection){if(typeof entry.projection==="function"){entry.projection.apply(null,[eventEntry,event]);}} const dropItem=dropItems[0];if(dropItem){return dropItem.getAttribute('index')||"";}
entry.reference.invokeMethodAsync('OnEventOccur',key,JSON.stringify(eventEntry));} return"";},makeDropZonesRelative:()=>{const dropZones=document.getElementsByClassName('mud-drop-zone');for(let dropZone of dropZones){dropZone.style.position='relative';}
unsubscribe(key){const entry=this.mapper[key];if(!entry){return;} 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(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(id);if(elem){elem.style.webkitTransform=elem.style.transform='';elem.setAttribute('data-x',0);elem.setAttribute('data-y',0);}}};
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;}};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;};
+26 -11
View File
@@ -1,21 +1,36 @@
# Building # Building
You just want to use the app? Then simply [download the appropriate setup for your operating system](Setup.md). This chapter is intended for developers who want to modify and customize the code. You just want to use the app? Then simply [download the appropriate setup for your operating system](Setup.md). This chapter is intended for developers who want to modify and customize the code.
In order to build MindWork AI Studio from source instead of using the pre-built binaries, follow these steps: ## Prerequisites
1. Install the [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0). 1. Install the [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest version. 2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest version.
3. [Install NuShell](https://www.nushell.sh/). This shell works on all operating systems and is required because the build script is written in NuShell. 3. Met the prerequisites for building [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/). Node.js is **not** required, though.
4. Clone the repository. 4. Install the Tauri CLI by running `cargo install tauri-cli`.
5. Open a terminal with NuShell. 5. [Install NuShell](https://www.nushell.sh/) when you want to prepare a release or build a local binary. NuShell works on all operating systems and is required because the build script is written in NuShell.
6. Navigate to the `/app/MindWork AI Studio` directory within the repository. 6. Clone the repository.
7. To build the current version, run `nu build.nu publish`.
## Build instructions
In order to build MindWork AI Studio from source instead of using the pre-built binaries, follow these steps:
1. Ensure you have met all the prerequisites.
2. Open a terminal with NuShell.
3. Navigate to the `/app/MindWork AI Studio` directory within the repository.
4. To build the current version, run `nu build.nu publish`.
- This will build the app for the current operating system, for both x64 (Intel, AMD) and ARM64 (e.g., Apple Silicon, Raspberry Pi). - This will build the app for the current operating system, for both x64 (Intel, AMD) and ARM64 (e.g., Apple Silicon, Raspberry Pi).
- The setup program will be located in `runtime/target/release/bundle` afterward. - The setup program will be located in `runtime/target/release/bundle` afterward.
8. In order to create a new release: 5. In order to create a new release:
1. To prepare a new release, run `nu build.nu prepare <ACTION>`, where `<ACTION>` is either `patch`, `minor`, or `major`. In case you need to adjust the version manually, you can do so in the `metadata.txt` file at the root of the repository; the first line contains the version number. 1. Before finishing the PR, make sure to create a changelog file in the `/app/MindWork AI Studio/wwwroot/changelog` directory. The file should be named `vX.Y.Z.md` and contain the changes made in the release (your changes and any other changes that are part of the release).
2. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes. 2. To prepare a new release, run `nu build.nu prepare <ACTION>`, where `<ACTION>` is either `patch`, `minor`, or `major`.
3. Before finishing the PR, make sure to create a changelog file in the `/app/MindWork AI Studio/wwwroot/changelog` directory. The file should be named `vX.Y.Z.md` and contain the changes made in the release (your changes and any other changes that are part of the release). 3. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes.
4. Your proposed changes will be reviewed and merged. 4. Your proposed changes will be reviewed and merged.
5. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`. 5. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`.
6. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). 6. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest).
7. Building the release including virus scanning takes about 2 1/2 hours. 7. Building the release including virus scanning takes some time. Please be patient.
## Run the app locally with all your changes
Do you want to test your changes before creating a PR? Follow these steps:
1. Ensure you have met all the prerequisites.
2. Open a terminal (in this case, it doesn't have to be NuShell).
3. Navigate to the `runtime` directory within the repository, e.g. `cd repos/mindwork-ai-studio/runtime`.
4. Run `cargo run`.
Cargo will compile the Rust code and start the runtime. The runtime will then start the .NET compiler. When the .NET source code is compiled, the app will start. You can now test your changes.
+5 -5
View File
@@ -1,9 +1,9 @@
0.8.12 0.9.3
2024-08-24 08:30:01 UTC 2024-09-06 11:06:00 UTC
174 178
8.0.108 (commit 665a05cea7) 8.0.108 (commit 665a05cea7)
8.0.8 (commit 08338fcaa5) 8.0.8 (commit 08338fcaa5)
1.80.1 (commit 3f5fd8dd4) 1.81.0 (commit eeb90cda1)
7.6.0 7.6.0
1.7.1 1.7.1
2d82fcc5e16, release d52ec4b9697, release
+487 -624
View File
File diff suppressed because it is too large. Load diff
+17 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mindwork-ai-studio" name = "mindwork-ai-studio"
version = "0.8.12" version = "0.9.3"
edition = "2021" edition = "2021"
description = "MindWork AI Studio" description = "MindWork AI Studio"
authors = ["Thorsten Sommer"] authors = ["Thorsten Sommer"]
@@ -9,22 +9,31 @@ authors = ["Thorsten Sommer"]
tauri-build = { version = "1.5", features = [] } tauri-build = { version = "1.5", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.7.1", features = [ "updater", "http-all", "shell-sidecar", "path-all", "shell-open"] } tauri = { version = "1.7.1", features = [ "updater", "shell-sidecar", "shell-open"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
keyring = "2" keyring = { version = "3.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
arboard = "3.4.0" arboard = "3.4.0"
tokio = "1.39" tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
flexi_logger = "0.28" flexi_logger = "0.29"
log = "0.4" log = { version = "0.4", features = ["kv"] }
once_cell = "1.19.0" once_cell = "1.19.0"
rocket = { version = "0.5", features = ["json", "tls"] }
rand = "0.8"
rand_chacha = "0.3.1"
base64 = "0.22.1"
cipher = { version = "0.4.4", features = ["std"] }
aes = "0.8.4"
cbc = "0.1.2"
pbkdf2 = "0.12.2"
hmac = "0.12.1"
sha2 = "0.10.8"
rcgen = { version = "0.13.1", features = ["pem"] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
# See issue https://github.com/tauri-apps/tauri/issues/4470 # See issue https://github.com/tauri-apps/tauri/issues/4470
reqwest = { version = "0.12", features = ["native-tls-vendored"] } reqwest = { version = "0.12", features = ["native-tls-vendored"] }
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
+801 -248
View File
File diff suppressed because it is too large. Load diff
+3 -23
View File
@@ -1,12 +1,12 @@
{ {
"build": { "build": {
"devPath": "http://localhost:5000", "devPath": "ui/",
"distDir": "ui/", "distDir": "ui/",
"withGlobalTauri": true "withGlobalTauri": false
}, },
"package": { "package": {
"productName": "MindWork AI Studio", "productName": "MindWork AI Studio",
"version": "0.8.12" "version": "0.9.3"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@@ -22,16 +22,6 @@
"args": true "args": true
} }
] ]
},
"path": {
"all": true
},
"http" : {
"all": true,
"request": true,
"scope": [
"http://localhost"
]
} }
}, },
"windows": [ "windows": [
@@ -43,16 +33,6 @@
"height": 1080 "height": 1080
} }
], ],
"security": {
"csp": null,
"dangerousRemoteDomainIpcAccess": [
{
"domain": "localhost",
"windows": ["main"],
"enableTauriAPI": true
}
]
},
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",