Compare commits

..

No commits in common. "main" and "v0.9.32" have entirely different histories.

850 changed files with 3174 additions and 16460 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
github: [MindWorkAI] github: [MindWorkAI]
open_collective: mindwork-ai

View File

@ -1,8 +1,6 @@
name: Build and Release name: Build and Release
on: on:
push: push:
branches:
- main
tags: tags:
- "v*.*.*" - "v*.*.*"
@ -48,7 +46,6 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT" echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Check tag vs. metadata version - name: Check tag vs. metadata version
if: startsWith(github.ref, 'refs/tags/v')
run: | run: |
# Ensure, that the tag matches the version in the metadata file: # Ensure, that the tag matches the version in the metadata file:
if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then
@ -103,12 +100,6 @@ jobs:
dotnet_name_postfix: '-x86_64-unknown-linux-gnu' dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
tauri_bundle: 'appimage deb updater' 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: 'appimage deb updater'
- platform: 'windows-latest' # for x86-based Windows - platform: 'windows-latest' # for x86-based Windows
rust_target: 'x86_64-pc-windows-msvc' rust_target: 'x86_64-pc-windows-msvc'
dotnet_runtime: 'win-x64' dotnet_runtime: 'win-x64'
@ -160,13 +151,6 @@ jobs:
# Format the app version: # Format the app version:
formatted_app_version="v${app_version}" formatted_app_version="v${app_version}"
# Set the architecture:
if sed --version 2>/dev/null | grep -q GNU; then
sed -i "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
else
sed -i '' "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
fi
# Write the metadata to the environment: # Write the metadata to the environment:
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
@ -177,7 +161,6 @@ jobs:
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
# Log the metadata: # Log the metadata:
echo "App version: '${formatted_app_version}'" echo "App version: '${formatted_app_version}'"
@ -188,7 +171,6 @@ jobs:
echo "Rust version: '${rust_version}'" echo "Rust version: '${rust_version}'"
echo "MudBlazor version: '${mud_blazor_version}'" echo "MudBlazor version: '${mud_blazor_version}'"
echo "Tauri version: '${tauri_version}'" echo "Tauri version: '${tauri_version}'"
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
- name: Read and format metadata (Windows) - name: Read and format metadata (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
@ -221,12 +203,6 @@ jobs:
# Format the app version: # Format the app version:
$formatted_app_version = "v${app_version}" $formatted_app_version = "v${app_version}"
# Set the architecture:
$metadata[9] = "${{ matrix.dotnet_runtime }}"
# Write the changed metadata back to the file:
Set-Content -Path metadata.txt -Value $metadata
# Write the metadata to the environment: # Write the metadata to the environment:
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV
@ -236,7 +212,6 @@ jobs:
Write-Output "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $env:GITHUB_ENV Write-Output "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $env:GITHUB_ENV
Write-Output "RUST_VERSION=${rust_version}" >> $env:GITHUB_ENV Write-Output "RUST_VERSION=${rust_version}" >> $env:GITHUB_ENV
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
# Log the metadata: # Log the metadata:
Write-Output "App version: '${formatted_app_version}'" Write-Output "App version: '${formatted_app_version}'"
@ -247,7 +222,6 @@ jobs:
Write-Output "Rust version: '${rust_version}'" Write-Output "Rust version: '${rust_version}'"
Write-Output "MudBlazor version: '${mud_blazor_version}'" Write-Output "MudBlazor version: '${mud_blazor_version}'"
Write-Output "Tauri version: '${tauri_version}'" Write-Output "Tauri version: '${tauri_version}'"
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
@ -300,12 +274,6 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 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) - name: Setup Tauri (Unix)
if: matrix.platform != 'windows-latest' if: matrix.platform != 'windows-latest'
run: | run: |
@ -324,35 +292,6 @@ jobs:
Write-Output "Tauri is already installed" Write-Output "Tauri is already installed"
} }
- name: Delete previous artifact, which may exist due to caching (macOS)
if: startsWith(matrix.platform, 'macos')
run: |
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
- 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" -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" -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')
run: |
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
run: |
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
- name: Build Tauri project (Unix) - name: Build Tauri project (Unix)
if: matrix.platform != 'windows-latest' if: matrix.platform != 'windows-latest'
env: env:
@ -376,7 +315,7 @@ jobs:
cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
- name: Upload artifact (macOS) - name: Upload artifact (macOS)
if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'macos')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
@ -387,7 +326,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Windows - MSI) - name: Upload artifact (Windows - MSI)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
@ -398,7 +337,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Windows - NSIS) - name: Upload artifact (Windows - NSIS)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
@ -409,7 +348,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Linux - Debian Package) - name: Upload artifact (Linux - Debian Package)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
@ -419,7 +358,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Linux - AppImage) - name: Upload artifact (Linux - AppImage)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
@ -429,11 +368,204 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} 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
- 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: |
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' }}
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: create_release:
name: Prepare & create release name: Prepare & create release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_main, read_metadata] needs: [build_main, read_metadata, build_linux_arm64]
if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Create artifact directory - name: Create artifact directory
run: mkdir -p $GITHUB_WORKSPACE/artifacts run: mkdir -p $GITHUB_WORKSPACE/artifacts
@ -506,7 +638,6 @@ jobs:
# - platform=darwin-aarch64 when path contains 'aarch64-apple-darwin' # - platform=darwin-aarch64 when path contains 'aarch64-apple-darwin'
# - platform=darwin-x86_64 when path contains 'x86_64-apple-darwin' # - platform=darwin-x86_64 when path contains 'x86_64-apple-darwin'
# - platform=linux-x86_64 when path contains 'x86_64-unknown-linux-' # - platform=linux-x86_64 when path contains 'x86_64-unknown-linux-'
# - platform=linux-aarch64 when path contains 'aarch64-unknown-linux-'
# - platform=windows-x86_64 when path contains 'x86_64-pc-windows-' # - platform=windows-x86_64 when path contains 'x86_64-pc-windows-'
# - platform=windows-aarch64 when path contains 'aarch64-pc-windows-' # - platform=windows-aarch64 when path contains 'aarch64-pc-windows-'
# #
@ -516,8 +647,6 @@ jobs:
platform="darwin-x86_64" platform="darwin-x86_64"
elif [[ "$sig_file" == *"amd64.AppImage"* ]]; then elif [[ "$sig_file" == *"amd64.AppImage"* ]]; then
platform="linux-x86_64" platform="linux-x86_64"
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
platform="linux-aarch64"
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
platform="windows-x86_64" platform="windows-x86_64"
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
@ -594,7 +723,6 @@ jobs:
name: Publish release name: Publish release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [read_metadata, create_release] needs: [read_metadata, create_release]
if: startsWith(github.ref, 'refs/tags/v')
permissions: permissions:
contents: write contents: write

View File

