mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-19 22:58:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac731f6f78 | ||
|
|
1993576903 | ||
|
|
364941701b | ||
|
|
f321d6982e | ||
|
|
a3ec2bbe7b | ||
|
|
be430766c5 | ||
|
|
6185db733a | ||
|
|
670b0653b5 | ||
|
|
f97612de0e | ||
|
|
c56232cba7 | ||
|
|
b456319434 | ||
|
|
ceefc0114b | ||
|
|
158252dc34 | ||
|
|
07c1182611 | ||
|
|
3ce6e168cb | ||
|
|
ff61df047a | ||
|
|
d0074a6fc7 | ||
|
|
b632854cd4 | ||
|
|
3f8f399cd9 | ||
|
|
e2b371f9a1 | ||
|
|
ee2a73ccd8 | ||
|
|
97454c59e9 | ||
|
|
d25a47789e | ||
|
|
820294c14d | ||
|
|
dd40ca3e5b | ||
|
|
c67f17cf41 | ||
|
|
b948909afa | ||
|
|
3e66d543b9 | ||
|
|
1ac125b7f5 | ||
|
|
aa0c2e7870 | ||
|
|
f5a49ff077 | ||
|
|
a5863a176c | ||
|
|
0c81f90b84 | ||
|
|
81417daa78 | ||
|
|
780e45911d | ||
|
|
dad709cccb | ||
|
|
75b3238e56 | ||
|
|
293b0ffdb0 | ||
|
|
08c18cd387 |
No files matched your search
+1
-2
@@ -1,2 +1 @@
|
||||
github: [MindWorkAI]
|
||||
open_collective: mindwork-ai
|
||||
github: [MindWorkAI]
|
||||
@@ -102,6 +102,12 @@ jobs:
|
||||
dotnet_runtime: 'linux-x64'
|
||||
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
||||
tauri_bundle: 'appimage deb updater'
|
||||
|
||||
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
|
||||
rust_target: 'aarch64-unknown-linux-gnu'
|
||||
dotnet_runtime: 'linux-arm64'
|
||||
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
|
||||
tauri_bundle: 'deb'
|
||||
|
||||
- platform: 'windows-latest' # for x86-based Windows
|
||||
rust_target: 'x86_64-pc-windows-msvc'
|
||||
@@ -277,6 +283,12 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Setup dependencies (Ubuntu-specific, ARM)
|
||||
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Setup Tauri (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
run: |
|
||||
@@ -304,14 +316,14 @@ jobs:
|
||||
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
|
||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
||||
run: |
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio_*.msi"
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio*msi.zip*"
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio_*.msi" -ErrorAction SilentlyContinue
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio*msi.zip*" -ErrorAction SilentlyContinue
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (Windows - NSIS)
|
||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
|
||||
run: |
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe"
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*"
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
|
||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
||||
@@ -399,216 +411,11 @@ jobs:
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
||||
if-no-files-found: error
|
||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||
|
||||
build_linux_arm64:
|
||||
name: Build app (linux-arm64)
|
||||
runs-on: ubuntu-22.04
|
||||
needs: read_metadata
|
||||
env:
|
||||
SKIP: false # allows disabling this long-running job temporarily
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Read and format metadata
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
id: metadata
|
||||
run: |
|
||||
# Read the lines of the metadata file:
|
||||
app_version=$(sed -n '1p' metadata.txt)
|
||||
build_time=$(sed -n '2p' metadata.txt)
|
||||
build_number=$(sed -n '3p' metadata.txt)
|
||||
|
||||
# Next line is the .NET SDK version.
|
||||
# The format is '8.0.205 (commit 3e1383b780)'.
|
||||
# We extract only the version number, though:
|
||||
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||
|
||||
# Next line is the .NET runtime version.
|
||||
# The format is '8.0.5 (commit 087e15321b)'.
|
||||
# We extract only the version number, though:
|
||||
dotnet_runtime_version=$(sed -n '5p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||
|
||||
# Next line is the Rust version.
|
||||
# The format is '1.78.0 (commit 9b00956e5)'.
|
||||
# We extract only the version number, though:
|
||||
rust_version=$(sed -n '6p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||
|
||||
# Next line is the MudBlazor version:
|
||||
mud_blazor_version=$(sed -n '7p' metadata.txt)
|
||||
|
||||
# Next line is the Tauri version:
|
||||
tauri_version=$(sed -n '8p' metadata.txt)
|
||||
|
||||
# Format the app version:
|
||||
formatted_app_version="v${app_version}"
|
||||
|
||||
# Write the metadata to the environment:
|
||||
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
||||
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
|
||||
echo "BUILD_TIME=${build_time}" >> $GITHUB_ENV
|
||||
echo "BUILD_NUMBER=${build_number}" >> $GITHUB_ENV
|
||||
echo "DOTNET_SDK_VERSION=${dotnet_sdk_version}" >> $GITHUB_ENV
|
||||
echo "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $GITHUB_ENV
|
||||
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
|
||||
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
|
||||
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
||||
|
||||
# Log the metadata:
|
||||
echo "App version: '${formatted_app_version}'"
|
||||
echo "Build time: '${build_time}'"
|
||||
echo "Build number: '${build_number}'"
|
||||
echo ".NET SDK version: '${dotnet_sdk_version}'"
|
||||
echo ".NET runtime version: '${dotnet_runtime_version}'"
|
||||
echo "Rust version: '${rust_version}'"
|
||||
echo "MudBlazor version: '${mud_blazor_version}'"
|
||||
echo "Tauri version: '${tauri_version}'"
|
||||
|
||||
- name: Setup .NET
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSI }}
|
||||
cache: true
|
||||
cache-dependency-path: 'app/MindWork AI Studio/packages.lock.json'
|
||||
|
||||
- name: Build .NET project
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
run: |
|
||||
cd "app/MindWork AI Studio"
|
||||
dotnet publish --configuration release --runtime linux-arm64 --disable-build-servers --force --output ../../publish/dotnet
|
||||
|
||||
- name: Move & rename the .NET artifact
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
run: |
|
||||
mkdir -p "app/MindWork AI Studio/bin/dist"
|
||||
cd publish/dotnet
|
||||
mv mindworkAIStudio "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer-aarch64-unknown-linux-gnu"
|
||||
|
||||
- name: Cache linux arm64 runner image
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
uses: actions/cache@v4
|
||||
id: linux_arm_cache
|
||||
with:
|
||||
path: ${{ runner.temp }}/linux_arm_qemu_cache.img
|
||||
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Build linux arm runner image
|
||||
uses: pguyot/arm-runner-action@v2
|
||||
id: build-linux-arm-runner
|
||||
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
|
||||
env:
|
||||
RUST_VERSION: ${{ env.RUST_VERSION }}
|
||||
TAURI_VERSION: ${{ env.TAURI_VERSION }}
|
||||
|
||||
with:
|
||||
base_image: dietpi:rpi_armv8_bullseye
|
||||
cpu: cortex-a53
|
||||
image_additional_mb: 6000 # ~ 6GB
|
||||
optimize_image: false
|
||||
shell: /bin/bash
|
||||
commands: |
|
||||
# Rust complains (rightly) that $HOME doesn't match eid home:
|
||||
export HOME=/root
|
||||
|
||||
# Workaround to CI worker being stuck on Updating crates.io index:
|
||||
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
|
||||
# Update and upgrade the system:
|
||||
apt-get update --yes --allow-releaseinfo-change
|
||||
apt-get upgrade --yes
|
||||
apt-get autoremove --yes
|
||||
apt-get install curl wget --yes
|
||||
|
||||
# Install Rust:
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y
|
||||
source "$HOME/.cargo/env"
|
||||
rustup toolchain install $RUST_VERSION
|
||||
|
||||
# Install build tools and tauri-cli requirements:
|
||||
apt-get install --yes libwebkit2gtk-4.0-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
# Setup Tauri:
|
||||
cargo install --version 1.6.2 tauri-cli
|
||||
|
||||
- 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
|
||||
|
||||
#
|
||||
# This step does not work, because we start a VM with qemu to run the build.
|
||||
#
|
||||
#- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
|
||||
# if: ${{ env.SKIP != 'true' }}
|
||||
# run: |
|
||||
# rm -f result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||
|
||||
- name: Build Tauri project
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
uses: pguyot/arm-runner-action@v2
|
||||
id: build-linux-arm
|
||||
|
||||
with:
|
||||
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,
|
||||
# because we cannot produce the AppImage on arm64. Only the AppImage supports the automatic
|
||||
# update feature. The Debian package does not support this feature.
|
||||
#
|
||||
#PRIVATE_PUBLISH_KEY: ${{ secrets.PRIVATE_PUBLISH_KEY }}
|
||||
#PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||
#
|
||||
|
||||
shell: /bin/bash
|
||||
commands: |
|
||||
# Delete all previous artifacts, which may exist due to caching:
|
||||
rm -f result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||
|
||||
export HOME=/root
|
||||
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: Update the runner image to cache the Rust runtime build
|
||||
if: ${{ env.SKIP != 'true' }}
|
||||
run: |
|
||||
mv ${{ steps.build-linux-arm.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img
|
||||
|
||||
- name: Upload artifact (Linux - Debian Package)
|
||||
if: ${{ env.SKIP != 'true' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MindWork AI Studio (Linux - deb linux-arm64)
|
||||
path: |
|
||||
result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||
if-no-files-found: warn
|
||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||
|
||||
create_release:
|
||||
name: Prepare & create release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_main, read_metadata, build_linux_arm64]
|
||||
needs: [build_main, read_metadata]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Create artifact directory
|
||||
|
||||
@@ -13,7 +13,7 @@ Things we are currently working on:
|
||||
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
|
||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [ ] App: Implement external embedding providers
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
|
||||
@@ -28,10 +28,17 @@
|
||||
@this.Body
|
||||
</CascadingValue>
|
||||
|
||||
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
|
||||
@this.SubmitText
|
||||
</MudButton>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3">
|
||||
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
|
||||
@this.SubmitText
|
||||
</MudButton>
|
||||
@if (this.isProcessing && this.cancellationTokenSource is not null)
|
||||
{
|
||||
<MudTooltip Text="Stop generation">
|
||||
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</MudForm>
|
||||
<Issues IssuesData="@(this.inputIssues)"/>
|
||||
@@ -73,7 +80,7 @@
|
||||
{
|
||||
@if (this.ShowSendTo)
|
||||
{
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
||||
{
|
||||
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
|
||||
@@ -103,7 +110,7 @@
|
||||
break;
|
||||
|
||||
case SendToButton sendToButton:
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
||||
{
|
||||
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
|
||||
|
||||
@@ -100,6 +100,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMe
|
||||
|
||||
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private ContentBlock? resultingContentBlock;
|
||||
private string[] inputIssues = [];
|
||||
private bool isProcessing;
|
||||
@@ -285,12 +286,16 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMe
|
||||
|
||||
this.isProcessing = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
|
||||
|
||||
|
||||
using (this.cancellationTokenSource = new())
|
||||
{
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
@@ -298,6 +303,13 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMe
|
||||
return aiText.Text;
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
{
|
||||
if (this.cancellationTokenSource is not null)
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
}
|
||||
|
||||
protected async Task CopyToClipboard()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
|
||||
@@ -314,7 +326,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMe
|
||||
protected async Task OpenSettingsDialog()
|
||||
{
|
||||
var dialogParameters = new DialogParameters();
|
||||
await this.DialogService.ShowAsync<TSettings>("Open Settings", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await this.DialogService.ShowAsync<TSettings>(null, dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
|
||||
|
||||
@@ -228,18 +228,20 @@ else
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Type</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.EmbeddingName</MudTd>
|
||||
<MudTd>@context.EmbeddingType</MudTd>
|
||||
<MudTd Style="text-align: left;">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
@@ -274,17 +276,19 @@ else
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<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.EditRetrievalProcess(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@@ -210,7 +210,7 @@ public sealed record ChatThread
|
||||
ChatRole.SYSTEM => Role.SYSTEM,
|
||||
ChatRole.NONE => Role.NONE,
|
||||
|
||||
_ => Role.UNKNOW,
|
||||
_ => Role.UNKNOWN,
|
||||
},
|
||||
|
||||
Content = contentData,
|
||||
|
||||
@@ -41,10 +41,10 @@ public partial class ContentBlockComponent : ComponentBase
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool IsLastContentBlock { get; set; } = false;
|
||||
public bool IsLastContentBlock { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsSecondToLastBlock { get; set; } = false;
|
||||
public bool IsSecondToLastBlock { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Data about a workspace.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the workspace.</param>
|
||||
public sealed class Workspace(string name)
|
||||
{
|
||||
public string Name { get; set; } = name;
|
||||
|
||||
public List<ChatThread> Threads { get; set; } = new();
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (214, "v0.9.39, build 214 (2025-04-07 17:39 UTC)", "v0.9.39.md"),
|
||||
new (213, "v0.9.38, build 213 (2025-03-17 18:18 UTC)", "v0.9.38.md"),
|
||||
new (212, "v0.9.37, build 212 (2025-03-16 20:32 UTC)", "v0.9.37.md"),
|
||||
new (211, "v0.9.36, build 211 (2025-03-15 10:42 UTC)", "v0.9.36.md"),
|
||||
new (210, "v0.9.35, build 210 (2025-03-13 08:44 UTC)", "v0.9.35.md"),
|
||||
new (209, "v0.9.34, build 209 (2025-03-11 13:02 UTC)", "v0.9.34.md"),
|
||||
|
||||
@@ -3,7 +3,6 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
@@ -41,9 +40,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private DataSourceService DataSourceService { get; init; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE)
|
||||
{
|
||||
<div class="d-flex">
|
||||
<MudTooltip Text="Select the data sources you want to use here." Placement="Placement.Top">
|
||||
<MudTooltip Text="Select the data you want to use here." Placement="Placement.Top">
|
||||
@if (this.PopoverTriggerMode is PopoverTriggerMode.ICON)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/>
|
||||
@@ -11,7 +11,7 @@
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())">
|
||||
Select data sources
|
||||
Select data
|
||||
</MudButton>
|
||||
}
|
||||
</MudTooltip>
|
||||
@@ -21,7 +21,13 @@
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<PreviewPrototype/>
|
||||
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="Manage your data sources" Placement="Placement.Top">
|
||||
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="@this.OpenSettingsDialog"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
|
||||
@@ -31,6 +37,26 @@
|
||||
<MudSkeleton Width="80%"/>
|
||||
<MudSkeleton Width="100%"/>
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
You haven't configured any data sources. To grant the AI access to your data, you need to
|
||||
add such a source. However, if you wish to use data from your device, you first have to set up
|
||||
a so-called embedding. This embedding is necessary so the AI can effectively search your data,
|
||||
find and retrieve the correct information required for each task. In addition to local data,
|
||||
you can also incorporate your company's data. To do so, your company must provide the data through
|
||||
an ERI (External Retrieval Interface).
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudStack StretchItems="StretchItems.None" AlignItems="AlignItems.Start">
|
||||
<MudButton Variant="Variant.Filled" OnClick="this.OpenSettingsDialog" StartIcon="@Icons.Material.Filled.Settings">
|
||||
Manage Data Sources
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Href="https://mindworkai.org/#eri---external-retrieval-interface" Target="_blank" StartIcon="@Icons.Material.Filled.Settings">
|
||||
Read more about ERI
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
else if (this.showDataSourceSelection)
|
||||
{
|
||||
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="Yes, I want to use data sources." LabelOff="No, I don't want to use data sources." ValueChanged="@this.EnabledChanged"/>
|
||||
@@ -43,24 +69,29 @@
|
||||
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
||||
}
|
||||
|
||||
@if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
|
||||
@switch (this.aiBasedSourceSelection)
|
||||
{
|
||||
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
@source.Name
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudField>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
|
||||
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||
case true when this.availableDataSources.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case true when this.DataSourcesAISelected.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case false when this.availableDataSources.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case false:
|
||||
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
@@ -68,27 +99,42 @@
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
|
||||
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||
@foreach (var source in this.DataSourcesAISelected)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<ChildContent>
|
||||
<MudText Typo="Typo.body1">
|
||||
@source.DataSource.Name
|
||||
</MudText>
|
||||
</MudField>
|
||||
break;
|
||||
|
||||
case true:
|
||||
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
|
||||
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
@source.Name
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
|
||||
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||
@foreach (var source in this.DataSourcesAISelected)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<ChildContent>
|
||||
<MudText Typo="Typo.body1">
|
||||
@source.DataSource.Name
|
||||
</MudText>
|
||||
|
||||
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@this.GetAIReasoning(source)
|
||||
</MudJustifiedText>
|
||||
</ChildContent>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@(this.GetAIReasoning(source))
|
||||
</MudJustifiedText>
|
||||
</ChildContent>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
@@ -45,7 +48,7 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
||||
private DataSourceService DataSourceService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<DataSourceSelection> Logger { get; init; } = null!;
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private bool internalChange;
|
||||
private bool showDataSourceSelection;
|
||||
@@ -131,6 +134,20 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task OpenSettingsDialog()
|
||||
{
|
||||
this.showDataSourceSelection = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
var dialogParameters = new DialogParameters();
|
||||
var dialogReference = await this.DialogService.ShowAsync<SettingsDialogDataSources>(null, dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
await this.LoadAndApplyFilters();
|
||||
|
||||
this.showDataSourceSelection = true;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
</MudSelect>
|
||||
@if (this.AllowOther && this.Value.Equals(this.OtherValue))
|
||||
{
|
||||
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherInputChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/>
|
||||
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -1,5 +1,3 @@
|
||||
using AIStudio.Layout;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
@@ -30,9 +28,6 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
[Parameter]
|
||||
public string Style { get; set; } = string.Empty;
|
||||
|
||||
[CascadingParameter]
|
||||
private MainLayout MainLayout { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3">
|
||||
Alpha
|
||||
|
||||
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class PreviewAlpha : ComponentBase;
|
||||
public partial class PreviewAlpha : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3">
|
||||
Beta
|
||||
|
||||
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class PreviewBeta : ComponentBase;
|
||||
public partial class PreviewBeta : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3">
|
||||
Experimental
|
||||
|
||||
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class PreviewExperimental : ComponentBase;
|
||||
public partial class PreviewExperimental : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3">
|
||||
Prototype
|
||||
|
||||
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class PreviewPrototype : ComponentBase;
|
||||
public partial class PreviewPrototype : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3">
|
||||
Release Candidate
|
||||
|
||||
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class PreviewReleaseCandidate : ComponentBase;
|
||||
public partial class PreviewReleaseCandidate : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Provider</MudTh>
|
||||
<MudTh>Model</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
@@ -44,16 +44,18 @@
|
||||
<MudTd>@context.UsedLLMProvider</MudTd>
|
||||
<MudTd>@this.GetEmbeddingProviderModelName(context)</MudTd>
|
||||
|
||||
<MudTd Style="text-align: left;">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())">
|
||||
Open Dashboard
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbeddingProvider(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbeddingProvider(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())">
|
||||
Open Dashboard
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbeddingProvider(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbeddingProvider(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@@ -23,18 +23,20 @@
|
||||
<HeaderContent>
|
||||
<MudTh>#</MudTh>
|
||||
<MudTh>Profile Name</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>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>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<MudTh>Instance Name</MudTh>
|
||||
<MudTh>Provider</MudTh>
|
||||
<MudTh>Model</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
@@ -44,16 +44,18 @@
|
||||
@("as selected by provider")
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd Style="text-align: left;">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())">
|
||||
Open Dashboard
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditLLMProvider(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteLLMProvider(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())">
|
||||
Open Dashboard
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditLLMProvider(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteLLMProvider(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@@ -105,7 +105,23 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
||||
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
var issueDialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", $"Couldn't delete the provider '{provider.InstanceName}'. The issue: {deleteSecretResponse.Issue}. We can ignore this issue and delete the provider anyway. Do you want to ignore it and delete this provider?" },
|
||||
};
|
||||
|
||||
var issueDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete LLM Provider", issueDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var issueDialogResult = await issueDialogReference.Result;
|
||||
if (issueDialogResult is null || issueDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
// Case: The user wants to ignore the issue and delete the provider anyway:
|
||||
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
|
||||
await this.UpdateProviders();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,6 @@ namespace AIStudio.Components;
|
||||
|
||||
public partial class Workspaces : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<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)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.availableLLMProviders" 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>
|
||||
</DialogContent>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</MudField>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public abstract class SettingsDialogBase : ComponentBase
|
||||
public abstract class SettingsDialogBase : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
[CascadingParameter]
|
||||
protected IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public List<ConfigurationSelectData<string>> AvailableLLMProviders { get; set; } = new();
|
||||
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
@@ -23,14 +21,24 @@ public abstract class SettingsDialogBase : ComponentBase
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected RustService RustService { get; init; } = null!;
|
||||
|
||||
protected readonly List<ConfigurationSelectData<string>> availableLLMProviders = new();
|
||||
protected readonly List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
this.UpdateProviders();
|
||||
base.OnInitialized();
|
||||
this.UpdateEmbeddingProviders();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -40,8 +48,47 @@ public abstract class SettingsDialogBase : ComponentBase
|
||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||
private void UpdateProviders()
|
||||
{
|
||||
this.AvailableLLMProviders.Clear();
|
||||
this.availableLLMProviders.Clear();
|
||||
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
|
||||
this.availableLLMProviders.Add(new (provider.InstanceName, provider.Id));
|
||||
}
|
||||
|
||||
private void UpdateEmbeddingProviders()
|
||||
{
|
||||
this.availableEmbeddingProviders.Clear();
|
||||
foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders)
|
||||
this.availableEmbeddingProviders.Add(new (provider.Name, provider.Id));
|
||||
}
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public string ComponentName => nameof(Settings);
|
||||
|
||||
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<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)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.availableLLMProviders" 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>
|
||||
</DialogContent>
|
||||
|
||||
+23
-16
@@ -1,13 +1,15 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits SettingsPanelBase
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.IntegrationInstructions" HeaderText="Configure Data Sources">
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<PreviewPrototype/>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.IntegrationInstructions" Class="mr-2"/>
|
||||
Configured Data Sources
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
You might configure different data sources. A data source can include one file, all files
|
||||
in a directory, or data from your company. Later, you can incorporate these data sources
|
||||
@@ -27,7 +29,7 @@
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Type</MudTh>
|
||||
<MudTh>Embedding</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
@@ -35,14 +37,16 @@
|
||||
<MudTd>@context.Type.GetDisplayName()</MudTd>
|
||||
<MudTd>@this.GetEmbeddingName(context)</MudTd>
|
||||
|
||||
<MudTd Style="text-align: left;">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteDataSource(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
@@ -57,5 +61,8 @@
|
||||
<MudMenuItem OnClick="() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY)">Local Directory</MudMenuItem>
|
||||
<MudMenuItem OnClick="() => this.AddDataSource(DataSourceType.LOCAL_FILE)">Local File</MudMenuItem>
|
||||
</MudMenu>
|
||||
</ExpansionPanel>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
+6
-42
@@ -1,35 +1,11 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
public List<ConfigurationSelectData<string>> AvailableDataSources { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<List<ConfigurationSelectData<string>>> AvailableDataSourcesChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IReadOnlyList<ConfigurationSelectData<string>>> AvailableEmbeddingsFunc { get; set; } = () => [];
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await this.UpdateDataSources();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetEmbeddingName(IDataSource dataSource)
|
||||
{
|
||||
if(dataSource is IInternalDataSource internalDataSource)
|
||||
@@ -56,7 +32,7 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
{ x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
|
||||
};
|
||||
|
||||
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>("Add Local File as Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||
@@ -73,7 +49,7 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
{ x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
|
||||
};
|
||||
|
||||
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>("Add Local Directory as Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||
@@ -107,7 +83,6 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource);
|
||||
await this.UpdateDataSources();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
@@ -122,7 +97,7 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localFile },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
{ x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
|
||||
};
|
||||
|
||||
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>("Edit Local File Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||
@@ -138,7 +113,7 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localDirectory },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
{ x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
|
||||
};
|
||||
|
||||
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>("Edit Local Directory Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||
@@ -170,7 +145,6 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
|
||||
this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource;
|
||||
|
||||
await this.UpdateDataSources();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
@@ -210,7 +184,6 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.DataSources.Remove(dataSource);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.UpdateDataSources();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
@@ -247,13 +220,4 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDataSources()
|
||||
{
|
||||
this.AvailableDataSources.Clear();
|
||||
foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
this.AvailableDataSources.Add(new (dataSource.Name, dataSource.Id));
|
||||
|
||||
await this.AvailableDataSourcesChanged.InvokeAsync(this.AvailableDataSources);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<PreviewPrototype/>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.PrivateConnectivity" Class="mr-2" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.PrivateConnectivity" Class="mr-2"/>
|
||||
Assistant: ERI Server Options
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
@@ -18,10 +19,6 @@
|
||||
Most ERI server options can be customized and saved directly in the ERI server assistant.
|
||||
For this, the ERI server assistant has an auto-save function.
|
||||
</MudText>
|
||||
|
||||
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.PrivateConnectivity" Color="Color.Default" Href="@Routes.ASSISTANT_ERI">
|
||||
Switch to ERI server assistant
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<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)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.GRAMMAR_SPELLING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.GRAMMAR_SPELLING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<ConfigurationOption OptionDescription="Preselect icon options?" LabelOn="Icon options are preselected" LabelOff="No icon options are preselected" State="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect the icon options. This is might be useful when you prefer a specific icon source or LLM model."/>
|
||||
<ConfigurationSelect OptionDescription="Preselect the icon source" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedSource)" Data="@ConfigurationSelectDataFactory.GetIconSourcesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedSource = selectedValue)" OptionHelp="Which icon source should be preselected?"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.ICON_FINDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.ICON_FINDER_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectOtherLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<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."/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.availableLLMProviders" 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>
|
||||
</DialogContent>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<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?"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.REWRITE_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.REWRITE_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<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)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.SYNONYMS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.SYNONYMS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<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)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TEXT_SUMMARIZER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TEXT_SUMMARIZER_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TRANSLATION_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TRANSLATION_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
<ConfigurationSelect OptionDescription="Preselect a writing style" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle)" Data="@ConfigurationSelectDataFactory.GetWritingStyles4EMailData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle = selectedValue)" OptionHelp="Which writing style should be preselected?"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.availableLLMProviders" 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>
|
||||
</DialogContent>
|
||||
|
||||
@@ -45,26 +45,11 @@
|
||||
|
||||
<MudMainContent Class="mud-height-full pt-1" Style="@this.PaddingLeft">
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mud-height-full" Style="margin-left: 5em; width: calc(100% - 5em);">
|
||||
@if (!this.performingUpdate && this.IsUpdateAlertVisible)
|
||||
{
|
||||
<MudAlert NoIcon="@true" Severity="Severity.Info" Variant="Variant.Filled" ShowCloseIcon="@true" Dense="@true" CloseIconClicked="() => this.DismissUpdate()" Class="mt-2 mb-2">
|
||||
<div class="d-inline-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Medium" Class="mr-3"/>
|
||||
An update to version @this.updateToVersion is available.
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Dark" Size="Size.Small" Class="ml-3" OnClick="() => this.ShowUpdateDialog()">
|
||||
Show details
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (!this.performingUpdate)
|
||||
{
|
||||
<CascadingValue Value="@this" IsFixed="true">
|
||||
@this.Body
|
||||
</CascadingValue>
|
||||
@this.Body
|
||||
}
|
||||
|
||||
|
||||
<MudOverlay Visible="@this.performingUpdate" DarkBackground="@true" LockScroll="@true">
|
||||
<MudText Typo="Typo.h3">Please wait for the update to complete...</MudText>
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true" Size="Size.Large" Rounded="@true"/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
@@ -36,8 +37,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
|
||||
[Inject]
|
||||
private MudTheme ColorTheme { get; init; } = null!;
|
||||
|
||||
public string AdditionalHeight { get; private set; } = "0em";
|
||||
|
||||
private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
|
||||
|
||||
@@ -47,10 +46,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
private static readonly string NAVBAR_EXPANDED_WIDTH = $"{NAVBAR_EXPANDED_WIDTH_INT}em";
|
||||
|
||||
private bool navBarOpen;
|
||||
private bool isUpdateAvailable;
|
||||
private bool performingUpdate;
|
||||
private bool userDismissedUpdate;
|
||||
private string updateToVersion = string.Empty;
|
||||
private UpdateResponse? currentUpdateResponse;
|
||||
private MudThemeProvider themeProvider = null!;
|
||||
private bool useDarkMode;
|
||||
@@ -78,12 +74,35 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
SettingsManager.DataDirectory = dataDir;
|
||||
Directory.CreateDirectory(SettingsManager.DataDirectory);
|
||||
|
||||
//
|
||||
// Read the user language from Rust:
|
||||
//
|
||||
var userLanguage = await this.RustService.ReadUserLanguage();
|
||||
this.Logger.LogInformation($"The user language is: '{userLanguage}'");
|
||||
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
|
||||
//
|
||||
// We cannot process the plugins before the settings are loaded,
|
||||
// and we know our data directory.
|
||||
//
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
// Ensure that all internal plugins are present:
|
||||
await PluginFactory.EnsureInternalPlugins();
|
||||
|
||||
// Load (but not start) all plugins, without waiting for them:
|
||||
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
_ = PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
}
|
||||
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED ]);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR ]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
UpdateService.SetBlazorDependencies(this.Snackbar);
|
||||
@@ -93,6 +112,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||
this.navBarOpen = true;
|
||||
|
||||
// Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
|
||||
MudGlobal.TooltipDefaults.Duration = TimeSpan.Zero;
|
||||
|
||||
await this.themeProvider.WatchSystemPreference(this.SystemeThemeChanged);
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
@@ -102,33 +124,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
|
||||
private void LoadNavItems()
|
||||
{
|
||||
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
|
||||
var isWriterModePreviewEnabled = PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager);
|
||||
if (!isWriterModePreviewEnabled)
|
||||
{
|
||||
this.navItems = new List<NavBarItem>
|
||||
{
|
||||
new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
|
||||
new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
|
||||
new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
|
||||
new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
|
||||
new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
|
||||
new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
this.navItems = new List<NavBarItem>
|
||||
{
|
||||
new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
|
||||
new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
|
||||
new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
|
||||
new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false),
|
||||
new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
|
||||
new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
|
||||
new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
|
||||
};
|
||||
}
|
||||
this.navItems = new List<NavBarItem>(this.GetNavItems());
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -141,19 +137,24 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.USER_SEARCH_FOR_UPDATE:
|
||||
this.userDismissedUpdate = false;
|
||||
break;
|
||||
|
||||
case Event.UPDATE_AVAILABLE:
|
||||
if (data is UpdateResponse updateResponse)
|
||||
{
|
||||
this.currentUpdateResponse = updateResponse;
|
||||
this.isUpdateAvailable = updateResponse.UpdateIsAvailable;
|
||||
this.updateToVersion = updateResponse.NewVersion;
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
await this.SendMessage<bool>(Event.STATE_HAS_CHANGED);
|
||||
this.Snackbar.Add($"An update to version {updateResponse.NewVersion} is available.", Severity.Info, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Update;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 32_000;
|
||||
config.OnClick = async _ =>
|
||||
{
|
||||
await this.ShowUpdateDialog();
|
||||
};
|
||||
config.Action = "Show details";
|
||||
config.ActionVariant = Variant.Filled;
|
||||
config.ActionColor = Color.Dark;
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -172,6 +173,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
case Event.COLOR_THEME_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_ERROR:
|
||||
if (data is Error error)
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,24 +188,24 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task DismissUpdate()
|
||||
|
||||
private IEnumerable<NavBarItem> GetNavItems()
|
||||
{
|
||||
this.userDismissedUpdate = true;
|
||||
this.AdditionalHeight = "0em";
|
||||
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
|
||||
|
||||
await this.SendMessage<bool>(Event.STATE_HAS_CHANGED);
|
||||
}
|
||||
|
||||
private bool IsUpdateAlertVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
var state = this.isUpdateAvailable && !this.userDismissedUpdate;
|
||||
this.AdditionalHeight = state ? "3em" : "0em";
|
||||
|
||||
return state;
|
||||
}
|
||||
yield return new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true);
|
||||
yield return new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
|
||||
yield return new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false);
|
||||
|
||||
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
|
||||
yield return new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false);
|
||||
|
||||
if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
yield return new("Plugins", Icons.Material.TwoTone.Extension, palette.DarkLighten, palette.GrayLight, Routes.PLUGINS, false);
|
||||
|
||||
yield return new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false);
|
||||
yield return new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false);
|
||||
yield return new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false);
|
||||
}
|
||||
|
||||
private async Task ShowUpdateDialog()
|
||||
|
||||
@@ -41,17 +41,18 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Embed all files in wwwroot folder -->
|
||||
<EmbeddedResource Include="wwwroot\**" CopyToOutputDirectory="PreserveNewest" />
|
||||
<EmbeddedResource Include="Plugins\**" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.0.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.3" />
|
||||
<PackageReference Include="MudBlazor" Version="8.3.0" />
|
||||
<PackageReference Include="MudBlazor" Version="8.5.1" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.0.0" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -102,12 +102,19 @@
|
||||
<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="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & 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="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow."/>
|
||||
<ThirdPartyComponent Name="async-stream" Developer="Carl Lerche, Taiki Endo & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/async-stream/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/async-stream" UseCase="This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."/>
|
||||
<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="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file."/>
|
||||
<ThirdPartyComponent Name="calamine" Developer="Johann Tuffe, Joel Natividad, Eric Jolibois, Dmitriy & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tafia/calamine/blob/master/LICENSE-MIT.md" RepositoryUrl="https://github.com/tafia/calamine" UseCase="This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."/>
|
||||
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."/>
|
||||
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."/>
|
||||
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." />
|
||||
<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."/>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@using AIStudio.Tools.PluginSystem
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudText Typo="Typo.h3" Class="mb-2">
|
||||
Plugins
|
||||
</MudText>
|
||||
<PreviewExperimental ApplyInnerScrollingFix="true"/>
|
||||
|
||||
<InnerScrolling>
|
||||
|
||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
<col style="width: 2em;" />
|
||||
<col style="width: 2.1em; "/>
|
||||
<col/>
|
||||
<col style="width: 12em;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh colspan="2">Plugins</MudTh>
|
||||
<MudTh>Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<GroupHeaderTemplate>
|
||||
<MudTh Class="mud-table-cell-custom-group" colspan="3">
|
||||
@switch (context.Key)
|
||||
{
|
||||
case GROUP_ENABLED:
|
||||
<MudText Typo="Typo.h6" Class="mb-2">
|
||||
Enabled Plugins
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case GROUP_DISABLED:
|
||||
<MudText Typo="Typo.h6" Class="mb-2">
|
||||
Disabled Plugins
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case GROUP_INTERNAL:
|
||||
<MudText Typo="Typo.h6" Class="mb-2">
|
||||
Internal Plugins
|
||||
</MudText>
|
||||
break;
|
||||
}
|
||||
</MudTh>
|
||||
</GroupHeaderTemplate>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudAvatar Size="Size.Medium" Style="width: 2em; height: 2em;" Class="align-content-stretch">
|
||||
<div class="plugin-icon-container">
|
||||
@((MarkupString)context.IconSVG)
|
||||
</div>
|
||||
</MudAvatar>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h6" Class="mb-0 pb-0 gap-0">
|
||||
@context.Name
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1">
|
||||
@context.Description
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@if (!context.IsInternal)
|
||||
{
|
||||
var isEnabled = this.SettingsManager.IsPluginEnabled(context);
|
||||
<MudTooltip Text="@(isEnabled ? "Disable plugin" : "Enable plugin")">
|
||||
<MudSwitch T="bool" Value="@isEnabled" ValueChanged="@(_ => this.PluginActivationStateChanged(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
@@ -0,0 +1,82 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
{
|
||||
private const string GROUP_ENABLED = "Enabled";
|
||||
private const string GROUP_DISABLED = "Disabled";
|
||||
private const string GROUP_INTERNAL = "Internal";
|
||||
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
private TableGroupDefinition<IPluginMetadata> groupConfig = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.PLUGINS_RELOADED ]);
|
||||
|
||||
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
||||
{
|
||||
Expandable = true,
|
||||
IsInitiallyExpanded = true,
|
||||
Selector = pluginMeta =>
|
||||
{
|
||||
if (pluginMeta.IsInternal)
|
||||
return GROUP_INTERNAL;
|
||||
|
||||
return this.SettingsManager.IsPluginEnabled(pluginMeta)
|
||||
? GROUP_ENABLED
|
||||
: GROUP_DISABLED;
|
||||
}
|
||||
};
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||
{
|
||||
if (this.SettingsManager.IsPluginEnabled(pluginMeta))
|
||||
this.SettingsManager.ConfigurationData.EnabledPlugins.Remove(pluginMeta.Id);
|
||||
else
|
||||
this.SettingsManager.ConfigurationData.EnabledPlugins.Add(pluginMeta.Id);
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public string ComponentName => nameof(Plugins);
|
||||
|
||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders"/>
|
||||
<SettingsPanelDataSources AvailableLLMProvidersFunc="() => this.availableLLMProviders" AvailableEmbeddingsFunc="() => this.availableEmbeddingProviders" @bind-AvailableDataSources="@this.availableDataSources"/>
|
||||
}
|
||||
|
||||
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||
<SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||
|
||||
@@ -14,7 +14,6 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
|
||||
private List<ConfigurationSelectData<string>> availableLLMProviders = new();
|
||||
private List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
||||
private List<ConfigurationSelectData<string>> availableDataSources = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<Supporter Name="DevNullx64" Type="SupporterType.INDIVIDUAL" URL="https://github.com/DevNullx64" Acknowledgment="Thanks Luc for your build script contribution."/>
|
||||
<Supporter Name="SolsticeSpectrum" Type="SupporterType.INDIVIDUAL" URL="https://github.com/SolsticeSpectrum" Acknowledgment="Thanks for your build script contribution."/>
|
||||
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thanks for your contributions to the project."/>
|
||||
<Supporter Name="nilskruthoff" Type="SupporterType.INDIVIDUAL" URL="https://github.com/nilskruthoff" Acknowledgment="Thanks, Nils, for taking the time to learn Rust and build the foundation for local retrieval."/>
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
<MudText Typo="Typo.h3" Class="mb-2">
|
||||
Writer
|
||||
</MudText>
|
||||
|
||||
<PreviewExperimental/>
|
||||
<PreviewExperimental ApplyInnerScrollingFix="true"/>
|
||||
|
||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
|
||||
<InnerScrolling>
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CONTENT_HOME = {
|
||||
LetsGetStarted = "Lass uns anfangen",
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
SVG = [[<svg viewBox="0 0 600 600" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||
<path style="fill:none" d="M0 0h600v600H0z"/>
|
||||
<clipPath id="a">
|
||||
<path d="M0 0h600v600H0z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="M17.467 200C58.688 83.529 169.851 0 300.369 0S542.05 83.529 583.271 200c11.072 31.284 17.098 64.944 17.098 100s-6.026 68.716-17.098 100C542.05 516.471 430.887 600 300.369 600S58.688 516.471 17.467 400C6.395 368.716.369 335.056.369 300s6.026-68.716 17.098-100Z"/>
|
||||
<path d="M583.271 200c11.072 31.284 17.098 64.944 17.098 100s-6.026 68.716-17.098 100C542.05 516.471 430.887 600 300.369 600S58.688 516.471 17.467 400C6.395 368.716.369 335.056.369 300s6.026-68.716 17.098-100h565.804Z" style="fill:#d00"/>
|
||||
<path d="M583.271 400C542.05 516.471 430.887 600 300.369 600S58.688 516.471 17.467 400h565.804Z" style="fill:#ffce00"/>
|
||||
</g>
|
||||
</svg>]]
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
require("contentHome")
|
||||
require("icon")
|
||||
|
||||
-- The ID for this plugin:
|
||||
ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc"
|
||||
|
||||
-- The icon for the plugin:
|
||||
ICON_SVG = SVG
|
||||
|
||||
-- The name of the plugin:
|
||||
NAME = "MindWork AI Studio - German / Deutsch"
|
||||
|
||||
-- The description of the plugin:
|
||||
DESCRIPTION = "Dieses Plugin bietet deutsche Sprachunterstützung für MindWork AI Studio."
|
||||
|
||||
-- The version of the plugin:
|
||||
VERSION = "1.0.0"
|
||||
|
||||
-- The type of the plugin:
|
||||
TYPE = "LANGUAGE"
|
||||
|
||||
-- The authors of the plugin:
|
||||
AUTHORS = {"MindWork AI Community"}
|
||||
|
||||
-- The support contact for the plugin:
|
||||
SUPPORT_CONTACT = "MindWork AI Community"
|
||||
|
||||
-- The source URL for the plugin:
|
||||
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
|
||||
|
||||
-- The categories for the plugin:
|
||||
CATEGORIES = { "CORE" }
|
||||
|
||||
-- The target groups for the plugin:
|
||||
TARGET_GROUPS = { "EVERYONE" }
|
||||
|
||||
-- The flag for whether the plugin is maintained:
|
||||
IS_MAINTAINED = true
|
||||
|
||||
-- When the plugin is deprecated, this message will be shown to users:
|
||||
DEPRECATION_MESSAGE = ""
|
||||
|
||||
-- The IETF BCP 47 tag for the language. It's the ISO 639 language
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "de-DE"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CONTENT_HOME = {
|
||||
LetsGetStarted = "Let's get started",
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
SVG = [[<svg viewBox="0 0 650 650" style="fill-rule:evenodd;clip-rule:evenodd">
|
||||
<path style="fill:none" d="M0 0h650v650H0z"/>
|
||||
<clipPath id="a">
|
||||
<path d="M0 0h650v650H0z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="M208.018 21.723C244.323 7.694 283.77 0 325 0c44.15 0 86.256 8.823 124.649 24.8a319.796 319.796 0 0 1 9.251 4.02A324.269 324.269 0 0 1 494 47.402 326.269 326.269 0 0 1 532.582 75c33.111 27.531 60.675 61.509 80.757 100a322.631 322.631 0 0 1 32.831 100 326.842 326.842 0 0 1 3.83 50c0 17.002-1.308 33.7-3.83 50a322.631 322.631 0 0 1-32.831 100c-20.082 38.491-47.646 72.469-80.757 100-56.319 46.827-128.686 75-207.582 75-78.896 0-151.263-28.173-207.582-75-33.111-27.531-60.675-61.509-80.757-100A322.631 322.631 0 0 1 3.83 375a325.032 325.032 0 0 1-2.881-25A328.869 328.869 0 0 1 0 325c0-17.002 1.308-33.7 3.83-50a322.493 322.493 0 0 1 31.196-96.822 323.44 323.44 0 0 1 8.553-15.752C62.893 129.089 87.96 99.493 117.418 75A325.476 325.476 0 0 1 191.1 28.82a324.203 324.203 0 0 1 16.918-7.097Z" style="fill:#b31942;fill-rule:nonzero"/>
|
||||
<path d="M0 75h1235m0 100H0m0 100h1235m0 100H0m0 100h1235m0 100H0" style="fill-rule:nonzero;stroke:#fff;stroke-width:50px"/>
|
||||
<path d="M208.018 21.723C244.323 7.694 283.77 0 325 0c44.15 0 86.256 8.823 124.649 24.8a319.796 319.796 0 0 1 9.251 4.02A324.269 324.269 0 0 1 494 47.402V350H.949A328.869 328.869 0 0 1 0 325c0-17.002 1.308-33.7 3.83-50a322.493 322.493 0 0 1 31.196-96.822 323.44 323.44 0 0 1 8.553-15.752C62.893 129.089 87.96 99.493 117.418 75A325.476 325.476 0 0 1 191.1 28.82a324.203 324.203 0 0 1 16.918-7.097Z" style="fill:#0a3161;fill-rule:nonzero"/>
|
||||
<path d="M30.895 186.613A317.843 317.843 0 0 1 36.661 175a323.44 323.44 0 0 1 6.918-12.574l2.078 6.394h14.531l-11.756 8.541 4.49 13.819-11.755-8.541-11.756 8.541 1.484-4.567ZM41.167 225l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L41.167 225ZM41.167 295l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L41.167 295ZM82.333 120l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L82.333 120ZM82.333 190l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L82.333 190ZM82.333 260l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L82.333 260ZM123.5 85l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L123.5 85ZM123.5 155l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L123.5 155ZM123.5 225l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L123.5 225ZM123.5 295l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L123.5 295ZM164.667 50l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L164.667 50ZM164.667 120l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L164.667 120ZM164.667 190l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L164.667 190ZM164.667 260l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L164.667 260ZM198.568 37.361l-10.096-7.336c.874-.405 1.75-.807 2.628-1.205h10.243l1.66-5.111a334.886 334.886 0 0 1 5.015-1.986l2.306 7.097h14.53l-11.755 8.541 4.49 13.819-11.756-8.541-11.755 8.541 4.49-13.819ZM205.833 85l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L205.833 85ZM205.833 155l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L205.833 155ZM205.833 225l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L205.833 225ZM205.833 295l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L205.833 295ZM247 50l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L247 50ZM247 120l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L247 120ZM247 190l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L247 190ZM247 260l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L247 260ZM288.167 15l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L288.167 15ZM288.167 85l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L288.167 85ZM288.167 155l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L288.167 155ZM288.167 225l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L288.167 225ZM288.167 295l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L288.167 295ZM329.333 50l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L329.333 50ZM329.333 120l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L329.333 120ZM329.333 190l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L329.333 190ZM329.333 260l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L329.333 260ZM370.5 15l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L370.5 15ZM370.5 85l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L370.5 85ZM370.5 155l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L370.5 155ZM370.5 225l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L370.5 225ZM370.5 295l11.756 36.18-30.777-22.36h38.042l-30.777 22.36L370.5 295ZM411.667 50l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L411.667 50ZM411.667 120l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L411.667 120ZM411.667 190l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L411.667 190ZM411.667 260l11.755 36.18-30.776-22.36h38.042l-30.777 22.36L411.667 260ZM448.343 28.82l1.306-4.02a319.796 319.796 0 0 1 7.405 3.19l.27.83h1.576a320.481 320.481 0 0 1 7.869 3.695l-6.67 4.846 4.49 13.819-11.756-8.541-11.755 8.541 4.49-13.819-11.756-8.541h14.531ZM452.833 85l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L452.833 85ZM452.833 155l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L452.833 155ZM452.833 225l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L452.833 225ZM452.833 295l11.756 36.18-30.777-22.36h38.042l-30.776 22.36L452.833 295Z" style="fill:#fff;fill-rule:nonzero"/>
|
||||
</g>
|
||||
</svg>]]
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
require("contentHome")
|
||||
require("icon")
|
||||
|
||||
-- The ID for this plugin:
|
||||
ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8"
|
||||
|
||||
-- The icon for the plugin:
|
||||
ICON_SVG = SVG
|
||||
|
||||
-- The name of the plugin:
|
||||
NAME = "MindWork AI Studio - US English"
|
||||
|
||||
-- The description of the plugin:
|
||||
DESCRIPTION = "This plugin provides US English language support for MindWork AI Studio."
|
||||
|
||||
-- The version of the plugin:
|
||||
VERSION = "1.0.0"
|
||||
|
||||
-- The type of the plugin:
|
||||
TYPE = "LANGUAGE"
|
||||
|
||||
-- The authors of the plugin:
|
||||
AUTHORS = {"MindWork AI Community"}
|
||||
|
||||
-- The support contact for the plugin:
|
||||
SUPPORT_CONTACT = "MindWork AI Community"
|
||||
|
||||
-- The source URL for the plugin:
|
||||
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
|
||||
|
||||
-- The categories for the plugin:
|
||||
CATEGORIES = { "CORE" }
|
||||
|
||||
-- The target groups for the plugin:
|
||||
TARGET_GROUPS = { "EVERYONE" }
|
||||
|
||||
-- The flag for whether the plugin is maintained:
|
||||
IS_MAINTAINED = true
|
||||
|
||||
-- When the plugin is deprecated, this message will be shown to users:
|
||||
DEPRECATION_MESSAGE = ""
|
||||
|
||||
-- The IETF BCP 47 tag for the language. It's the ISO 639 language
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "en-US"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
@@ -22,6 +23,7 @@ internal sealed class Program
|
||||
public static Encryption ENCRYPTION = null!;
|
||||
public static string API_TOKEN = null!;
|
||||
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||
public static ILoggerFactory LOGGER_FACTORY = null!;
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
@@ -147,6 +149,9 @@ internal sealed class Program
|
||||
// Execute the builder to get the app:
|
||||
var app = builder.Build();
|
||||
|
||||
// Get the logging factory for e.g., static classes:
|
||||
LOGGER_FACTORY = app.Services.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
// Get a program logger:
|
||||
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
programLogger.LogInformation("Starting the AI Studio server.");
|
||||
@@ -205,6 +210,7 @@ internal sealed class Program
|
||||
await serverTask;
|
||||
|
||||
RUST_SERVICE.Dispose();
|
||||
PluginFactory.Dispose();
|
||||
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||
}
|
||||
}
|
||||
@@ -116,9 +116,50 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
response = nextResponse;
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, $"Tried to communicate with the LLM provider '{this.InstanceName}'. You might not be able to use this provider from your location. The provider message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.BadRequest)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, $"Tried to communicate with the LLM provider '{this.InstanceName}'. The required message format might be changed. The provider message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.NotFound)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, $"Tried to communicate with the LLM provider '{this.InstanceName}'. Something was not found. The provider message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Key, $"Tried to communicate with the LLM provider '{this.InstanceName}'. The API key might be invalid. The provider message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.InternalServerError)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, $"Tried to communicate with the LLM provider '{this.InstanceName}'. The server might be down or having issues. The provider message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, $"Tried to communicate with the LLM provider '{this.InstanceName}'. The provider is overloaded. The message is: '{nextResponse.ReasonPhrase}'"));
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
break;
|
||||
@@ -134,8 +175,11 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
|
||||
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, $"Tried to communicate with the LLM provider '{this.InstanceName}'. Even after {MAX_RETRIES} retries, there were some problems with the request. The provider message is: '{errorMessage}'"));
|
||||
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
|
||||
|
||||
}
|
||||
|
||||
return new HttpRateLimitedStreamResult(true, false, string.Empty, response);
|
||||
}
|
||||
|
||||
@@ -160,6 +204,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, $"Tried to communicate with the LLM provider '{this.InstanceName}'. There were some problems with the request. The provider message is: '{e.Message}'"));
|
||||
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
|
||||
@@ -176,6 +221,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, $"Tried to stream the LLM provider '{this.InstanceName}' answer. There were some problems with the stream. The message is: '{e.Message}'"));
|
||||
this.logger.LogWarning($"Failed to read the end-of-stream state from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
@@ -196,6 +242,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, $"Tried to stream the LLM provider '{this.InstanceName}' answer. Was not able to read the stream. The message is: '{e.Message}'"));
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public sealed partial class Routes
|
||||
public const string SETTINGS = "/settings";
|
||||
public const string SUPPORTERS = "/supporters";
|
||||
public const string WRITER = "/writer";
|
||||
public const string PLUGINS = "/plugins";
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
public const string ASSISTANT_TRANSLATION = "/assistant/translation";
|
||||
|
||||
@@ -36,6 +36,11 @@ public sealed class Data
|
||||
/// </summary>
|
||||
public List<Profile> Profiles { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// List of enabled plugins.
|
||||
/// </summary>
|
||||
public List<Guid> EnabledPlugins { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The next provider number to use.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,4 +8,6 @@ public enum PreviewFeatures
|
||||
//
|
||||
PRE_WRITER_MODE_2024,
|
||||
PRE_RAG_2024,
|
||||
|
||||
PRE_PLUGINS_2025,
|
||||
}
|
||||
@@ -6,6 +6,7 @@ public static class PreviewFeaturesExtensions
|
||||
{
|
||||
PreviewFeatures.PRE_WRITER_MODE_2024 => "Writer Mode: Experiments about how to write long texts using AI",
|
||||
PreviewFeatures.PRE_RAG_2024 => "RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company",
|
||||
PreviewFeatures.PRE_PLUGINS_2025 => "Plugins: Preview of our plugin system where you can extend the functionality of the app",
|
||||
|
||||
_ => "Unknown preview feature"
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ public static class PreviewVisibilityExtensions
|
||||
if (visibility >= PreviewVisibility.EXPERIMENTAL)
|
||||
{
|
||||
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
|
||||
features.Add(PreviewFeatures.PRE_PLUGINS_2025);
|
||||
}
|
||||
|
||||
return features;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||
|
||||
@@ -142,6 +143,8 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return minimumLevel;
|
||||
}
|
||||
|
||||
public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
|
||||
|
||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class CommonTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all the values (the names) of an enum as a string, separated by commas.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type to get the values of.</typeparam>
|
||||
/// <param name="exceptions">The values to exclude from the result.</param>
|
||||
/// <returns>The values of the enum as a string, separated by commas.</returns>
|
||||
public static string GetAllEnumValues<TEnum>(params TEnum[] exceptions) where TEnum : struct, Enum
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var value in Enum.GetValues<TEnum>())
|
||||
if(!exceptions.Contains(value))
|
||||
sb.Append(value).Append(", ");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace AIStudio.Tools.ERIClient.DataModel;
|
||||
public enum Role
|
||||
{
|
||||
NONE,
|
||||
UNKNOW,
|
||||
UNKNOWN,
|
||||
|
||||
SYSTEM,
|
||||
USER,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct Error(string Icon, string Message)
|
||||
{
|
||||
public void Show(ISnackbar snackbar)
|
||||
{
|
||||
var icon = this.Icon;
|
||||
snackbar.Add(this.Message, Severity.Error, config =>
|
||||
{
|
||||
config.Icon = icon;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 14_000;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ public enum Event
|
||||
STATE_HAS_CHANGED,
|
||||
CONFIGURATION_CHANGED,
|
||||
COLOR_THEME_CHANGED,
|
||||
PLUGINS_RELOADED,
|
||||
SHOW_ERROR,
|
||||
|
||||
// Update events:
|
||||
USER_SEARCH_FOR_UPDATE,
|
||||
|
||||
@@ -65,7 +65,9 @@ public sealed class MessageBus
|
||||
this.sendingSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task SendError(Error error) => this.SendMessage(null, Event.SHOW_ERROR, error);
|
||||
|
||||
public void DeferMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data = default)
|
||||
{
|
||||
if (this.deferredMessages.TryGetValue(triggeredEvent, out var queue))
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a plugin is forbidden.
|
||||
/// </summary>
|
||||
public static class ForbiddenPlugins
|
||||
{
|
||||
private const string ID_PATTERN = "ID = \"";
|
||||
private static readonly int ID_PATTERN_LEN = ID_PATTERN.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given code represents a forbidden plugin.
|
||||
/// </summary>
|
||||
/// <param name="code">The code to check.</param>
|
||||
/// <returns>The result of the check.</returns>
|
||||
public static PluginCheckResult Check(ReadOnlySpan<char> code)
|
||||
{
|
||||
var endIndex = 0;
|
||||
var foundAnyId = false;
|
||||
var id = ReadOnlySpan<char>.Empty;
|
||||
while (true)
|
||||
{
|
||||
// Create a slice of the code starting at the end index.
|
||||
// This way we can search for all IDs in the code:
|
||||
code = code[endIndex..];
|
||||
|
||||
// Read the next ID as a string:
|
||||
if (!TryGetId(code, out id, out endIndex))
|
||||
{
|
||||
// When no ID was found at all, we block this plugin.
|
||||
// When another ID was found previously, we allow this plugin.
|
||||
if(foundAnyId)
|
||||
return new PluginCheckResult(false, null);
|
||||
|
||||
return new PluginCheckResult(true, "No ID was found.");
|
||||
}
|
||||
|
||||
// Try to parse the ID as a GUID:
|
||||
if (!Guid.TryParse(id, out var parsedGuid))
|
||||
{
|
||||
// Again, when no ID was found at all, we block this plugin.
|
||||
if(foundAnyId)
|
||||
return new PluginCheckResult(false, null);
|
||||
|
||||
return new PluginCheckResult(true, "The ID is not a valid GUID.");
|
||||
}
|
||||
|
||||
// Check if the GUID is forbidden:
|
||||
if (FORBIDDEN_PLUGINS.TryGetValue(parsedGuid, out var reason))
|
||||
return new PluginCheckResult(true, reason);
|
||||
|
||||
foundAnyId = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetId(ReadOnlySpan<char> code, out ReadOnlySpan<char> id, out int endIndex)
|
||||
{
|
||||
//
|
||||
// Please note: the code variable is a slice of the original code.
|
||||
// That means the indices are relative to the slice, not the original code.
|
||||
//
|
||||
|
||||
id = ReadOnlySpan<char>.Empty;
|
||||
endIndex = 0;
|
||||
|
||||
// Find the next ID:
|
||||
var idStartIndex = code.IndexOf(ID_PATTERN);
|
||||
if (idStartIndex < 0)
|
||||
return false;
|
||||
|
||||
// Find the start index of the value (Guid):
|
||||
var valueStartIndex = idStartIndex + ID_PATTERN_LEN;
|
||||
|
||||
// Find the end index of the value. In order to do that,
|
||||
// we create a slice of the code starting at the value
|
||||
// start index. That means that the end index is relative
|
||||
// to the inner slice, not the original code nor the outer slice.
|
||||
var valueEndIndex = code[valueStartIndex..].IndexOf('"');
|
||||
if (valueEndIndex < 0)
|
||||
return false;
|
||||
|
||||
// From the perspective of the start index is the end index
|
||||
// the length of the value:
|
||||
endIndex = valueStartIndex + valueEndIndex;
|
||||
id = code.Slice(valueStartIndex, valueEndIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The forbidden plugins.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A dictionary that maps the GUID of a plugin to the reason why it is forbidden.
|
||||
/// </remarks>
|
||||
// ReSharper disable once CollectionNeverUpdated.Local
|
||||
private static readonly Dictionary<Guid, string> FORBIDDEN_PLUGINS =
|
||||
[
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a contract for a language plugin.
|
||||
/// </summary>
|
||||
public interface ILanguagePlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key does not exist, the value will be an empty string.
|
||||
/// Please note that the key is case-sensitive. Furthermore, the keys
|
||||
/// are in the format "root::key". That means that the keys are
|
||||
/// hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public interface IPluginMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The icon of this plugin.
|
||||
/// </summary>
|
||||
public string IconSVG { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this plugin.
|
||||
/// </summary>
|
||||
public PluginType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of this plugin.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this plugin.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of this plugin.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of this plugin.
|
||||
/// </summary>
|
||||
public PluginVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The authors of this plugin.
|
||||
/// </summary>
|
||||
public string[] Authors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The support contact for this plugin.
|
||||
/// </summary>
|
||||
public string SupportContact { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The source URL of this plugin.
|
||||
/// </summary>
|
||||
public string SourceURL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The categories of this plugin.
|
||||
/// </summary>
|
||||
public PluginCategory[] Categories { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The target groups of this plugin.
|
||||
/// </summary>
|
||||
public PluginTargetGroup[] TargetGroups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True, when the plugin is maintained.
|
||||
/// </summary>
|
||||
public bool IsMaintained { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The message that should be displayed when the plugin is deprecated.
|
||||
/// </summary>
|
||||
public string DeprecationMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True, when the plugin is AI Studio internal.
|
||||
/// </summary>
|
||||
public bool IsInternal { get; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public enum InternalPlugin
|
||||
{
|
||||
LANGUAGE_EN_US,
|
||||
LANGUAGE_DE_DE,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public readonly record struct InternalPluginData(PluginType Type, Guid Id, string ShortName)
|
||||
{
|
||||
public string ResourcePath => $"{this.Type.GetDirectory()}/{this.ShortName.ToLowerInvariant()}-{this.Id}";
|
||||
|
||||
public string ResourceName => $"{this.ShortName.ToLowerInvariant()}-{this.Id}";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class InternalPluginExtensions
|
||||
{
|
||||
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
|
||||
{
|
||||
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
|
||||
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
|
||||
|
||||
_ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// This Lua module loader does not load any modules.
|
||||
/// </summary>
|
||||
public sealed class NoModuleLoader : ILuaModuleLoader
|
||||
{
|
||||
#region Implementation of ILuaModuleLoader
|
||||
|
||||
public bool Exists(string moduleName) => false;
|
||||
|
||||
public ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.FromResult(new LuaModule());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a plugin that could not be loaded.
|
||||
/// </summary>
|
||||
/// <param name="parsingError">The error message that occurred while parsing the plugin.</param>
|
||||
public sealed class NoPlugin(string parsingError) : PluginBase(false, LuaState.Create(), PluginType.NONE, parsingError);
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public abstract partial class PluginBase
|
||||
{
|
||||
private const string DEFAULT_ICON_SVG =
|
||||
"""
|
||||
<svg height="1.5em" width="1.5em" viewBox="0 0 24 24" fill="#1f1f1f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-2V7h-6V5c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2H4l.01 2.12C5.76 9.8 7 11.51 7 13.5c0 1.99-1.25 3.7-3 4.38V20h2.12c.68-1.75 2.39-3 4.38-3 1.99 0 3.7 1.25 4.38 3H17v-6h2c.28 0 .5-.22.5-.5s-.22-.5-.5-.5z" opacity=".3"/><path d="M19 11V7c0-1.1-.9-2-2-2h-4c0-1.38-1.12-2.5-2.5-2.5S8 3.62 8 5H4c-1.1 0-1.99.9-1.99 2v3.8h.29c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-.3c0-1.49 1.21-2.7 2.7-2.7s2.7 1.21 2.7 2.7v.3H17c1.1 0 2-.9 2-2v-4c1.38 0 2.5-1.12 2.5-2.5S20.38 11 19 11zm0 3h-2v6h-2.12c-.68-1.75-2.39-3-4.38-3-1.99 0-3.7 1.25-4.38 3H4v-2.12c1.75-.68 3-2.39 3-4.38 0-1.99-1.24-3.7-2.99-4.38L4 7h6V5c0-.28.22-.5.5-.5s.5.22.5.5v2h6v6h2c.28 0 .5.22.5.5s-.22.5-.5.5z"/></svg>
|
||||
""";
|
||||
|
||||
#region Initialization-related methods
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the icon of the plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no icon is specified, the default icon will be used.
|
||||
/// </remarks>
|
||||
/// <param name="message">The error message, when the icon could not be read.</param>
|
||||
/// <param name="iconSVG">The read icon as SVG.</param>
|
||||
/// <returns>True, when the icon could be read successfully.</returns>
|
||||
|
||||
// ReSharper disable once OutParameterValueIsAlwaysDiscarded.Local
|
||||
// ReSharper disable once UnusedMethodReturnValue.Local
|
||||
private bool TryInitIconSVG(out string message, out string iconSVG)
|
||||
{
|
||||
if (!this.state.Environment["ICON_SVG"].TryRead(out iconSVG))
|
||||
{
|
||||
iconSVG = DEFAULT_ICON_SVG;
|
||||
message = "The field ICON_SVG does not exist or is not a valid string.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(iconSVG))
|
||||
{
|
||||
iconSVG = DEFAULT_ICON_SVG;
|
||||
message = "The field ICON_SVG is empty. The icon must be a non-empty string.";
|
||||
return true;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
using Lua;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the base of any AI Studio plugin.
|
||||
/// </summary>
|
||||
public abstract partial class PluginBase : IPluginMetadata
|
||||
{
|
||||
private readonly IReadOnlyCollection<string> baseIssues;
|
||||
protected readonly LuaState state;
|
||||
|
||||
protected readonly List<string> pluginIssues = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IconSVG { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginType Type { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginVersion Version { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string[] Authors { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SupportContact { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SourceURL { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginCategory[] Categories { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginTargetGroup[] TargetGroups { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMaintained { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DeprecationMessage { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsInternal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The issues that occurred during the initialization of this plugin.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Issues => this.baseIssues.Concat(this.pluginIssues);
|
||||
|
||||
/// <summary>
|
||||
/// True, when the plugin is valid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// False means that there were issues during the initialization of the plugin.
|
||||
/// Please check the Issues property for more information.
|
||||
/// </remarks>
|
||||
public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0;
|
||||
|
||||
protected PluginBase(bool isInternal, LuaState state, PluginType type, string parseError = "")
|
||||
{
|
||||
this.state = state;
|
||||
this.Type = type;
|
||||
|
||||
var issues = new List<string>();
|
||||
if(!string.IsNullOrWhiteSpace(parseError))
|
||||
issues.Add(parseError);
|
||||
|
||||
// Notice: when no icon is specified, the default icon will be used.
|
||||
this.TryInitIconSVG(out _, out var iconSVG);
|
||||
this.IconSVG = iconSVG;
|
||||
|
||||
if(this.TryInitId(out var issue, out var id))
|
||||
{
|
||||
this.Id = id;
|
||||
this.IsInternal = isInternal;
|
||||
}
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitName(out issue, out var name))
|
||||
this.Name = name;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitDescription(out issue, out var description))
|
||||
this.Description = description;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitVersion(out issue, out var version))
|
||||
this.Version = version;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitAuthors(out issue, out var authors))
|
||||
this.Authors = authors;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitSupportContact(out issue, out var contact))
|
||||
this.SupportContact = contact;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitSourceURL(out issue, out var url))
|
||||
this.SourceURL = url;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitCategories(out issue, out var categories))
|
||||
this.Categories = categories;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitTargetGroups(out issue, out var targetGroups))
|
||||
this.TargetGroups = targetGroups;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitIsMaintained(out issue, out var isMaintained))
|
||||
this.IsMaintained = isMaintained;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
if(this.TryInitDeprecationMessage(out issue, out var deprecationMessage))
|
||||
this.DeprecationMessage = deprecationMessage;
|
||||
else if(this is not NoPlugin)
|
||||
issues.Add(issue);
|
||||
|
||||
this.baseIssues = issues;
|
||||
}
|
||||
|
||||
#region Initialization-related methods
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the ID of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the ID could not be read.</param>
|
||||
/// <param name="id">The read ID.</param>
|
||||
/// <returns>True, when the ID could be read successfully.</returns>
|
||||
private bool TryInitId(out string message, out Guid id)
|
||||
{
|
||||
if (!this.state.Environment["ID"].TryRead<string>(out var idText))
|
||||
{
|
||||
message = "The field ID does not exist or is not a valid string.";
|
||||
id = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(idText, out id))
|
||||
{
|
||||
message = "The field ID is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
|
||||
id = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(id == Guid.Empty)
|
||||
{
|
||||
message = "The field ID is empty. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the name of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the name could not be read.</param>
|
||||
/// <param name="name">The read name.</param>
|
||||
/// <returns>True, when the name could be read successfully.</returns>
|
||||
private bool TryInitName(out string message, out string name)
|
||||
{
|
||||
if (!this.state.Environment["NAME"].TryRead(out name))
|
||||
{
|
||||
message = "The field NAME does not exist or is not a valid string.";
|
||||
name = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
message = "The field NAME is empty. The name must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the description of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the description could not be read.</param>
|
||||
/// <param name="description">The read description.</param>
|
||||
/// <returns>True, when the description could be read successfully.</returns>
|
||||
private bool TryInitDescription(out string message, out string description)
|
||||
{
|
||||
if (!this.state.Environment["DESCRIPTION"].TryRead(out description))
|
||||
{
|
||||
message = "The field DESCRIPTION does not exist or is not a valid string.";
|
||||
description = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
message = "The field DESCRIPTION is empty. The description must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the version of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the version could not be read.</param>
|
||||
/// <param name="version">The read version.</param>
|
||||
/// <returns>True, when the version could be read successfully.</returns>
|
||||
private bool TryInitVersion(out string message, out PluginVersion version)
|
||||
{
|
||||
if (!this.state.Environment["VERSION"].TryRead<string>(out var versionText))
|
||||
{
|
||||
message = "The field VERSION does not exist or is not a valid string.";
|
||||
version = PluginVersion.NONE;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PluginVersion.TryParse(versionText, out version))
|
||||
{
|
||||
message = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
|
||||
version = PluginVersion.NONE;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(version == PluginVersion.NONE)
|
||||
{
|
||||
message = "The field VERSION is empty. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the authors of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the authors could not be read.</param>
|
||||
/// <param name="authors">The read authors.</param>
|
||||
/// <returns>True, when the authors could be read successfully.</returns>
|
||||
private bool TryInitAuthors(out string message, out string[] authors)
|
||||
{
|
||||
if (!this.state.Environment["AUTHORS"].TryRead<LuaTable>(out var authorsTable))
|
||||
{
|
||||
authors = [];
|
||||
message = "The table AUTHORS does not exist or is using an invalid syntax.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var authorList = new List<string>();
|
||||
foreach(var author in authorsTable.GetArraySpan())
|
||||
if(author.TryRead<string>(out var authorName))
|
||||
authorList.Add(authorName);
|
||||
|
||||
authors = authorList.ToArray();
|
||||
if(authorList.Count == 0)
|
||||
{
|
||||
message = "The table AUTHORS is empty. At least one author must be specified.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the support contact for the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the support contact could not be read.</param>
|
||||
/// <param name="contact">The read support contact.</param>
|
||||
/// <returns>True, when the support contact could be read successfully.</returns>
|
||||
private bool TryInitSupportContact(out string message, out string contact)
|
||||
{
|
||||
if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out contact))
|
||||
{
|
||||
contact = string.Empty;
|
||||
message = "The field SUPPORT_CONTACT does not exist or is not a valid string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(contact))
|
||||
{
|
||||
message = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to read the source URL of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the source URL could not be read.</param>
|
||||
/// <param name="url">The read source URL.</param>
|
||||
/// <returns>True, when the source URL could be read successfully.</returns>
|
||||
private bool TryInitSourceURL(out string message, out string url)
|
||||
{
|
||||
if (!this.state.Environment["SOURCE_URL"].TryRead(out url))
|
||||
{
|
||||
url = string.Empty;
|
||||
message = "The field SOURCE_URL does not exist or is not a valid string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
url = string.Empty;
|
||||
message = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the categories of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the categories could not be read.</param>
|
||||
/// <param name="categories">The read categories.</param>
|
||||
/// <returns>True, when the categories could be read successfully.</returns>
|
||||
private bool TryInitCategories(out string message, out PluginCategory[] categories)
|
||||
{
|
||||
if (!this.state.Environment["CATEGORIES"].TryRead<LuaTable>(out var categoriesTable))
|
||||
{
|
||||
categories = [];
|
||||
message = "The table CATEGORIES does not exist or is using an invalid syntax.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var categoryList = new List<PluginCategory>();
|
||||
foreach(var luaCategory in categoriesTable.GetArraySpan())
|
||||
if(luaCategory.TryRead<string>(out var categoryName))
|
||||
if(Enum.TryParse<PluginCategory>(categoryName, out var category) && category != PluginCategory.NONE)
|
||||
categoryList.Add(category);
|
||||
|
||||
categories = categoryList.ToArray();
|
||||
if(categoryList.Count == 0)
|
||||
{
|
||||
message = $"The table CATEGORIES is empty. At least one category is necessary. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the intended target groups for the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the target groups could not be read.</param>
|
||||
/// <param name="targetGroups">The read target groups.</param>
|
||||
/// <returns>True, when the target groups could be read successfully.</returns>
|
||||
private bool TryInitTargetGroups(out string message, out PluginTargetGroup[] targetGroups)
|
||||
{
|
||||
if (!this.state.Environment["TARGET_GROUPS"].TryRead<LuaTable>(out var targetGroupsTable))
|
||||
{
|
||||
targetGroups = [];
|
||||
message = "The table TARGET_GROUPS does not exist or is using an invalid syntax.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetGroupList = new List<PluginTargetGroup>();
|
||||
foreach(var luaTargetGroup in targetGroupsTable.GetArraySpan())
|
||||
if(luaTargetGroup.TryRead<string>(out var targetGroupName))
|
||||
if(Enum.TryParse<PluginTargetGroup>(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE)
|
||||
targetGroupList.Add(targetGroup);
|
||||
|
||||
targetGroups = targetGroupList.ToArray();
|
||||
if(targetGroups.Length == 0)
|
||||
{
|
||||
message = "The table TARGET_GROUPS is empty or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the maintenance status of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the maintenance status could not be read.</param>
|
||||
/// <param name="isMaintained">The read maintenance status.</param>
|
||||
/// <returns>True, when the maintenance status could be read successfully.</returns>
|
||||
private bool TryInitIsMaintained(out string message, out bool isMaintained)
|
||||
{
|
||||
if (!this.state.Environment["IS_MAINTAINED"].TryRead(out isMaintained))
|
||||
{
|
||||
isMaintained = false;
|
||||
message = "The field IS_MAINTAINED does not exist or is not a valid boolean.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the deprecation message of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the deprecation message could not be read.</param>
|
||||
/// <param name="deprecationMessage">The read deprecation message.</param>
|
||||
/// <returns>True, when the deprecation message could be read successfully.</returns>
|
||||
private bool TryInitDeprecationMessage(out string message, out string deprecationMessage)
|
||||
{
|
||||
if (!this.state.Environment["DEPRECATION_MESSAGE"].TryRead(out deprecationMessage))
|
||||
{
|
||||
deprecationMessage = string.Empty;
|
||||
message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This message is optional: use an empty string to indicate that the plugin is not deprecated.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the UI text content of the plugin.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the UI text content could not be read.</param>
|
||||
/// <param name="pluginContent">The read UI text content.</param>
|
||||
/// <returns>True, when the UI text content could be read successfully.</returns>
|
||||
protected bool TryInitUITextContent(out string message, out Dictionary<string, string> pluginContent)
|
||||
{
|
||||
if (!this.state.Environment["UI_TEXT_CONTENT"].TryRead<LuaTable>(out var textTable))
|
||||
{
|
||||
message = "The UI_TEXT_CONTENT table does not exist or is not a valid table.";
|
||||
pluginContent = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ReadTextTable("root", textTable, out pluginContent);
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a flat or hierarchical text table.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent key(s).</param>
|
||||
/// <param name="table">The table to read.</param>
|
||||
/// <param name="tableContent">The read table content.</param>
|
||||
protected void ReadTextTable(string parent, LuaTable table, out Dictionary<string, string> tableContent)
|
||||
{
|
||||
tableContent = [];
|
||||
var lastKey = LuaValue.Nil;
|
||||
while (table.TryGetNext(lastKey, out var pair))
|
||||
{
|
||||
var keyText = pair.Key.ToString();
|
||||
if (pair.Value.TryRead<string>(out var value))
|
||||
tableContent[$"{parent}::{keyText}"] = value;
|
||||
|
||||
else if (pair.Value.TryRead<LuaTable>(out var t))
|
||||
{
|
||||
this.ReadTextTable($"{parent}::{keyText}", t, out var subContent);
|
||||
foreach (var (k, v) in subContent)
|
||||
tableContent[k] = v;
|
||||
}
|
||||
|
||||
lastKey = pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public enum PluginCategory
|
||||
{
|
||||
NONE,
|
||||
CORE,
|
||||
|
||||
BUSINESS,
|
||||
INDUSTRY,
|
||||
UTILITY,
|
||||
SOFTWARE_DEVELOPMENT,
|
||||
GAMING,
|
||||
EDUCATION,
|
||||
ENTERTAINMENT,
|
||||
SOCIAL,
|
||||
SHOPPING,
|
||||
TRAVEL,
|
||||
HEALTH,
|
||||
FITNESS,
|
||||
FOOD,
|
||||
PARTY,
|
||||
SPORTS,
|
||||
NEWS,
|
||||
WEATHER,
|
||||
MUSIC,
|
||||
POLITICAL,
|
||||
SCIENCE,
|
||||
TECHNOLOGY,
|
||||
ART,
|
||||
FICTION,
|
||||
WRITING,
|
||||
CONTENT_CREATION,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class PluginCategoryExtensions
|
||||
{
|
||||
public static string GetName(this PluginCategory type) => type switch
|
||||
{
|
||||
PluginCategory.NONE => "None",
|
||||
PluginCategory.CORE => "AI Studio Core",
|
||||
|
||||
PluginCategory.BUSINESS => "Business",
|
||||
PluginCategory.INDUSTRY => "Industry",
|
||||
PluginCategory.UTILITY => "Utility",
|
||||
PluginCategory.SOFTWARE_DEVELOPMENT => "Software Development",
|
||||
PluginCategory.GAMING => "Gaming",
|
||||
PluginCategory.EDUCATION => "Education",
|
||||
PluginCategory.ENTERTAINMENT => "Entertainment",
|
||||
PluginCategory.SOCIAL => "Social",
|
||||
PluginCategory.SHOPPING => "Shopping",
|
||||
PluginCategory.TRAVEL => "Travel",
|
||||
PluginCategory.HEALTH => "Health",
|
||||
PluginCategory.FITNESS => "Fitness",
|
||||
PluginCategory.FOOD => "Food",
|
||||
PluginCategory.PARTY => "Party",
|
||||
PluginCategory.SPORTS => "Sports",
|
||||
PluginCategory.NEWS => "News",
|
||||
PluginCategory.WEATHER => "Weather",
|
||||
PluginCategory.MUSIC => "Music",
|
||||
PluginCategory.POLITICAL => "Political",
|
||||
PluginCategory.SCIENCE => "Science",
|
||||
PluginCategory.TECHNOLOGY => "Technology",
|
||||
PluginCategory.ART => "Art",
|
||||
PluginCategory.FICTION => "Fiction",
|
||||
PluginCategory.WRITING => "Writing",
|
||||
PluginCategory.CONTENT_CREATION => "Content Creation",
|
||||
|
||||
_ => "Unknown plugin category",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a plugin check.
|
||||
/// </summary>
|
||||
/// <param name="IsForbidden">In case the plugin is forbidden, this is true.</param>
|
||||
/// <param name="Message">The message that describes why the plugin is forbidden.</param>
|
||||
public readonly record struct PluginCheckResult(bool IsForbidden, string? Message);
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
public static void SetUpHotReloading()
|
||||
{
|
||||
LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'.");
|
||||
try
|
||||
{
|
||||
var messageBus = Program.SERVICE_PROVIDER.GetRequiredService<MessageBus>();
|
||||
|
||||
HOT_RELOAD_WATCHER.IncludeSubdirectories = true;
|
||||
HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
|
||||
HOT_RELOAD_WATCHER.Filter = "*.lua";
|
||||
HOT_RELOAD_WATCHER.Changed += async (_, args) =>
|
||||
{
|
||||
LOG.LogInformation($"File changed: {args.FullPath}");
|
||||
await LoadAll();
|
||||
await messageBus.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
||||
};
|
||||
|
||||
HOT_RELOAD_WATCHER.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError(e, "Error while setting up hot reloading.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LOG.LogInformation("Hot reloading plugins set up.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
#if RELEASE
|
||||
using System.Reflection;
|
||||
#endif
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
public static async Task EnsureInternalPlugins()
|
||||
{
|
||||
LOG.LogInformation("Start ensuring internal plugins.");
|
||||
foreach (var plugin in Enum.GetValues<InternalPlugin>())
|
||||
{
|
||||
LOG.LogInformation($"Ensure plugin: {plugin}");
|
||||
await EnsurePlugin(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsurePlugin(InternalPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
var basePath = Path.Join(Environment.CurrentDirectory, "Plugins");
|
||||
var resourceFileProvider = new PhysicalFileProvider(basePath);
|
||||
#else
|
||||
var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins");
|
||||
#endif
|
||||
|
||||
var metaData = plugin.MetaData();
|
||||
var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua";
|
||||
var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath);
|
||||
|
||||
if(!resourceInfo.Exists)
|
||||
{
|
||||
LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the additional resources exist:
|
||||
foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
|
||||
{
|
||||
if(content.IsDirectory)
|
||||
{
|
||||
LOG.LogError("The plugin contains a directory. This is not allowed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
await CopyInternalPluginFile(content, metaData);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
LOG.LogError($"Was not able to ensure the plugin: {plugin}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
|
||||
{
|
||||
await using var inputStream = resourceInfo.CreateReadStream();
|
||||
|
||||
var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory());
|
||||
|
||||
if (!Directory.Exists(INTERNAL_PLUGINS_ROOT))
|
||||
Directory.CreateDirectory(INTERNAL_PLUGINS_ROOT);
|
||||
|
||||
if (!Directory.Exists(pluginTypeBasePath))
|
||||
Directory.CreateDirectory(pluginTypeBasePath);
|
||||
|
||||
var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName);
|
||||
if (!Directory.Exists(pluginPath))
|
||||
Directory.CreateDirectory(pluginPath);
|
||||
|
||||
var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
|
||||
|
||||
await using var outputStream = File.Create(pluginFilePath);
|
||||
await inputStream.CopyToAsync(outputStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory");
|
||||
|
||||
private static readonly string DATA_DIR = SettingsManager.DataDirectory!;
|
||||
|
||||
private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
|
||||
private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
|
||||
private static readonly FileSystemWatcher HOT_RELOAD_WATCHER;
|
||||
|
||||
private static readonly List<IPluginMetadata> AVAILABLE_PLUGINS = [];
|
||||
|
||||
/// <summary>
|
||||
/// A list of all available plugins.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<IPluginMetadata> AvailablePlugins => AVAILABLE_PLUGINS;
|
||||
|
||||
static PluginFactory()
|
||||
{
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||
|
||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to load all plugins from the plugins directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Loading plugins means:<br/>
|
||||
/// - Parsing and checking the plugin code<br/>
|
||||
/// - Check for forbidden plugins<br/>
|
||||
/// - Creating a new instance of the allowed plugin<br/>
|
||||
/// - Read the plugin metadata<br/>
|
||||
/// <br/>
|
||||
/// Loading a plugin does not mean to start the plugin, though.
|
||||
/// </remarks>
|
||||
public static async Task LoadAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
LOG.LogInformation("Start loading plugins.");
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
{
|
||||
LOG.LogInformation("No plugins found.");
|
||||
return;
|
||||
}
|
||||
|
||||
AVAILABLE_PLUGINS.Clear();
|
||||
|
||||
//
|
||||
// The easiest way to load all plugins is to find all `plugin.lua` files and load them.
|
||||
// By convention, each plugin is enforced to have a `plugin.lua` file.
|
||||
//
|
||||
var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories);
|
||||
foreach (var pluginMainFile in pluginMainFiles)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
LOG.LogInformation($"Try to load plugin: {pluginMainFile}");
|
||||
var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
|
||||
var pluginPath = Path.GetDirectoryName(pluginMainFile)!;
|
||||
var plugin = await Load(pluginPath, code, cancellationToken);
|
||||
|
||||
switch (plugin)
|
||||
{
|
||||
case NoPlugin noPlugin when noPlugin.Issues.Any():
|
||||
LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: {noPlugin.Issues.First()}");
|
||||
continue;
|
||||
|
||||
case NoPlugin:
|
||||
LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: Unknown.");
|
||||
continue;
|
||||
|
||||
case { IsValid: false }:
|
||||
LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix.");
|
||||
#if DEBUG
|
||||
foreach (var pluginIssue in plugin.Issues)
|
||||
LOG.LogError($"Plugin issue: {pluginIssue}");
|
||||
#endif
|
||||
continue;
|
||||
|
||||
case { IsMaintained: false }:
|
||||
LOG.LogWarning($"The plugin '{pluginMainFile}' is not maintained anymore. Please consider to disable it.");
|
||||
break;
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Load(string pluginPath, string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
|
||||
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
|
||||
|
||||
var state = LuaState.Create();
|
||||
|
||||
// Add the module loader so that the plugin can load other Lua modules:
|
||||
state.ModuleLoader = new PluginLoader(pluginPath);
|
||||
|
||||
// Add some useful libraries:
|
||||
state.OpenModuleLibrary();
|
||||
state.OpenStringLibrary();
|
||||
state.OpenTableLibrary();
|
||||
state.OpenMathLibrary();
|
||||
state.OpenBitwiseLibrary();
|
||||
state.OpenCoroutineLibrary();
|
||||
|
||||
try
|
||||
{
|
||||
await state.DoStringAsync(code, cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (LuaParseException e)
|
||||
{
|
||||
return new NoPlugin($"Was not able to parse the plugin: {e.Message}");
|
||||
}
|
||||
catch (LuaRuntimeException e)
|
||||
{
|
||||
return new NoPlugin($"Was not able to run the plugin: {e.Message}");
|
||||
}
|
||||
|
||||
if (!state.Environment["TYPE"].TryRead<string>(out var typeText))
|
||||
return new NoPlugin("TYPE does not exist or is not a valid string.");
|
||||
|
||||
if (!Enum.TryParse<PluginType>(typeText, out var type))
|
||||
return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues<PluginType>()}");
|
||||
|
||||
if(type is PluginType.NONE)
|
||||
return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues<PluginType>()}");
|
||||
|
||||
var isInternal = pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
|
||||
return type switch
|
||||
{
|
||||
PluginType.LANGUAGE => new PluginLanguage(isInternal, state, type),
|
||||
|
||||
_ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
|
||||
};
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
HOT_RELOAD_WATCHER.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
{
|
||||
private readonly Dictionary<string, string> content = [];
|
||||
private readonly List<ILanguagePlugin> otherLanguagePlugins = [];
|
||||
private readonly string langCultureTag;
|
||||
|
||||
private ILanguagePlugin? baseLanguage;
|
||||
|
||||
public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type)
|
||||
{
|
||||
if(!this.TryInitIETFTag(out var issue, out this.langCultureTag))
|
||||
this.pluginIssues.Add(issue);
|
||||
|
||||
if (this.TryInitUITextContent(out issue, out var readContent))
|
||||
this.content = readContent;
|
||||
else
|
||||
this.pluginIssues.Add(issue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the base language plugin. This plugin will be used to fill in missing keys.
|
||||
/// </summary>
|
||||
/// <param name="baseLanguagePlugin">The base language plugin to use.</param>
|
||||
public void SetBaseLanguage(ILanguagePlugin baseLanguagePlugin) => this.baseLanguage = baseLanguagePlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Add another language plugin. This plugin will be used to fill in missing keys.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method to add (i.e., register) an assistant plugin as a language plugin.
|
||||
/// This is necessary because the assistant plugins need to serve their own texts.
|
||||
/// </remarks>
|
||||
/// <param name="languagePlugin">The language plugin to add.</param>
|
||||
public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key neither in the base language nor in this language exist,
|
||||
/// the value will be an empty string. Please note that the key is case-sensitive.
|
||||
/// Furthermore, the keys are in the format "root::key". That means that
|
||||
/// the keys are hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value)
|
||||
{
|
||||
// First, we check if the key is part of the main language pack:
|
||||
if (this.content.TryGetValue(key, out value!))
|
||||
return true;
|
||||
|
||||
// Second, we check if the key is part of the other language packs, such as the assistant plugins:
|
||||
foreach (var otherLanguagePlugin in this.otherLanguagePlugins)
|
||||
if(otherLanguagePlugin.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
// Finally, we check if the key is part of the base language pack. This is the case,
|
||||
// when a language plugin does not cover all keys. In this case, the base language plugin
|
||||
// will be used to fill in the missing keys:
|
||||
if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the IETF tag.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message, when the IETF tag could not be read.</param>
|
||||
/// <param name="readLangCultureTag">The read IETF tag.</param>
|
||||
/// <returns>True, when the IETF tag could be read, false otherwise.</returns>
|
||||
private bool TryInitIETFTag(out string message, out string readLangCultureTag)
|
||||
{
|
||||
if (!this.state.Environment["IETF_TAG"].TryRead(out readLangCultureTag))
|
||||
{
|
||||
message = "The field IETF_TAG does not exist or is not a valid string.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(readLangCultureTag))
|
||||
{
|
||||
message = "The field IETF_TAG is empty. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readLangCultureTag.Length != 5)
|
||||
{
|
||||
message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readLangCultureTag[2] != '-')
|
||||
{
|
||||
message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the first part consists of only lower case letters:
|
||||
for (var i = 0; i < 2; i++)
|
||||
if (!char.IsLower(readLangCultureTag[i]))
|
||||
{
|
||||
message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the second part consists of only upper case letters:
|
||||
for (var i = 3; i < 5; i++)
|
||||
if (!char.IsUpper(readLangCultureTag[i]))
|
||||
{
|
||||
message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code.";
|
||||
readLangCultureTag = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Loads Lua modules from a plugin directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Any plugin can load Lua modules from its own directory. This class is used to load these modules.
|
||||
/// Loading other modules outside the plugin directory is not allowed.
|
||||
/// </remarks>
|
||||
/// <param name="pluginDirectory">The directory where the plugin is located.</param>
|
||||
public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader
|
||||
{
|
||||
private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins");
|
||||
|
||||
#region Implementation of ILuaModuleLoader
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Exists(string moduleName)
|
||||
{
|
||||
// Ensure that the user doesn't try to escape the plugin directory:
|
||||
if (moduleName.Contains("..") || pluginDirectory.Contains(".."))
|
||||
return false;
|
||||
|
||||
// Ensure that the plugin directory is nested in the plugin base path:
|
||||
if (!pluginDirectory.StartsWith(PLUGIN_BASE_PATH, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
|
||||
var code = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
|
||||
|
||||
return new(moduleName, code);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
|
||||
{
|
||||
#region Implementation of IPluginMetadata
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IconSVG { get; } = plugin.IconSVG;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginType Type { get; } = plugin.Type;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; } = plugin.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; } = plugin.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description { get; } = plugin.Description;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginVersion Version { get; } = plugin.Version;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string[] Authors { get; } = plugin.Authors;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SupportContact { get; } = plugin.SupportContact;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SourceURL { get; } = plugin.SourceURL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginCategory[] Categories { get; } = plugin.Categories;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginTargetGroup[] TargetGroups { get; } = plugin.TargetGroups;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMaintained { get; } = plugin.IsMaintained;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DeprecationMessage { get; } = plugin.DeprecationMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsInternal { get; } = plugin.IsInternal;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public enum PluginTargetGroup
|
||||
{
|
||||
NONE,
|
||||
|
||||
EVERYONE,
|
||||
CHILDREN,
|
||||
TEENAGERS,
|
||||
STUDENTS,
|
||||
ADULTS,
|
||||
|
||||
INDUSTRIAL_WORKERS,
|
||||
OFFICE_WORKERS,
|
||||
BUSINESS_PROFESSIONALS,
|
||||
SOFTWARE_DEVELOPERS,
|
||||
SCIENTISTS,
|
||||
TEACHERS,
|
||||
ARTISTS,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class PluginTargetGroupExtensions
|
||||
{
|
||||
public static string Name(this PluginTargetGroup group) => group switch
|
||||
{
|
||||
PluginTargetGroup.NONE => "No target group",
|
||||
|
||||
PluginTargetGroup.EVERYONE => "Everyone",
|
||||
PluginTargetGroup.CHILDREN => "Children",
|
||||
PluginTargetGroup.TEENAGERS => "Teenagers",
|
||||
PluginTargetGroup.STUDENTS => "Students",
|
||||
PluginTargetGroup.ADULTS => "Adults",
|
||||
|
||||
PluginTargetGroup.INDUSTRIAL_WORKERS => "Industrial workers",
|
||||
PluginTargetGroup.OFFICE_WORKERS => "Office workers",
|
||||
PluginTargetGroup.BUSINESS_PROFESSIONALS => "Business professionals",
|
||||
PluginTargetGroup.SOFTWARE_DEVELOPERS => "Software developers",
|
||||
PluginTargetGroup.SCIENTISTS => "Scientists",
|
||||
PluginTargetGroup.TEACHERS => "Teachers",
|
||||
PluginTargetGroup.ARTISTS => "Artists",
|
||||
|
||||
_ => "Unknown target group",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public enum PluginType
|
||||
{
|
||||
NONE,
|
||||
|
||||
LANGUAGE,
|
||||
ASSISTANT,
|
||||
CONFIGURATION,
|
||||
THEME,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class PluginTypeExtensions
|
||||
{
|
||||
public static string GetName(this PluginType type) => type switch
|
||||
{
|
||||
PluginType.LANGUAGE => "Language plugin",
|
||||
PluginType.ASSISTANT => "Assistant plugin",
|
||||
PluginType.CONFIGURATION => "Configuration plugin",
|
||||
PluginType.THEME => "Theme plugin",
|
||||
|
||||
_ => "Unknown plugin type",
|
||||
};
|
||||
|
||||
public static string GetDirectory(this PluginType type) => type switch
|
||||
{
|
||||
PluginType.LANGUAGE => "languages",
|
||||
PluginType.ASSISTANT => "assistants",
|
||||
PluginType.CONFIGURATION => "configurations",
|
||||
PluginType.THEME => "themes",
|
||||
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a version number for a plugin.
|
||||
/// </summary>
|
||||
/// <param name="Major">The major version number.</param>
|
||||
/// <param name="Minor">The minor version number.</param>
|
||||
/// <param name="Patch">The patch version number.</param>
|
||||
public readonly record struct PluginVersion(int Major, int Minor, int Patch) : IComparable<PluginVersion>
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents no version number.
|
||||
/// </summary>
|
||||
public static readonly PluginVersion NONE = new(0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the input string as a plugin version number.
|
||||
/// </summary>
|
||||
/// <param name="input">The input string to parse.</param>
|
||||
/// <param name="version">The parsed version number.</param>
|
||||
/// <returns>True when the input string was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string input, out PluginVersion version)
|
||||
{
|
||||
try
|
||||
{
|
||||
version = Parse(input);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
version = NONE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the input string as a plugin version number.
|
||||
/// </summary>
|
||||
/// <param name="input">The input string to parse.</param>
|
||||
/// <returns>The parsed version number.</returns>
|
||||
/// <exception cref="FormatException">The input string is not in the correct format.</exception>
|
||||
public static PluginVersion Parse(string input)
|
||||
{
|
||||
var segments = input.Split('.');
|
||||
if (segments.Length != 3)
|
||||
throw new FormatException("The input string must be in the format 'major.minor.patch'.");
|
||||
|
||||
var major = int.Parse(segments[0]);
|
||||
var minor = int.Parse(segments[1]);
|
||||
var patch = int.Parse(segments[2]);
|
||||
|
||||
if(major < 0 || minor < 0 || patch < 0)
|
||||
throw new FormatException("The major, minor, and patch numbers must be greater than or equal to 0.");
|
||||
|
||||
return new PluginVersion(major, minor, patch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the plugin version number to a string in the format 'major.minor.patch'.
|
||||
/// </summary>
|
||||
/// <returns>The plugin version number as a string.</returns>
|
||||
public override string ToString() => $"{this.Major}.{this.Minor}.{this.Patch}";
|
||||
|
||||
/// <summary>
|
||||
/// Compares the plugin version number to another plugin version number.
|
||||
/// </summary>
|
||||
/// <param name="other">The other plugin version number to compare to.</param>
|
||||
/// <returns>A value indicating the relative order of the plugin version numbers.</returns>
|
||||
public int CompareTo(PluginVersion other)
|
||||
{
|
||||
var majorCompare = this.Major.CompareTo(other.Major);
|
||||
if (majorCompare != 0)
|
||||
return majorCompare;
|
||||
|
||||
var minorCompare = this.Minor.CompareTo(other.Minor);
|
||||
if (minorCompare != 0)
|
||||
return minorCompare;
|
||||
|
||||
return this.Patch.CompareTo(other.Patch);
|
||||
}
|
||||
|
||||
public static bool operator >(PluginVersion left, PluginVersion right) => left.CompareTo(right) > 0;
|
||||
|
||||
public static bool operator <(PluginVersion left, PluginVersion right) => left.CompareTo(right) < 0;
|
||||
|
||||
public static bool operator >=(PluginVersion left, PluginVersion right) => left.CompareTo(right) >= 0;
|
||||
|
||||
public static bool operator <=(PluginVersion left, PluginVersion right) => left.CompareTo(right) <= 0;
|
||||
}
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user