Compare commits

...
37 Commits
Author SHA1 Message Date
Thorsten Sommer bf01fbd706 Prepared release v0.9.9 (#141) 2024-09-11 23:13:45 +02:00
Thorsten Sommer 4722c1baa1 Add confidence levels (#140) 2024-09-11 23:08:02 +02:00
Thorsten Sommer 222c1e929f Fixed metadata for v0.9.8 (#139) 2024-09-09 17:31:37 +02:00
Thorsten Sommer 2a648105e6 Prepared the release of v0.9.8 (#138) 2024-09-09 15:13:39 +02:00
Thorsten Sommer 09f5b83e66 Implemented the "my tasks" assistant (#137) 2024-09-09 15:08:16 +02:00
Thorsten Sommer 7a5f2d4aec Improved Rust caching for the build pipeline (#136) 2024-09-09 12:39:53 +02:00
Thorsten Sommer 0d1164c0eb Prepared release v0.9.7 (#135) 2024-09-08 23:01:01 +02:00
Thorsten Sommer 54588fc92d Fixed the bug where the user input was not cleared in the chat (#134) 2024-09-08 22:53:34 +02:00
Thorsten Sommer a5bdb3267b Fixed a bug where it was possible that the chat interface was not scrolling to the bottom (#133) 2024-09-08 21:32:02 +02:00
Thorsten Sommer d7c124926b Add profiles (#132) 2024-09-08 21:01:51 +02:00
Thorsten Sommer 00f45f8998 Prepared release v0.9.6 (#131) 2024-09-08 11:35:19 +02:00
Thorsten Sommer d0d9fa3d22 Upgraded MudBlazor.Markdown (#130) 2024-09-08 11:27:57 +02:00
Thorsten Sommer b3dd213538 Improved the automatic update check for slow connections (#129) 2024-09-08 11:17:10 +02:00
Thorsten Sommer 9dfabb3c55 Fixed a bug where you couldn't send results to the synonym assistant (#128) 2024-09-08 11:12:15 +02:00
Thorsten Sommer d0dbbccd56 Allow opening external links in an external browser (#127) 2024-09-07 18:36:57 +02:00
Thorsten Sommer 75576129fe Fixed external links (#126) 2024-09-07 16:26:04 +02:00
Thorsten Sommer 2efd08dbbc Prepared release v0.9.4 (#125) 2024-09-06 22:24:23 +02:00
Thorsten Sommer 4bd22108de Added synonym assistant (#124) 2024-09-06 22:02:20 +02:00
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
147 changed files with 4454 additions and 1825 deletions

No files matched your search

+17 -57
View File
@@ -249,18 +249,6 @@ jobs:
cd publish/dotnet
mv mindworkAIStudio.exe "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer${{ matrix.dotnet_name_postfix }}"
- name: Create parts for the Rust cache key (Unix)
if: matrix.platform != 'windows-latest'
run: |
cd runtime
echo "CARGO_LOCK_HASH=${{ hashFiles('**/Cargo.lock') }}" >> $GITHUB_ENV
- name: Create parts for the Rust cache key (Windows)
if: matrix.platform == 'windows-latest'
run: |
cd runtime
echo "CARGO_LOCK_HASH=${{ hashFiles('**/Cargo.lock') }}" >> $env:GITHUB_ENV
- name: Cache Rust
uses: actions/cache@v4
with:
@@ -272,17 +260,7 @@ jobs:
~/.rustup/toolchains
runtime/target
# When the entire key matches, Rust might just create the bundles using the current .NET build:
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}-dependencies-${{ env.CARGO_LOCK_HASH }}
# - 1st key: the Rust runtime dependencies changed. Anyway, we might be able to re-use many previously built packages.
#
# - No match: alright, a new Rust version was released. Sadly, we cannot re-use anything now. Why?
# The updated Rust compiler might mitigate some bugs or vulnerabilities. In order to apply
# these changes to our app, we have to re-compile everything. That's the reason why it makes
# no sense to use more parts for the cache key, like Tauri or Tauri build versions.
restore-keys: |
target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}-dependencies-
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
- name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@master
@@ -478,30 +456,13 @@ jobs:
cd publish/dotnet
mv mindworkAIStudio "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer-aarch64-unknown-linux-gnu"
- name: Create parts for the Rust cache key
if: ${{ env.SKIP != 'true' }}
run: |
cd runtime
echo "CARGO_LOCK_HASH=${{ hashFiles('**/Cargo.lock') }}" >> $GITHUB_ENV
- name: Cache linux arm64 runner image
if: ${{ env.SKIP != 'true' }}
uses: actions/cache@v4
id: linux_arm_cache
with:
path: $RUNNER_TEMP/linux_arm_qemu_cache.img
# 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 }}
# - 1st key: the Rust runtime dependencies changed. Anyway, we might be able to re-use many previously built packages.
#
# - No match: alright, a new Rust version was released. Sadly, we cannot re-use anything now. Why?
# The updated Rust compiler might mitigate some bugs or vulnerabilities. In order to apply
# these changes to our app, we have to re-compile everything. That's the reason why it makes
# no sense to use more parts for the cache key, like Tauri or Tauri build versions.
restore-keys: |
target-linux-arm64-rust-${{ env.RUST_VERSION }}-dependencies-
path: ${{ runner.temp }}/linux_arm_qemu_cache.img
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}
- name: Build linux arm runner image
uses: pguyot/arm-runner-action@v2
@@ -544,7 +505,7 @@ jobs:
- name: Add the built runner image to the cache
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
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
if: ${{ env.SKIP != 'true' }}
@@ -552,11 +513,12 @@ jobs:
id: build-linux-arm
with:
base_image: file://$RUNNER_TEMP/linux_arm_qemu_cache.img
base_image: file://${{ runner.temp }}/linux_arm_qemu_cache.img
cpu: cortex-a53
optimize_image: false
copy_artifact_path: runtime
copy_artifact_dest: result
bind_mount_repository: true
#
# We do not need to set the PRIVATE_PUBLISH_KEY and PRIVATE_PUBLISH_KEY_PASSWORD here,
@@ -573,19 +535,17 @@ jobs:
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
source "$HOME/.cargo/env"
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
- name: Debug
if: ${{ env.SKIP != 'true' }}
run: |
echo "Current directory: $(pwd)"
ls -lhat
echo "Searching for linux_arm_qemu_cache.img"
find $RUNNER_TEMP -name 'linux_arm_qemu_cache.img' -print 2>/dev/null
echo "Searching for mind-work-ai-studio_*.deb"
find . -name 'mind-work-ai-studio_*.deb' -print 2>/dev/null
# Save the built libraries for the next job:
rm -fr /rust-cache/target
cp -Rp target /rust-cache
- name: Update the runner image to cache the Rust runtime build
if: ${{ env.SKIP != 'true' }}
@@ -845,7 +805,7 @@ jobs:
- name: Create release
uses: softprops/action-gh-release@v2
with:
prerelease: false
prerelease: true
draft: false
make_latest: true
body: ${{ env.CHANGELOG }}
+1
View File
@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
+4 -5
View File
@@ -1,19 +1,18 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
// ReSharper disable MemberCanBePrivate.Global
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 IJSRuntime JsRuntime { get; init; } = jsRuntime;
protected ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary>
/// Represents the type or category of this agent.
@@ -104,6 +103,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(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.Settings;
using AIStudio.Tools;
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()
{
@@ -48,8 +48,4 @@
}
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.CreateAgenda()">
Create agenda
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,12 +1,13 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
protected override string Title => "Agenda Planner";
protected override string Description =>
@@ -94,13 +95,11 @@ public partial class AssistantAgenda : AssistantBaseCore
- Mary Jane: Work package 3
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.AGENDA_ASSISTANT,
},
];
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Create Agenda";
protected override Func<Task> SubmitAction => this.CreateAgenda;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -159,7 +158,6 @@ public partial class AssistantAgenda : AssistantBaseCore
this.durationBreaks = this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime;
this.activeParticipation = this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation;
this.numberParticipants = this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider);
return true;
}
@@ -195,7 +193,6 @@ public partial class AssistantAgenda : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_AGENDA_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContent = deferredContent;
@@ -8,12 +8,16 @@
<ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@(this.Description)
@this.Description
</MudText>
@if (this.Body is not null)
{
@(this.Body)
@this.Body
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
@@ -35,49 +39,74 @@
</div>
</ChildContent>
<FooterContent>
@if (this.FooterButtons.Count > 0)
{
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@foreach (var button in this.FooterButtons)
{
switch (button)
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@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))
{
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;
if (assistant is Components.NONE || this.Component == assistant)
continue;
case ButtonData buttonData:
<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()">
@buttonData.Text
</MudButton>
break;
</MudTooltip>
break;
case SendToButton sendToButton:
<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<SendTo>().OrderBy(n => n.Name().Length))
{
if(assistant is SendTo.NONE || sendToButton.Self == assistant)
continue;
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
case SendToButton sendToButton:
<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 || sendToButton.Self == assistant)
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" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
</MudStack>
}
}
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" Provider="@this.providerSettings.UsedProvider"/>
}
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
</MudStack>
</FooterContent>
</InnerScrolling>
@@ -1,16 +1,17 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase
{
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
@@ -22,11 +23,14 @@ public abstract partial class AssistantBase : ComponentBase
protected ISnackbar Snackbar { get; init; } = null!;
[Inject]
protected Rust Rust { get; init; } = null!;
protected RustService RustService { get; init; } = null!;
[Inject]
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 RESULT_DIV_ID = "assistantResult";
@@ -36,6 +40,8 @@ public abstract partial class AssistantBase : ComponentBase
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
{
ContentText textBlock => textBlock.Text,
@@ -46,9 +52,19 @@ public abstract partial class AssistantBase : ComponentBase
protected abstract bool MightPreselectValues();
protected abstract string SubmitText { get; }
protected abstract Func<Task> SubmitAction { get; }
protected virtual bool SubmitDisabled => false;
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual bool AllowProfiles => true;
protected virtual bool ShowProfileSelection => true;
protected virtual bool ShowDedicatedProgress => false;
@@ -61,6 +77,7 @@ public abstract partial class AssistantBase : ComponentBase
protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form;
protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
@@ -69,6 +86,14 @@ public abstract partial class AssistantBase : ComponentBase
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
// Configure the spellchecking for the user input:
@@ -88,6 +113,8 @@ public abstract partial class AssistantBase : ComponentBase
}
#endregion
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedProvider.GetConfidence(this.SettingsManager).StyleBorder() : string.Empty;
protected string? ValidatingProvider(AIStudio.Settings.Provider provider)
{
@@ -105,7 +132,12 @@ public abstract partial class AssistantBase : ComponentBase
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = this.SystemPrompt,
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
$"""
{this.SystemPrompt}
{this.currentProfile.ToSystemPrompt()}
""",
Blocks = [],
};
}
@@ -151,7 +183,7 @@ public abstract partial class AssistantBase : ComponentBase
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
@@ -162,7 +194,7 @@ public abstract partial class AssistantBase : ComponentBase
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)
@@ -173,7 +205,7 @@ public abstract partial class AssistantBase : ComponentBase
return icon;
}
private Task SendToAssistant(SendTo destination, SendToButton sendToButton)
private Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{
var contentToSend = sendToButton.UseResultingContentBlockData switch
{
@@ -188,7 +220,7 @@ public abstract partial class AssistantBase : ComponentBase
var sendToData = destination.GetData();
switch (destination)
{
case SendTo.CHAT:
case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
break;
@@ -210,6 +242,7 @@ public abstract partial class AssistantBase : ComponentBase
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false;
this.inputIssues = [];
@@ -24,8 +24,4 @@
</MudStack>
<MudTextField T="string" @bind-Text="@this.questions" Validation="@this.ValidateQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionMark" Adornment="Adornment.Start" Label="Your question(s)" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Color="Color.Info" OnClick="() => this.GetSupport()" Class="mb-3">
Get support
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,11 +1,11 @@
using System.Text;
using AIStudio.Tools;
namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
protected override string Title => "Coding Assistant";
protected override string Description =>
@@ -26,13 +26,11 @@ public partial class AssistantCoding : AssistantBaseCore
When the user asks in a different language than English, you answer in the same language!
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.CODING_ASSISTANT,
},
];
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Get Support";
protected override Func<Task> SubmitAction => this.GetSupport;
protected override void ResetFrom()
{
@@ -50,7 +48,6 @@ public partial class AssistantCoding : AssistantBaseCore
if (this.SettingsManager.ConfigurationData.Coding.PreselectOptions)
{
this.provideCompilerMessages = this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Coding.PreselectedProvider);
return true;
}
@@ -66,7 +63,6 @@ public partial class AssistantCoding : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CODING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.questions = deferredContent;
@@ -20,8 +20,4 @@
<MudTextField T="string" @bind-Text="@this.inputName" Label="(Optional) Your name for the closing salutation" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Your name for the closing salutation of your e-mail." Class="mb-3"/>
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Select the writing style" ValidateSelection="@this.ValidateWritingStyle"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.CreateMail()">
Create e-mail
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,12 +1,13 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
protected override string Title => "E-Mail";
protected override string Description =>
@@ -21,13 +22,11 @@ public partial class AssistantEMail : AssistantBaseCore
{this.SystemPromptGreeting()} {this.SystemPromptName()} You write the email in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.EMAIL_ASSISTANT,
},
];
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Create email";
protected override Func<Task> SubmitAction => this.CreateMail;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -60,7 +59,6 @@ public partial class AssistantEMail : AssistantBaseCore
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.EMail.PreselectedProvider);
return true;
}
@@ -89,7 +87,6 @@ public partial class AssistantEMail : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_EMAIL_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputBulletPoints = deferredContent;
@@ -3,8 +3,4 @@
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.ProofreadText()">
Proofread
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.GrammarSpelling;
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 Description =>
@@ -21,6 +22,8 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
Your response includes only the corrected text. Do not explain your changes. If no changes are needed,
you return the text unchanged.
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
@@ -32,12 +35,16 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
[
new SendToButton
{
Self = SendTo.GRAMMAR_SPELLING_ASSISTANT,
Self = Tools.Components.GRAMMAR_SPELLING_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.correctedText) ? this.inputText : this.correctedText
},
];
protected override string SubmitText => "Proofread";
protected override Func<Task> SubmitAction => this.ProofreadText;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
@@ -60,7 +67,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider);
return true;
}
@@ -71,7 +77,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
@@ -15,8 +15,4 @@
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
}
</MudStack>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
Find icon
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,11 +1,8 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore
{
private string inputContext = string.Empty;
private IconSources selectedIconSource;
protected override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
protected override string Title => "Icon Finder";
@@ -27,14 +24,14 @@ public partial class AssistantIconFinder : AssistantBaseCore
related to the keyword "buildings" might be the best match. Provide your keywords in a Markdown list without
quotation marks.
""";
protected override bool AllowProfiles => false;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.ICON_FINDER_ASSISTANT,
},
];
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Find Icon";
protected override Func<Task> SubmitAction => this.FindIcon;
protected override void ResetFrom()
{
@@ -50,18 +47,19 @@ public partial class AssistantIconFinder : AssistantBaseCore
if (this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)
{
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 false;
}
private string inputContext = string.Empty;
private IconSources selectedIconSource;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_ICON_FINDER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContext = deferredContent;
@@ -8,8 +8,4 @@
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Legal document" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="Your questions" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Disabled="@this.isAgentRunning" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.AksQuestions()">
Ask your questions
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,9 +1,9 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
protected override string Title => "Legal Check";
protected override string Description =>
@@ -22,13 +22,13 @@ public partial class AssistantLegalCheck : AssistantBaseCore
Never invent facts!
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.LEGAL_CHECK_ASSISTANT,
},
];
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Ask your questions";
protected override Func<Task> SubmitAction => this.AksQuestions;
protected override bool SubmitDisabled => this.isAgentRunning;
protected override void ResetFrom()
{
@@ -37,17 +37,8 @@ public partial class AssistantLegalCheck : AssistantBaseCore
this.MightPreselectValues();
}
protected override bool MightPreselectValues()
{
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;
}
protected override bool MightPreselectValues() => false;
private bool isAgentRunning;
private string inputLegalDocument = string.Empty;
private string inputQuestions = string.Empty;
@@ -56,7 +47,6 @@ public partial class AssistantLegalCheck : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_LEGAL_CHECK_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputQuestions = deferredContent;
@@ -0,0 +1,7 @@
@attribute [Route(Routes.ASSISTANT_MY_TASKS)]
@inherits AssistantBaseCore
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.currentProfile"/>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Text or email" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -0,0 +1,123 @@
using AIStudio.Settings;
namespace AIStudio.Assistants.MyTasks;
public partial class AssistantMyTasks : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
protected override string Title => "My Tasks";
protected override string Description =>
"""
You received a cryptic email that was sent to many recipients and you are now wondering
if you need to do something? Copy the email into the input field. You also need to select
a personal profile. In this profile, you should describe your role in the organization.
The AI will then try to give you hints on what your tasks might be.
""";
protected override string SystemPrompt =>
$"""
You are a friendly and professional business expert. You receive business emails, protocols,
reports, etc. as input. Additionally, you know the user's role in the organization. The user
wonders if any tasks arise for them in their role based on the text. You now try to give hints
and advice on whether and what the user should do. When you believe there are no tasks for the
user, you tell them this. You consider typical business etiquette in your advice.
You write your advice in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Analyze text";
protected override Func<Task> SubmitAction => this.AnalyzeText;
protected override bool ShowProfileSelection => false;
protected override void ResetFrom()
{
this.inputText = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.MyTasks.PreselectOtherLanguage;
return true;
}
return false;
}
private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_MY_TASKS_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide some text as input. For example, an email.";
return null;
}
private string? ValidateProfile(Profile profile)
{
if(profile == default || profile == Profile.NO_PROFILE)
return "Please select one of your profiles.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.AS_IS)
return "Use the same language as the input";
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private async Task AnalyzeText()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(this.inputText);
await this.AddAIResponseAsync(time);
}
}
@@ -5,8 +5,4 @@
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Writing style" AllowOther="@false" />
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="Sentence structure" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.RewriteText()">
Improve
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
protected override string Title => "Rewrite & Improve Text";
protected override string Description =>
@@ -23,6 +24,8 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
You follow the rules according to {this.SystemPromptLanguage()} in all your changes.
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
@@ -33,12 +36,16 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
[
new SendToButton
{
Self = SendTo.REWRITE_ASSISTANT,
Self = Tools.Components.REWRITE_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.rewrittenText) ? this.inputText : this.rewrittenText,
},
];
protected override string SubmitText => "Improve";
protected override Func<Task> SubmitAction => this.RewriteText;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
@@ -63,7 +70,6 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage;
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.selectedSentenceStructure = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure;
return true;
@@ -76,7 +82,6 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_REWRITE_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
@@ -0,0 +1,8 @@
@attribute [Route(Routes.ASSISTANT_SYNONYMS)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="Your word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -0,0 +1,170 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
protected override string Title => "Synonyms";
protected override string Description =>
"""
Find synonyms for words or phrases.
""";
protected override string SystemPrompt =>
$"""
You have a PhD in linguistics. Therefore, you are an expert in the {this.SystemPromptLanguage()} language.
You receive a word or phrase as input. You might also receive some context. You then provide
a list of synonyms as a Markdown list.
First, derive possible meanings from the word, phrase, and context, when available. Then, provide
possible synonyms for each meaning.
Example for the word "learn" and the language English (US):
Derive possible meanings (*this list is not part of the output*):
- Meaning "to learn"
- Meaning "to retain"
Next, provide possible synonyms for each meaning, which is your output:
# Meaning "to learn"
- absorb
- study
- acquire
- advance
- practice
# Meaning "to retain"
- remember
- note
- realize
You do not ask follow-up questions and never repeat the task instructions. When you do not know
any synonyms for the given word or phrase, you state this. Your output is always in
the {this.SystemPromptLanguage()} language.
""";
protected override bool AllowProfiles => false;
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Find synonyms";
protected override Func<Task> SubmitAction => this.FindSynonyms;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
this.inputContext = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)
{
this.selectedLanguage = this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Synonyms.PreselectedOtherLanguage;
return true;
}
return false;
}
private string inputText = string.Empty;
private string inputContext = string.Empty;
private CommonLanguages selectedLanguage;
private string customTargetLanguage = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_SYNONYMS_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContext = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a word or phrase as input.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
var lang = this.selectedLanguage switch
{
CommonLanguages.AS_IS => "source",
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedLanguage.Name()}",
};
if (string.IsNullOrWhiteSpace(lang))
return "source";
return lang;
}
private string UserPromptContext()
{
if(string.IsNullOrWhiteSpace(this.inputContext))
return string.Empty;
return $"""
The given context is:
```
{this.inputContext}
```
""";
}
private async Task FindSynonyms()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.UserPromptContext()}
The given word or phrase is:
```
{this.inputText}
```
""");
await this.AddAIResponseAsync(time);
}
}
@@ -9,8 +9,4 @@
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="Custom target language" ValidateOther="@this.ValidateCustomLanguage" />
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="Target complexity" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="Your expertise" ValidateOther="@this.ValidateExpertInField" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SummarizeText()" Disabled="@this.isAgentRunning">
Summarize
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
protected override string Title => "Text Summarizer";
protected override string Description =>
@@ -24,13 +25,15 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
a summary with the requested complexity. In any case, do not add any information.
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.TEXT_SUMMARIZER_ASSISTANT,
},
];
protected override bool AllowProfiles => false;
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Summarize";
protected override Func<Task> SubmitAction => this.SummarizeText;
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -57,7 +60,6 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
this.customTargetLanguage = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage;
this.selectedComplexity = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity;
this.expertInField = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider);
return true;
}
@@ -75,7 +77,6 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
@@ -17,8 +17,4 @@ else
}
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Disabled="@this.isAgentRunning" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.TranslateText(force: true)">
Translate
</MudButton>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
protected override string Title => "Translation";
protected override string Description =>
@@ -20,13 +21,15 @@ public partial class AssistantTranslation : AssistantBaseCore
language requires, e.g., shorter sentences, you should split the text into shorter sentences.
""";
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = SendTo.TRANSLATION_ASSISTANT,
},
];
protected override bool AllowProfiles => false;
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Translate";
protected override Func<Task> SubmitAction => () => this.TranslateText(true);
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -52,7 +55,6 @@ public partial class AssistantTranslation : AssistantBaseCore
this.liveTranslation = this.SettingsManager.ConfigurationData.Translation.PreselectLiveTranslation;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Translation.PreselectedProvider);
return true;
}
@@ -70,7 +72,6 @@ public partial class AssistantTranslation : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TRANSLATION_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
@@ -1,7 +1,7 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Chat;
/// <summary>
@@ -40,10 +40,7 @@ public partial class ContentBlockComponent : ComponentBase
public string Class { get; set; } = string.Empty;
[Inject]
private Rust Rust { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
@@ -100,7 +97,7 @@ public partial class ContentBlockComponent : ComponentBase
{
case ContentType.TEXT:
var textContent = (ContentText) this.Content;
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, textContent.Text);
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
break;
default:
+1 -1
View File
@@ -29,7 +29,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <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();
}
+5 -5
View File
@@ -35,18 +35,18 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <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)
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
// new content or not. Depends on the energy saving mode
// the user chose.
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:
await Task.Run(async () =>
{
@@ -54,7 +54,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true;
// 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:
if (token.IsCancellationRequested)
@@ -89,7 +89,7 @@ public sealed class ContentText : IContent
}
// 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.IsStreaming = false;
}, token);
+1 -1
View File
@@ -42,5 +42,5 @@ public interface IContent
/// <summary>
/// Uses the provider to create the content.
/// </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,16 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (184, "v0.9.9, build 184 (2024-09-11 21:10 UTC)", "v0.9.9.md"),
new (183, "v0.9.8, build 183 (2024-09-09 13:10 UTC)", "v0.9.8.md"),
new (182, "v0.9.7, build 182 (2024-09-08 20:55 UTC)", "v0.9.7.md"),
new (181, "v0.9.6, build 181 (2024-09-08 09:30 UTC)", "v0.9.6.md"),
new (180, "v0.9.5, build 180 (2024-09-07 16:33 UTC)", "v0.9.5.md"),
new (179, "v0.9.4, build 179 (2024-09-06 20:11 UTC)", "v0.9.4.md"),
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 (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"),
@@ -0,0 +1,56 @@
@using AIStudio.Provider
<div class="d-flex">
@if (this.Mode is ConfidenceInfoMode.ICON)
{
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.Provider.GetConfidence(this.SettingsManager).SetColorStyle()" OnClick="@(() => this.ToggleConfidence())"/>
}
else
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.Provider.GetConfidence(this.SettingsManager).SetColorStyle()" OnClick="@(() => this.ToggleConfidence())">
Confidence
</MudButton>
}
<MudPopover Open="@this.showConfidence" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" Style="@this.GetPopoverStyle()" DropShadow="@true" Class="border-solid border-4 rounded-lg">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Confidence Card</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.h6">Description</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/>
@if (this.currentConfidence.Sources.Count > 0)
{
<MudText Typo="Typo.h6">Sources</MudText>
<MudList T="@string">
@foreach (var sourceTuple in this.GetConfidenceSources())
{
<MudListItem Icon="@Icons.Material.Filled.Link" Href="@sourceTuple.Source" Target="_blank" Text="@sourceTuple.Index"/>
}
</MudList>
}
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
{
<MudText Typo="Typo.h6">Region</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
<b>@this.currentConfidence.Region</b>
</MudText>
}
<MudText Typo="Typo.h6">Confidence Level</MudText>
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
<b>@this.currentConfidence.Level.GetName()</b>
</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideConfidence())">
Close
</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
</div>
@@ -0,0 +1,57 @@
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ConfidenceInfo : ComponentBase
{
[Parameter]
public ConfidenceInfoMode Mode { get; set; } = ConfidenceInfoMode.BUTTON;
[Parameter]
public Providers Provider { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private Confidence currentConfidence;
private bool showConfidence;
public ConfidenceInfo()
{
this.currentConfidence = Providers.NONE.GetConfidence(this.SettingsManager);
}
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
this.currentConfidence = this.Provider.GetConfidence(this.SettingsManager);
await base.OnParametersSetAsync();
}
#endregion
private void ToggleConfidence()
{
this.showConfidence = !this.showConfidence;
}
private void HideConfidence()
{
this.showConfidence = false;
}
private IEnumerable<(string Index, string Source)> GetConfidenceSources()
{
var index = 0;
foreach (var source in this.currentConfidence.Sources)
yield return ($"Source {++index}", source);
}
private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor()};";
private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor()}; max-width: calc(35vw);";
}
@@ -0,0 +1,7 @@
namespace AIStudio.Components;
public enum ConfidenceInfoMode
{
BUTTON,
ICON,
}
@@ -1,5 +1,4 @@
using AIStudio.Settings;
using AIStudio.Tools;
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.Tools;
using Microsoft.AspNetCore.Components;
@@ -22,6 +21,9 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
[Parameter]
public Func<bool> Disabled { get; set; } = () => false;
[Parameter]
public Func<string> HelpText { get; set; } = () => "Select a provider that is preselected.";
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
@@ -1,5 +1,4 @@
using AIStudio.Layout;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
@@ -0,0 +1,16 @@
namespace AIStudio.Components;
public class MudJustifiedText : MudText
{
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.Align = Align.Justify;
this.Style = "hyphens: auto; word-break: auto-phrase;";
await base.OnInitializedAsync();
}
#endregion
}
@@ -2,7 +2,7 @@
@foreach(var item in this.Items)
{
<MudListItem T="string" Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;"><b>@item.Header:</b> @item.Text</MudText>
<MudText Typo="Typo.body1" Align="Align.Justify" Style="hyphens: auto; word-break: auto-phrase;"><b>@item.Header:</b> @item.Text</MudText>
</MudListItem>
}
</MudList>
@@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
@@ -0,0 +1,10 @@
@using AIStudio.Settings
<MudSelect T="Profile" Strict="@true" Value="@this.Profile" ValueChanged="@this.SelectionChanged" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person4" Margin="Margin.Dense" Label="Select one of your profiles" Variant="Variant.Outlined" Class="mb-3" Validation="@this.Validation">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{
<MudSelectItem Value="profile">
@profile.Name
</MudSelectItem>
}
</MudSelect>
@@ -0,0 +1,26 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ProfileFormSelection : ComponentBase
{
[Parameter]
public Profile Profile { get; set; } = Profile.NO_PROFILE;
[Parameter]
public EventCallback<Profile> ProfileChanged { get; set; }
[Parameter]
public Func<Profile, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private async Task SelectionChanged(Profile profile)
{
this.Profile = profile;
await this.ProfileChanged.InvokeAsync(profile);
}
}
@@ -0,0 +1,10 @@
<MudTooltip Text="You can switch between your profiles here">
<MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{
<MudMenuItem OnClick="() => this.SelectionChanged(profile)">
@profile.Name
</MudMenuItem>
}
</MudMenu>
</MudTooltip>
@@ -0,0 +1,28 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ProfileSelection : ComponentBase
{
[Parameter]
public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE;
[Parameter]
public EventCallback<Profile> CurrentProfileChanged { get; set; }
[Parameter]
public string MarginLeft { get; set; } = "ml-3";
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private string MarginClass => $"{this.MarginLeft}";
private async Task SelectionChanged(Profile profile)
{
this.CurrentProfile = profile;
await this.CurrentProfileChanged.InvokeAsync(profile);
}
}
@@ -1,7 +1,6 @@
using AIStudio.Agents;
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -5,7 +5,6 @@ using System.Text.Json.Serialization;
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -24,6 +23,9 @@ public partial class Workspaces : ComponentBase
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private ILogger<Workspaces> Logger { get; init; } = null!;
[Parameter]
public ChatThread? CurrentChatThread { get; set; }
@@ -309,7 +311,7 @@ public partial class Workspaces : ComponentBase
}
catch (Exception e)
{
Console.WriteLine(e);
this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
}
return null;
@@ -0,0 +1,93 @@
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Store personal data about yourself in various profiles so that the AIs know your personal context.
This saves you from having to explain your context each time, for example, in every chat. When you
have different roles, you can create a profile for each role.
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Are you a project manager in a research facility? You might want to create a profile for your project
management activities, one for your scientific work, and a profile for when you need to write program
code. In these profiles, you can record how much experience you have or which methods you like or
dislike using. Later, you can choose when and where you want to use each profile.
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
The name of the profile is mandatory. Each profile must have a unique name. Whether you provide
information about yourself or only fill out the actions is up to you. Only one of these pieces
is required.
</MudJustifiedText>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataName"
Label="Profile Name"
Class="mb-3"
Immediate="@true"
MaxLength="40"
Counter="40"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Badge"
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Variant="Variant.Outlined"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudTextField
T="string"
@bind-Text="@this.DataNeedToKnow"
Validation="@this.ValidateNeedToKnow"
AdornmentIcon="@Icons.Material.Filled.ListAlt"
Adornment="Adornment.Start"
Immediate="@true"
Label="What should the AI know about you?"
Variant="Variant.Outlined"
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="444"
Counter="444"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?"
/>
<MudTextField
T="string"
@bind-Text="@this.DataActions"
Validation="@this.ValidateActions"
AdornmentIcon="@Icons.Material.Filled.ListAlt"
Adornment="Adornment.Start"
Immediate="@true"
Label="What should the AI do for you?"
Variant="Variant.Outlined"
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="256"
Counter="256"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
/>
</MudForm>
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if(this.IsEditing)
{
@:Update
}
else
{
@:Add
}
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,168 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class ProfileDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The profile's number in the list.
/// </summary>
[Parameter]
public uint DataNum { get; set; }
/// <summary>
/// The profile's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The profile name chosen by the user.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// What should the LLM know about you?
/// </summary>
[Parameter]
public string DataNeedToKnow { get; set; } = string.Empty;
/// <summary>
/// What actions should the LLM take?
/// </summary>
[Parameter]
public string DataActions { get; set; } = string.Empty;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used profile names. We need this to check for uniqueness.
/// </summary>
private List<string> UsedNames { get; set; } = [];
private bool dataIsValid;
private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
private Profile CreateProfileSettings() => new()
{
Num = this.DataNum,
Id = this.DataId,
Name = this.DataName,
NeedToKnow = this.DataNeedToKnow,
Actions = this.DataActions,
};
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data:
if(this.IsEditing)
{
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
}
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private async Task Store()
{
await this.form.Validate();
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
// Use the data model to store the profile.
// We just return this data to the parent component:
var addedProfileSettings = this.CreateProfileSettings();
if(this.IsEditing)
this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'.");
else
this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedProfileSettings));
}
private string? ValidateNeedToKnow(string text)
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return "Please enter what the LLM should know about you and/or what actions it should take.";
if(text.Length > 444)
return "The text must not exceed 444 characters.";
return null;
}
private string? ValidateActions(string text)
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return "Please enter what the LLM should know about you and/or what actions it should take.";
if(text.Length > 256)
return "The text must not exceed 256 characters.";
return null;
}
private string? ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "Please enter a profile name.";
if (name.Length > 40)
return "The profile name must not exceed 40 characters.";
// The instance name must be unique:
var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return "The profile name must be unique; the chosen name is already in use.";
return null;
}
private void Cancel() => this.MudDialog.Cancel();
}
@@ -1,7 +1,5 @@
@using AIStudio.Components
@using AIStudio.Provider
@using AIStudio.Provider.SelfHosted
@using MudBlazor
<MudDialog>
<DialogContent>
@@ -85,6 +83,9 @@
@bind-Text="@this.DataInstanceName"
Label="Instance Name"
Class="mb-3"
MaxLength="40"
Counter="40"
Immediate="@true"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
@@ -1,12 +1,12 @@
using System.Text.RegularExpressions;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Settings;
namespace AIStudio.Dialogs;
/// <summary>
/// The provider settings dialog.
@@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[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();
@@ -94,8 +97,9 @@ public partial class ProviderDialog : ComponentBase
private MudForm form = null!;
private readonly List<Model> availableModels = new();
private Provider CreateProviderSettings() => new()
private readonly Encryption encryption = Program.ENCRYPTION;
private Settings.Provider CreateProviderSettings() => new()
{
Num = this.DataNum,
Id = this.DataId,
@@ -122,6 +126,10 @@ public partial class ProviderDialog : ComponentBase
{
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
// When using Fireworks, we must copy the model name:
if (this.DataProvider is Providers.FIREWORKS)
this.dataManuallyModel = this.DataModel.Id;
//
// We cannot load the API key for self-hosted providers:
//
@@ -133,7 +141,7 @@ public partial class ProviderDialog : ComponentBase
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider();
var provider = loadedProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
{
await base.OnInitializedAsync();
@@ -141,10 +149,10 @@ public partial class ProviderDialog : ComponentBase
}
// Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
var requestedSecret = await this.RustService.GetAPIKey(provider);
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:
await this.ReloadModels();
@@ -187,10 +195,10 @@ public partial class ProviderDialog : ComponentBase
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
{
// 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:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
var storeResponse = await this.RustService.SetAPIKey(provider, this.dataAPIKey);
if (!storeResponse.Success)
{
this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again.";
@@ -239,40 +247,14 @@ public partial class ProviderDialog : ComponentBase
return null;
}
[GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")]
private static partial Regex InstanceNameRegex();
private static readonly string[] RESERVED_NAMES = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"];
private string? ValidatingInstanceName(string instanceName)
{
if (string.IsNullOrWhiteSpace(instanceName))
return "Please enter an instance name.";
if (instanceName.StartsWith(' ') || instanceName.StartsWith('.'))
return "The instance name must not start with a space or a dot.";
if (instanceName.EndsWith(' ') || instanceName.EndsWith('.'))
return "The instance name must not end with a space or a dot.";
if (instanceName.StartsWith('-') || instanceName.StartsWith('_'))
return "The instance name must not start with a hyphen or an underscore.";
if (instanceName.Length > 255)
return "The instance name must not exceed 255 characters.";
if (!InstanceNameRegex().IsMatch(instanceName))
return "The instance name must only contain letters, numbers, spaces, hyphens, underscores, and dots.";
if (instanceName.Contains(" "))
return "The instance name must not contain consecutive spaces.";
if (RESERVED_NAMES.Contains(instanceName.ToUpperInvariant()))
return "This name is reserved and cannot be used.";
if (instanceName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
return "The instance name contains invalid characters.";
if (instanceName.Length > 40)
return "The instance name must not exceed 40 characters.";
// The instance name must be unique:
var lowerInstanceName = instanceName.ToLowerInvariant();
@@ -318,11 +300,11 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels()
{
var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider();
var provider = currentProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
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:
var orderedModels = models.OrderByDescending(n => n.Id);
@@ -1,6 +1,6 @@
using System.Reflection;
using AIStudio.Tools;
using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;
+2
View File
@@ -1,5 +1,7 @@
// Global using directives
global using AIStudio.Tools;
global using Microsoft.JSInterop;
global using MudBlazor;
@@ -1,21 +1,19 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Layout;
public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDisposable
{
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
@@ -26,13 +24,16 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
private IDialogService DialogService { get; init; } = null!;
[Inject]
private Rust Rust { get; init; } = null!;
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private NavigationManager NavigationManager { get; init; } = null!;
[Inject]
private ILogger<MainLayout> Logger { get; init; } = null!;
public string AdditionalHeight { get; private set; } = "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
// for this app.
//
var dataDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appLocalDataDir");
var configDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appConfigDir");
var dataDir = await this.RustService.GetDataDirectory();
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:
SettingsManager.ConfigDirectory = configDir;
SettingsManager.DataDirectory = Path.Join(dataDir, "data");
SettingsManager.DataDirectory = dataDir;
Directory.CreateDirectory(SettingsManager.DataDirectory);
// Ensure that all settings are loaded:
@@ -85,8 +89,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED ]);
// Set the js runtime for the update service:
UpdateService.SetBlazorDependencies(this.JsRuntime, this.Snackbar);
// Set the snackbar for the update service:
UpdateService.SetBlazorDependencies(this.Snackbar);
TemporaryChatService.Initialize();
// Should the navigation bar be open by default?
@@ -189,7 +193,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
this.performingUpdate = true;
this.StateHasChanged();
await this.Rust.InstallUpdate(this.JsRuntime);
await this.RustService.InstallUpdate();
}
private async ValueTask OnLocationChanging(LocationChangingContext context)
@@ -45,10 +45,10 @@
</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="MudBlazor" Version="7.6.0" />
<PackageReference Include="MudBlazor.Markdown" Version="1.0.2" />
<PackageReference Include="MudBlazor" Version="7.8.0" />
<PackageReference Include="MudBlazor.Markdown" Version="7.8.0" />
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
</ItemGroup>
+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="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="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="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="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="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="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="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" 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="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="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="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="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="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."/>
@@ -1,7 +1,5 @@
using System.Reflection;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages;
@@ -14,6 +14,7 @@
<AssistantBlock Name="Translation" Description="Translate text into another language." Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_TRANSLATION"/>
<AssistantBlock Name="Grammar & Spelling" Description="Check grammar and spelling of a given text." Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_GRAMMAR_SPELLING"/>
<AssistantBlock Name="Rewrite & Improve" Description="Rewrite and improve a given text for a chosen style." Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/>
<AssistantBlock Name="Synonyms" Description="Find synonyms for a given word or phrase." Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
</MudStack>
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
@@ -21,6 +22,7 @@
</MudText>
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
<AssistantBlock Name="E-Mail" Description="Generate an e-mail for a given context." Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
<AssistantBlock Name="My Tasks" Description="Analyze a text or an email for tasks you need to complete." Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
<AssistantBlock Name="Agenda Planner" Description="Generate an agenda for a given meeting, seminar, etc." Icon="@Icons.Material.Filled.CalendarToday" Link="@Routes.ASSISTANT_AGENDA"/>
<AssistantBlock Name="Legal Check" Description="Ask a question about a legal document." Icon="@Icons.Material.Filled.Gavel" Link="@Routes.ASSISTANT_LEGAL_CHECK"/>
<AssistantBlock Name="Icon Finder" Description="Using a LLM to find an icon for a given context." Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
+8 -1
View File
@@ -28,7 +28,7 @@
</ChildContent>
<FooterContent>
<MudElement Style="flex: 0 0 auto;">
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="@this.UserInputClass" Style="@this.UserInputStyle"/>
</MudElement>
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
@@ -69,6 +69,13 @@
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" Provider="@this.providerSettings.UsedProvider"/>
}
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
</MudToolBar>
</FooterContent>
</InnerScrolling>
+73 -23
View File
@@ -4,7 +4,6 @@ using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@@ -19,23 +18,24 @@ namespace AIStudio.Pages;
public partial class Chat : MSGComponentBase, IAsyncDisposable
{
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
[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 const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private AIStudio.Settings.Provider providerSettings;
private Profile currentProfile = Profile.NO_PROFILE;
private ChatThread? chatThread;
private bool hasUnsavedChanges;
private bool isStreaming;
@@ -45,10 +45,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private bool workspacesVisible;
private Workspaces? workspaces;
private bool mustScrollToBottomAfterRender;
private byte scrollRenderCountdown;
// 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,
// the clearing would be done automatically.
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
private MudTextField<string> inputField = null!;
#region Overrides of ComponentBase
@@ -60,11 +60,8 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
if (this.SettingsManager.ConfigurationData.Chat.PreselectOptions)
{
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.Chat.PreselectedProvider);
}
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
if (deferredContent is not null)
{
@@ -81,6 +78,13 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
};
}
}
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
this.StateHasChanged();
}
}
await base.OnInitializedAsync();
@@ -90,8 +94,15 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
{
if(this.mustScrollToBottomAfterRender)
{
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
if (--this.scrollRenderCountdown == 0)
{
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
}
else
{
this.StateHasChanged();
}
}
await base.OnAfterRenderAsync(firstRender);
@@ -108,12 +119,36 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0;
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedProvider.GetConfidence(this.SettingsManager).SetColorStyle() : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
private void ProfileWasChanged(Profile profile)
{
this.currentProfile = profile;
if(this.chatThread is null)
return;
this.chatThread = this.chatThread with
{
SystemPrompt = $"""
{SystemPrompts.DEFAULT}
{this.currentProfile.ToSystemPrompt()}
"""
};
}
private async Task SendMessage()
{
if (!this.IsProviderSelected)
return;
// We need to blur the focus away from the input field
// to be able to clear the field:
await this.inputField.BlurAsync();
// Create a new chat thread if necessary:
var threadName = this.ExtractThreadName(this.userInput);
@@ -125,7 +160,11 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
ChatId = Guid.NewGuid(),
Name = threadName,
Seed = this.RNG.Next(),
SystemPrompt = SystemPrompts.DEFAULT,
SystemPrompt = $"""
{SystemPrompts.DEFAULT}
{this.currentProfile.ToSystemPrompt()}
""",
Blocks = [],
};
}
@@ -178,18 +217,24 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
});
// Clear the input field:
await this.inputField.Clear();
this.userInput = string.Empty;
// Enable the stream state for the chat component:
this.isStreaming = true;
this.hasUnsavedChanges = true;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
}
this.StateHasChanged();
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
@@ -310,12 +355,16 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = "You are a helpful assistant!",
SystemPrompt = $"""
{SystemPrompts.DEFAULT}
{this.currentProfile.ToSystemPrompt()}
""",
Blocks = [],
};
}
await this.inputField.Clear();
this.userInput = string.Empty;
}
private async Task MoveChatToWorkspace()
@@ -384,11 +433,12 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
this.userInput = string.Empty;
this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty;
this.currentWorkspaceName = this.chatThread is null ? string.Empty : await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId);
await this.inputField.Clear();
this.userInput = string.Empty;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
this.StateHasChanged();
}
}
+143 -7
View File
@@ -11,7 +11,13 @@
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Layers" HeaderText="Configure Providers">
<MudText Typo="Typo.h4" Class="mb-3">Configured Providers</MudText>
<MudTable Items="@this.SettingsManager.ConfigurationData.Providers" Class="border-dashed border rounded-lg">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o.
You can configure as many providers as you want. This way, you can use the appropriate model for each
task. As an LLM provider, you can also choose local providers. However, to use this app, you must
configure at least one provider.
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.Providers" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col style="width: 12em;"/>
@@ -66,6 +72,105 @@
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddProvider">
Add Provider
</MudButton>
<MudText Typo="Typo.h4" Class="mb-3">LLM Provider Confidence</MudText>
<MudJustifiedText Class="mb-3">
Do you want to always be able to recognize how trustworthy your LLM providers are? This way,
you keep control over which provider you send your data to. You have two options for this:
Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.
</MudJustifiedText>
<ConfigurationOption OptionDescription="Show provider's confidence level?" LabelOn="Yes, show me the confidence level" LabelOff="No, please hide the confidence level" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence = updatedState)" OptionHelp="When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc."/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfigurationSelect OptionDescription="Select a confidence scheme" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme = selectedValue)" OptionHelp="Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself."/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
{
<MudTable Items="@(Enum.GetValues<Providers>().Where(x => x is not Providers.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>LLM Provider</MudTh>
<MudTh>Description</MudTh>
<MudTh>Confidence Level</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd Style="vertical-align: top;">
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
{
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
continue;
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
@confidenceLevel.GetName()
</MudMenuItem>
}
</MudMenu>
</MudTd>
</RowTemplate>
</MudTable>
}
}
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Person4" HeaderText="Configure Profiles">
<MudText Typo="Typo.h4" Class="mb-3">Your Profiles</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Store personal data about yourself in various profiles so that the AIs know your personal context.
This saves you from having to explain your context each time, for example, in every chat. When you
have different roles, you can create a profile for each role.
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Are you a project manager in a research facility? You might want to create a profile for your project
management activities, one for your scientific work, and a profile for when you need to write program
code. In these profiles, you can record how much experience you have or which methods you like or
dislike using. Later, you can choose when and where you want to use each profile.
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.Profiles" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col/>
<col style="width: 40em;"/>
</ColGroup>
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>Profile Name</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Num</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditProfile(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteProfile(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if(this.SettingsManager.ConfigurationData.Profiles.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No profiles configured yet.</MudText>
}
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddProfile">
Add Profile
</MudButton>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="App Options">
@@ -73,6 +178,8 @@
<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="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.")"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Chat" HeaderText="Chat Options">
@@ -81,6 +188,7 @@
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/>
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for chats?"/>
</MudPaper>
</ExpansionPanel>
@@ -121,9 +229,10 @@
<ConfigurationSelect OptionDescription="Preselect a programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonCodingLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage = selectedValue)" OptionHelp="Which programming language should be preselected for added contexts?"/>
@if (this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage is CommonCodingLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" Icon="@Icons.Material.Filled.Code" Text="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage = updatedText)"/>
<ConfigurationText OptionDescription="Preselect another programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" Icon="@Icons.Material.Filled.Code" Text="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage = updatedText)"/>
}
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>
@@ -136,12 +245,12 @@
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage = updatedText)"/>
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationSelect OptionDescription="Preselect the summarizer complexity" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity)" Data="@ConfigurationSelectDataFactory.GetComplexityData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity = selectedValue)" OptionHelp="Which summarizer complexity should be preselected?"/>
@if(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
{
<ConfigurationText OptionDescription="Preselect your expertise" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" Icon="@Icons.Material.Filled.Person" Text="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField = updatedText)"/>
<ConfigurationText OptionDescription="Preselect your expertise" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" Icon="@Icons.Material.Filled.Person" Text="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField = updatedText)"/>
}
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
</MudPaper>
@@ -166,20 +275,20 @@
<ConfigurationOption OptionDescription="Preselect whether participants needs to arrive and depart" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants need to arrive and depart" LabelOff="Participants do not need to arrive and depart" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart = updatedState)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. lunch time" Min="30" Max="120" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime = updatedValue)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. break time" Min="10" Max="60" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime = updatedValue)" />
<ConfigurationSelect OptionDescription="Preselect the agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which agenda language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Edit" HeaderText="Assistant: Grammar & Spelling Checker">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect grammar & spell checker options?" LabelOn="Grammar & spell checker options are preselected" LabelOff="No grammar & spell checker options are preselected" State="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect the grammar & spell checker options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage = updatedText)"/>
@@ -191,7 +300,7 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Edit" HeaderText="Assistant: Rewrite & Improve Text">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect rewrite & improve text options?" LabelOn="Rewrite & improve text options are preselected" LabelOff="No rewrite & improve text options are preselected" State="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect the rewrite & improve text options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedOtherLanguage = updatedText)"/>
@@ -214,6 +323,7 @@
}
<ConfigurationSelect OptionDescription="Preselect a writing style" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle)" Data="@ConfigurationSelectDataFactory.GetWritingStyles4EMailData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle = selectedValue)" OptionHelp="Which writing style should be preselected?"/>
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>
@@ -224,6 +334,32 @@
<ConfigurationOption OptionDescription="Preselect the web content reader?" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions || this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)" LabelOn="Web content reader is preselected" LabelOff="Web content reader is not preselected" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader = updatedState)" OptionHelp="When enabled, the web content reader is preselected. This is might be useful when you prefer to load legal content from the web very often."/>
<ConfigurationOption OptionDescription="Preselect the content cleaner agent?" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions || this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)" LabelOn="Content cleaner agent is preselected" LabelOff="Content cleaner agent is not preselected" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent = updatedState)" OptionHelp="When enabled, the content cleaner agent is preselected. This is might be useful when you prefer to clean up the legal content before translating it."/>
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Spellcheck" HeaderText="Assistant: Synonyms">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect synonym options?" LabelOn="Synonym options are preselected" LabelOff="No synonym options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationSelect OptionDescription="Preselect the language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage = selectedValue)" OptionHelp="Which language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Synonyms.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Task" HeaderText="Assistant: My Tasks">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationSelect OptionDescription="Preselect the language" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another language" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.MyTasks.PreselectOtherLanguage = updatedText)"/>
}
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
+99 -7
View File
@@ -1,11 +1,11 @@
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
// ReSharper disable ClassNeverInstantiated.Global
@@ -14,16 +14,19 @@ namespace AIStudio.Pages;
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{
[Inject]
public SettingsManager SettingsManager { get; init; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public IDialogService DialogService { get; init; } = null!;
private IDialogService DialogService { get; init; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
private MessageBus MessageBus { get; init; } = null!;
[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();
@@ -111,8 +114,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled)
return;
var providerInstance = provider.CreateProvider();
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
var providerInstance = provider.CreateProvider(this.Logger);
var deleteSecretResponse = await this.RustService.DeleteAPIKey(providerInstance);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
@@ -157,6 +160,95 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
this.availableProviders.Add(new (provider.InstanceName, provider.Id));
}
private string GetCurrentConfidenceLevelName(Providers provider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(provider, out var level))
return level.GetName();
return "Not yet configured";
}
private string SetCurrentConfidenceLevelColorStyle(Providers provider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(provider, out var level))
return $"background-color: {level.GetColor()};";
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor()};";
}
private async Task ChangeCustomConfidenceLevel(Providers provider, ConfidenceLevel level)
{
this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[provider] = level;
await this.SettingsManager.StoreSettings();
}
#endregion
#region Profile related
private async Task AddProfile()
{
var dialogParameters = new DialogParameters<ProfileDialog>
{
{ x => x.IsEditing, false },
};
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>("Add Profile", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var addedProfile = (Profile)dialogResult.Data!;
addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ };
this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditProfile(Profile profile)
{
var dialogParameters = new DialogParameters<ProfileDialog>
{
{ x => x.DataNum, profile.Num },
{ x => x.DataId, profile.Id },
{ x => x.DataName, profile.Name },
{ x => x.DataNeedToKnow, profile.NeedToKnow },
{ x => x.DataActions, profile.Actions },
{ x => x.IsEditing, true },
};
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>("Edit Profile", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var editedProfile = (Profile)dialogResult.Data!;
this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task DeleteProfile(Profile profile)
{
var dialogParameters = new DialogParameters
{
{ "Message", $"Are you sure you want to delete the profile '{profile.Name}'?" },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Profile", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.Profiles.Remove(profile);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
#endregion
#region Implementation of IMessageBusReceiver
+157 -61
View File
@@ -1,9 +1,10 @@
using AIStudio;
using AIStudio.Agents;
using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Logging.Console;
using MudBlazor.Services;
#if !DEBUG
@@ -11,73 +12,168 @@ using System.Reflection;
using Microsoft.Extensions.FileProviders;
#endif
var port = args.Length > 0 ? args[0] : "5000";
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;
});
namespace AIStudio;
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 =>
internal sealed class Program
{
public static RustService RUST_SERVICE = null!;
public static Encryption ENCRYPTION = null!;
public static string API_TOKEN = null!;
public static async Task Main(string[] args)
{
options.MaximumReceiveMessageSize = null;
options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
if(args.Length == 0)
{
Console.WriteLine("Error: Please provide the port of the runtime API.");
return;
}
builder.Services.AddSingleton(new HttpClient
{
BaseAddress = new Uri($"http://localhost:{port}")
});
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
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
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
#endif
var app = builder.Build();
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
app.UseStaticFiles();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseDeveloperExceptionPage();
#else
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = string.Empty,
});
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = string.Empty,
});
#endif
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
var serverTask = app.RunAsync();
var serverTask = app.RunAsync();
Console.WriteLine("RUST/TAURI SERVER STARTED");
await serverTask;
await encryptionInitializer;
await rust.AppIsReady();
await serverTask;
}
}
@@ -4,11 +4,10 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
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()
{
@@ -22,10 +21,10 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
public string InstanceName { get; set; } = "Anthropic";
/// <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:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@@ -64,7 +63,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
// 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:
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
/// <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;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
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
/// <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>());
}
@@ -1,3 +1,5 @@
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Provider;
/// <summary>
@@ -9,13 +11,31 @@ public abstract class BaseProvider
/// The HTTP client to use for all requests.
/// </summary>
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>
/// Constructor for the base provider.
/// </summary>
/// <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:
this.httpClient.BaseAddress = new(url);
}
@@ -0,0 +1,65 @@
namespace AIStudio.Provider;
public sealed record Confidence
{
public ConfidenceLevel Level { get; private init; } = ConfidenceLevel.UNKNOWN;
public string Region { get; private init; } = string.Empty;
public string Description { get; private init; } = string.Empty;
public List<string> Sources { get; private init; } = [];
private Confidence()
{
}
public Confidence WithSources(params string[] sources) => this with { Sources = sources.ToList() };
public Confidence WithRegion(string region) => this with { Region = region };
public Confidence WithLevel(ConfidenceLevel level) => this with { Level = level };
public string StyleBorder() => $"border: 2px solid {this.Level.GetColor()}; border-radius: 6px;";
public string SetColorStyle() => $"--confidence-color: {this.Level.GetColor()};";
public static readonly Confidence NONE = new()
{
Level = ConfidenceLevel.NONE,
Description =
"""
No provider selected. Please select a provider to get see its confidence level.
""",
};
public static readonly Confidence USA_NOT_TRUSTED = new()
{
Level = ConfidenceLevel.UNTRUSTED,
Description = "The provider operates its service from the USA and is subject to **U.S. jurisdiction**. In case of suspicion, authorities in the USA can access your data. The provider's terms of service state that **all your data can be used by the provider at will.**",
};
public static readonly Confidence UNKNOWN = new()
{
Level = ConfidenceLevel.UNKNOWN,
Description = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.",
};
public static readonly Confidence USA_NO_TRAINING = new()
{
Level = ConfidenceLevel.MODERATE,
Description = "The provider operates its service from the USA and is subject to **US jurisdiction**. In case of suspicion, authorities in the USA can access your data. However, **your data is not used for training** purposes.",
};
public static readonly Confidence GDPR_NO_TRAINING = new()
{
Level = ConfidenceLevel.MEDIUM,
Description = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**.",
};
public static readonly Confidence SELF_HOSTED = new()
{
Level = ConfidenceLevel.HIGH,
Description = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way.",
};
}
@@ -0,0 +1,18 @@
namespace AIStudio.Provider;
public enum ConfidenceLevel
{
NONE = 0,
UNTRUSTED = 1,
UNKNOWN = 10,
VERY_LOW = 90,
LOW = 100,
MODERATE = 150,
MEDIUM = 200,
HIGH = 300,
}
@@ -0,0 +1,34 @@
namespace AIStudio.Provider;
public static class ConfidenceLevelExtensions
{
public static string GetName(this ConfidenceLevel level) => level switch
{
ConfidenceLevel.NONE => "No provider selected",
ConfidenceLevel.UNTRUSTED => "Untrusted",
ConfidenceLevel.VERY_LOW => "Very Low",
ConfidenceLevel.LOW => "Low",
ConfidenceLevel.MODERATE => "Moderate",
ConfidenceLevel.MEDIUM => "Medium",
ConfidenceLevel.HIGH => "High",
_ => "Unknown confidence level",
};
public static string GetColor(this ConfidenceLevel level) => level switch
{
ConfidenceLevel.NONE => "#cccccc",
ConfidenceLevel.UNTRUSTED => "#ff0000",
ConfidenceLevel.VERY_LOW => "#ff6600",
ConfidenceLevel.LOW => "#ffcc00",
ConfidenceLevel.MODERATE => "#99cc00",
ConfidenceLevel.MEDIUM => "#86b300",
ConfidenceLevel.HIGH => "#009933",
_ => "#cc6600",
};
public static string SetColorStyle(this ConfidenceLevel level) => $"--confidence-color: {level.GetColor()};";
}
@@ -4,11 +4,10 @@ using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks;
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider
public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider
{
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";
/// <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:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@@ -73,7 +72,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// 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:
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
/// <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;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
+4 -13
View File
@@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider;
@@ -22,44 +21,36 @@ public interface IProvider
/// <summary>
/// Starts a chat completion stream.
/// </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="chatThread">The chat thread to continue.</param>
/// <param name="token">The cancellation token.</param>
/// <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>
/// Starts an image completion stream.
/// </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="promptPositive">The positive prompt.</param>
/// <param name="promptNegative">The negative prompt.</param>
/// <param name="referenceImageURL">The reference image URL.</param>
/// <param name="token">The cancellation token.</param>
/// <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>
/// Load all possible text models that can be used with this provider.
/// </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="token">The cancellation token.</param>
/// <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>
/// Load all possible image models that can be used with this provider.
/// </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="token">The cancellation token.</param>
/// <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.Provider.OpenAI;
using AIStudio.Settings;
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()
{
@@ -23,10 +22,10 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
public string InstanceName { get; set; } = "Mistral";
/// <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:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@@ -75,7 +74,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// 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:
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
/// <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;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <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
{
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,
}
};
@@ -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
/// <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>());
}
@@ -1,7 +1,6 @@
using System.Runtime.CompilerServices;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider;
@@ -13,17 +12,17 @@ public class NoProvider : IProvider
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);
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);
yield break;
@@ -4,14 +4,13 @@ using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.OpenAI;
/// <summary>
/// The OpenAI provider.
/// </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()
{
@@ -27,10 +26,10 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
public string InstanceName { get; set; } = "OpenAI";
/// <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:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@@ -79,7 +78,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// 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:
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
/// <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;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
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 />
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
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
{
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,
}
};
@@ -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;
/// <summary>
@@ -20,59 +14,4 @@ public enum Providers
FIREWORKS = 5,
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,88 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
using AIStudio.Settings;
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>
/// Get a provider's confidence.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="settingsManager">The settings manager.</param>
/// <returns>The confidence of the provider.</returns>
public static Confidence GetConfidence(this Providers provider, SettingsManager settingsManager) => provider switch
{
Providers.NONE => Confidence.NONE,
Providers.FIREWORKS => Confidence.USA_NOT_TRUSTED.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
Providers.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources(
"https://openai.com/policies/terms-of-use/",
"https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance",
"https://openai.com/enterprise-privacy/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
Providers.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
Providers.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
Providers.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
};
/// <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.Provider.OpenAI;
using AIStudio.Settings;
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()
{
@@ -21,7 +20,8 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
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:
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
/// <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;
}
#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
{
@@ -162,14 +162,14 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
}
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 [];
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <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>());
}
+2
View File
@@ -19,5 +19,7 @@ public sealed partial class Routes
public const string ASSISTANT_AGENDA = "/assistant/agenda";
public const string ASSISTANT_EMAIL = "/assistant/email";
public const string ASSISTANT_LEGAL_CHECK = "/assistant/legal-check";
public const string ASSISTANT_SYNONYMS = "/assistant/synonyms";
public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks";
// ReSharper restore InconsistentNaming
}
@@ -5,7 +5,6 @@ using AIStudio.Assistants.RewriteImprove;
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Assistants.EMail;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles;
using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles;
@@ -87,6 +86,15 @@ public static class ConfigurationSelectDataFactory
yield return new(language.Name(), language);
}
public static IEnumerable<ConfigurationSelectData<CommonLanguages>> GetCommonLanguagesOptionalData()
{
foreach (var language in Enum.GetValues<CommonLanguages>())
if(language is CommonLanguages.AS_IS)
yield return new("Do not specify the language", language);
else
yield return new(language.Name(), language);
}
public static IEnumerable<ConfigurationSelectData<CommonCodingLanguages>> GetCommonCodingLanguagesData()
{
foreach (var language in Enum.GetValues<CommonCodingLanguages>())
@@ -122,4 +130,16 @@ public static class ConfigurationSelectDataFactory
foreach (var voice in Enum.GetValues<SentenceStructure>())
yield return new(voice.Name(), voice);
}
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
{
foreach (var profile in profiles.GetAllProfiles())
yield return new(profile.Name, profile.Id);
}
public static IEnumerable<ConfigurationSelectData<ConfidenceSchemes>> GetConfidenceSchemesData()
{
foreach (var scheme in Enum.GetValues<ConfidenceSchemes>())
yield return new(scheme.GetListDescription(), scheme);
}
}
@@ -15,12 +15,27 @@ public sealed class Data
/// List of configured providers.
/// </summary>
public List<Provider> Providers { get; init; } = [];
/// <summary>
/// Settings concerning the LLM providers.
/// </summary>
public DataLLMProviders LLMProviders { get; init; } = new();
/// <summary>
/// List of configured profiles.
/// </summary>
public List<Profile> Profiles { get; init; } = [];
/// <summary>
/// The next provider number to use.
/// </summary>
public uint NextProviderNum { get; set; } = 1;
/// <summary>
/// The next profile number to use.
/// </summary>
public uint NextProfileNum { get; set; } = 1;
public DataApp App { get; init; } = new();
public DataChat Chat { get; init; } = new();
@@ -46,4 +61,8 @@ public sealed class Data
public DataEMail EMail { get; set; } = new();
public DataLegalCheck LegalCheck { get; set; } = new();
public DataSynonyms Synonyms { get; set; } = new();
public DataMyTasks MyTasks { get; set; } = new();
}
@@ -1,5 +1,4 @@
using AIStudio.Assistants.Agenda;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
@@ -56,4 +55,9 @@ public sealed class DataAgenda
/// Preselect a agenda provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
@@ -22,4 +22,14 @@ public sealed class DataApp
/// The navigation behavior.
/// </summary>
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;
/// <summary>
/// Should we preselect a profile for the entire app?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
@@ -16,6 +16,11 @@ public sealed class DataChat
/// Should we preselect a provider for the chat?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
/// <summary>
/// Should we show the latest message after loading? When false, we show the first (aka oldest) message.
@@ -28,4 +28,9 @@ public sealed class DataCoding
/// Which coding provider should be preselected?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
@@ -1,5 +1,4 @@
using AIStudio.Assistants.EMail;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
@@ -29,6 +28,11 @@ public sealed class DataEMail
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
/// <summary>
/// Preselect a greeting phrase?
@@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
public sealed class DataGrammarSpelling
@@ -0,0 +1,21 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataLLMProviders
{
/// <summary>
/// Should we show the provider confidence level?
/// </summary>
public bool ShowProviderConfidence { get; set; } = true;
/// <summary>
/// Which confidence scheme to use.
/// </summary>
public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_USA_EUROPE;
/// <summary>
/// Provide custom confidence levels for each LLM provider.
/// </summary>
public Dictionary<Providers, ConfidenceLevel> CustomConfidenceScheme { get; set; } = new();
}
@@ -26,4 +26,9 @@ public class DataLegalCheck
/// The preselected translator provider.
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
@@ -0,0 +1,29 @@
namespace AIStudio.Settings.DataModel;
public sealed class DataMyTasks
{
/// <summary>
/// Do you want to preselect any options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// Preselect the target language?
/// </summary>
public CommonLanguages PreselectedTargetLanguage { get; set; }
/// <summary>
/// Preselect any other language?
/// </summary>
public string PreselectOtherLanguage { get; set; } = string.Empty;
/// <summary>
/// The preselected provider.
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
@@ -1,5 +1,4 @@
using AIStudio.Assistants.RewriteImprove;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
@@ -0,0 +1,24 @@
namespace AIStudio.Settings.DataModel;
public sealed class DataSynonyms
{
/// <summary>
/// Preselect any rewrite options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// Preselect the language?
/// </summary>
public CommonLanguages PreselectedLanguage { get; set; }
/// <summary>
/// Preselect any other language?
/// </summary>
public string PreselectedOtherLanguage { get; set; } = string.Empty;
/// <summary>
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
@@ -1,5 +1,4 @@
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
@@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
public sealed class DataTranslation
@@ -1,7 +1,6 @@
using AIStudio.Assistants.Coding;
using AIStudio.Assistants.IconFinder;
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel.PreviousModels;
@@ -0,0 +1,59 @@
namespace AIStudio.Settings;
public readonly record struct Profile(uint Num, string Id, string Name, string NeedToKnow, string Actions)
{
public static readonly Profile NO_PROFILE = new()
{
Name = "Use no profile",
NeedToKnow = string.Empty,
Actions = string.Empty,
Id = Guid.Empty.ToString(),
Num = uint.MaxValue,
};
#region Overrides of ValueType
/// <summary>
/// Returns a string that represents the profile in a human-readable format.
/// </summary>
/// <returns>A string that represents the profile in a human-readable format.</returns>
public override string ToString() => this.Name;
#endregion
public string ToSystemPrompt()
{
if(this.Num == uint.MaxValue)
return string.Empty;
var needToKnow =
$"""
What should you know about the user?
```
{this.NeedToKnow}
```
""";
var actions =
$"""
The user wants you to consider the following things.
```
{this.Actions}
```
""";
if (string.IsNullOrWhiteSpace(this.NeedToKnow))
return actions;
if (string.IsNullOrWhiteSpace(this.Actions))
return needToKnow;
return $"""
{needToKnow}
{actions}
""";
}
}
@@ -11,7 +11,7 @@ namespace AIStudio.Settings;
/// <summary>
/// The settings manager.
/// </summary>
public sealed class SettingsManager
public sealed class SettingsManager(ILogger<SettingsManager> logger)
{
private const string SETTINGS_FILENAME = "settings.json";
@@ -20,6 +20,8 @@ public sealed class SettingsManager
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
private ILogger<SettingsManager> logger = logger;
/// <summary>
/// The directory where the configuration files are stored.
@@ -37,64 +39,6 @@ public sealed class SettingsManager
public Data ConfigurationData { get; private set; } = new();
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>
/// Loads the settings from the file system.
@@ -102,12 +46,18 @@ public sealed class SettingsManager
public async Task LoadSettings()
{
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!File.Exists(settingsPath))
{
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
return;
}
// We read the `"Version": "V3"` line to determine the version of the settings file:
await foreach (var line in File.ReadLinesAsync(settingsPath))
{
@@ -123,16 +73,16 @@ public sealed class SettingsManager
Enum.TryParse(settingsVersionText, out Version settingsVersion);
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();
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;
}
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();
}
@@ -142,15 +92,126 @@ public sealed class SettingsManager
public async Task StoreSettings()
{
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!Directory.Exists(ConfigDirectory))
{
this.logger.LogInformation("Creating the configuration directory.");
Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
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 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,
Tools.Components.SYNONYMS_ASSISTANT => this.ConfigurationData.Synonyms.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.Synonyms.PreselectedProvider) : default,
Tools.Components.MY_TASKS_ASSISTANT => this.ConfigurationData.MyTasks.PreselectOptions ? this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.MyTasks.PreselectedProvider) : default,
_ => default,
};
if (preselection != default)
return preselection;
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider);
}
public Profile GetPreselectedProfile(Tools.Components component)
{
var preselection = component switch
{
Tools.Components.CHAT => this.ConfigurationData.Chat.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Chat.PreselectedProfile) : default,
Tools.Components.AGENDA_ASSISTANT => this.ConfigurationData.Agenda.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Agenda.PreselectedProfile) : default,
Tools.Components.CODING_ASSISTANT => this.ConfigurationData.Coding.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Coding.PreselectedProfile) : default,
Tools.Components.EMAIL_ASSISTANT => this.ConfigurationData.EMail.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.EMail.PreselectedProfile) : default,
Tools.Components.LEGAL_CHECK_ASSISTANT => this.ConfigurationData.LegalCheck.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.LegalCheck.PreselectedProfile) : default,
Tools.Components.MY_TASKS_ASSISTANT => this.ConfigurationData.MyTasks.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.MyTasks.PreselectedProfile) : default,
_ => default,
};
if (preselection != default)
return preselection;
preselection = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProfile);
return preselection != default ? preselection : Profile.NO_PROFILE;
}
public ConfidenceLevel GetConfiguredConfidenceLevel(Providers provider)
{
if(provider is Providers.NONE)
return ConfidenceLevel.NONE;
switch (this.ConfigurationData.LLMProviders.ConfidenceScheme)
{
case ConfidenceSchemes.TRUST_USA_EUROPE:
return provider switch
{
Providers.SELF_HOSTED => ConfidenceLevel.HIGH,
Providers.FIREWORKS => ConfidenceLevel.UNTRUSTED,
_ => ConfidenceLevel.MEDIUM,
};
case ConfidenceSchemes.TRUST_USA:
return provider switch
{
Providers.SELF_HOSTED => ConfidenceLevel.HIGH,
Providers.FIREWORKS => ConfidenceLevel.UNTRUSTED,
Providers.MISTRAL => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
};
case ConfidenceSchemes.TRUST_EUROPE:
return provider switch
{
Providers.SELF_HOSTED => ConfidenceLevel.HIGH,
Providers.FIREWORKS => ConfidenceLevel.UNTRUSTED,
Providers.MISTRAL => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};
case ConfidenceSchemes.LOCAL_TRUST_ONLY:
return provider switch
{
Providers.SELF_HOSTED => ConfidenceLevel.HIGH,
Providers.FIREWORKS => ConfidenceLevel.UNTRUSTED,
_ => ConfidenceLevel.VERY_LOW,
};
case ConfidenceSchemes.CUSTOM:
return this.ConfigurationData.LLMProviders.CustomConfidenceScheme.GetValueOrDefault(provider, ConfidenceLevel.UNKNOWN);
default:
return ConfidenceLevel.UNKNOWN;
}
}
}
@@ -9,7 +9,7 @@ namespace AIStudio.Settings;
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)
{
@@ -17,41 +17,41 @@ public static class SettingsMigrations
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
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();
}
configV1 = MigrateV1ToV2(configV1);
configV1 = MigrateV2ToV3(configV1);
return MigrateV3ToV4(configV1);
configV1 = MigrateV1ToV2(logger, configV1);
configV1 = MigrateV2ToV3(logger, configV1);
return MigrateV3ToV4(logger, configV1);
case Version.V2:
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
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();
}
configV2 = MigrateV2ToV3(configV2);
return MigrateV3ToV4(configV2);
configV2 = MigrateV2ToV3(logger, configV2);
return MigrateV3ToV4(logger, configV2);
case Version.V3:
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
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 MigrateV3ToV4(configV3);
return MigrateV3ToV4(logger, configV3);
default:
Console.WriteLine("No configuration migration needed.");
logger.LogInformation("No configuration migration is needed.");
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
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();
}
@@ -59,14 +59,14 @@ public static class SettingsMigrations
}
}
private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData)
private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
{
//
// Summary:
// 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()
{
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:
// 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()
{
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:
// We grouped the settings into different categories.
//
Console.WriteLine("Migrating from v3 to v4...");
logger.LogInformation("Migrating from v3 to v4...");
return new()
{
Version = Version.V4,
@@ -1,6 +1,6 @@
namespace AIStudio.Tools;
public enum SendTo
public enum Components
{
NONE = 0,
@@ -13,6 +13,8 @@ public enum SendTo
TEXT_SUMMARIZER_ASSISTANT,
EMAIL_ASSISTANT,
LEGAL_CHECK_ASSISTANT,
SYNONYMS_ASSISTANT,
MY_TASKS_ASSISTANT,
CHAT,
}
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff. Show more