@ -13,9 +13,8 @@ Things we are currently working on:
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ - [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: 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))~~ - [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))~~
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~ - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
- [ ] App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393))
- [ ] App: Implement external embedding providers - [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings - [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb) - [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
@ -25,37 +24,26 @@ Things we are currently working on:
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~ - [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[PR #167](https://github.com/MindWorkAI/AI-Studio/pull/167), [PR #226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~, [PR #376](https://github.com/MindWorkAI/AI-Studio/pull/376). - Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
- Since March 2025: We have started developing the plugin system. There will be language plugins to offer AI Studio in other languages, configuration plugins to centrally manage certain providers and rules within an organization, and assistant plugins that allow anyone to develop their own assistants. We are using Lua as the plugin language:
- [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~
- [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~
- [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~
- [ ] Add support for other languages (I18N) to AI Studio (~~[PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429))~~
- [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~
- [ ] Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430))
- [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations
- [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform.
- [ ] Add assistant plugins
Other News: Other News:
- April 2025: We have two active financial supporters: Peer `peerschuett` and Dominic `donework`. Thank you very much for your support. MindWork AI reinvests these donations by passing them on to our AI Studio dependencies ([see here](https://github.com/orgs/MindWorkAI/sponsoring)). In the event that we receive large donations, we will first sign the app ([#56](https://github.com/MindWorkAI/Planning/issues/56)). In case we receive more donations, we will look for and pay staff to develop features for AI Studio. - October 2024: We've found the first two financial supporters. Huge thanks to `richard-stanton` and `peerschuett` for backing the project. Thanks for having the courage to be the first to support us.
- April 2025: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of three projects and will also contribute to its further development. This is great news. - October 2024: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of one project and will also contribute to its further development. This is great news.
Features we have recently released: Features we have recently released:
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
- v0.9.39: Added the plugin system as a preview feature.
- v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well.
- v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process)
- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature.
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content. - v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio. - v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview). - v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options. - v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
- v0.9.16: Added workspace display options & improved the layout of the app window.
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
- v0.9.13: You can use `ollama` providers secured with API keys.
## What is AI Studio? ## What is AI Studio?
@ -68,19 +56,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
**Key advantages:** **Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes. - **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support: - **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
- [OpenAI](https://openai.com/) (GPT4o, GPT4.1, o1, o3, o4, etc.)
- [Mistral](https://mistral.ai/)
- [Anthropic](https://www.anthropic.com/) (Claude)
- [Google Gemini](https://gemini.google.com)
- [xAI](https://x.ai/) (Grok)
- [DeepSeek](https://www.deepseek.com/en)
- [Alibaba Cloud](https://www.alibabacloud.com) (Qwen)
- [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face
- Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/)
- [Groq](https://groq.com/)
- [Fireworks](https://fireworks.ai/)
- For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants. - **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API. - **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. - **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.

View File

@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Collect I18N content" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="dotnet run collect-i18n" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/Build" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/Build" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/opt/homebrew/bin/nu" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -12,8 +12,6 @@
<option name="EXECUTE_IN_TERMINAL" value="false" /> <option name="EXECUTE_IN_TERMINAL" value="false" />
<option name="EXECUTE_SCRIPT_FILE" value="false" /> <option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs /> <envs />
<method v="2"> <method v="2" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
</method>
</configuration> </configuration>
</component> </component>

View File

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Build</RootNamespace>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>build</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cocona" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
</ItemGroup>
</Project>

View File

@ -1,3 +0,0 @@
namespace Build.Commands;
public record AppVersion(string VersionText, int Major, int Minor, int Patch);

View File

@ -1,21 +0,0 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
namespace Build.Commands;
public sealed class CheckRidsCommand
{
[Command("check-rids", Description = "Check the RIDs for the current OS")]
public void GetRids()
{
if(!Environment.IsWorkingDirectoryValid())
return;
var rids = Environment.GetRidsForCurrentOS();
Console.WriteLine("The following RIDs are available for the current OS:");
foreach (var rid in rids)
{
Console.WriteLine($"- {rid}");
}
}
}

View File

@ -1,272 +0,0 @@
using System.Text.RegularExpressions;
using SharedTools;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
namespace Build.Commands;
public sealed partial class CollectI18NKeysCommand
{
[Command("collect-i18n", Description = "Collect I18N keys")]
public async Task CollectI18NKeys()
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("=========================");
Console.Write("- Collecting I18N keys ...");
var cwd = Environment.GetAIStudioDirectory();
var binPath = Path.Join(cwd, "bin");
var objPath = Path.Join(cwd, "obj");
var wwwrootPath = Path.Join(cwd, "wwwroot");
var allFiles = Directory.EnumerateFiles(cwd, "*", SearchOption.AllDirectories);
var counter = 0;
var allI18NContent = new Dictionary<string, string>();
foreach (var filePath in allFiles)
{
counter++;
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
continue;
if(filePath.StartsWith(objPath, StringComparison.OrdinalIgnoreCase))
continue;
if(filePath.StartsWith(wwwrootPath, StringComparison.OrdinalIgnoreCase))
continue;
var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
var matches = this.FindAllTextTags(content);
if (matches.Count == 0)
continue;
var ns = this.DetermineNamespace(filePath);
var fileInfo = new FileInfo(filePath);
var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty).Replace(".razor", string.Empty);
var langNamespace = $"{ns}.{name}".ToUpperInvariant();
foreach (var match in matches)
{
// The key in the format A.B.C.D.T{hash}:
var key = $"UI_TEXT_CONTENT.{langNamespace}.T{match.ToFNV32()}";
allI18NContent.TryAdd(key, match);
}
}
Console.WriteLine($" {counter:###,###} files processed, {allI18NContent.Count:###,###} keys found.");
Console.Write("- Creating Lua code ...");
var luaCode = this.ExportToLuaAssignments(allI18NContent);
// Build the path, where we want to store the Lua code:
var luaPath = Path.Join(cwd, "Assistants", "I18N", "allTexts.lua");
// Store the Lua code:
await File.WriteAllTextAsync(luaPath, luaCode, Encoding.UTF8);
Console.WriteLine(" done.");
}
private string ExportToLuaAssignments(Dictionary<string, string> keyValuePairs)
{
var sb = new StringBuilder();
// Add the mandatory plugin metadata:
sb.AppendLine(
"""
-- The ID for this plugin:
ID = "77c2688a-a68f-45cc-820e-fa8f3038a146"
-- The icon for the plugin:
ICON_SVG = ""
-- The name of the plugin:
NAME = "Collected I18N keys"
-- The description of the plugin:
DESCRIPTION = "This plugin is not meant to be used directly. Its a collection of all I18N keys found in the project."
-- 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"
-- The language name in the user's language:
LANG_NAME = "English (United States)"
"""
);
// Add the UI_TEXT_CONTENT table:
LuaTable.Create(ref sb, "UI_TEXT_CONTENT", keyValuePairs);
return sb.ToString();
}
private List<string> FindAllTextTags(ReadOnlySpan<char> fileContent)
{
const string START_TAG1 = """
T("
""";
const string START_TAG2 = """
TB("
""";
const string END_TAG = """
")
""";
(int Index, int Len) FindNextStart(ReadOnlySpan<char> content)
{
var startIdx1 = content.IndexOf(START_TAG1);
var startIdx2 = content.IndexOf(START_TAG2);
if (startIdx1 == -1 && startIdx2 == -1)
return (-1, 0);
if (startIdx1 == -1)
return (startIdx2, START_TAG2.Length);
if (startIdx2 == -1)
return (startIdx1, START_TAG1.Length);
if (startIdx1 < startIdx2)
return (startIdx1, START_TAG1.Length);
return (startIdx2, START_TAG2.Length);
}
var matches = new List<string>();
var startIdx = FindNextStart(fileContent);
var content = fileContent;
while (startIdx.Index > -1)
{
//
// In some cases, after the initial " there follow more " characters.
// We need to skip them:
//
content = content[(startIdx.Index + startIdx.Len)..];
while(content[0] == '"')
content = content[1..];
var endIdx = content.IndexOf(END_TAG);
if (endIdx == -1)
break;
var match = content[..endIdx];
while (match[^1] == '"')
match = match[..^1];
matches.Add(match.ToString());
startIdx = FindNextStart(content);
}
return matches;
}
private string? DetermineNamespace(string filePath)
{
// Is it a C# file? Then we can read the namespace from it:
if (filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
return this.ReadNamespaceFromCSharp(filePath);
// Is it a Razor file? Then, it depends:
if (filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
{
// Check if the file contains a namespace declaration:
var blazorNamespace = this.ReadNamespaceFromRazor(filePath);
if (blazorNamespace != null)
return blazorNamespace;
// Alright, no namespace declaration. Let's check the corresponding C# file:
var csFilePath = $"{filePath}.cs";
if (File.Exists(csFilePath))
{
var csNamespace = this.ReadNamespaceFromCSharp(csFilePath);
if (csNamespace != null)
return csNamespace;
Console.WriteLine($"- Error: Neither the blazor file '{filePath}' nor the corresponding C# file '{csFilePath}' contain a namespace declaration.");
return null;
}
Console.WriteLine($"- Error: The blazor file '{filePath}' does not contain a namespace declaration and the corresponding C# file '{csFilePath}' does not exist.");
return null;
}
// Not a C# or Razor file. We can't determine the namespace:
Console.WriteLine($"- Error: The file '{filePath}' is neither a C# nor a Razor file. We can't determine the namespace.");
return null;
}
private string? ReadNamespaceFromCSharp(string filePath)
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
var matches = CSharpNamespaceRegex().Matches(content);
if (matches.Count == 0)
return null;
if (matches.Count > 1)
{
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
return null;
}
var match = matches[0];
return match.Groups[1].Value;
}
private string? ReadNamespaceFromRazor(string filePath)
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
var matches = BlazorNamespaceRegex().Matches(content);
if (matches.Count == 0)
return null;
if (matches.Count > 1)
{
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
return null;
}
var match = matches[0];
return match.Groups[1].Value;
}
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex BlazorNamespaceRegex();
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex CSharpNamespaceRegex();
}

View File

@ -1,10 +0,0 @@
namespace Build.Commands;
public enum PrepareAction
{
NONE,
PATCH,
MINOR,
MAJOR,
}

View File

@ -1,651 +0,0 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using SharedTools;
namespace Build.Commands;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
public sealed partial class UpdateMetadataCommands
{
[Command("release", Description = "Prepare & build the next release")]
public async Task Release(PrepareAction action)
{
if(!Environment.IsWorkingDirectoryValid())
return;
// Prepare the metadata for the next release:
await this.PerformPrepare(action, true);
// Build once to allow the Rust compiler to read the changed metadata
// and to update all .NET artifacts:
await this.Build();
// Now, we update the web assets (which may were updated by the first build):
new UpdateWebAssetsCommand().UpdateWebAssets();
// Collect the I18N keys from the source code. This step yields a I18N file
// that must be part of the final release:
await new CollectI18NKeysCommand().CollectI18NKeys();
// Build the final release, where Rust knows the updated metadata, the .NET
// artifacts are already in place, and .NET knows the updated web assets, etc.:
await this.Build();
}
[Command("update-versions", Description = "The command will update the package versions in the metadata file")]
public async Task UpdateVersions()
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("==============================");
Console.WriteLine("- Update the main package versions ...");
await this.UpdateDotnetVersion();
await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion();
}
[Command("prepare", Description = "Prepare the metadata for the next release")]
public async Task Prepare(PrepareAction action)
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("==============================");
Console.Write("- Are you trying to prepare a new release? (y/n) ");
var userAnswer = Console.ReadLine();
if (userAnswer?.ToLowerInvariant() == "y")
{
Console.WriteLine("- Please use the 'release' command instead");
return;
}
await this.PerformPrepare(action, false);
}
private async Task PerformPrepare(PrepareAction action, bool internalCall)
{
if(internalCall)
Console.WriteLine("==============================");
Console.WriteLine("- Prepare the metadata for the next release ...");
var appVersion = await this.UpdateAppVersion(action);
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
{
var buildNumber = await this.IncreaseBuildNumber();
var buildTime = await this.UpdateBuildTime();
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
await this.CreateNextChangelog(buildNumber, appVersion);
await this.UpdateDotnetVersion();
await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion();
await this.UpdateProjectCommitHash();
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "About.razor.cs")));
Console.WriteLine();
}
}
[Command("build", Description = "Build MindWork AI Studio")]
public async Task Build()
{
if(!Environment.IsWorkingDirectoryValid())
return;
//
// Build the .NET project:
//
var pathApp = Environment.GetAIStudioDirectory();
var rids = Environment.GetRidsForCurrentOS();
foreach (var rid in rids)
{
Console.WriteLine("==============================");
await this.UpdateArchitecture(rid);
Console.Write($"- Start .NET build for '{rid.AsMicrosoftRid()}' ...");
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
var dotnetBuildOutput = await this.ReadCommandOutput(pathApp, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --disable-build-servers --force");
var dotnetBuildOutputLines = dotnetBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundIssue = false;
foreach (var buildOutputLine in dotnetBuildOutputLines)
{
if(buildOutputLine.Contains(" error ") || buildOutputLine.Contains("#warning"))
{
if(!foundIssue)
{
foundIssue = true;
Console.WriteLine();
Console.WriteLine("- Build has issues:");
}
Console.Write(" - ");
Console.WriteLine(buildOutputLine);
}
}
if(foundIssue)
Console.WriteLine();
else
{
Console.WriteLine(" completed successfully.");
}
//
// Prepare the .NET artifact to be used by Tauri as sidecar:
//
var os = Environment.GetOS();
var tauriSidecarArtifactName = rid switch
{
RID.WIN_X64 => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
RID.WIN_ARM64 => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
RID.LINUX_X64 => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
RID.LINUX_ARM64 => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
RID.OSX_ARM64 => "mindworkAIStudioServer-aarch64-apple-darwin",
RID.OSX_X64 => "mindworkAIStudioServer-x86_64-apple-darwin",
_ => string.Empty,
};
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
{
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
return;
}
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
if(!Directory.Exists(dotnetArtifactPath))
Directory.CreateDirectory(dotnetArtifactPath);
var dotnetArtifactFilename = os switch
{
"windows" => "mindworkAIStudio.exe",
_ => "mindworkAIStudio",
};
var dotnetPublishedPath = Path.Combine(pathApp, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", dotnetArtifactFilename);
var finalDestination = Path.Combine(dotnetArtifactPath, tauriSidecarArtifactName);
if(File.Exists(dotnetPublishedPath))
Console.WriteLine("- Published .NET artifact found.");
else
{
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
return;
}
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
try
{
File.Move(dotnetPublishedPath, finalDestination, true);
Console.WriteLine(" done.");
}
catch (Exception e)
{
Console.WriteLine(" failed.");
Console.WriteLine($" - Error: {e.Message}");
}
Console.WriteLine();
}
//
// Build the Rust project / runtime:
//
Console.WriteLine("==============================");
Console.WriteLine("- Start building the Rust runtime ...");
var pathRuntime = Environment.GetRustRuntimeDirectory();
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundRustIssue = false;
foreach (var buildOutputLine in rustBuildOutputLines)
{
if(buildOutputLine.Contains("error", StringComparison.OrdinalIgnoreCase) || buildOutputLine.Contains("warning"))
{
if(!foundRustIssue)
{
foundRustIssue = true;
Console.WriteLine();
Console.WriteLine("- Build has issues:");
}
Console.Write(" - ");
Console.WriteLine(buildOutputLine);
}
}
if(foundRustIssue)
Console.WriteLine();
else
{
Console.WriteLine();
Console.WriteLine("- Compilation completed successfully.");
Console.WriteLine();
}
}
private async Task CreateNextChangelog(int currentBuildNumber, AppVersion currentAppVersion)
{
Console.Write("- Create the next changelog ...");
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
var nextBuildNumber = currentBuildNumber + 1;
//
// We assume that most of the time, there will be patch releases:
//
var nextMajor = currentAppVersion.Major;
var nextMinor = currentAppVersion.Minor;
var nextPatch = currentAppVersion.Patch + 1;
var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}";
var nextChangelogFilename = $"v{nextAppVersion}.md";
var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename);
//
// Regarding the next build time: We assume that the next release will take place in one week from now.
// Thus, we check how many days this month has left. In the end, we want to predict the year and month
// for the next build. Day, hour, minute and second are all set to x.
//
var nextBuildMonth = (DateTime.Today + TimeSpan.FromDays(7)).Month;
var nextBuildYear = (DateTime.Today + TimeSpan.FromDays(7)).Year;
var nextBuildTimeString = $"{nextBuildYear}-{nextBuildMonth:00}-xx xx:xx UTC";
var changelogHeader = $"""
# v{nextAppVersion}, build {nextBuildNumber} ({nextBuildTimeString})
""";
if(!File.Exists(nextChangelogFilePath))
{
await File.WriteAllTextAsync(nextChangelogFilePath, changelogHeader, Environment.UTF8_NO_BOM);
Console.WriteLine($" done. Changelog '{nextChangelogFilename}' created.");
}
else
{
Console.WriteLine(" failed.");
Console.WriteLine("- Error: The changelog file already exists.");
}
}
private async Task UpdateChangelog(int buildNumber, string appVersion, string buildTime)
{
Console.Write("- Updating the in-app changelog list ...");
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
var expectedLogFilename = $"v{appVersion}.md";
var expectedLogFilePath = Path.Combine(pathChangelogs, expectedLogFilename);
if(!File.Exists(expectedLogFilePath))
{
Console.WriteLine(" failed.");
Console.WriteLine($"- Error: The changelog file '{expectedLogFilename}' does not exist.");
return;
}
// Right now, the build time is formatted as "yyyy-MM-dd HH:mm:ss UTC", but must remove the seconds:
buildTime = buildTime[..^7] + " UTC";
const string CODE_START =
"""
LOGS =
[
""";
var changelogCodePath = Path.Join(Environment.GetAIStudioDirectory(), "Components", "Changelog.Logs.cs");
var changelogCode = await File.ReadAllTextAsync(changelogCodePath, Encoding.UTF8);
var updatedCode =
$"""
{CODE_START}
new ({buildNumber}, "v{appVersion}, build {buildNumber} ({buildTime})", "{expectedLogFilename}"),
""";
changelogCode = changelogCode.Replace(CODE_START, updatedCode);
await File.WriteAllTextAsync(changelogCodePath, changelogCode, Environment.UTF8_NO_BOM);
Console.WriteLine(" done.");
}
private async Task UpdateArchitecture(RID rid)
{
const int ARCHITECTURE_INDEX = 9;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
Console.Write("- Updating architecture ...");
lines[ARCHITECTURE_INDEX] = rid.AsMicrosoftRid();
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
Console.WriteLine(" done.");
}
private async Task UpdateProjectCommitHash()
{
const int COMMIT_HASH_INDEX = 8;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentCommitHash = lines[COMMIT_HASH_INDEX].Trim();
var headCommitHash = await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD");
var first10Chars = headCommitHash[..11];
var updatedCommitHash = $"{first10Chars}, release";
Console.WriteLine($"- Updating commit hash from '{currentCommitHash}' to '{updatedCommitHash}'.");
lines[COMMIT_HASH_INDEX] = updatedCommitHash;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task<AppVersion> UpdateAppVersion(PrepareAction action)
{
const int APP_VERSION_INDEX = 0;
if (action == PrepareAction.NONE)
{
Console.WriteLine("- No action specified. Skipping app version update.");
return new(string.Empty, 0, 0, 0);
}
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim();
var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value);
switch (action)
{
case PrepareAction.PATCH:
currentPatch++;
break;
case PrepareAction.MINOR:
currentPatch = 0;
currentMinor++;
break;
case PrepareAction.MAJOR:
currentPatch = 0;
currentMinor = 0;
currentMajor++;
break;
}
var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}";
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
lines[APP_VERSION_INDEX] = updatedAppVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return new(updatedAppVersion, currentMajor, currentMinor, currentPatch);
}
private async Task UpdateLicenceYear(string licenceFilePath)
{
var currentYear = DateTime.UtcNow.Year.ToString();
var lines = await File.ReadAllLinesAsync(licenceFilePath, Encoding.UTF8);
var found = false;
var copyrightYear = string.Empty;
var updatedLines = new List<string>(lines.Length);
foreach (var line in lines)
{
var match = FindCopyrightRegex().Match(line);
if (match.Success)
{
copyrightYear = match.Groups["year"].Value;
if(!found && copyrightYear != currentYear)
Console.WriteLine($"- Updating the licence's year in '{Path.GetFileName(licenceFilePath)}' from '{copyrightYear}' to '{currentYear}'.");
updatedLines.Add(ReplaceCopyrightYearRegex().Replace(line, currentYear));
found = true;
}
else
updatedLines.Add(line);
}
await File.WriteAllLinesAsync(licenceFilePath, updatedLines, Environment.UTF8_NO_BOM);
if (!found)
Console.WriteLine($"- Error: No copyright year found in '{Path.GetFileName(licenceFilePath)}'.");
else if (copyrightYear == currentYear)
Console.WriteLine($"- The copyright year in '{Path.GetFileName(licenceFilePath)}' is already up to date.");
}
private async Task UpdateTauriVersion()
{
const int TAURI_VERSION_INDEX = 7;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentTauriVersion = lines[TAURI_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Tauri", Environment.GetRustRuntimeDirectory(), TauriVersionRegex(), "cargo", "tree --depth 1");
if (matches.Count == 0)
return;
var updatedTauriVersion = matches[0].Groups["version"].Value;
if(currentTauriVersion == updatedTauriVersion)
{
Console.WriteLine("- The Tauri version is already up to date.");
return;
}
Console.WriteLine($"- Updated Tauri version from {currentTauriVersion} to {updatedTauriVersion}.");
lines[TAURI_VERSION_INDEX] = updatedTauriVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateMudBlazorVersion()
{
const int MUD_BLAZOR_VERSION_INDEX = 6;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentMudBlazorVersion = lines[MUD_BLAZOR_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("MudBlazor", Environment.GetAIStudioDirectory(), MudBlazorVersionRegex(), "dotnet", "list package");
if (matches.Count == 0)
return;
var updatedMudBlazorVersion = matches[0].Groups["version"].Value;
if(currentMudBlazorVersion == updatedMudBlazorVersion)
{
Console.WriteLine("- The MudBlazor version is already up to date.");
return;
}
Console.WriteLine($"- Updated MudBlazor version from {currentMudBlazorVersion} to {updatedMudBlazorVersion}.");
lines[MUD_BLAZOR_VERSION_INDEX] = updatedMudBlazorVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateRustVersion()
{
const int RUST_VERSION_INDEX = 5;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentRustVersion = lines[RUST_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Rust", Environment.GetRustRuntimeDirectory(), RustVersionRegex(), "rustc", "-Vv");
if (matches.Count == 0)
return;
var updatedRustVersion = matches[0].Groups["version"].Value + " (commit " + matches[0].Groups["commit"].Value + ")";
if(currentRustVersion == updatedRustVersion)
{
Console.WriteLine("- Rust version is already up to date.");
return;
}
Console.WriteLine($"- Updated Rust version from {currentRustVersion} to {updatedRustVersion}.");
lines[RUST_VERSION_INDEX] = updatedRustVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateDotnetVersion()
{
const int DOTNET_VERSION_INDEX = 4;
const int DOTNET_SDK_VERSION_INDEX = 3;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentDotnetVersion = lines[DOTNET_VERSION_INDEX].Trim();
var currentDotnetSdkVersion = lines[DOTNET_SDK_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion(".NET", Environment.GetAIStudioDirectory(), DotnetVersionRegex(), "dotnet", "--info");
if (matches.Count == 0)
return;
var updatedDotnetVersion = matches[0].Groups["hostVersion"].Value + " (commit " + matches[0].Groups["hostCommit"].Value + ")";
var updatedDotnetSdkVersion = matches[0].Groups["sdkVersion"].Value + " (commit " + matches[0].Groups["sdkCommit"].Value + ")";
if(currentDotnetVersion == updatedDotnetVersion && currentDotnetSdkVersion == updatedDotnetSdkVersion)
{
Console.WriteLine("- .NET version is already up to date.");
return;
}
Console.WriteLine($"- Updated .NET SDK version from {currentDotnetSdkVersion} to {updatedDotnetSdkVersion}.");
Console.WriteLine($"- Updated .NET version from {currentDotnetVersion} to {updatedDotnetVersion}.");
lines[DOTNET_VERSION_INDEX] = updatedDotnetVersion;
lines[DOTNET_SDK_VERSION_INDEX] = updatedDotnetSdkVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task<IList<Match>> DetermineVersion(string name, string workingDirectory, Regex regex, string program, string command)
{
var processInfo = new ProcessStartInfo
{
WorkingDirectory = workingDirectory,
FileName = program,
Arguments = command,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process();
process.StartInfo = processInfo;
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
var matches = regex.Matches(output);
if (matches.Count == 0)
{
Console.WriteLine($"- Error: Was not able to determine the {name} version.");
return [];
}
return matches;
}
private async Task<string> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
{
var processInfo = new ProcessStartInfo
{
WorkingDirectory = workingDirectory,
FileName = program,
Arguments = command,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var sb = new StringBuilder();
using var process = new Process();
process.StartInfo = processInfo;
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.OutputDataReceived += (_, args) =>
{
if(!string.IsNullOrWhiteSpace(args.Data))
{
if(showLiveOutput)
Console.WriteLine(args.Data);
sb.AppendLine(args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if(!string.IsNullOrWhiteSpace(args.Data))
{
if(showLiveOutput)
Console.WriteLine(args.Data);
sb.AppendLine(args.Data);
}
};
await process.WaitForExitAsync();
return sb.ToString();
}
private async Task<int> IncreaseBuildNumber()
{
const int BUILD_NUMBER_INDEX = 2;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var buildNumber = int.Parse(lines[BUILD_NUMBER_INDEX]) + 1;
Console.WriteLine($"- Updating build number from '{lines[BUILD_NUMBER_INDEX]}' to '{buildNumber}'.");
lines[BUILD_NUMBER_INDEX] = buildNumber.ToString();
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return buildNumber;
}
private async Task<string> UpdateBuildTime()
{
const int BUILD_TIME_INDEX = 1;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var buildTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC";
Console.WriteLine($"- Updating build time from '{lines[BUILD_TIME_INDEX]}' to '{buildTime}'.");
lines[BUILD_TIME_INDEX] = buildTime;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return buildTime;
}
[GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?<sdkVersion>[0-9.]+).+Commit:\s+(?<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?<hostVersion>[0-9.]+).+Commit:\s+(?<hostCommit>[a-zA-Z0-9]+)""")]
private static partial Regex DotnetVersionRegex();
[GeneratedRegex("""rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)""")]
private static partial Regex RustVersionRegex();
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
private static partial Regex MudBlazorVersionRegex();
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
private static partial Regex TauriVersionRegex();
[GeneratedRegex("""^\s*Copyright\s+(?<year>[0-9]{4})""")]
private static partial Regex FindCopyrightRegex();
[GeneratedRegex("""([0-9]{4})""")]
private static partial Regex ReplaceCopyrightYearRegex();
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
private static partial Regex AppVersionRegex();
}

View File

@ -1,49 +0,0 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
using SharedTools;
namespace Build.Commands;
public sealed class UpdateWebAssetsCommand
{
[Command("update-web", Description = "Update web assets")]
public void UpdateWebAssets()
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("=========================");
Console.Write("- Updating web assets ...");
var rid = Environment.GetRidsForCurrentOS().First();
var cwd = Environment.GetAIStudioDirectory();
var contentPath = Path.Join(cwd, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", "wwwroot", "_content");
var isMudBlazorDirectoryPresent = Directory.Exists(Path.Join(contentPath, "MudBlazor"));
if (!isMudBlazorDirectoryPresent)
{
Console.WriteLine();
Console.WriteLine($"- Error: No web assets found for RID '{rid}'. Please publish the project first.");
return;
}
Directory.CreateDirectory(Path.Join(cwd, "wwwroot", "system"));
var sourcePaths = Directory.EnumerateFiles(contentPath, "*", SearchOption.AllDirectories);
var counter = 0;
foreach(var sourcePath in sourcePaths)
{
counter++;
var relativePath = Path.GetRelativePath(cwd, sourcePath);
var targetPath = Path.Join(cwd, "wwwroot", relativePath);
var targetDirectory = Path.GetDirectoryName(targetPath);
if (targetDirectory != null)
Directory.CreateDirectory(targetDirectory);
File.Copy(sourcePath, targetPath, true);
}
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
Console.WriteLine();
}
}

View File

@ -1,7 +0,0 @@
// Global using directives
global using System.Text;
global using Cocona;
global using Environment = Build.Tools.Environment;

View File

@ -1,9 +0,0 @@
using Build.Commands;
var builder = CoconaApp.CreateBuilder();
var app = builder.Build();
app.AddCommands<CheckRidsCommand>();
app.AddCommands<UpdateMetadataCommands>();
app.AddCommands<UpdateWebAssetsCommand>();
app.AddCommands<CollectI18NKeysCommand>();
app.Run();

View File

@ -1,79 +0,0 @@
using System.Runtime.InteropServices;
using SharedTools;
namespace Build.Tools;
public static class Environment
{
public const string DOTNET_VERSION = "net9.0";
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
private static readonly Dictionary<RID, string> ALL_RIDS = Enum.GetValues<RID>().Select(rid => new KeyValuePair<RID, string>(rid, rid.AsMicrosoftRid())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
public static bool IsWorkingDirectoryValid()
{
var currentDirectory = Directory.GetCurrentDirectory();
var mainFile = Path.Combine(currentDirectory, "Program.cs");
var projectFile = Path.Combine(currentDirectory, "Build Script.csproj");
if (!currentDirectory.EndsWith("Build", StringComparison.Ordinal) || !File.Exists(mainFile) || !File.Exists(projectFile))
{
Console.WriteLine("The current directory is not a valid working directory for the build script. Go to the /app/Build directory within the git repository.");
return false;
}
return true;
}
public static string GetAIStudioDirectory()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "MindWork AI Studio");
return Path.GetFullPath(directory);
}
public static string GetRustRuntimeDirectory()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "..", "runtime");
return Path.GetFullPath(directory);
}
public static string GetMetadataPath()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "..", "metadata.txt");
return Path.GetFullPath(directory);
}
public static string? GetOS()
{
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return "windows";
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return "linux";
if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "darwin";
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
return null;
}
public static IEnumerable<RID> GetRidsForCurrentOS()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("win-", StringComparison.Ordinal)).Select(n => n.Key);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("osx-", StringComparison.Ordinal)).Select(n => n.Key);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("linux-", StringComparison.Ordinal)).Select(n => n.Key);
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
return [];
}
}

View File

@ -4,10 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindW
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build Script.csproj", "{447A5590-68E1-4EF8-9451-A41AF5FBE571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -22,14 +18,6 @@ Global
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU {0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.Build.0 = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
EndGlobalSection EndGlobalSection

View File

@ -2,23 +2,15 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RID/@EntryIndexedValue">RID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TB/@EntryIndexedValue">TB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=I18N/@EntryIndexedValue">I18N</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=huggingface/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -199,7 +199,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
// Call the ERI server to get the server description: // Call the ERI server to get the server description:
// //
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!; using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token); var authResponse = await eriClient.AuthenticateAsync(eriDataSource, rustService, token);
if (authResponse.Successful) if (authResponse.Successful)
{ {
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token); var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
@ -316,6 +316,26 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
} }
} }
/// <summary>
/// Extracts the JSON list from the given text. The text may contain additional
/// information around the JSON list. The method tries to extract the JSON list
/// from the text.
/// </summary>
/// <remarks>
/// Algorithm: The method searches for the first line that contains only a '[' character.
/// Then, it searches for the first line that contains only a ']' character. The method
/// returns the text between these two lines (including the brackets). When the method
/// cannot find the JSON list, it returns an empty string.
/// <br/><br/>
/// This overload is using strings instead of spans. We can use this overload in any
/// async method. Thus, it is a wrapper around the span-based method. Yes, we are losing
/// the memory efficiency of the span-based method, but we still gain the performance
/// of the span-based method: the entire search algorithm is span-based.
/// </remarks>
/// <param name="text">The text that may contain the JSON list.</param>
/// <returns>The extracted JSON list.</returns>
private static string ExtractJson(string text) => ExtractJson(text.AsSpan()).ToString();
/// <summary> /// <summary>
/// Extracts the JSON list from the given text. The text may contain additional /// Extracts the JSON list from the given text. The text may contain additional
/// information around the JSON list. The method tries to extract the JSON list /// information around the JSON list. The method tries to extract the JSON list

View File

@ -319,6 +319,9 @@ public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalConte
} }
} }
// A wrapper around the span version, because we need to call this method from an async context.
private static string ExtractJson(string text) => ExtractJson(text.AsSpan()).ToString();
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> input) private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> input)
{ {
// //

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_AGENDA)] @attribute [Route(Routes.ASSISTANT_AGENDA)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAgenda> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/>

View File

@ -1,11 +1,10 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Agenda; namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda> public partial class AssistantAgenda : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT; public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;

View File

@ -1,6 +1,4 @@
@using AIStudio.Chat @using AIStudio.Chat
@inherits AssistantLowerBase
@typeparam TSettings
<div class="inner-scrolling-context"> <div class="inner-scrolling-context">
@ -11,16 +9,9 @@
<InnerScrolling> <InnerScrolling>
<ChildContent> <ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2"> <MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudGrid Class="mb-2"> <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
<MudItem xs="10"> @this.Description
<MudText Typo="Typo.body1" Align="Align.Justify"> </MudText>
@this.Description
</MudText>
</MudItem>
<MudItem xs="2" Class="d-flex justify-end align-start">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="() => this.OpenSettingsDialog()"/>
</MudItem>
</MudGrid>
@if (this.Body is not null) @if (this.Body is not null)
{ {
@ -28,17 +19,9 @@
@this.Body @this.Body
</CascadingValue> </CascadingValue>
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3"> <MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" OnClick="async () => await this.Start()" Style="@this.SubmitButtonStyle"> @this.SubmitText
@this.SubmitText </MudButton>
</MudButton>
@if (this.isProcessing && this.cancellationTokenSource is not null)
{
<MudTooltip Text="@TB("Stop generation")">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
</MudTooltip>
}
</MudStack>
} }
</MudForm> </MudForm>
<Issues IssuesData="@(this.inputIssues)"/> <Issues IssuesData="@(this.inputIssues)"/>
@ -80,7 +63,7 @@
{ {
@if (this.ShowSendTo) @if (this.ShowSendTo)
{ {
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@TB("Send to ...")" Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded"> <MudMenu 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)) @foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{ {
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())"> <MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@ -97,20 +80,20 @@
{ {
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip): case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip"> <MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" Disabled="@buttonData.DisabledAction()" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()"> <MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text @buttonData.Text
</MudButton> </MudButton>
</MudTooltip> </MudTooltip>
break; break;
case ButtonData buttonData: case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" Disabled="@buttonData.DisabledAction()" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()"> <MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text @buttonData.Text
</MudButton> </MudButton>
break; break;
case SendToButton sendToButton: case SendToButton sendToButton:
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@TB("Send to ...")" Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded"> <MudMenu 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)) @foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{ {
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)"> <MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@ -125,14 +108,14 @@
@if (this.ShowCopyResult) @if (this.ShowCopyResult)
{ {
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()"> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
@TB("Copy result") Copy result
</MudButton> </MudButton>
} }
@if (this.ShowReset) @if (this.ShowReset)
{ {
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()"> <MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
@TB("Reset") Reset
</MudButton> </MudButton>
} }

View File

@ -9,14 +9,12 @@ using MudBlazor.Utilities;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants; namespace AIStudio.Assistants;
public abstract partial class AssistantBase<TSettings> : AssistantLowerBase where TSettings : IComponent public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; protected SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
protected IJSRuntime JsRuntime { get; init; } = null!; protected IJSRuntime JsRuntime { get; init; } = null!;
@ -30,15 +28,25 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
[Inject] [Inject]
protected RustService RustService { get; init; } = null!; protected RustService RustService { get; init; } = null!;
[Inject]
protected DataSourceService DataSourceService { get; init; } = null!;
[Inject] [Inject]
protected NavigationManager NavigationManager { get; init; } = null!; protected NavigationManager NavigationManager { get; init; } = null!;
[Inject] [Inject]
protected ILogger<AssistantBase<TSettings>> Logger { get; init; } = null!; protected ILogger<AssistantBase> Logger { get; init; } = null!;
[Inject] [Inject]
private MudTheme ColorTheme { get; init; } = null!; private MudTheme ColorTheme { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
protected abstract string Title { get; } protected abstract string Title { get; }
protected abstract string Description { get; } protected abstract string Description { get; }
@ -85,13 +93,14 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected virtual IReadOnlyList<IButtonData> FooterButtons => []; protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings; protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form; protected MudForm? form;
protected bool inputIsValid; protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE; protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread; protected ChatThread? chatThread;
protected IContent? lastUserPrompt; protected IContent? lastUserPrompt;
protected CancellationTokenSource? cancellationTokenSource;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6)); private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
@ -103,8 +112,6 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync();
this.formChangeTimer.AutoReset = false; this.formChangeTimer.AutoReset = false;
this.formChangeTimer.Elapsed += async (_, _) => this.formChangeTimer.Elapsed += async (_, _) =>
{ {
@ -115,6 +122,11 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.MightPreselectValues(); this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
await base.OnInitializedAsync();
} }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
@ -137,7 +149,28 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
#endregion #endregion
private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>)); #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBase);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_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
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty; private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
@ -149,16 +182,6 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
return null; return null;
} }
private async Task Start()
{
using (this.cancellationTokenSource = new())
{
await this.SubmitAction();
}
this.cancellationTokenSource = null;
}
private void TriggerFormChange(FormFieldChangedEventArgs _) private void TriggerFormChange(FormFieldChangedEventArgs _)
{ {
this.formChangeTimer.Stop(); this.formChangeTimer.Stop();
@ -196,7 +219,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
SystemPrompt = this.SystemPrompt, SystemPrompt = this.SystemPrompt,
WorkspaceId = Guid.Empty, WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(), ChatId = Guid.NewGuid(),
Name = string.Format(T("Assistant - {0}"), this.Title), Name = $"Assistant - {this.Title}",
Seed = this.RNG.Next(), Seed = this.RNG.Next(),
Blocks = [], Blocks = [],
}; };
@ -270,7 +293,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread, this.cancellationTokenSource!.Token); this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
this.isProcessing = false; this.isProcessing = false;
this.StateHasChanged(); this.StateHasChanged();
@ -279,13 +302,6 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
return aiText.Text; 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() protected async Task CopyToClipboard()
{ {
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy()); await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
@ -299,12 +315,6 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
return icon; return icon;
} }
protected async Task OpenSettingsDialog()
{
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<TSettings>(null, dialogParameters, DialogOptions.FULLSCREEN);
}
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{ {
if (!destination.AllowSendTo()) if (!destination.AllowSendTo())
@ -369,10 +379,11 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}", false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}",
}; };
#region Overrides of MSGComponentBase #region Implementation of IDisposable
protected override void DisposeResources() public void Dispose()
{ {
this.MessageBus.Unregister(this);
this.formChangeTimer.Dispose(); this.formChangeTimer.Dispose();
} }

View File

@ -7,7 +7,7 @@ namespace AIStudio.Assistants;
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary // See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
// //
public abstract class AssistantBaseCore<TSettings> : AssistantBase<TSettings> where TSettings : IComponent public abstract class AssistantBaseCore : AssistantBase
{ {
private protected sealed override RenderFragment Body => this.BuildRenderTree; private protected sealed override RenderFragment Body => this.BuildRenderTree;

View File

@ -1,12 +0,0 @@
using AIStudio.Components;
namespace AIStudio.Assistants;
public abstract class AssistantLowerBase : MSGComponentBase
{
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
}

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_BIAS)] @attribute [Route(Routes.ASSISTANT_BIAS)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBias> @inherits AssistantBaseCore
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
<b>Links:</b> <b>Links:</b>

View File

@ -1,12 +1,11 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BiasDay; namespace AIStudio.Assistants.BiasDay;
public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAssistantBias> public partial class BiasOfTheDayAssistant : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT; public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_CODING)] @attribute [Route(Routes.ASSISTANT_CODING)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogCoding> @inherits AssistantBaseCore
<MudExpansionPanels Class="mb-3"> <MudExpansionPanels Class="mb-3">
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++) @for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)

View File

@ -1,10 +1,8 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Coding; namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding> public partial class AssistantCoding : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT; public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;

View File

@ -1,25 +1,23 @@
@attribute [Route(Routes.ASSISTANT_EMAIL)] @attribute [Route(Routes.ASSISTANT_EMAIL)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogWritingEMails> @inherits AssistantBaseCore
<MudTextSwitch Label="@T("Is there a history, a previous conversation?")" @bind-Value="@this.provideHistory" LabelOn="@T("Yes, I provide the previous conversation")" LabelOff="@T("No, I don't provide a previous conversation")" /> <MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" />
@if (this.provideHistory) @if (this.provideHistory)
{ {
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="@T("Previous conversation")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Provide the previous conversation, e.g., the last e-mail, the last chat, etc.")" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/> <MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="Previous conversation" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Provide the previous conversation, e.g., the last e-mail, the last chat, etc." Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
</MudPaper> </MudPaper>
} }
<MudTextField T="string" @bind-Text="@this.inputGreeting" Label="@T("(Optional) The greeting phrase to use")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="@T("Dear Colleagues")" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputGreeting" Label="(Optional) The greeting phrase to use" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="Dear Colleagues" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="@T("Your bullet points")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items.")" Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/> <MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="Your bullet points" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items." Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/>
<MudSelect T="string" Label="@T("(Optional) Are any of your points particularly important?")" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt"> <MudSelect T="string" Label="(Optional) Are any of your points particularly important?" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
@foreach (var contentLine in this.bulletPointsLines) @foreach (var contentLine in this.bulletPointsLines)
{ {
<MudSelectItem T="string" Value="@contentLine"> <MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
@contentLine
</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudTextField T="string" @bind-Text="@this.inputName" Label="@T("(Optional) Your name for the closing salutation")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Your name for the closing salutation of your e-mail.")" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputName" Label="(Optional) Your name for the closing salutation" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Your name for the closing salutation of your e-mail." Class="mb-3"/>
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Select the writing style")" ValidateSelection="@this.ValidateWritingStyle"/> <EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Select the writing style" ValidateSelection="@this.ValidateWritingStyle"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,17 +1,19 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.EMail; namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMails> public partial class AssistantEMail : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT; public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
protected override string Title => T("E-Mail"); protected override string Title => "E-Mail";
protected override string Description => T("Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input."); protected override string Description =>
"""
Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
$""" $"""
@ -22,7 +24,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Create email"); protected override string SubmitText => "Create email";
protected override Func<Task> SubmitAction => this.CreateMail; protected override Func<Task> SubmitAction => this.CreateMail;
@ -97,12 +99,12 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string? ValidateBulletPoints(string content) private string? ValidateBulletPoints(string content)
{ {
if(string.IsNullOrWhiteSpace(content)) if(string.IsNullOrWhiteSpace(content))
return T("Please provide some content for the e-mail."); return "Please provide some content for the e-mail.";
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
if(!line.TrimStart().StartsWith('-')) if(!line.TrimStart().StartsWith('-'))
return T("Please start each line of your content list with a dash (-) to create a bullet point list."); return "Please start each line of your content list with a dash (-) to create a bullet point list.";
return null; return null;
} }
@ -110,7 +112,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string? ValidateTargetLanguage(CommonLanguages language) private string? ValidateTargetLanguage(CommonLanguages language)
{ {
if(language is CommonLanguages.AS_IS) if(language is CommonLanguages.AS_IS)
return T("Please select a target language for the e-mail."); return "Please select a target language for the e-mail.";
return null; return null;
} }
@ -118,7 +120,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }
@ -126,7 +128,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string? ValidateWritingStyle(WritingStyles style) private string? ValidateWritingStyle(WritingStyles style)
{ {
if(style == WritingStyles.NONE) if(style == WritingStyles.NONE)
return T("Please select a writing style for the e-mail."); return "Please select a writing style for the e-mail.";
return null; return null;
} }
@ -134,7 +136,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
private string? ValidateHistory(string history) private string? ValidateHistory(string history)
{ {
if(this.provideHistory && string.IsNullOrWhiteSpace(history)) if(this.provideHistory && string.IsNullOrWhiteSpace(history))
return T("Please provide some history for the e-mail."); return "Please provide some history for the e-mail.";
return null; return null;
} }

View File

@ -1,7 +1,7 @@
@attribute [Route(Routes.ASSISTANT_ERI)] @attribute [Route(Routes.ASSISTANT_ERI)]
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@using MudExtensions @using MudExtensions
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogERIServer> @inherits AssistantBaseCore
<MudJustifiedText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
@ -228,20 +228,18 @@ else
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Type</MudTh> <MudTh>Type</MudTh>
<MudTh>Actions</MudTh> <MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.EmbeddingName</MudTd> <MudTd>@context.EmbeddingName</MudTd>
<MudTd>@context.EmbeddingType</MudTd> <MudTd>@context.EmbeddingType</MudTd>
<MudTd> <MudTd Style="text-align: left;">
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)"> Edit
Edit </MudButton>
</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)">
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)"> Delete
Delete </MudButton>
</MudButton>
</MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
@ -276,19 +274,17 @@ else
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Actions</MudTh> <MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.Name</MudTd> <MudTd>@context.Name</MudTd>
<MudTd> <MudTd Style="text-align: left;">
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditRetrievalProcess(context)">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)"> Edit
Edit </MudButton>
</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)">
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)"> Delete
Delete </MudButton>
</MudButton>
</MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>

View File

@ -3,7 +3,6 @@ using System.Text.RegularExpressions;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -12,7 +11,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants.ERI; namespace AIStudio.Assistants.ERI;
public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer> public partial class AssistantERI : AssistantBaseCore
{ {
[Inject] [Inject]
private HttpClient HttpClient { get; set; } = null!; private HttpClient HttpClient { get; set; } = null!;

View File

@ -1,6 +1,6 @@
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)] @attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to check")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,17 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.GrammarSpelling; namespace AIStudio.Assistants.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialogGrammarSpelling> public partial class AssistantGrammarSpelling : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT; public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
protected override string Title => T("Grammar & Spelling Checker"); protected override string Title => "Grammar & Spelling Checker";
protected override string Description => T("Check the grammar and spelling of a text."); protected override string Description =>
"""
Check the grammar and spelling of a text.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
$""" $"""
@ -37,7 +39,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
}, },
]; ];
protected override string SubmitText => T("Proofread"); protected override string SubmitText => "Proofread";
protected override Func<Task> SubmitAction => this.ProofreadText; protected override Func<Task> SubmitAction => this.ProofreadText;
@ -90,7 +92,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
private string? ValidateText(string text) private string? ValidateText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a text as input. You might copy the desired text from a document or a website."); return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null; return null;
} }
@ -98,7 +100,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }

View File

@ -1,124 +0,0 @@
@attribute [Route(Routes.ASSISTANT_AI_STUDIO_I18N)]
@using AIStudio.Settings
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" SelectionUpdated="_ => this.OnChangedLanguage()" />
<ConfigurationSelect OptionDescription="Language plugin used for comparision" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="Select the language plugin used for comparision."/>
@if (this.isLoading)
{
<MudText Typo="Typo.body1" Class="mb-6">
The data is being loaded, please wait...
</MudText>
} else if (!this.isLoading && !string.IsNullOrWhiteSpace(this.loadingIssue))
{
<MudText Typo="Typo.body1" Class="mb-6">
While loading the I18N data, an issue occurred: @this.loadingIssue
</MudText>
}
else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
{
<MudText Typo="Typo.h6">
Added Content (@this.addedContent.Count entries)
</MudText>
<MudTable Items="@this.addedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
</ColGroup>
<HeaderContent>
<MudTh>Key</MudTh>
<MudTh>Text</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<pre style="font-size: 0.8em;">
@context.Key
</pre>
</MudTd>
<MudTd>
@context.Value
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
<MudText Typo="Typo.h6">
Removed Content (@this.removedContent.Count entries)
</MudText>
<MudTable Items="@this.removedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
</ColGroup>
<HeaderContent>
<MudTh>Key</MudTh>
<MudTh>Text</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<pre style="font-size: 0.8em;">
@context.Key
</pre>
</MudTd>
<MudTd>
@context.Value
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
@if (this.selectedTargetLanguage is CommonLanguages.EN_US)
{
<MudJustifiedText Typo="Typo.body1" Class="mb-6">
Please note: neither is a translation needed nor performed for English (USA). Anyway, you might want to generate the related Lua code.
</MudJustifiedText>
}
else
{
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
}
@if (this.localizedContent.Count > 0)
{
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6 mb-6"/>
<MudText Typo="Typo.h6">
Localized Content (@this.localizedContent.Count entries of @this.NumTotalItems)
</MudText>
<MudTable Items="@this.localizedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
</ColGroup>
<HeaderContent>
<MudTh>Key</MudTh>
<MudTh>Text</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<pre style="font-size: 0.8em;">
@context.Key
</pre>
</MudTd>
<MudTd>
@context.Value
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
}
}

View File

@ -1,374 +0,0 @@
using System.Diagnostics;
using System.Text;
using AIStudio.Dialogs.Settings;
using AIStudio.Tools.PluginSystem;
using Microsoft.Extensions.FileProviders;
using SharedTools;
#if RELEASE
using System.Reflection;
#endif
namespace AIStudio.Assistants.I18N;
public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
{
public override Tools.Components Component => Tools.Components.I18N_ASSISTANT;
protected override string Title => "Localization";
protected override string Description =>
"""
Translate MindWork AI Studio text content into another language.
""";
protected override string SystemPrompt =>
$"""
# Assignment
You are an expert in professional translations from English (US) to {this.SystemPromptLanguage()}.
You translate the texts without adding any new information. When necessary, you correct
spelling and grammar.
# Context
The texts to be translated come from the open source app "MindWork AI Studio". The goal
is to localize the app so that it can be offered in other languages. You will always
receive one text at a time. A text may be, for example, for a button, a label, or an
explanation within the app. The app "AI Studio" is a desktop app for macOS, Linux,
and Windows. Users can use Large Language Models (LLMs) in practical ways in their
daily lives with it. The app offers the regular chat mode for which LLMs have become
known. However, AI Studio also offers so-called assistants, where users no longer
have to prompt.
# Target Audience
The app is intended for everyone, not just IT specialists or scientists. When translating,
make sure the texts are easy for everyone to understand.
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
protected override bool ShowCopyResult => false;
protected override bool ShowSendTo => false;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new ButtonData
{
Text = "Copy Lua code to clipboard",
Icon = Icons.Material.Filled.Extension,
Color = Color.Default,
AsyncAction = async () => await this.RustService.CopyText2Clipboard(this.Snackbar, this.finalLuaCode.ToString()),
DisabledActionParam = () => this.finalLuaCode.Length == 0,
},
];
protected override string SubmitText => "Localize AI Studio & generate the Lua code";
protected override Func<Task> SubmitAction => this.LocalizeTextContent;
protected override bool SubmitDisabled => !this.localizationPossible;
protected override bool ShowDedicatedProgress => true;
protected override void ResetForm()
{
if (!this.MightPreselectValues())
{
this.selectedLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id;
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
_ = this.OnChangedLanguage();
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.I18N.PreselectOptions)
{
this.selectedLanguagePluginId = this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.I18N.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.I18N.PreselectOtherLanguage;
return true;
}
return false;
}
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private bool isLoading = true;
private string loadingIssue = string.Empty;
private bool localizationPossible;
private string searchString = string.Empty;
private Guid selectedLanguagePluginId;
private ILanguagePlugin? selectedLanguagePlugin;
private Dictionary<string, string> addedContent = [];
private Dictionary<string, string> removedContent = [];
private Dictionary<string, string> localizedContent = [];
private StringBuilder finalLuaCode = new();
#region Overrides of AssistantBase<SettingsDialogI18N>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await this.OnLanguagePluginChanged(this.selectedLanguagePluginId);
await this.LoadData();
}
#endregion
private string SystemPromptLanguage() => this.selectedTargetLanguage switch
{
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}",
};
private async Task OnLanguagePluginChanged(Guid pluginId)
{
this.selectedLanguagePluginId = pluginId;
await this.OnChangedLanguage();
}
private async Task OnChangedLanguage()
{
this.finalLuaCode.Clear();
this.localizedContent.Clear();
this.localizationPossible = false;
if (PluginFactory.RunningPlugins.FirstOrDefault(n => n is PluginLanguage && n.Id == this.selectedLanguagePluginId) is not PluginLanguage comparisonPlugin)
{
this.loadingIssue = $"Was not able to load the language plugin for comparison ({this.selectedLanguagePluginId}). Please select a valid, loaded & running language plugin.";
this.selectedLanguagePlugin = null;
}
else if (comparisonPlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
{
this.loadingIssue = $"The selected language plugin for comparison uses the IETF tag '{comparisonPlugin.IETFTag}' which does not match the selected target language '{this.selectedTargetLanguage.ToIETFTag()}'. Please select a valid, loaded & running language plugin which matches the target language.";
this.selectedLanguagePlugin = null;
}
else
{
this.selectedLanguagePlugin = comparisonPlugin;
this.loadingIssue = string.Empty;
await this.LoadData();
}
this.StateHasChanged();
}
private async Task LoadData()
{
if (this.selectedLanguagePlugin is null)
{
this.loadingIssue = "Please select a language plugin for comparison.";
this.localizationPossible = false;
this.isLoading = false;
this.StateHasChanged();
return;
}
this.isLoading = true;
this.StateHasChanged();
//
// Read the file `Assistants\I18N\allTexts.lua`:
//
#if DEBUG
var filePath = Path.Join(Environment.CurrentDirectory, "Assistants", "I18N");
var resourceFileProvider = new PhysicalFileProvider(filePath);
#else
var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Assistants.I18N");
#endif
var file = resourceFileProvider.GetFileInfo("allTexts.lua");
await using var fileStream = file.CreateReadStream();
using var reader = new StreamReader(fileStream);
var newI18NDataLuaCode = await reader.ReadToEndAsync();
//
// Next, we try to load the text as a language plugin -- without
// actually starting the plugin:
//
var newI18NPlugin = await PluginFactory.Load(null, newI18NDataLuaCode);
switch (newI18NPlugin)
{
case NoPlugin noPlugin when noPlugin.Issues.Any():
this.loadingIssue = noPlugin.Issues.First();
break;
case NoPlugin:
this.loadingIssue = "Was not able to load the I18N plugin. Please check the plugin code.";
break;
case { IsValid: false } plugin when plugin.Issues.Any():
this.loadingIssue = plugin.Issues.First();
break;
case PluginLanguage pluginLanguage:
this.loadingIssue = string.Empty;
var newI18NContent = pluginLanguage.Content;
var currentI18NContent = this.selectedLanguagePlugin.Content;
this.addedContent = newI18NContent.ExceptBy(currentI18NContent.Keys, n => n.Key).ToDictionary();
this.removedContent = currentI18NContent.ExceptBy(newI18NContent.Keys, n => n.Key).ToDictionary();
this.localizationPossible = true;
break;
}
this.isLoading = false;
this.StateHasChanged();
}
private bool FilterFunc(KeyValuePair<string, string> element)
{
if (string.IsNullOrWhiteSpace(this.searchString))
return true;
if (element.Key.Contains(this.searchString, StringComparison.OrdinalIgnoreCase))
return true;
if (element.Value.Contains(this.searchString, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
private string? ValidatingTargetLanguage(CommonLanguages language)
{
if(language == CommonLanguages.AS_IS)
return "Please select a target language.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count;
private async Task LocalizeTextContent()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
if(this.selectedLanguagePlugin is null)
return;
if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
return;
this.localizedContent.Clear();
if (this.selectedTargetLanguage is not CommonLanguages.EN_US)
{
// Phase 1: Translate added content
await this.Phase1TranslateAddedContent();
}
else
{
// Case: no translation needed
this.localizedContent = this.addedContent.ToDictionary();
}
if(this.cancellationTokenSource!.IsCancellationRequested)
return;
//
// Now, we have localized the added content. Next, we must merge
// the localized content with the existing content. However, we
// must skip the removed content. We use the localizedContent
// dictionary for the final result:
//
foreach (var keyValuePair in this.selectedLanguagePlugin.Content)
{
if (this.cancellationTokenSource!.IsCancellationRequested)
break;
if (this.localizedContent.ContainsKey(keyValuePair.Key))
continue;
if (this.removedContent.ContainsKey(keyValuePair.Key))
continue;
this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value);
}
if(this.cancellationTokenSource!.IsCancellationRequested)
return;
//
// Phase 2: Create the Lua code. We want to use the base language
// for the comments, though:
//
var commentContent = new Dictionary<string, string>(this.addedContent);
foreach (var keyValuePair in PluginFactory.BaseLanguage.Content)
{
if (this.cancellationTokenSource!.IsCancellationRequested)
break;
if (this.removedContent.ContainsKey(keyValuePair.Key))
continue;
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
this.Phase2CreateLuaCode(commentContent);
}
private async Task Phase1TranslateAddedContent()
{
var stopwatch = new Stopwatch();
var minimumTime = TimeSpan.FromMilliseconds(500);
foreach (var keyValuePair in this.addedContent)
{
if(this.cancellationTokenSource!.IsCancellationRequested)
break;
//
// We measure the time for each translation.
// We do not want to make more than 120 requests
// per minute, i.e., 2 requests per second.
//
stopwatch.Reset();
stopwatch.Start();
//
// Translate one text at a time:
//
this.CreateChatThread();
var time = this.AddUserRequest(keyValuePair.Value);
this.localizedContent.Add(keyValuePair.Key, await this.AddAIResponseAsync(time));
if (this.cancellationTokenSource!.IsCancellationRequested)
break;
//
// Ensure that we do not exceed the rate limit of 2 requests per second:
//
stopwatch.Stop();
if (stopwatch.Elapsed < minimumTime)
await Task.Delay(minimumTime - stopwatch.Elapsed);
}
}
private void Phase2CreateLuaCode(IReadOnlyDictionary<string, string> commentContent)
{
this.finalLuaCode.Clear();
LuaTable.Create(ref this.finalLuaCode, "UI_TEXT_CONTENT", this.localizedContent, commentContent, this.cancellationTokenSource!.Token);
// Next, we must remove the `root::` prefix from the keys:
this.finalLuaCode.Replace("""UI_TEXT_CONTENT["root::""", """
UI_TEXT_CONTENT["
""");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,18 @@
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)] @attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogIconFinder> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="@T("Your context")" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3"> <MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="@T("Your icon source")" Variant="Variant.Outlined" Margin="Margin.Dense"> <MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var source in Enum.GetValues<IconSources>()) @foreach (var source in Enum.GetValues<IconSources>())
{ {
<MudSelectItem Value="@source"> <MudSelectItem Value="@source">@source.Name()</MudSelectItem>
@source.Name()
</MudSelectItem>
} }
</MudSelect> </MudSelect>
@if (this.selectedIconSource is not IconSources.GENERIC) @if (this.selectedIconSource is not IconSources.GENERIC)
{ {
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium"> <MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
@T("Open website")
</MudButton>
} }
</MudStack> </MudStack>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,14 +1,20 @@
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.IconFinder; namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconFinder> public partial class AssistantIconFinder : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT; public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
protected override string Title => T("Icon Finder"); protected override string Title => "Icon Finder";
protected override string Description => T("""Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge: You need to extract a concept from your context, such as from a text. Let's take an example where your text contains statements about multiple departments. The sought-after concept could be "departments." The next challenge is that we need to anticipate the bias of the icon designers: under the search term "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source, it might be more effective to search for "buildings," for instance. LLMs assist you with both steps."""); protected override string Description =>
"""
Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
You need to extract a concept from your context, such as from a text. Let's take an example where
your text contains statements about multiple departments. The sought-after concept could be "departments."
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
""" """
@ -23,7 +29,7 @@ public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconF
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Find Icon"); protected override string SubmitText => "Find Icon";
protected override Func<Task> SubmitAction => this.FindIcon; protected override Func<Task> SubmitAction => this.FindIcon;
@ -66,7 +72,7 @@ public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconF
private string? ValidatingContext(string context) private string? ValidatingContext(string context)
{ {
if(string.IsNullOrWhiteSpace(context)) if(string.IsNullOrWhiteSpace(context))
return T("Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon."); return "Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon.";
return null; return null;
} }

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_JOB_POSTING)] @attribute [Route(Routes.ASSISTANT_JOB_POSTING)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogJobPostings> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/> <MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/>

View File

@ -1,9 +1,8 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.JobPosting; namespace AIStudio.Assistants.JobPosting;
public partial class AssistantJobPostings : AssistantBaseCore<SettingsDialogJobPostings> public partial class AssistantJobPostings : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT; public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT;

View File

@ -1,11 +1,11 @@
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)] @attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogLegalCheck> @inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
{ {
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/> <ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/>
} }
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Legal document")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Legal document" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="@T("Your questions")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="Your questions" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,19 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.LegalCheck; namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegalCheck> public partial class AssistantLegalCheck : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT; public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
protected override string Title => T("Legal Check"); protected override string Title => "Legal Check";
protected override string Description => T("Provide a legal document and ask a question about it. This assistant does not replace legal advice. Consult a lawyer to get professional advice. Remember that LLMs can invent answers and facts. Please do not rely on this answers."); protected override string Description =>
"""
Provide a legal document and ask a question about it. This assistant does not
replace legal advice. Consult a lawyer to get professional advice. Remember
that LLMs can invent answers and facts. Please do not rely on this answers.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
""" """
@ -22,7 +26,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegal
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Ask your questions"); protected override string SubmitText => "Ask your questions";
protected override Func<Task> SubmitAction => this.AksQuestions; protected override Func<Task> SubmitAction => this.AksQuestions;
@ -62,7 +66,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegal
private string? ValidatingLegalDocument(string text) private string? ValidatingLegalDocument(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a legal document as input. You might copy the desired text from a document or a website."); return "Please provide a legal document as input. You might copy the desired text from a document or a website.";
return null; return null;
} }
@ -70,7 +74,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegal
private string? ValidatingQuestions(string text) private string? ValidatingQuestions(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide your questions as input."); return "Please provide your questions as input.";
return null; return null;
} }

View File

@ -1,7 +1,7 @@
@attribute [Route(Routes.ASSISTANT_MY_TASKS)] @attribute [Route(Routes.ASSISTANT_MY_TASKS)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks> @inherits AssistantBaseCore
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.currentProfile"/> <ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.currentProfile"/>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Text or email")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Text or email" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,16 +1,21 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings; using AIStudio.Settings;
namespace AIStudio.Assistants.MyTasks; namespace AIStudio.Assistants.MyTasks;
public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks> public partial class AssistantMyTasks : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT; public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
protected override string Title => T("My Tasks"); protected override string Title => "My Tasks";
protected override string Description => T("You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."); protected override string Description =>
"""
You received a cryptic email that was sent to many recipients and you are now wondering
if you need to do something? Copy the email into the input field. You also need to select
a personal profile. In this profile, you should describe your role in the organization.
The AI will then try to give you hints on what your tasks might be.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
$""" $"""
@ -25,7 +30,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Analyze text"); protected override string SubmitText => "Analyze text";
protected override Func<Task> SubmitAction => this.AnalyzeText; protected override Func<Task> SubmitAction => this.AnalyzeText;
@ -78,7 +83,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
private string? ValidatingText(string text) private string? ValidatingText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide some text as input. For example, an email."); return "Please provide some text as input. For example, an email.";
return null; return null;
} }
@ -86,7 +91,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
private string? ValidateProfile(Profile profile) private string? ValidateProfile(Profile profile)
{ {
if(profile == default || profile == Profile.NO_PROFILE) if(profile == default || profile == Profile.NO_PROFILE)
return T("Please select one of your profiles."); return "Please select one of your profiles.";
return null; return null;
} }
@ -94,7 +99,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }

View File

@ -1,8 +1,8 @@
@attribute [Route(Routes.ASSISTANT_REWRITE)] @attribute [Route(Routes.ASSISTANT_REWRITE)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to improve")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Writing style")" AllowOther="@false" /> <EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Writing style" AllowOther="@false" />
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="@T("Sentence structure")" /> <EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="Sentence structure" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,17 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.RewriteImprove; namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogRewrite> public partial class AssistantRewriteImprove : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT; public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
protected override string Title => T("Rewrite & Improve Text"); protected override string Title => "Rewrite & Improve Text";
protected override string Description => T("Rewrite and improve your text. Please note, that the capabilities of the different LLM providers will vary."); protected override string Description =>
"""
Rewrite and improve your text. Please note, that the capabilities of the different LLM providers will vary.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
$""" $"""
@ -38,7 +40,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
}, },
]; ];
protected override string SubmitText => T("Improve your text"); protected override string SubmitText => "Improve";
protected override Func<Task> SubmitAction => this.RewriteText; protected override Func<Task> SubmitAction => this.RewriteText;
@ -97,7 +99,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
private string? ValidateText(string text) private string? ValidateText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a text as input. You might copy the desired text from a document or a website."); return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null; return null;
} }
@ -105,7 +107,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }

View File

@ -1,8 +1,8 @@
@attribute [Route(Routes.ASSISTANT_SYNONYMS)] @attribute [Route(Routes.ASSISTANT_SYNONYMS)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogSynonyms> @inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="@T("Your word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="Your word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="@T("(Optional) The context for the given word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,17 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Synonym; namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonyms> public partial class AssistantSynonyms : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT; public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
protected override string Title => T("Synonyms"); protected override string Title => "Synonyms";
protected override string Description => T("Find synonyms for words or phrases."); protected override string Description =>
"""
Find synonyms for words or phrases.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
$""" $"""
@ -49,7 +51,7 @@ public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonym
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Find synonyms"); protected override string SubmitText => "Find synonyms";
protected override Func<Task> SubmitAction => this.FindSynonyms; protected override Func<Task> SubmitAction => this.FindSynonyms;
@ -102,7 +104,7 @@ public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonym
private string? ValidatingText(string text) private string? ValidatingText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a word or phrase as input."); return "Please provide a word or phrase as input.";
return null; return null;
} }
@ -110,7 +112,7 @@ public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonym
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }

View File

@ -1,12 +1,12 @@
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)] @attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTextSummarizer> @inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
{ {
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectContentCleanerAgent)"/> <ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectContentCleanerAgent)"/>
} }
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomLanguage" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="Custom target language" ValidateOther="@this.ValidateCustomLanguage" />
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="@T("Target complexity")" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="@T("Your expertise")" ValidateOther="@this.ValidateExpertInField" /> <EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="Target complexity" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="Your expertise" ValidateOther="@this.ValidateExpertInField" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,20 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.TextSummarizer; namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogTextSummarizer> public partial class AssistantTextSummarizer : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT; public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
protected override string Title => T("Text Summarizer"); protected override string Title => "Text Summarizer";
protected override string Description => T("Summarize long text into a shorter version while retaining the main points. You might want to change the language of the summary to make it more readable. It is also possible to change the complexity of the summary to make it easy to understand."); protected override string Description =>
"""
Summarize long text into a shorter version while retaining the main points.
You might want to change the language of the summary to make it more readable.
It is also possible to change the complexity of the summary to make it
easy to understand.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
""" """
@ -24,7 +29,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Summarize"); protected override string SubmitText => "Summarize";
protected override Func<Task> SubmitAction => this.SummarizeText; protected override Func<Task> SubmitAction => this.SummarizeText;
@ -84,7 +89,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
private string? ValidatingText(string text) private string? ValidatingText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a text as input. You might copy the desired text from a document or a website."); return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null; return null;
} }
@ -92,7 +97,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }
@ -100,7 +105,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
private string? ValidateExpertInField(string field) private string? ValidateExpertInField(string field)
{ {
if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field)) if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field))
return T("Please provide your field of expertise."); return "Please provide your field of expertise.";
return null; return null;
} }

View File

@ -1,20 +1,20 @@
@attribute [Route(Routes.ASSISTANT_TRANSLATION)] @attribute [Route(Routes.ASSISTANT_TRANSLATION)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTranslation> @inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
{ {
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectContentCleanerAgent)"/> <ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectContentCleanerAgent)"/>
} }
<MudTextSwitch Label="@T("Live translation")" @bind-Value="@this.liveTranslation" LabelOn="@T("Live translation")" LabelOff="@T("No live translation")"/> <MudTextSwitch Label="Live translation" @bind-Value="@this.liveTranslation" LabelOn="Live translation" LabelOff="No live translation"/>
@if (this.liveTranslation) @if (this.liveTranslation)
{ {
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="@this.SettingsManager.ConfigurationData.Translation.DebounceIntervalMilliseconds" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="@this.SettingsManager.ConfigurationData.Translation.DebounceIntervalMilliseconds" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
} }
else else
{ {
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
} }
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,15 +1,17 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Translation; namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTranslation> public partial class AssistantTranslation : AssistantBaseCore
{ {
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT; public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
protected override string Title => T("Translation"); protected override string Title => "Translation";
protected override string Description => T("Translate text from one language to another."); protected override string Description =>
"""
Translate text from one language to another.
""";
protected override string SystemPrompt => protected override string SystemPrompt =>
""" """
@ -23,7 +25,7 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
protected override IReadOnlyList<IButtonData> FooterButtons => []; protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Translate"); protected override string SubmitText => "Translate";
protected override Func<Task> SubmitAction => () => this.TranslateText(true); protected override Func<Task> SubmitAction => () => this.TranslateText(true);
@ -82,7 +84,7 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
private string? ValidatingText(string text) private string? ValidatingText(string text)
{ {
if(string.IsNullOrWhiteSpace(text)) if(string.IsNullOrWhiteSpace(text))
return T("Please provide a text as input. You might copy the desired text from a document or a website."); return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null; return null;
} }
@ -90,7 +92,7 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
private string? ValidatingTargetLanguage(CommonLanguages language) private string? ValidatingTargetLanguage(CommonLanguages language)
{ {
if(language == CommonLanguages.AS_IS) if(language == CommonLanguages.AS_IS)
return T("Please select a target language."); return "Please select a target language.";
return null; return null;
} }
@ -98,7 +100,7 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
private string? ValidateCustomLanguage(string language) private string? ValidateCustomLanguage(string language)
{ {
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return T("Please provide a custom language."); return "Please provide a custom language.";
return null; return null;
} }

View File

@ -210,7 +210,7 @@ public sealed record ChatThread
ChatRole.SYSTEM => Role.SYSTEM, ChatRole.SYSTEM => Role.SYSTEM,
ChatRole.NONE => Role.NONE, ChatRole.NONE => Role.NONE,
_ => Role.UNKNOWN, _ => Role.UNKNOW,
}, },
Content = contentData, Content = contentData,

View File

@ -1,6 +1,6 @@
@using AIStudio.Tools @using AIStudio.Tools
@using MudBlazor @using MudBlazor
@inherits AIStudio.Components.MSGComponentBase
<MudCard Class="@this.CardClasses" Outlined="@true"> <MudCard Class="@this.CardClasses" Outlined="@true">
<MudCardHeader> <MudCardHeader>
<CardHeaderAvatar> <CardHeaderAvatar>
@ -9,36 +9,34 @@
</MudAvatar> </MudAvatar>
</CardHeaderAvatar> </CardHeaderAvatar>
<CardHeaderContent> <CardHeaderContent>
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
@this.Role.ToName() (@this.Time)
</MudText>
</CardHeaderContent> </CardHeaderContent>
<CardHeaderActions> <CardHeaderActions>
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null) @if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
{ {
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom"> <MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/> <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
</MudTooltip> </MudTooltip>
} }
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null) @if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
{ {
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom"> <MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/> <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
</MudTooltip> </MudTooltip>
} }
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null) @if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{ {
<MudTooltip Text="@T("Regenerate")" Placement="Placement.Bottom"> <MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/> <MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
</MudTooltip> </MudTooltip>
} }
@if (this.RemoveBlockFunc is not null) @if (this.RemoveBlockFunc is not null)
{ {
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom"> <MudTooltip Text="Removes this block" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/> <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
</MudTooltip> </MudTooltip>
} }
<MudTooltip Text="@T("Copies the content to the clipboard")" Placement="Placement.Bottom"> <MudTooltip Text="Copies the content to the clipboard" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/> <MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/>
</MudTooltip> </MudTooltip>
</CardHeaderActions> </CardHeaderActions>
@ -89,7 +87,7 @@
default: default:
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@string.Format(T("Cannot render content of type {0} yet."), this.Type) Cannot render content of type @this.Type yet.
</MudText> </MudText>
break; break;
} }

