Compare commits

..
47 Commits
Author SHA1 Message Date
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
Thorsten Sommer e4ef4bc06e Fixed an issue with the update file due to double quotes (#100) 2024-08-24 14:00:27 +02:00
Thorsten Sommer ae17104886 Prepared release v0.8.12 (#98) 2024-08-24 10:34:52 +02:00
Thorsten Sommer 2d82fcc5e1 Fix #65: changelog variable assignment (#97) 2024-08-24 10:24:00 +02:00
Thorsten Sommer 69a6fcf094 Moved legal check assistant to release 0.8.12 due to #65 (#96) 2024-08-24 09:31:06 +02:00
Thorsten Sommer 4aaa2fcbea Added a legal check assistant & the possibility to preselect some options (#94) 2024-08-23 21:47:07 +02:00
Thorsten Sommer d855a33e6f Fixed the changelog due to #65 (#95) 2024-08-23 21:43:42 +02:00
Thorsten Sommer ca16a749fd Prepare to release 0.8.12 (#93) 2024-08-23 18:18:56 +02:00
Thorsten Sommer bcdbf7dcbd Improved the rewrite assistant by letting you choose the voice for the sentence structure (#92) 2024-08-23 18:11:54 +02:00
Thorsten Sommer 537a608e7a Improved assistant footer handling (#91) 2024-08-23 13:44:27 +02:00
Thorsten Sommer a6c5b3de23 Improved the coding assistant by adding a button to delete a context (#90) 2024-08-23 11:23:36 +02:00
Thorsten Sommer 3fd0d4d5c1 Improved some default settings (#89) 2024-08-23 10:49:30 +02:00
Thorsten Sommer 5efb6b0158 Improved the chat options readability (#88) 2024-08-23 10:43:21 +02:00
Thorsten Sommer 1b3fff67ec Improved chat page by scrolling to the bottom after loading (#87) 2024-08-23 10:32:27 +02:00
Thorsten Sommer 3409872090 All assistants now have a button to copy their respective result to the clipboard (#86) 2024-08-23 08:59:30 +02:00
Thorsten Sommer 18718a6c47 Refactored the "send to" implementation of assistants (#85) 2024-08-22 21:27:16 +02:00
Thorsten Sommer 34884859d7 Add mail assistant (#84) 2024-08-22 15:41:35 +02:00
135 changed files with 4478 additions and 1793 deletions

No files matched your search

+23 -5
View File
@@ -489,7 +489,7 @@ jobs:
uses: actions/cache@v4
id: linux_arm_cache
with:
path: $RUNNER_TEMP/linux_arm_qemu_cache.img
path: ${{ runner.temp }}/linux_arm_qemu_cache.img
# When the entire key matches, Rust might just create the bundles using the current .NET build:
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}-dependencies-${{ env.CARGO_LOCK_HASH }}
@@ -544,7 +544,7 @@ jobs:
- name: Add the built runner image to the cache
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 +552,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,7 +574,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
# Save the built libraries for the next job:
rm -fr /rust-cache/target
cp -Rp target /rust-cache
- name: Debug
if: ${{ env.SKIP != 'true' }}
@@ -729,6 +740,9 @@ jobs:
# Replace newlines in changelog with \n
changelog=$(echo "$CHANGELOG" | awk '{printf "%s\\n", $0}')
# Escape double quotes in changelog:
changelog=$(echo "$changelog" | sed 's/"/\\"/g')
# Create the latest.json file:
cat <<EOOOF > $GITHUB_WORKSPACE/release/assets/latest.json
{
@@ -794,7 +808,11 @@ jobs:
- name: Append scan results to changelog
run: |
changelog="${{ env.CHANGELOG }}"
changelog=$(cat <<'EOOOOOOOF'
${{ env.CHANGELOG }}
EOOOOOOOF
)
links="${{ steps.virus_total.outputs.analysis }}"
# Create a temporary file for the changelog:
@@ -838,7 +856,7 @@ jobs:
- name: Create release
uses: softprops/action-gh-release@v2
with:
prerelease: false
prerelease: true
draft: false
make_latest: true
body: ${{ env.CHANGELOG }}
+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()
{
@@ -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,7 @@ 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 ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -159,7 +154,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 +189,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;
@@ -303,7 +296,7 @@ public partial class AssistantAgenda : AssistantBaseCore
if(string.IsNullOrWhiteSpace(content))
return "Please provide some content for the agenda. What are the main points of the meeting or the seminar?";
var lines = content.Split('\n');
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
if(!line.TrimStart().StartsWith('-'))
return "Please start each line of your content list with a dash (-) to create a bullet point list.";
@@ -363,6 +356,9 @@ public partial class AssistantAgenda : AssistantBaseCore
private string PromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.AS_IS)
return "Use the same language as the input.";
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
@@ -4,7 +4,7 @@
@(this.Title)
</MudText>
<InnerScrolling HeaderHeight="12.3em">
<InnerScrolling HeaderHeight="6em">
<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">
@@ -33,47 +33,71 @@
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.FooterButtons.Count > 0)
{
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="mt-3 mr-2">
@foreach (var button in this.FooterButtons)
{
switch (button)
</ChildContent>
<FooterContent>
<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;
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;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
</MudTooltip>
break;
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
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" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
</MudStack>
}
</ChildContent>
}
<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.AllowProfiles)
{
<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";
@@ -35,6 +39,14 @@ public abstract partial class AssistantBase : ComponentBase
protected abstract string Description { get; }
protected abstract string SystemPrompt { get; }
protected abstract Tools.Components Component { get; }
protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch
{
ContentText textBlock => textBlock.Text,
_ => string.Empty,
};
protected abstract void ResetFrom();
@@ -43,6 +55,8 @@ public abstract partial class AssistantBase : ComponentBase
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual bool AllowProfiles => true;
protected virtual bool ShowDedicatedProgress => false;
@@ -60,9 +74,18 @@ public abstract partial class AssistantBase : ComponentBase
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
private bool isProcessing;
private Profile currentProfile = Profile.NO_PROFILE;
#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:
@@ -99,7 +122,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 = [],
};
}
@@ -145,7 +173,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();
@@ -154,9 +182,9 @@ public abstract partial class AssistantBase : ComponentBase
return aiText.Text;
}
protected async Task CopyToClipboard(string text)
protected async Task CopyToClipboard()
{
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
}
private static string? GetButtonIcon(string icon)
@@ -167,7 +195,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
{
@@ -179,33 +207,19 @@ public abstract partial class AssistantBase : ComponentBase
},
};
var (eventItem, path) = destination switch
{
SendTo.AGENDA_ASSISTANT => (Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
SendTo.CODING_ASSISTANT => (Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
SendTo.REWRITE_ASSISTANT => (Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
SendTo.TRANSLATION_ASSISTANT => (Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
SendTo.ICON_FINDER_ASSISTANT => (Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
SendTo.GRAMMAR_SPELLING_ASSISTANT => (Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
SendTo.TEXT_SUMMARIZER_ASSISTANT => (Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
SendTo.CHAT => (Event.SEND_TO_CHAT, Routes.CHAT),
_ => (Event.NONE, Routes.ASSISTANTS),
};
var sendToData = destination.GetData();
switch (destination)
{
case SendTo.CHAT:
MessageBus.INSTANCE.DeferMessage(this, eventItem, this.ConvertToChatThread);
case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
break;
default:
MessageBus.INSTANCE.DeferMessage(this, eventItem, contentToSend);
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, contentToSend);
break;
}
this.NavigationManager.NavigateTo(path);
this.NavigationManager.NavigateTo(sendToData.Route);
return Task.CompletedTask;
}
@@ -218,6 +232,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 = [];
@@ -5,7 +5,8 @@
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
{
var codingContext = this.codingContexts[contextIndex];
<ExpansionPanel HeaderText="@codingContext.Id" HeaderIcon="@Icons.Material.Filled.Code">
var index = contextIndex;
<ExpansionPanel HeaderText="@codingContext.Id" HeaderIcon="@Icons.Material.Filled.Code" ShowEndButton="@true" EndButtonColor="Color.Error" EndButtonIcon="@Icons.Material.Filled.Delete" EndButtonTooltip="Delete context" EndButtonClickAsync="@(() => this.DeleteContext(index))">
<CodingContextItem @bind-CodingContext="@codingContext"/>
</ExpansionPanel>
}
@@ -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,7 @@ 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 void ResetFrom()
{
@@ -50,7 +44,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 +59,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;
@@ -105,6 +97,18 @@ public partial class AssistantCoding : AssistantBaseCore
});
}
private ValueTask DeleteContext(int index)
{
if(this.codingContexts.Count < index + 1)
return ValueTask.CompletedTask;
this.codingContexts.RemoveAt(index);
this.form?.ResetValidation();
this.StateHasChanged();
return ValueTask.CompletedTask;
}
private async Task GetSupport()
{
await this.form!.Validate();
@@ -0,0 +1,27 @@
@attribute [Route(Routes.ASSISTANT_EMAIL)]
@inherits AssistantBaseCore
<MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" />
@if (this.provideHistory)
{
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="Previous conversation" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Provide the previous conversation, e.g., the last e-mail, the last chat, etc." Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
</MudPaper>
}
<MudTextField T="string" @bind-Text="@this.inputGreeting" Label="(Optional) The greeting phrase to use" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="Dear Colleagues" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="Your bullet points" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items." Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/>
<MudSelect T="string" Label="(Optional) Are any of your points particularly important?" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
@foreach (var contentLine in this.bulletPointsLines)
{
<MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
}
</MudSelect>
<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>
@@ -0,0 +1,245 @@
using System.Text;
using AIStudio.Chat;
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 =>
"""
Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input.
""";
protected override string SystemPrompt =>
$"""
You are an automated system that writes emails. {this.SystemPromptHistory()} The user provides you with bullet points on what
he want to address in the response. Regarding the writing style of the email: {this.selectedWritingStyle.Prompt()}
{this.SystemPromptGreeting()} {this.SystemPromptName()} You write the email in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputBulletPoints = string.Empty;
this.bulletPointsLines.Clear();
this.selectedFoci = [];
this.provideHistory = false;
this.inputHistory = string.Empty;
if (!this.MightPreselectValues())
{
this.inputName = string.Empty;
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.selectedWritingStyle = WritingStyles.NONE;
this.inputGreeting = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.EMail.PreselectOptions)
{
this.inputName = this.SettingsManager.ConfigurationData.EMail.SenderName;
this.inputGreeting = this.SettingsManager.ConfigurationData.EMail.Greeting;
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage;
return true;
}
return false;
}
private const string PLACEHOLDER_BULLET_POINTS = """
- The last meeting was good
- Thank you for feedback
- Next is milestone 3
- I need your input by next Wednesday
""";
private WritingStyles selectedWritingStyle = WritingStyles.NONE;
private string inputGreeting = string.Empty;
private string inputBulletPoints = string.Empty;
private readonly List<string> bulletPointsLines = [];
private IEnumerable<string> selectedFoci = new HashSet<string>();
private string inputName = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
private bool provideHistory;
private string inputHistory = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_EMAIL_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputBulletPoints = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidateBulletPoints(string content)
{
if(string.IsNullOrWhiteSpace(content))
return "Please provide some content for the e-mail.";
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
if(!line.TrimStart().StartsWith('-'))
return "Please start each line of your content list with a dash (-) to create a bullet point list.";
return null;
}
private string? ValidateTargetLanguage(CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Please select a target language for the e-mail.";
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? ValidateWritingStyle(WritingStyles style)
{
if(style == WritingStyles.NONE)
return "Please select a writing style for the e-mail.";
return null;
}
private string? ValidateHistory(string history)
{
if(this.provideHistory && string.IsNullOrWhiteSpace(history))
return "Please provide some history for the e-mail.";
return null;
}
private void OnContentChanged(string content)
{
this.bulletPointsLines.Clear();
var previousSelectedFoci = new HashSet<string>();
foreach (var line in content.AsSpan().EnumerateLines())
{
var trimmedLine = line.Trim();
if (trimmedLine.StartsWith("-"))
trimmedLine = trimmedLine[1..].Trim();
if (trimmedLine.Length == 0)
continue;
var finalLine = trimmedLine.ToString();
if(this.selectedFoci.Any(x => x.StartsWith(finalLine, StringComparison.InvariantCultureIgnoreCase)))
previousSelectedFoci.Add(finalLine);
this.bulletPointsLines.Add(finalLine);
}
this.selectedFoci = previousSelectedFoci;
}
private string SystemPromptHistory()
{
if (this.provideHistory)
return "You receive the previous conversation as context.";
return string.Empty;
}
private string SystemPromptGreeting()
{
if(!string.IsNullOrWhiteSpace(this.inputGreeting))
return $"Your greeting should consider the following formulation: {this.inputGreeting}.";
return string.Empty;
}
private string SystemPromptName()
{
if(!string.IsNullOrWhiteSpace(this.inputName))
return $"For the closing phrase of the email, please use the following name: {this.inputName}.";
return string.Empty;
}
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 string PromptFoci()
{
if(!this.selectedFoci.Any())
return string.Empty;
var sb = new StringBuilder();
sb.AppendLine("I want to amplify the following points:");
foreach (var focus in this.selectedFoci)
sb.AppendLine($"- {focus}");
return sb.ToString();
}
private string PromptHistory()
{
if(!this.provideHistory)
return string.Empty;
return $"""
The previous conversation was:
```
{this.inputHistory}
```
""";
}
private async Task CreateMail()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.PromptHistory()}
My bullet points for the e-mail are:
{this.inputBulletPoints}
{this.PromptFoci()}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,11 @@
namespace AIStudio.Assistants.EMail;
public enum WritingStyles
{
NONE = 0,
BUSINESS_FORMAL,
BUSINESS_INFORMAL,
ACADEMIC,
PERSONAL,
}
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.EMail;
public static class WritingStylesExtensions
{
public static string Name(this WritingStyles style) => style switch
{
WritingStyles.ACADEMIC => "Academic",
WritingStyles.PERSONAL => "Personal",
WritingStyles.BUSINESS_FORMAL => "Business formal",
WritingStyles.BUSINESS_INFORMAL => "Business informal",
_ => "Not specified",
};
public static string Prompt(this WritingStyles style) => style switch
{
WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.",
WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.",
WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.",
WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.",
_ => "Use a formal business style for this e-mail.",
};
}
@@ -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,17 +22,20 @@ 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;
protected override bool ShowDedicatedProgress => true;
protected override Func<string> Result2Copy => () => this.correctedText;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new ButtonData("Copy result", Icons.Material.Filled.ContentCopy, Color.Default, string.Empty, () => this.CopyToClipboard(this.correctedText)),
new SendToButton
{
Self = SendTo.GRAMMAR_SPELLING_ASSISTANT,
Self = Tools.Components.GRAMMAR_SPELLING_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.correctedText) ? this.inputText : this.correctedText
},
@@ -59,7 +63,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;
}
@@ -70,7 +73,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;
@@ -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,10 @@ 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 void ResetFrom()
{
@@ -50,18 +43,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;
@@ -0,0 +1,15 @@
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
@inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
{
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/>
}
<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>
@@ -0,0 +1,87 @@
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 =>
"""
Provide a legal document and ask a question about it. This assistant does not
replace legal advice. Consult a lawyer to get professional advice. Remember
that LLMs can invent answers and facts. Please do not rely on this answers.
""";
protected override string SystemPrompt =>
"""
You are an expert in legal matters. You have studied international law and are familiar with the law in
various countries. You receive a legal document and answer the user's questions. You are a friendly and
professional assistant. You respond to the user in the language in which the questions were asked. If
you are unsure, unfamiliar with the legal area, or do not know the answers, you inform the user.
Never invent facts!
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override void ResetFrom()
{
this.inputLegalDocument = string.Empty;
this.inputQuestions = string.Empty;
this.MightPreselectValues();
}
protected override bool MightPreselectValues() => false;
private bool isAgentRunning;
private string inputLegalDocument = string.Empty;
private string inputQuestions = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_LEGAL_CHECK_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputQuestions = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingLegalDocument(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a legal document as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidatingQuestions(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide your questions as input.";
return null;
}
private async Task AksQuestions()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
# The legal document
{this.inputLegalDocument}
# The questions
{this.inputQuestions}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -4,6 +4,7 @@
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" 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" />
<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()">
@@ -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 =>
@@ -19,20 +20,23 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
text. You can also correct spelling and grammar issues. You never add additional information. You never
ask the user for additional information. Your response only contains the improved text. You do not explain
your changes. If no changes are needed, you return the text unchanged.
The style of the text: {this.selectedWritingStyle.Prompt()}. You follow the rules according
to {this.SystemPromptLanguage()} in all your changes.
The style of the text: {this.selectedWritingStyle.Prompt()}.{this.selectedSentenceStructure.Prompt()}
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;
protected override Func<string> Result2Copy => () => this.rewrittenText;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new ButtonData("Copy result", Icons.Material.Filled.ContentCopy, Color.Default, string.Empty, () => this.CopyToClipboard(this.rewrittenText)),
new SendToButton
{
Self = SendTo.REWRITE_ASSISTANT,
Self = Tools.Components.REWRITE_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.rewrittenText) ? this.inputText : this.rewrittenText,
},
@@ -52,6 +56,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.selectedWritingStyle = WritingStyles.NOT_SPECIFIED;
this.selectedSentenceStructure = SentenceStructure.NOT_SPECIFIED;
}
}
@@ -61,8 +66,8 @@ 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;
}
@@ -73,7 +78,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;
@@ -88,6 +92,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
private string customTargetLanguage = string.Empty;
private string rewrittenText = string.Empty;
private WritingStyles selectedWritingStyle;
private SentenceStructure selectedSentenceStructure;
private string? ValidateText(string text)
{
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.RewriteImprove;
public enum SentenceStructure
{
NOT_SPECIFIED = 0,
ACTIVE,
PASSIVE,
}
@@ -0,0 +1,20 @@
namespace AIStudio.Assistants.RewriteImprove;
public static class SentenceStructureExtensions
{
public static string Name(this SentenceStructure sentenceStructure) => sentenceStructure switch
{
SentenceStructure.ACTIVE => "Active voice",
SentenceStructure.PASSIVE => "Passive voice",
_ => "Not Specified",
};
public static string Prompt(this SentenceStructure sentenceStructure) => sentenceStructure switch
{
SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.",
SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.",
_ => string.Empty,
};
}
@@ -2,39 +2,33 @@ namespace AIStudio.Assistants.RewriteImprove;
public static class WritingStylesExtensions
{
public static string Name(this WritingStyles style)
public static string Name(this WritingStyles style) => style switch
{
return style switch
{
WritingStyles.EVERYDAY => "Everyday (personal texts, social media)",
WritingStyles.BUSINESS => "Business (business emails, reports, presentations)",
WritingStyles.SCIENTIFIC => "Scientific (scientific papers, research reports)",
WritingStyles.JOURNALISTIC => "Journalistic (magazines, newspapers, news)",
WritingStyles.LITERARY => "Literary (fiction, poetry)",
WritingStyles.TECHNICAL => "Technical (manuals, documentation)",
WritingStyles.MARKETING => "Marketing (advertisements, sales texts)",
WritingStyles.ACADEMIC => "Academic (essays, seminar papers)",
WritingStyles.LEGAL => "Legal (legal texts, contracts)",
_ => "Not specified",
};
}
public static string Prompt(this WritingStyles style)
WritingStyles.EVERYDAY => "Everyday (personal texts, social media)",
WritingStyles.BUSINESS => "Business (business emails, reports, presentations)",
WritingStyles.SCIENTIFIC => "Scientific (scientific papers, research reports)",
WritingStyles.JOURNALISTIC => "Journalistic (magazines, newspapers, news)",
WritingStyles.LITERARY => "Literary (fiction, poetry)",
WritingStyles.TECHNICAL => "Technical (manuals, documentation)",
WritingStyles.MARKETING => "Marketing (advertisements, sales texts)",
WritingStyles.ACADEMIC => "Academic (essays, seminar papers)",
WritingStyles.LEGAL => "Legal (legal texts, contracts)",
_ => "Not specified",
};
public static string Prompt(this WritingStyles style) => style switch
{
return style switch
{
WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.",
WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.",
WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.",
WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.",
WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.",
WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.",
WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.",
WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.",
WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.",
_ => "Keep the style of the text as it is.",
};
}
WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.",
WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.",
WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.",
WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.",
WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.",
WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.",
WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.",
WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.",
WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.",
_ => "Keep the style of the text as it is.",
};
}
@@ -0,0 +1,12 @@
@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"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindSynonyms()">
Get synonyms
</MudButton>
@@ -0,0 +1,166 @@
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 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);
}
}
@@ -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,9 @@ 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 ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -57,7 +54,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 +71,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;
@@ -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,9 @@ 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 ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
@@ -52,7 +49,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 +66,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,15 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
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"),
new (171, "v0.8.9, build 171 (2024-08-18 10:35 UTC)", "v0.8.9.md"),
@@ -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!;
@@ -3,6 +3,20 @@
<div class="d-flex align-center">
<MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/>
<MudText Typo="Typo.h6">@this.HeaderText</MudText>
@if (this.ShowEndButton)
{
<MudSpacer/>
if (!string.IsNullOrWhiteSpace(this.EndButtonTooltip))
{
<MudTooltip Text="@this.EndButtonTooltip">
<MudIconButton Icon="@this.EndButtonIcon" Color="@this.EndButtonColor" Size="Size.Medium" OnClick="async () => await this.EndButtonClickAsync()"/>
</MudTooltip>
}
else
{
<MudIconButton Icon="@this.EndButtonIcon" Color="@this.EndButtonColor" Size="Size.Medium" OnClick="async () => await this.EndButtonClickAsync()"/>
}
}
</div>
</TitleContent>
<ChildContent>
@@ -21,4 +21,19 @@ public partial class ExpansionPanel : ComponentBase
[Parameter]
public bool IsExpanded { get; set; }
[Parameter]
public bool ShowEndButton { get; set; }
[Parameter]
public Func<ValueTask> EndButtonClickAsync { get; set; } = () => ValueTask.CompletedTask;
[Parameter]
public string EndButtonIcon { get; set; } = Icons.Material.Filled.Delete;
[Parameter]
public Color EndButtonColor { get; set; } = Color.Error;
[Parameter]
public string EndButtonTooltip { get; set; } = string.Empty;
}
@@ -3,7 +3,16 @@
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
@this.ChildContent
<div @ref="@this.AnchorAfterChildContent">
&nbsp;
</div>
</div>
@this.FooterContent
@if (this.FooterContent is not null)
{
<MudPaper Class="pa-3 border-solid border rounded-lg">
@this.FooterContent
</MudPaper>
}
</div>
@@ -1,5 +1,4 @@
using AIStudio.Layout;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -26,6 +25,11 @@ public partial class InnerScrolling : MSGComponentBase
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
private ElementReference AnchorAfterChildContent { get; set; }
#region Overrides of ComponentBase
@@ -59,4 +63,9 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
public async Task ScrollToBottom()
{
await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime);
}
}
@@ -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 @@
<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,13 +14,16 @@
<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">
Business
</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="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"/>
</MudStack>
+42 -42
View File
@@ -16,7 +16,7 @@
</MudText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
<InnerScrolling HeaderHeight="12.3em">
<InnerScrolling @ref="@this.scrollingArea" HeaderHeight="12.3em">
<ChildContent>
@if (this.chatThread is not null)
{
@@ -27,51 +27,51 @@
}
</ChildContent>
<FooterContent>
<MudPaper Style="flex: 0 0 auto;">
<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"/>
</MudPaper>
<MudPaper Class="mt-1" Outlined="@true">
<MudToolBar WrapContent="true">
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<MudTooltip Text="Your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaces()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</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)
{
<MudTooltip Text="Your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaces()"/>
</MudTooltip>
}
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
{
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip>
}
</MudToolBar>
</MudPaper>
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</MudTooltip>
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
{
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip>
}
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
</MudToolBar>
</FooterContent>
</InnerScrolling>
+86 -21
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,21 +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;
@@ -42,10 +44,11 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private Guid currentWorkspaceId = Guid.Empty;
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
@@ -57,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)
{
@@ -78,11 +78,36 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
};
}
}
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
this.StateHasChanged();
}
}
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(this.mustScrollToBottomAfterRender)
{
if (--this.scrollRenderCountdown == 0)
{
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
}
else
{
this.StateHasChanged();
}
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private bool IsProviderSelected => this.providerSettings.UsedProvider != Providers.NONE;
@@ -94,12 +119,32 @@ 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 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);
@@ -111,7 +156,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 = [],
};
}
@@ -164,18 +213,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)
@@ -296,12 +351,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()
@@ -370,8 +429,14 @@ 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();
}
}
private void ResetState()
+111 -9
View File
@@ -11,6 +11,12 @@
<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>
<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" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
@@ -68,17 +74,72 @@
</MudButton>
</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" 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">
<ConfigurationOption OptionDescription="Save energy?" LabelOn="Energy saving is enabled" LabelOff="Energy saving is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available."/>
<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">
<ConfigurationSelect OptionDescription="Shortcut to send input" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)" Data="@ConfigurationSelectDataFactory.GetSendBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior = selectedValue)" OptionHelp="Do you want to use any shortcut to send your input?"/>
<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)"/>
<ConfigurationOption OptionDescription="Show the latest message after loading?" LabelOn="Latest message is shown, after loading a chat" LabelOff="First (oldest) message is shown, after loading a chat" State="@(() => this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading = updatedState)" OptionHelp="When enabled, the latest message is shown after loading a chat. When disabled, the first (oldest) message is shown."/>
<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>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Work" HeaderText="Workspace Options">
@@ -118,9 +179,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>
@@ -133,12 +195,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>
@@ -163,20 +225,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)"/>
@@ -188,15 +250,55 @@
<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)"/>
}
<ConfigurationSelect OptionDescription="Preselect a writing style" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle)" Data="@ConfigurationSelectDataFactory.GetWritingStylesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle = selectedValue)" OptionHelp="Which writing style should be preselected?"/>
<ConfigurationSelect OptionDescription="Preselect a writing style" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle)" Data="@ConfigurationSelectDataFactory.GetWritingStyles4RewriteData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle = selectedValue)" OptionHelp="Which writing style should be preselected?"/>
<ConfigurationSelect OptionDescription="Preselect a sentence structure" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure)" Data="@ConfigurationSelectDataFactory.GetSentenceStructureData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure = selectedValue)" OptionHelp="Which voice should be preselected for the sentence structure?"/>
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Email" HeaderText="Assistant: Writing E-Mails">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect e-mail options?" LabelOn="E-Mail options are preselected" LabelOff="No e-mail options are preselected" State="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.EMail.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect the e-mail options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationText OptionDescription="Preselect a greeting?" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" Icon="@Icons.Material.Filled.Person" Text="@(() => this.SettingsManager.ConfigurationData.EMail.Greeting)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.EMail.Greeting = updatedText)" />
<ConfigurationText OptionDescription="Preselect your name for the closing salutation?" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" Icon="@Icons.Material.Filled.Person" Text="@(() => this.SettingsManager.ConfigurationData.EMail.SenderName)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.EMail.SenderName = updatedText)" />
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage = updatedText)"/>
}
<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>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Gavel" HeaderText="Assistant: Legal Check">
<ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect legal check options?" LabelOn="Legal check options are preselected" LabelOff="No legal check options are preselected" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model."/>
<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.TextFields" HeaderText="Agent: Text Content Cleaner Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
+77 -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,73 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
this.availableProviders.Add(new (provider.InstanceName, provider.Id));
}
#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);
}
@@ -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,60 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Provider;
public static class ProvidersExtensions
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.NONE => "No provider selected",
Providers.OPEN_AI => "OpenAI",
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <param name="logger">The logger to use.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
{
try
{
return providerSettings.UsedProvider switch
{
Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(),
};
}
catch (Exception e)
{
logger.LogError($"Failed to create provider: {e.Message}");
return new NoProvider();
}
}
}
@@ -4,11 +4,10 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.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>());
}
+3
View File
@@ -17,5 +17,8 @@ public sealed partial class Routes
public const string ASSISTANT_SUMMARIZER = "/assistant/summarizer";
public const string ASSISTANT_CODING = "/assistant/coding";
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";
// ReSharper restore InconsistentNaming
}
@@ -3,8 +3,11 @@ using AIStudio.Assistants.Coding;
using AIStudio.Assistants.IconFinder;
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;
namespace AIStudio.Settings;
@@ -83,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>())
@@ -101,9 +113,27 @@ public static class ConfigurationSelectDataFactory
yield return new(number.Name(), number);
}
public static IEnumerable<ConfigurationSelectData<WritingStyles>> GetWritingStylesData()
public static IEnumerable<ConfigurationSelectData<WritingStylesRewrite>> GetWritingStyles4RewriteData()
{
foreach (var style in Enum.GetValues<WritingStyles>())
foreach (var style in Enum.GetValues<WritingStylesRewrite>())
yield return new(style.Name(), style);
}
public static IEnumerable<ConfigurationSelectData<WritingStylesEMail>> GetWritingStyles4EMailData()
{
foreach (var style in Enum.GetValues<WritingStylesEMail>())
yield return new(style.Name(), style);
}
public static IEnumerable<ConfigurationSelectData<SentenceStructure>> GetSentenceStructureData()
{
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);
}
}
@@ -15,12 +15,22 @@ public sealed class Data
/// List of configured providers.
/// </summary>
public List<Provider> Providers { get; init; } = [];
/// <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();
@@ -42,4 +52,10 @@ public sealed class Data
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
public DataRewriteImprove RewriteImprove { get; init; } = new();
public DataEMail EMail { get; set; } = new();
public DataLegalCheck LegalCheck { get; set; } = new();
public DataSynonyms Synonyms { 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;
}
@@ -16,10 +16,20 @@ public sealed class DataApp
/// <summary>
/// If and when we should look for updates.
/// </summary>
public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.ONCE_STARTUP;
public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.HOURLY;
/// <summary>
/// The navigation behavior.
/// </summary>
public NavBehavior NavigationBehavior { get; set; } = NavBehavior.EXPAND_ON_HOVER;
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;
}
@@ -5,7 +5,7 @@ public sealed class DataChat
/// <summary>
/// Shortcuts to send the input to the AI.
/// </summary>
public SendBehavior ShortcutSendBehavior { get; set; } = SendBehavior.MODIFER_ENTER_IS_SENDING;
public SendBehavior ShortcutSendBehavior { get; set; } = SendBehavior.ENTER_IS_SENDING;
/// <summary>
/// Preselect any chat options?
@@ -16,4 +16,14 @@ 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.
/// </summary>
public bool ShowLatestMessageAfterLoading { get; set; } = true;
}
@@ -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;
}
@@ -0,0 +1,46 @@
using AIStudio.Assistants.EMail;
namespace AIStudio.Settings.DataModel;
public sealed class DataEMail
{
/// <summary>
/// Preselect any rewrite 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>
/// Preselect any writing style?
/// </summary>
public WritingStyles PreselectedWritingStyle { get; set; }
/// <summary>
/// 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?
/// </summary>
public string Greeting { get; set; } = string.Empty;
/// <summary>
/// Preselect the sender name for the closing salutation?
/// </summary>
public string SenderName { get; set; } = string.Empty;
}
@@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
public sealed class DataGrammarSpelling
@@ -0,0 +1,34 @@
namespace AIStudio.Settings.DataModel;
public class DataLegalCheck
{
/// <summary>
/// Do you want to preselect any legal check options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// Hide the web content reader?
/// </summary>
public bool HideWebContentReader { get; set; }
/// <summary>
/// Preselect the web content reader?
/// </summary>
public bool PreselectWebContentReader { get; set; }
/// <summary>
/// Preselect the content cleaner agent?
/// </summary>
public bool PreselectContentCleanerAgent { get; set; }
/// <summary>
/// The preselected translator 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;
@@ -24,6 +23,11 @@ public sealed class DataRewriteImprove
/// Preselect any writing style?
/// </summary>
public WritingStyles PreselectedWritingStyle { get; set; }
/// <summary>
/// Preselect any voice style?
/// </summary>
public SentenceStructure PreselectedSentenceStructure { get; set; }
/// <summary>
/// Preselect a provider?
@@ -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}
""";
}
}
@@ -1,7 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings.DataModel;
// ReSharper disable NotAccessedPositionalProperty.Local
@@ -11,7 +10,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 +19,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 +38,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 +45,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 +72,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 +91,71 @@ 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,
_ => 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,
_ => 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;
}
}
@@ -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,
@@ -11,6 +11,9 @@ public enum SendTo
AGENDA_ASSISTANT,
CODING_ASSISTANT,
TEXT_SUMMARIZER_ASSISTANT,
EMAIL_ASSISTANT,
LEGAL_CHECK_ASSISTANT,
SYNONYMS_ASSISTANT,
CHAT,
}
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
public static class ElementReferenceExtensions
{
public static async ValueTask ScrollIntoViewAsync(this ElementReference elementReference, IJSRuntime jsRuntime)
{
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference);
}
}
@@ -0,0 +1,13 @@
using System.Security;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
[JsonConverter(typeof(EncryptedTextJsonConverter))]
public readonly record struct EncryptedText(string EncryptedData)
{
public EncryptedText() : this(string.Empty)
{
throw new SecurityException("Please provide the encrypted data.");
}
}
@@ -0,0 +1,8 @@
namespace AIStudio.Tools;
public static class EncryptedTextExtensions
{
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
}
@@ -0,0 +1,27 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
public sealed class EncryptedTextJsonConverter : JsonConverter<EncryptedText>
{
#region Overrides of JsonConverter<EncryptedText>
public override EncryptedText Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.String)
{
var value = reader.GetString()!;
return new EncryptedText(value);
}
throw new JsonException($"Unexpected token type when parsing EncryptedText. Expected {JsonTokenType.String}, but got {reader.TokenType}.");
}
public override void Write(Utf8JsonWriter writer, EncryptedText value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.EncryptedData);
}
#endregion
}
+142
View File
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace AIStudio.Tools;
public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword, byte[] secretKeySalt)
{
/// <summary>
/// The number of iterations to derive the key and IV from the password. For a password manager
/// where the user has to enter their primary password, 100 iterations would be too few and
/// insecure. Here, the use case is different: We generate a 512-byte long and cryptographically
/// secure password at every start. This password already contains enough entropy. In our case,
/// we need key and IV primarily because AES, with the algorithms we chose, requires a fixed key
/// length, and our password is too long.
/// </summary>
private const int ITERATIONS = 100;
private readonly byte[] key = new byte[32];
private readonly byte[] iv = new byte[16];
public async Task Initialize()
{
logger.LogInformation("Initializing encryption service...");
var stopwatch = Stopwatch.StartNew();
if (secretPassword.Length != 512)
{
logger.LogError($"The secret password must be 512 bytes long. It was {secretPassword.Length} bytes long.");
throw new CryptographicException("The secret password must be 512 bytes long.");
}
if(secretKeySalt.Length != 16)
{
logger.LogError($"The salt data must be 16 bytes long. It was {secretKeySalt.Length} bytes long.");
throw new CryptographicException("The salt data must be 16 bytes long.");
}
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512);
var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
Array.Copy(keyBytes, this.key, this.key.Length);
Array.Copy(ivBytes, this.iv, this.iv.Length);
});
var initDuration = stopwatch.Elapsed;
stopwatch.Stop();
logger.LogInformation($"Encryption service initialized in {initDuration.TotalMilliseconds} milliseconds.");
}
public async Task<EncryptedText> Encrypt(string data)
{
// Create AES encryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
aes.Mode = CipherMode.CBC;
using var encryption = aes.CreateEncryptor();
// Copy the given string data into a memory stream:
await using var plainDataStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
// A memory stream for the final, encrypted data:
await using var encryptedAndEncodedData = new MemoryStream();
// A base64 stream for the encoding:
await using var base64Stream = new CryptoStream(encryptedAndEncodedData, new ToBase64Transform(), CryptoStreamMode.Write);
// Write the salt into the base64 stream:
await base64Stream.WriteAsync(secretKeySalt);
// Create the encryption stream:
await using var cryptoStream = new CryptoStream(base64Stream, encryption, CryptoStreamMode.Write);
// Write the payload into the encryption stream:
await plainDataStream.CopyToAsync(cryptoStream);
// Flush the final block. Please note that it is not enough to call the regular flush method.
await cryptoStream.FlushFinalBlockAsync();
// Convert the base64 encoded data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return new EncryptedText(Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]));
}
public async Task<string> Decrypt(EncryptedText encryptedData)
{
// Build a memory stream to access the given base64 encoded data:
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData.EncryptedData));
// Wrap around the base64 decoder stream:
await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read);
// A buffer for the salt's bytes:
var readSaltBytes = new byte[16]; // 16 bytes = Guid
// Read the salt's bytes out of the stream:
var readBytes = 0;
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
while(readBytes < readSaltBytes.Length && !cts.Token.IsCancellationRequested)
{
readBytes += await base64Stream.ReadAsync(readSaltBytes, readBytes, readSaltBytes.Length - readBytes, cts.Token);
await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token);
}
// Check the salt bytes:
if(!readSaltBytes.SequenceEqual(secretKeySalt))
{
logger.LogError("The salt bytes do not match. The data is corrupted or tampered.");
throw new CryptographicException("The salt bytes do not match. The data is corrupted or tampered.");
}
// Create AES decryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
using var decryption = aes.CreateDecryptor();
// A memory stream for the final, decrypted data:
await using var decryptedData = new MemoryStream();
// The crypto stream:
await using var cryptoStream = new CryptoStream(base64Stream, decryption, CryptoStreamMode.Read);
// Reads all remaining data through the decrypt stream. Note that this operation
// starts at the current position, i.e., after the salt bytes:
await cryptoStream.CopyToAsync(decryptedData);
// Convert the decrypted data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]);
}
}
+3
View File
@@ -25,4 +25,7 @@ public enum Event
SEND_TO_CODING_ASSISTANT,
SEND_TO_TEXT_SUMMARIZER_ASSISTANT,
SEND_TO_CHAT,
SEND_TO_EMAIL_ASSISTANT,
SEND_TO_LEGAL_CHECK_ASSISTANT,
SEND_TO_SYNONYMS_ASSISTANT,
}
@@ -0,0 +1,18 @@
using System.Net.Http.Headers;
namespace AIStudio.Tools;
public static class HttpRequestHeadersExtensions
{
private static readonly string API_TOKEN;
static HttpRequestHeadersExtensions()
{
API_TOKEN = Program.API_TOKEN;
}
public static void AddApiToken(this HttpRequestHeaders headers)
{
headers.Add("token", API_TOKEN);
}
}
@@ -0,0 +1,13 @@
using AIStudio.Settings;
namespace AIStudio.Tools;
public static class ProfileExtensions
{
public static IEnumerable<Profile> GetAllProfiles(this IEnumerable<Profile> profiles)
{
yield return Profile.NO_PROFILE;
foreach (var profile in profiles)
yield return profile;
}
}
-54
View File
@@ -1,54 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class Rust
{
/// <summary>
/// Tries to copy the given text to the clipboard.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="snackbar">The snackbar to show the result.</param>
/// <param name="text">The text to copy to the clipboard.</param>
public async Task CopyText2Clipboard(IJSRuntime jsRuntime, ISnackbar snackbar, string text)
{
var response = await jsRuntime.InvokeAsync<SetClipboardResponse>("window.__TAURI__.invoke", "set_clipboard", new SetClipboardText(text));
var msg = response.Success switch
{
true => "Successfully copied text to clipboard!",
false => $"Failed to copy text to clipboard: {response.Issue}",
};
var severity = response.Success switch
{
true => Severity.Success,
false => Severity.Error,
};
snackbar.Add(msg, severity, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = severity switch
{
Severity.Success => Color.Success,
Severity.Error => Color.Error,
_ => Color.Default,
};
});
}
public async Task<UpdateResponse> CheckForUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
return await jsRuntime.InvokeAsync<UpdateResponse>("window.__TAURI__.invoke", cts.Token, "check_for_update");
}
public async Task InstallUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource();
await jsRuntime.InvokeVoidAsync("window.__TAURI__.invoke", cts.Token, "install_update");
}
}
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more