View File

@ -1,4 +1,4 @@
using AIStudio.Components; using AIStudio.Settings;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -8,7 +8,7 @@ namespace AIStudio.Chat;
/// <summary> /// <summary>
/// The UI component for a chat content block, i.e., for any IContent. /// The UI component for a chat content block, i.e., for any IContent.
/// </summary> /// </summary>
public partial class ContentBlockComponent : MSGComponentBase public partial class ContentBlockComponent : ComponentBase
{ {
/// <summary> /// <summary>
/// The role of the chat content block. /// The role of the chat content block.
@ -41,10 +41,10 @@ public partial class ContentBlockComponent : MSGComponentBase
public string Class { get; set; } = string.Empty; public string Class { get; set; } = string.Empty;
[Parameter] [Parameter]
public bool IsLastContentBlock { get; set; } public bool IsLastContentBlock { get; set; } = false;
[Parameter] [Parameter]
public bool IsSecondToLastBlock { get; set; } public bool IsSecondToLastBlock { get; set; } = false;
[Parameter] [Parameter]
public Func<IContent, Task>? RemoveBlockFunc { get; set; } public Func<IContent, Task>? RemoveBlockFunc { get; set; }
@ -67,6 +67,9 @@ public partial class ContentBlockComponent : MSGComponentBase
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@ -129,7 +132,7 @@ public partial class ContentBlockComponent : MSGComponentBase
break; break;
default: default:
this.Snackbar.Add(T("Cannot copy this content type to clipboard!"), Severity.Error, config => this.Snackbar.Add("Cannot copy this content type to clipboard!", Severity.Error, config =>
{ {
config.Icon = Icons.Material.Filled.ContentCopy; config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large; config.IconSize = Size.Large;
@ -149,10 +152,10 @@ public partial class ContentBlockComponent : MSGComponentBase
return; return;
var remove = await this.DialogService.ShowMessageBox( var remove = await this.DialogService.ShowMessageBox(
T("Remove Message"), "Remove Message",
T("Do you really want to remove this message?"), "Do you really want to remove this message?",
T("Yes, remove it"), "Yes, remove it",
T("No, keep it")); "No, keep it");
if (remove.HasValue && remove.Value) if (remove.HasValue && remove.Value)
await this.RemoveBlockFunc(this.Content); await this.RemoveBlockFunc(this.Content);
@ -167,10 +170,10 @@ public partial class ContentBlockComponent : MSGComponentBase
return; return;
var regenerate = await this.DialogService.ShowMessageBox( var regenerate = await this.DialogService.ShowMessageBox(
T("Regenerate Message"), "Regenerate Message",
T("Do you really want to regenerate this message?"), "Do you really want to regenerate this message?",
T("Yes, regenerate it"), "Yes, regenerate it",
T("No, keep it")); "No, keep it");
if (regenerate.HasValue && regenerate.Value) if (regenerate.HasValue && regenerate.Value)
await this.RegenerateFunc(this.Content); await this.RegenerateFunc(this.Content);
@ -196,10 +199,10 @@ public partial class ContentBlockComponent : MSGComponentBase
return; return;
var edit = await this.DialogService.ShowMessageBox( var edit = await this.DialogService.ShowMessageBox(
T("Edit Message"), "Edit Message",
T("Do you really want to edit this message? In order to edit this message, the AI response will be deleted."), "Do you really want to edit this message? In order to edit this message, the AI response will be deleted.",
T("Yes, remove the AI response and edit it"), "Yes, remove the AI response and edit it",
T("No, keep it")); "No, keep it");
if (edit.HasValue && edit.Value) if (edit.HasValue && edit.Value)
await this.EditLastUserBlockFunc(this.Content); await this.EditLastUserBlockFunc(this.Content);

View File

@ -0,0 +1,12 @@
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();
}

View File

@ -1,6 +1,3 @@
@inherits MSGComponentBase
@typeparam TSettings
<MudCard Outlined="@true" Style="@this.BlockStyle"> <MudCard Outlined="@true" Style="@this.BlockStyle">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
@ -20,11 +17,8 @@
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButtonGroup Variant="Variant.Outlined"> <MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link"> @this.ButtonText
@this.ButtonText </MudButton>
</MudButton>
<MudIconButton Variant="Variant.Text" Icon="@Icons.Material.Filled.Settings" Color="Color.Default" OnClick="@this.OpenSettingsDialog"/>
</MudButtonGroup>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>

View File

@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Components; using AIStudio.Settings;
using DialogOptions = AIStudio.Dialogs.DialogOptions; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Parameter] [Parameter]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -25,15 +25,46 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
private MudTheme ColorTheme { get; init; } = null!; private MudTheme ColorTheme { get; init; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
private async Task OpenSettingsDialog() [Inject]
private MessageBus MessageBus { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{ {
var dialogParameters = new DialogParameters(); this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
await this.DialogService.ShowAsync<TSettings>(T("Open Settings"), dialogParameters, DialogOptions.FULLSCREEN); await base.OnInitializedAsync();
} }
#endregion
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBlock);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_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
private string BorderColor => this.SettingsManager.IsDarkMode switch private string BorderColor => this.SettingsManager.IsDarkMode switch
{ {
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight, true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight,
@ -41,4 +72,13 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
}; };
private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;"; private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
}
#endregion
} }

View File

@ -13,15 +13,6 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (216, "v0.9.41, build 216 (2025-04-27 14:18 UTC)", "v0.9.41.md"),
new (215, "v0.9.40, build 215 (2025-04-20 13:30 UTC)", "v0.9.40.md"),
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"),
new (208, "v0.9.33, build 208 (2025-03-11 08:14 UTC)", "v0.9.33.md"),
new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"), new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"),
new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"), new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"),
new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"), new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"),

View File

@ -1,5 +1,4 @@
@inherits MSGComponentBase <MudSelect T="Log" @bind-Value="@this.SelectedLog" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Margin="Margin.Dense" Label="Changelog" Class="mb-2 rounded-lg" Variant="Variant.Outlined" SelectedValuesChanged="() => this.ReadLogAsync()" OnKeyUp="() => this.ReadLogAsync()">
<MudSelect T="Log" @bind-Value="@this.SelectedLog" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Margin="Margin.Dense" Label="@T("Changelog")" Class="mb-2 rounded-lg" Variant="Variant.Outlined" SelectedValuesChanged="() => this.ReadLogAsync()" OnKeyUp="() => this.ReadLogAsync()">
@foreach (var log in LOGS) @foreach (var log in LOGS)
{ {
<MudSelectItem Value="@log"/> <MudSelectItem Value="@log"/>

View File

@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class Changelog : MSGComponentBase public partial class Changelog : ComponentBase
{ {
[Inject] [Inject]
private HttpClient HttpClient { get; set; } = null!; private HttpClient HttpClient { get; set; } = null!;

View File

@ -58,19 +58,19 @@
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
{ {
<MudTooltip Text="@T("Show your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/> <MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
</MudTooltip> </MudTooltip>
} }
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{ {
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/> <MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip> </MudTooltip>
} }
<MudTooltip Text="@T("Start temporary chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/> <MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</MudTooltip> </MudTooltip>
@ -83,14 +83,14 @@
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/> <MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip> </MudTooltip>
} }
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{ {
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/> <MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip> </MudTooltip>
} }
@ -102,7 +102,7 @@
@if (this.isStreaming && this.cancellationTokenSource is not null) @if (this.isStreaming && this.cancellationTokenSource is not null)
{ {
<MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Stop generation" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/> <MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
</MudTooltip> </MudTooltip>
} }
@ -116,7 +116,7 @@
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider)) @if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
{ {
<MudTooltip Text="@T("The selected provider is not allowed in this chat due to data security reasons.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="The selected provider is not allowed in this chat due to data security reasons." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/> <MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
</MudTooltip> </MudTooltip>
} }

View File

@ -3,6 +3,7 @@ using AIStudio.Dialogs;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
@ -40,6 +41,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject]
private DataSourceService DataSourceService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
@ -255,22 +259,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE; private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first"); private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
private string InputLabel private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.Provider.InstanceName}', provider '{this.Provider.UsedLLMProvider.ToName()}')" : "Select a provider first";
{
get
{
if (this.IsProviderSelected)
return string.Format(T("Your Prompt (use selected instance '{0}', provider '{1}')"), this.Provider.InstanceName, this.Provider.UsedLLMProvider.ToName());
return this.T("Select a provider first");
}
}
private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0; private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0;
private string TooltipAddChatToWorkspace => string.Format(T(@"Start new chat in workspace ""{0}"""), this.currentWorkspaceName); private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty; private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
@ -662,7 +657,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{ {
var confirmationDialogParameters = new DialogParameters var confirmationDialogParameters = new DialogParameters
{ {
{ "Message", T("Are you sure you want to move this chat? All unsaved changes will be lost.") }, { "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
}; };
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN); var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
@ -673,12 +668,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
var dialogParameters = new DialogParameters var dialogParameters = new DialogParameters
{ {
{ "Message", T("Please select the workspace where you want to move the chat to.") }, { "Message", "Please select the workspace where you want to move the chat to." },
{ "SelectedWorkspace", this.ChatThread?.WorkspaceId }, { "SelectedWorkspace", this.ChatThread?.WorkspaceId },
{ "ConfirmText", T("Move chat") }, { "ConfirmText", "Move chat" },
}; };
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
@ -848,7 +843,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override string ComponentName => nameof(ChatComponent);
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -867,7 +864,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
} }
protected override Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -887,6 +884,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
this.MessageBus.Unregister(this);
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
await this.SaveThread(); await this.SaveThread();

View File

@ -1,8 +1,6 @@
@using AIStudio.Provider @using AIStudio.Provider
@inherits MSGComponentBase
<div class="d-flex"> <div class="d-flex">
<MudTooltip Text="@T("Shows and hides the confidence card with information about the selected LLM provider.")" Placement="Placement.Top"> <MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
@if (this.Mode is PopoverTriggerMode.ICON) @if (this.Mode is PopoverTriggerMode.ICON)
{ {
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/> <MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
@ -10,7 +8,7 @@
else else
{ {
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())">
@T("Confidence") Confidence
</MudButton> </MudButton>
} }
</MudTooltip> </MudTooltip>
@ -19,22 +17,16 @@
<MudCard> <MudCard>
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<MudText Typo="Typo.h5"> <MudText Typo="Typo.h5">Confidence Card</MudText>
@T("Confidence Card")
</MudText>
</CardHeaderContent> </CardHeaderContent>
</MudCardHeader> </MudCardHeader>
<MudCardContent Style="max-height: 50vh; max-width: 35vw; overflow: auto;"> <MudCardContent Style="max-height: 50vh; overflow: auto;">
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Description</MudText>
@T("Description")
</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/> <MudMarkdown Value="@this.currentConfidence.Description"/>
@if (this.currentConfidence.Sources.Count > 0) @if (this.currentConfidence.Sources.Count > 0)
{ {
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Sources</MudText>
@T("Sources")
</MudText>
<MudList T="@string"> <MudList T="@string">
@foreach (var sourceTuple in this.GetConfidenceSources()) @foreach (var sourceTuple in this.GetConfidenceSources())
{ {
@ -45,17 +37,13 @@
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region)) @if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
{ {
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Region</MudText>
@T("Region")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3"> <MudText Typo="Typo.body1" Class="mb-3">
<b>@this.currentConfidence.Region</b> <b>@this.currentConfidence.Region</b>
</MudText> </MudText>
} }
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Confidence Level</MudText>
@T("Confidence Level")
</MudText>
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()"> <MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
<b>@this.currentConfidence.Level.GetName()</b> <b>@this.currentConfidence.Level.GetName()</b>
</MudText> </MudText>

View File

@ -1,10 +1,11 @@
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ConfidenceInfo : MSGComponentBase public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Parameter] [Parameter]
public PopoverTriggerMode Mode { get; set; } = PopoverTriggerMode.BUTTON; public PopoverTriggerMode Mode { get; set; } = PopoverTriggerMode.BUTTON;
@ -12,6 +13,12 @@ public partial class ConfidenceInfo : MSGComponentBase
[Parameter] [Parameter]
public LLMProviders LLMProvider { get; set; } public LLMProviders LLMProvider { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
private Confidence currentConfidence; private Confidence currentConfidence;
private bool showConfidence; private bool showConfidence;
@ -24,6 +31,9 @@ public partial class ConfidenceInfo : MSGComponentBase
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
this.currentConfidence = this.LLMProvider.GetConfidence(this.SettingsManager); this.currentConfidence = this.LLMProvider.GetConfidence(this.SettingsManager);
await base.OnParametersSetAsync(); await base.OnParametersSetAsync();
} }
@ -49,5 +59,38 @@ public partial class ConfidenceInfo : MSGComponentBase
private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};"; private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};";
private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};"; private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor(this.SettingsManager)}; max-width: calc(35vw);";
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.showConfidence = false;
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
} }

View File

@ -1 +0,0 @@
@inherits MSGComponentBase

View File

@ -1,3 +1,5 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
@ -5,7 +7,7 @@ namespace AIStudio.Components;
/// <summary> /// <summary>
/// A base class for configuration options. /// A base class for configuration options.
/// </summary> /// </summary>
public partial class ConfigurationBase : MSGComponentBase public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDisposable
{ {
/// <summary> /// <summary>
/// The description of the option, i.e., the name. Should be /// The description of the option, i.e., the name. Should be
@ -26,6 +28,12 @@ public partial class ConfigurationBase : MSGComponentBase
[Parameter] [Parameter]
public Func<bool> Disabled { get; set; } = () => false; public Func<bool> Disabled { get; set; } = () => false;
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
protected const string MARGIN_CLASS = "mb-6"; protected const string MARGIN_CLASS = "mb-6";
protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
@ -33,8 +41,13 @@ public partial class ConfigurationBase : MSGComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
// Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -42,9 +55,11 @@ public partial class ConfigurationBase : MSGComponentBase
protected async Task InformAboutChange() => await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); protected async Task InformAboutChange() => await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
#region Overrides of MSGComponentBase #region Implementation of IMessageBusReceiver
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public string ComponentName => nameof(ConfigurationBase);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -56,5 +71,19 @@ public partial class ConfigurationBase : MSGComponentBase
return Task.CompletedTask; 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 #endregion
} }

View File

@ -1,3 +1,2 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits MSGComponentBase <ConfigurationSelect Disabled="@this.Disabled" OptionDescription="Select a minimum confidence level" SelectedValue="@this.FilteredSelectedValue" Data="@ConfigurationSelectDataFactory.GetConfidenceLevelsData(this.SettingsManager, this.RestrictToGlobalMinimumConfidence)" SelectionUpdate="@this.SelectionUpdate" OptionHelp="Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."/>
<ConfigurationSelect Disabled="@this.Disabled" OptionDescription="@T("Select a minimum confidence level")" SelectedValue="@this.FilteredSelectedValue" Data="@ConfigurationSelectDataFactory.GetConfidenceLevelsData(this.SettingsManager, this.RestrictToGlobalMinimumConfidence)" SelectionUpdate="@this.SelectionUpdate" OptionHelp="@T("Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.")"/>

View File

@ -1,10 +1,11 @@
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ConfigurationMinConfidenceSelection : MSGComponentBase public partial class ConfigurationMinConfidenceSelection : ComponentBase
{ {
/// <summary> /// <summary>
/// The selected value. /// The selected value.
@ -30,6 +31,9 @@ public partial class ConfigurationMinConfidenceSelection : MSGComponentBase
[Parameter] [Parameter]
public bool RestrictToGlobalMinimumConfidence { get; set; } public bool RestrictToGlobalMinimumConfidence { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private ConfidenceLevel FilteredSelectedValue() private ConfidenceLevel FilteredSelectedValue()
{ {
if (this.SelectedValue() is ConfidenceLevel.NONE) if (this.SelectedValue() is ConfidenceLevel.NONE)

View File

@ -1,8 +1,8 @@
@inherits ConfigurationBase @inherits ConfigurationBase
@typeparam TData @typeparam T
<MudSelectExtended <MudSelectExtended
T="TData" T="T"
MultiSelection="@true" MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionText" MultiSelectionTextFunc="@this.GetMultiSelectionText"
SelectedValues="@this.SelectedValues()" SelectedValues="@this.SelectedValues()"

View File

@ -7,28 +7,28 @@ namespace AIStudio.Components;
/// <summary> /// <summary>
/// Configuration component for selecting many values from a list. /// Configuration component for selecting many values from a list.
/// </summary> /// </summary>
/// <typeparam name="TData">The type of the value to select.</typeparam> /// <typeparam name="T">The type of the value to select.</typeparam>
public partial class ConfigurationMultiSelect<TData> : ConfigurationBase public partial class ConfigurationMultiSelect<T> : ConfigurationBase
{ {
/// <summary> /// <summary>
/// The data to select from. /// The data to select from.
/// </summary> /// </summary>
[Parameter] [Parameter]
public IEnumerable<ConfigurationSelectData<TData>> Data { get; set; } = []; public IEnumerable<ConfigurationSelectData<T>> Data { get; set; } = [];
/// <summary> /// <summary>
/// The selected values. /// The selected values.
/// </summary> /// </summary>
[Parameter] [Parameter]
public Func<HashSet<TData>> SelectedValues { get; set; } = () => []; public Func<HashSet<T>> SelectedValues { get; set; } = () => [];
/// <summary> /// <summary>
/// An action that is called when the selection changes. /// An action that is called when the selection changes.
/// </summary> /// </summary>
[Parameter] [Parameter]
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { }; public Action<HashSet<T>> SelectionUpdate { get; set; } = _ => { };
private async Task OptionChanged(IEnumerable<TData?>? updatedValues) private async Task OptionChanged(IEnumerable<T?>? updatedValues)
{ {
if(updatedValues is null) if(updatedValues is null)
this.SelectionUpdate([]); this.SelectionUpdate([]);
@ -41,14 +41,14 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBase
private static string GetClass => $"{MARGIN_CLASS} rounded-lg"; private static string GetClass => $"{MARGIN_CLASS} rounded-lg";
private string GetMultiSelectionText(List<TData?>? selectedValues) private string GetMultiSelectionText(List<T?>? selectedValues)
{ {
if(selectedValues is null || selectedValues.Count == 0) if(selectedValues is null || selectedValues.Count == 0)
return T("No preview features selected."); return "No preview features selected.";
if(selectedValues.Count == 1) if(selectedValues.Count == 1)
return T("You have selected 1 preview feature."); return $"You have selected 1 preview feature.";
return string.Format(T("You have selected {0} preview features."), selectedValues.Count); return $"You have selected {selectedValues.Count} preview features.";
} }
} }

View File

@ -1,2 +1 @@
@inherits MSGComponentBase <ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
<ConfigurationSelect OptionDescription="@T("Preselected provider")" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ConfigurationProviderSelection : MSGComponentBase public partial class ConfigurationProviderSelection : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Parameter] [Parameter]
public Func<string> SelectedValue { get; set; } = () => string.Empty; public Func<string> SelectedValue { get; set; } = () => string.Empty;
@ -30,11 +30,20 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
[Parameter] [Parameter]
public Tools.Components Component { get; set; } = Tools.Components.NONE; public Tools.Components Component { get; set; } = Tools.Components.NONE;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]); // Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
await base.OnParametersSetAsync(); await base.OnParametersSetAsync();
} }
@ -44,7 +53,7 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
private IEnumerable<ConfigurationSelectData<string>> FilteredData() private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{ {
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
yield return new(T("Use app default"), string.Empty); yield return new("Use app default", string.Empty);
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
foreach (var providerId in this.Data) foreach (var providerId in this.Data)
@ -55,9 +64,11 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
} }
} }
#region Overrides of MSGComponentBase #region Implementation of IMessageBusReceiver
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public string ComponentName => nameof(ConfigurationProviderSelection);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -79,5 +90,19 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
} }
} }
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 #endregion
} }

View File

@ -1,9 +1,9 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits MSGComponentBase
@if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE) @if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE)
{ {
<div class="d-flex"> <div class="d-flex">
<MudTooltip Text="@T("Select the data you want to use here.")" Placement="Placement.Top"> <MudTooltip Text="Select the data sources you want to use here." Placement="Placement.Top">
@if (this.PopoverTriggerMode is PopoverTriggerMode.ICON) @if (this.PopoverTriggerMode is PopoverTriggerMode.ICON)
{ {
<MudIconButton Icon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/> <MudIconButton Icon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/>
@ -11,7 +11,7 @@
else else
{ {
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())">
@T("Select data") Select data sources
</MudButton> </MudButton>
} }
</MudTooltip> </MudTooltip>
@ -21,15 +21,7 @@
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<PreviewPrototype/> <PreviewPrototype/>
<MudStack Row="true" AlignItems="AlignItems.Center"> <MudText Typo="Typo.h5">Data Source Selection</MudText>
<MudText Typo="Typo.h5">
@T("Data Source Selection")
</MudText>
<MudSpacer/>
<MudTooltip Text="@T("Manage your data sources")" Placement="Placement.Top">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="@this.OpenSettingsDialog"/>
</MudTooltip>
</MudStack>
</CardHeaderContent> </CardHeaderContent>
</MudCardHeader> </MudCardHeader>
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;"> <MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
@ -39,56 +31,36 @@
<MudSkeleton Width="80%"/> <MudSkeleton Width="80%"/>
<MudSkeleton Width="100%"/> <MudSkeleton Width="100%"/>
} }
else if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
{
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("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">
@T("Manage Data Sources")
</MudButton>
<MudButton Variant="Variant.Filled" Href="https://mindworkai.org/#eri---external-retrieval-interface" Target="_blank" StartIcon="@Icons.Material.Filled.Settings">
@T("Read more about ERI")
</MudButton>
</MudStack>
}
else if (this.showDataSourceSelection) else if (this.showDataSourceSelection)
{ {
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged"/> <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"/>
@if (this.areDataSourcesEnabled) @if (this.areDataSourcesEnabled)
{ {
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged"/> <MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation) @if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
{ {
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged"/> <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"/>
} }
@switch (this.aiBasedSourceSelection) @if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
{ {
case true when this.availableDataSources.Count == 0: <MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
<MudText Typo="Typo.body1" Class="mb-3"> <MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
@T("Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.") @foreach (var source in this.availableDataSources)
</MudText> {
break; <MudListItem Value="@source">
@source.Name
case true when this.DataSourcesAISelected.Count == 0: </MudListItem>
<MudText Typo="Typo.body1" Class="mb-3"> }
@T("The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.") </MudList>
</MudText> </MudField>
break; }
else
case false when this.availableDataSources.Count == 0: {
<MudText Typo="Typo.body1" Class="mb-3"> <MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
@T("Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.") <ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
</MudText> <MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
break;
case false:
<MudField Label="@T("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) @foreach (var source in this.availableDataSources)
{ {
<MudListItem Value="@source"> <MudListItem Value="@source">
@ -96,49 +68,34 @@
</MudListItem> </MudListItem>
} }
</MudList> </MudList>
</MudField> </ExpansionPanel>
break; <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>
case true: <MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;"> <MudJustifiedText Typo="Typo.body2">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="@T("Available Data Sources")"> @this.GetAIReasoning(source)
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;"> </MudJustifiedText>
@foreach (var source in this.availableDataSources) </ChildContent>
{ </MudListItem>
<MudListItem Value="@source"> }
@source.Name </MudList>
</MudListItem> </ExpansionPanel>
} </MudExpansionPanels>
</MudList>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="@T("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>
break;
} }
} }
} }
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideDataSourceSelection())"> <MudButton Variant="Variant.Filled" OnClick="@(() => this.HideDataSourceSelection())">
@T("Close") Close
</MudButton> </MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
@ -149,9 +106,7 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
{ {
<MudPaper Class="pa-3 mb-8 mt-3 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 mt-3 border-dashed border rounded-lg">
<PreviewPrototype/> <PreviewPrototype/>
<MudText Typo="Typo.h5"> <MudText Typo="Typo.h5">Data Source Selection</MudText>
@T("Data Source Selection")
</MudText>
@if (!string.IsNullOrWhiteSpace(this.ConfigurationHeaderMessage)) @if (!string.IsNullOrWhiteSpace(this.ConfigurationHeaderMessage))
{ {
@ -160,12 +115,12 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
</MudText> </MudText>
} }
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged"/> <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"/>
@if (this.areDataSourcesEnabled) @if (this.areDataSourcesEnabled)
{ {
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged"/> <MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged"/> <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"/>
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@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))"> <MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
@foreach (var source in this.availableDataSources) @foreach (var source in this.availableDataSources)
{ {

View File

@ -1,15 +1,12 @@
using AIStudio.Dialogs.Settings;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class DataSourceSelection : MSGComponentBase public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Parameter] [Parameter]
public DataSourceSelectionMode SelectionMode { get; set; } = DataSourceSelectionMode.SELECTION_MODE; public DataSourceSelectionMode SelectionMode { get; set; } = DataSourceSelectionMode.SELECTION_MODE;
@ -38,11 +35,17 @@ public partial class DataSourceSelection : MSGComponentBase
[Parameter] [Parameter]
public bool AutoSaveAppSettings { get; set; } public bool AutoSaveAppSettings { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
[Inject] [Inject]
private DataSourceService DataSourceService { get; init; } = null!; private DataSourceService DataSourceService { get; init; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private ILogger<DataSourceSelection> Logger { get; init; } = null!;
private bool internalChange; private bool internalChange;
private bool showDataSourceSelection; private bool showDataSourceSelection;
@ -57,7 +60,8 @@ public partial class DataSourceSelection : MSGComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.ApplyFilters([], [ Event.RAG_AUTO_DATA_SOURCES_SELECTED ]); this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED, Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
// //
// Load the settings: // Load the settings:
@ -128,20 +132,6 @@ public partial class DataSourceSelection : MSGComponentBase
#endregion #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; private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList(); private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
@ -246,12 +236,19 @@ public partial class DataSourceSelection : MSGComponentBase
private void HideDataSourceSelection() => this.showDataSourceSelection = false; private void HideDataSourceSelection() => this.showDataSourceSelection = false;
#region Overrides of MSGComponentBase #region Implementation of IMessageBusReceiver
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
case Event.COLOR_THEME_CHANGED:
this.showDataSourceSelection = false;
this.StateHasChanged();
break;
case Event.RAG_AUTO_DATA_SOURCES_SELECTED: case Event.RAG_AUTO_DATA_SOURCES_SELECTED:
if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources) if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources)
this.DataSourcesAISelected = aiSelectedDataSources; this.DataSourcesAISelected = aiSelectedDataSources;
@ -263,5 +260,19 @@ public partial class DataSourceSelection : MSGComponentBase
return Task.CompletedTask; 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 #endregion
} }

View File

@ -12,6 +12,6 @@
</MudSelect> </MudSelect>
@if (this.AllowOther && this.Value.Equals(this.OtherValue)) @if (this.AllowOther && this.Value.Equals(this.OtherValue))
{ {
<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"/> <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"/>
} }
</MudStack> </MudStack>

View File

@ -2,9 +2,7 @@
<TitleContent> <TitleContent>
<div class="d-flex align-center"> <div class="d-flex align-center">
<MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/> <MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/>
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">@this.HeaderText</MudText>
@this.HeaderText
</MudText>
@if (this.ShowEndButton) @if (this.ShowEndButton)
{ {
<MudSpacer/> <MudSpacer/>

View File

@ -1,3 +1,5 @@
using AIStudio.Layout;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
@ -28,6 +30,9 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter] [Parameter]
public string Style { get; set; } = string.Empty; public string Style { get; set; } = string.Empty;
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
[Inject] [Inject]
private IJSRuntime JsRuntime { get; init; } = null!; private IJSRuntime JsRuntime { get; init; } = null!;
@ -45,7 +50,9 @@ public partial class InnerScrolling : MSGComponentBase
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override string ComponentName => nameof(InnerScrolling);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -57,6 +64,11 @@ public partial class InnerScrolling : MSGComponentBase
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
{
return Task.FromResult(default(TResult));
}
#endregion #endregion
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; "; private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";

View File

@ -1,10 +1,7 @@
@inherits MSGComponentBase
@if (this.IssuesData.Any()) @if (this.IssuesData.Any())
{ {
<MudPaper Class="pa-3 mr-3 mt-3 border-dashed border rounded-lg mud-alert-outlined-error"> <MudPaper Class="pa-3 mr-3 mt-3 border-dashed border rounded-lg mud-alert-outlined-error">
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Issues</MudText>
@T("Issues")
</MudText>
<MudList T="string"> <MudList T="string">
@foreach (var issue in this.IssuesData) @foreach (var issue in this.IssuesData)
{ {

View File

@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class Issues : MSGComponentBase public partial class Issues : ComponentBase
{ {
[Parameter] [Parameter]
public IEnumerable<string> IssuesData { get; set; } = []; public IEnumerable<string> IssuesData { get; set; } = [];

View File

@ -1,11 +1,10 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver, ILang public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{ {
[Inject] [Inject]
protected SettingsManager SettingsManager { get; init; } = null!; protected SettingsManager SettingsManager { get; init; } = null!;
@ -13,37 +12,21 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
[Inject] [Inject]
protected MessageBus MessageBus { get; init; } = null!; protected MessageBus MessageBus { get; init; } = null!;
[Inject]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
private ILogger<MSGComponentBase> Logger { get; init; } = null!;
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override void OnInitialized()
{ {
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
this.MessageBus.RegisterComponent(this); this.MessageBus.RegisterComponent(this);
await base.OnInitializedAsync(); base.OnInitialized();
} }
#endregion #endregion
#region Implementation of ILang
/// <inheritdoc />
public string T(string fallbackEN) => this.GetText(this.Lang, fallbackEN);
/// <inheritdoc />
public string T(string fallbackEN, string? typeNamespace, string? typeName) => this.GetText(this.Lang, fallbackEN, typeNamespace, typeName);
#endregion
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public abstract string ComponentName { get; }
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
{ {
@ -51,32 +34,28 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
this.StateHasChanged(); this.StateHasChanged();
break; break;
case Event.PLUGINS_RELOADED: default:
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin(); return this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
await this.InvokeAsync(this.StateHasChanged);
break;
} }
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
}
public async Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return await this.ProcessIncomingMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data);
}
#endregion
protected virtual Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
return Task.CompletedTask; return Task.CompletedTask;
} }
protected virtual Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) public abstract Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
#endregion
#region Implementation of IDisposable
public void Dispose()
{ {
return Task.FromResult<TResult?>(default); this.MessageBus.Unregister(this);
} }
#endregion
protected async Task SendMessage<T>(Event triggeredEvent, T? data = default) protected async Task SendMessage<T>(Event triggeredEvent, T? data = default)
{ {
await this.MessageBus.SendMessage(this, triggeredEvent, data); await this.MessageBus.SendMessage(this, triggeredEvent, data);
@ -87,34 +66,14 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
return await this.MessageBus.SendMessageUseFirstResult<TPayload, TResult>(this, triggeredEvent, data); return await this.MessageBus.SendMessageUseFirstResult<TPayload, TResult>(this, triggeredEvent, data);
} }
/// <summary> protected void ApplyFilters(ComponentBase[] components, Event[] events)
/// Define for which components and events you want to receive messages.
/// </summary>
/// <param name="filterComponents">A list of components for which you want to receive messages. Use an empty list to receive messages from all components.</param>
/// <param name="events">A list of events for which you want to receive messages.</param>
protected void ApplyFilters(ComponentBase[] filterComponents, Event[] events)
{ {
// Append the color theme changed event to the list of events: // Append the color theme changed event to the list of events:
var eventsList = new List<Event>(events) var eventsList = new List<Event>(events)
{ {
Event.COLOR_THEME_CHANGED, Event.COLOR_THEME_CHANGED
Event.PLUGINS_RELOADED,
}; };
this.MessageBus.ApplyFilters(this, filterComponents, eventsList.ToArray()); this.MessageBus.ApplyFilters(this, components, eventsList.ToArray());
} }
protected virtual void DisposeResources()
{
}
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
this.DisposeResources();
}
#endregion
} }

View File

@ -1,32 +1,31 @@
@inherits MSGComponentBase
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time.") Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time.
</MudText> </MudText>
<MudText Typo="Typo.h4"> <MudText Typo="Typo.h4">
@T("Personal Needs and Limitations of Web Services") Personal Needs and Limitations of Web Services
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge.") Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge.
</MudText> </MudText>
<MudText Typo="Typo.h4"> <MudText Typo="Typo.h4">
@T("Limitations of Existing Solutions") Limitations of Existing Solutions
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations.") While exploring available solutions, I found a desktop application called <a href="https://github.com/Mintplex-Labs/anything-llm" target="_blank">Anything LLM</a>. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations.
</MudText> </MudText>
<MudText Typo="Typo.h4"> <MudText Typo="Typo.h4">
@T("Cross-Platform and Modern Development") Cross-Platform and Modern Development
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications.") Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications.
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.") Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.
</MudText> </MudText>

View File

@ -1,3 +1,7 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class Motivation : MSGComponentBase; public partial class Motivation : ComponentBase
{
}

View File

@ -2,9 +2,7 @@
@foreach(var item in this.Items) @foreach(var item in this.Items)
{ {
<MudListItem T="string" Icon="@this.Icon" Style="display: flex; align-items: flex-start;"> <MudListItem T="string" Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
<MudText Typo="Typo.body1" Align="Align.Justify" Style="hyphens: auto; word-break: auto-phrase;"> <MudText Typo="Typo.body1" Align="Align.Justify" Style="hyphens: auto; word-break: auto-phrase;"><b>@item.Header:</b> @item.Text</MudText>
<b>@item.Header:</b> @item.Text
</MudText>
</MudListItem> </MudListItem>
} }
</MudList> </MudList>

View File

@ -1,18 +1,19 @@
@inherits MSGComponentBase <MudTooltip Placement="Placement.Bottom" Arrow="@true">
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3">
@T("Alpha") Alpha
</MudChip> </MudChip>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<div style="max-width: 22em;"> <div style="max-width: 22em;">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@T("This feature is currently in the alpha phase. Expect bugs and unfinished work.") This feature is currently in the alpha phase.
Expect bugs and unfinished work.
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Alpha phase means that we are working on the last details before the beta phase.") Alpha phase means that we are working on the
last details before the beta phase.
</MudText> </MudText>
</div> </div>
</TooltipContent> </TooltipContent>

View File

@ -2,10 +2,4 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewAlpha : MSGComponentBase public partial class PreviewAlpha : ComponentBase;
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,18 +1,18 @@
@inherits MSGComponentBase <MudTooltip Placement="Placement.Bottom" Arrow="@true">
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3">
@T("Beta") Beta
</MudChip> </MudChip>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<div style="max-width: 20em;"> <div style="max-width: 20em;">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@T("This feature is currently in the beta phase. It is still be possible that there are some bugs.") This feature is currently in the beta phase.
It is still be possible that there are some bugs.
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Beta phase means that we are testing the feature.") Beta phase means that we are testing the feature.
</MudText> </MudText>
</div> </div>
</TooltipContent> </TooltipContent>

View File

@ -2,10 +2,4 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewBeta : MSGComponentBase public partial class PreviewBeta : ComponentBase;
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,18 +1,21 @@
@inherits MSGComponentBase <MudTooltip Placement="Placement.Bottom" Arrow="@true">
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3">
@T("Experimental") Experimental
</MudChip> </MudChip>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<div style="max-width: 26em;"> <div style="max-width: 26em;">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@T("This feature is currently in the experimental phase. Expect bugs, unfinished work, changes in future versions, and more.") This feature is currently in the experimental phase.
Expect bugs, unfinished work, changes in future
versions, and more.
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Experimental phase means that we have a vision for a feature but not a clear plan yet. We are still exploring the possibilities.") Experimental phase means that we have a vision for a feature
but not a clear plan yet. We are still exploring the
possibilities.
</MudText> </MudText>
</div> </div>
</TooltipContent> </TooltipContent>

View File

@ -2,10 +2,4 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewExperimental : MSGComponentBase public partial class PreviewExperimental : ComponentBase;
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,18 +1,20 @@
@inherits MSGComponentBase <MudTooltip Placement="Placement.Bottom" Arrow="@true">
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3">
@T("Prototype") Prototype
</MudChip> </MudChip>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<div style="max-width: 22em;"> <div style="max-width: 22em;">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@T("This feature is currently in the prototype phase. Expect bugs, unfinished work, changes in future versions, and more.") This feature is currently in the prototype phase.
Expect bugs, unfinished work, changes in future
versions, and more.
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Prototype phase means that we have a plan but we are still working on it.") Prototype phase means that we have a plan but we
are still working on it.
</MudText> </MudText>
</div> </div>
</TooltipContent> </TooltipContent>

View File

@ -2,10 +2,4 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewPrototype : MSGComponentBase public partial class PreviewPrototype : ComponentBase;
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,18 +1,18 @@
@inherits MSGComponentBase <MudTooltip Placement="Placement.Bottom" Arrow="@true">
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3">
@T("Release Candidate") Release Candidate
</MudChip> </MudChip>
</ChildContent> </ChildContent>
<TooltipContent> <TooltipContent>
<div style="max-width: 20em;"> <div style="max-width: 20em;">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@T("This feature is about to be released. We think it's ready for production. There should be no more bugs.") This feature is about to be released. We think it's ready for production.
There should be no more bugs.
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Release candidates are the final step before a feature is proven to be stable.") Release candidates are the final step before a feature is proven to be stable.
</MudText> </MudText>
</div> </div>
</TooltipContent> </TooltipContent>

View File

@ -2,10 +2,4 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewReleaseCandidate : MSGComponentBase public partial class PreviewReleaseCandidate : ComponentBase;
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,6 +1,6 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits MSGComponentBase
<MudSelect T="Profile" Strict="@true" Value="@this.Profile" ValueChanged="@this.SelectionChanged" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person4" Margin="Margin.Dense" Label="@T("Select one of your profiles")" Variant="Variant.Outlined" Class="mb-3" Validation="@this.Validation"> <MudSelect T="Profile" Strict="@true" Value="@this.Profile" ValueChanged="@this.SelectionChanged" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person4" Margin="Margin.Dense" Label="Select one of your profiles" Variant="Variant.Outlined" Class="mb-3" Validation="@this.Validation">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{ {
<MudSelectItem Value="profile"> <MudSelectItem Value="profile">

View File

@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ProfileFormSelection : MSGComponentBase public partial class ProfileFormSelection : ComponentBase
{ {
[Parameter] [Parameter]
public Profile Profile { get; set; } = Profile.NO_PROFILE; public Profile Profile { get; set; } = Profile.NO_PROFILE;
@ -15,6 +15,9 @@ public partial class ProfileFormSelection : MSGComponentBase
[Parameter] [Parameter]
public Func<Profile, string?> Validation { get; set; } = _ => null; public Func<Profile, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private async Task SelectionChanged(Profile profile) private async Task SelectionChanged(Profile profile)
{ {
this.Profile = profile; this.Profile = profile;

View File

@ -1,6 +1,5 @@
@inherits MSGComponentBase <MudTooltip Text="You can switch between your profiles here" Placement="Placement.Top">
<MudTooltip Text="@T("You can switch between your profiles here")" Placement="Placement.Top"> <MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
<MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{ {
<MudMenuItem OnClick="() => this.SelectionChanged(profile)"> <MudMenuItem OnClick="() => this.SelectionChanged(profile)">

View File

@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ProfileSelection : MSGComponentBase public partial class ProfileSelection : ComponentBase
{ {
[Parameter] [Parameter]
public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE; public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE;
@ -18,6 +18,9 @@ public partial class ProfileSelection : MSGComponentBase
[Parameter] [Parameter]
public string MarginRight { get; set; } = string.Empty; public string MarginRight { get; set; } = string.Empty;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}"; private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
private async Task SelectionChanged(Profile profile) private async Task SelectionChanged(Profile profile)

Some files were not shown because too many files have changed in this diff Show More