mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 20:16:27 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc53278c60 | ||
|
|
2acb6f2a57 | ||
|
|
5af616f565 | ||
|
|
6d48252db3 | ||
|
|
64e91ff4ff | ||
|
|
dddb40096d | ||
|
|
e65110a142 | ||
|
|
5045da3a91 | ||
|
|
e04879fd7f | ||
|
|
fc7197ec93 | ||
|
|
c3bf2563cd | ||
|
|
24952e796e | ||
|
|
4c328c8e72 | ||
|
|
c0e6a9a644 | ||
|
|
71ae52753a | ||
|
|
e4fa1cd72a | ||
|
|
0ea63a16c0 | ||
|
|
5272895441 | ||
|
|
f017b87abd | ||
|
|
c07a5227dc | ||
|
|
1c2d243c1f | ||
|
|
e9da7d31df | ||
|
|
b9813fcbe7 | ||
|
|
0a4208d91d | ||
|
|
102b344557 | ||
|
|
0b41f5eb96 | ||
|
|
9fc7eaff99 | ||
|
|
25595a39a5 | ||
|
|
f47dd5fdc2 | ||
|
|
5b5b6e0b28 | ||
|
|
1000d7fbc4 | ||
|
|
bd9597c706 | ||
|
|
b4c3abd6b0 | ||
|
|
86700847e9 | ||
|
|
b37f70d7ff | ||
|
|
e27cd27dba | ||
|
|
def685d2c2 | ||
|
|
a15c47b56d | ||
|
|
9f18a50f17 | ||
|
|
d05ff26e62 | ||
|
|
3e6e3bdcbd | ||
|
|
8417fa3984 | ||
|
|
fa9cdb87ed | ||
|
|
e9927ca769 | ||
|
|
a0753488b3 | ||
|
|
8853ea0cfe | ||
|
|
2317add71f | ||
|
|
7998fbcc48 | ||
|
|
c08f9e2ea1 | ||
|
|
277309cd19 | ||
|
|
d28184af1a | ||
|
|
cef1c99765 | ||
|
|
97e6003686 | ||
|
|
cad7a98e7b | ||
|
|
7a09241888 | ||
|
|
378aaaa368 | ||
|
|
9419c4ed44 | ||
|
|
91cfe8dcd0 | ||
|
|
8f0effd25b | ||
|
|
fc3c000de6 | ||
|
|
d46688f364 | ||
|
|
6fc69751b9 | ||
|
|
3360c2fa29 |
214
.github/workflows/build-and-release.yml
vendored
214
.github/workflows/build-and-release.yml
vendored
@ -12,6 +12,10 @@ on:
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') && github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') }}
|
||||
|
||||
env:
|
||||
RETENTION_INTERMEDIATE_ASSETS: 1
|
||||
RETENTION_RELEASE_ASSETS: 30
|
||||
@ -37,6 +41,8 @@ jobs:
|
||||
id: determine
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_ACTION: ${{ github.event.action }}
|
||||
ACTION_LABEL_NAME: ${{ github.event.label.name }}
|
||||
REF: ${{ github.ref }}
|
||||
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
|
||||
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -55,6 +61,11 @@ jobs:
|
||||
is_internal_pr=true
|
||||
fi
|
||||
|
||||
has_run_pipeline_label=false
|
||||
if [[ " $PR_LABELS " == *" run-pipeline "* ]]; then
|
||||
has_run_pipeline_label=true
|
||||
fi
|
||||
|
||||
if [[ "$REF" == refs/tags/v* ]]; then
|
||||
is_release=true
|
||||
build_enabled=true
|
||||
@ -65,13 +76,21 @@ jobs:
|
||||
build_enabled=true
|
||||
artifact_retention_days=7
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " == *" run-pipeline "* ]]; then
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" && "$ACTION_LABEL_NAME" == "run-pipeline" ]]; then
|
||||
is_labeled_pr=true
|
||||
is_pr_build=true
|
||||
build_enabled=true
|
||||
artifact_retention_days=3
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " != *" run-pipeline "* ]]; then
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" != "labeled" && "$has_run_pipeline_label" == "true" ]]; then
|
||||
is_labeled_pr=true
|
||||
is_pr_build=true
|
||||
build_enabled=true
|
||||
artifact_retention_days=3
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" ]]; then
|
||||
skip_reason="Build disabled: label '${ACTION_LABEL_NAME}' is not 'run-pipeline'."
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$has_run_pipeline_label" != "true" ]]; then
|
||||
skip_reason="Build disabled: PR does not have the required 'run-pipeline' label."
|
||||
fi
|
||||
|
||||
@ -310,8 +329,8 @@ jobs:
|
||||
pdfium_version=$(sed -n '11p' metadata.txt)
|
||||
pdfium_version=$(echo $pdfium_version | cut -d'.' -f3)
|
||||
|
||||
# Next line is the Qdrant version:
|
||||
qdrant_version="v$(sed -n '12p' metadata.txt)"
|
||||
# Next line is the vector store version:
|
||||
vector_store_version="$(sed -n '12p' metadata.txt)"
|
||||
|
||||
# Write the metadata to the environment:
|
||||
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
||||
@ -325,7 +344,7 @@ jobs:
|
||||
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
||||
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
|
||||
echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV
|
||||
echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV
|
||||
echo "VECTOR_STORE_VERSION=${vector_store_version}" >> $GITHUB_ENV
|
||||
|
||||
# Log the metadata:
|
||||
echo "App version: '${formatted_app_version}'"
|
||||
@ -338,7 +357,7 @@ jobs:
|
||||
echo "Tauri version: '${tauri_version}'"
|
||||
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||
echo "PDFium version: '${pdfium_version}'"
|
||||
echo "Qdrant version: '${qdrant_version}'"
|
||||
echo "Vector store version: '${vector_store_version}'"
|
||||
|
||||
- name: Read and format metadata (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
@ -383,8 +402,8 @@ jobs:
|
||||
$pdfium_version = $metadata[10]
|
||||
$pdfium_version = $pdfium_version.Split('.')[2]
|
||||
|
||||
# Next line is the necessary Qdrant version:
|
||||
$qdrant_version = "v$($metadata[11])"
|
||||
# Next line is the vector store version:
|
||||
$vector_store_version = $metadata[11]
|
||||
|
||||
# Write the metadata to the environment:
|
||||
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
|
||||
@ -397,7 +416,7 @@ jobs:
|
||||
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
|
||||
Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "QDRANT_VERSION=${qdrant_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "VECTOR_STORE_VERSION=${vector_store_version}" >> $env:GITHUB_ENV
|
||||
|
||||
# Log the metadata:
|
||||
Write-Output "App version: '${formatted_app_version}'"
|
||||
@ -410,7 +429,7 @@ jobs:
|
||||
Write-Output "Tauri version: '${tauri_version}'"
|
||||
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||
Write-Output "PDFium version: '${pdfium_version}'"
|
||||
Write-Output "Qdrant version: '${qdrant_version}'"
|
||||
Write-Output "Vector store version: '${vector_store_version}'"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
@ -539,129 +558,6 @@ jobs:
|
||||
} catch {
|
||||
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
|
||||
}
|
||||
- name: Deploy Qdrant (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
env:
|
||||
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
|
||||
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
|
||||
RUST_TARGET: ${{ matrix.rust_target }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
# Target directory:
|
||||
TDB_DIR="runtime/target/databases/qdrant"
|
||||
mkdir -p "$TDB_DIR"
|
||||
|
||||
case "${DOTNET_RUNTIME}" in
|
||||
linux-x64)
|
||||
QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz"
|
||||
DB_SOURCE="qdrant"
|
||||
DB_TARGET="qdrant-${RUST_TARGET}"
|
||||
;;
|
||||
linux-arm64)
|
||||
QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz"
|
||||
DB_SOURCE="qdrant"
|
||||
DB_TARGET="qdrant-${RUST_TARGET}"
|
||||
;;
|
||||
osx-x64)
|
||||
QDRANT_FILE="x86_64-apple-darwin.tar.gz"
|
||||
DB_SOURCE="qdrant"
|
||||
DB_TARGET="qdrant-${RUST_TARGET}"
|
||||
;;
|
||||
osx-arm64)
|
||||
QDRANT_FILE="aarch64-apple-darwin.tar.gz"
|
||||
DB_SOURCE="qdrant"
|
||||
DB_TARGET="qdrant-${RUST_TARGET}"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown platform: ${DOTNET_RUNTIME}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-${QDRANT_FILE}"
|
||||
|
||||
echo "Download Qdrant $QDRANT_URL ..."
|
||||
TMP=$(mktemp -d)
|
||||
ARCHIVE="${TMP}/qdrant.tgz"
|
||||
|
||||
curl -fsSL -o "$ARCHIVE" "$QDRANT_URL"
|
||||
|
||||
echo "Extracting Qdrant ..."
|
||||
tar xzf "$ARCHIVE" -C "$TMP"
|
||||
SRC="${TMP}/${DB_SOURCE}"
|
||||
|
||||
if [ ! -f "$SRC" ]; then
|
||||
echo "Was not able to find Qdrant source: $SRC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/"
|
||||
cp -f "$SRC" "$TDB_DIR/$DB_TARGET"
|
||||
|
||||
echo "Cleaning up ..."
|
||||
rm -fr "$TMP"
|
||||
|
||||
- name: Deploy Qdrant (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
env:
|
||||
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
|
||||
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
|
||||
RUST_TARGET: ${{ matrix.rust_target }}
|
||||
run: |
|
||||
$TDB_DIR = "runtime\target\databases\qdrant"
|
||||
New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null
|
||||
|
||||
switch ($env:DOTNET_RUNTIME) {
|
||||
"win-x64" {
|
||||
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
|
||||
$DB_SOURCE = "qdrant.exe"
|
||||
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
|
||||
}
|
||||
"win-arm64" {
|
||||
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
|
||||
$DB_SOURCE = "qdrant.exe"
|
||||
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
|
||||
}
|
||||
default {
|
||||
Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$QDRANT_URL = "https://github.com/qdrant/qdrant/releases/download/$($env:QDRANT_VERSION)/qdrant-$QDRANT_FILE"
|
||||
Write-Host "Download $QDRANT_URL ..."
|
||||
|
||||
# Create a unique temporary directory (not just a file)
|
||||
$TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
|
||||
New-Item -ItemType Directory -Path $TMP -Force | Out-Null
|
||||
$ARCHIVE = Join-Path $TMP "qdrant.tgz"
|
||||
|
||||
Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE
|
||||
|
||||
Write-Host "Extracting Qdrant ..."
|
||||
tar -xzf $ARCHIVE -C $TMP
|
||||
|
||||
$SRC = Join-Path $TMP $DB_SOURCE
|
||||
if (!(Test-Path $SRC)) {
|
||||
Write-Error "Cannot find Qdrant source: $SRC"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$DEST = Join-Path $TDB_DIR $DB_TARGET
|
||||
Copy-Item -Path $SRC -Destination $DEST -Force
|
||||
|
||||
Write-Host "Cleaning up ..."
|
||||
Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Try to remove the temporary directory, but ignore errors if files are still in use
|
||||
try {
|
||||
Remove-Item $TMP -Recurse -Force -ErrorAction Stop
|
||||
Write-Host "Successfully cleaned up temporary directory: $TMP"
|
||||
} catch {
|
||||
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
- name: Build .NET project
|
||||
run: |
|
||||
cd "app/MindWork AI Studio"
|
||||
@ -685,11 +581,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin
|
||||
~/.cargo/git/db/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.rustup/toolchains
|
||||
runtime/target
|
||||
|
||||
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
|
||||
@ -699,24 +593,33 @@ jobs:
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.rust_target }}
|
||||
|
||||
- name: Cache Tauri CLI
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo-tauri-cli
|
||||
key: tauri-cli-v2-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Setup dependencies (Ubuntu-specific, x86)
|
||||
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
|
||||
- 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.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
|
||||
- name: Setup Tauri (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
run: |
|
||||
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$HOME/.cargo-tauri-cli/bin:$PATH"
|
||||
|
||||
if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$HOME/.cargo-tauri-cli"
|
||||
else
|
||||
echo "Tauri CLI v2 is already installed"
|
||||
fi
|
||||
@ -724,9 +627,12 @@ jobs:
|
||||
- name: Setup Tauri (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: |
|
||||
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
|
||||
$env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH"
|
||||
|
||||
$tauriVersion = cargo tauri --version 2>$null
|
||||
if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') {
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli"
|
||||
} else {
|
||||
Write-Output "Tauri CLI v2 is already installed"
|
||||
}
|
||||
@ -771,17 +677,29 @@ jobs:
|
||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||
run: |
|
||||
bundles="${{ matrix.tauri_bundle }}"
|
||||
tauri_config_args=()
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
||||
echo "Running PR test build without updater bundle signing"
|
||||
bundles="${{ matrix.tauri_bundle_pr }}"
|
||||
tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||
else
|
||||
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
fi
|
||||
|
||||
cd runtime
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles"
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" "${tauri_config_args[@]}"
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
||||
updater_artifact_count=$(find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) | wc -l)
|
||||
|
||||
if [ "$updater_artifact_count" -ne 0 ]; then
|
||||
echo "PR builds must not generate updater artifacts."
|
||||
find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \)
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
|
||||
app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l)
|
||||
@ -800,17 +718,29 @@ jobs:
|
||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||
run: |
|
||||
$bundles = "${{ matrix.tauri_bundle }}"
|
||||
$tauriConfigArgs = @()
|
||||
|
||||
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
||||
Write-Output "Running PR test build without updater bundle signing"
|
||||
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
||||
$tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||
} else {
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
}
|
||||
|
||||
cd runtime
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles @tauriConfigArgs
|
||||
|
||||
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
||||
$updaterArtifacts = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle" -Recurse -File -Include "*.app.tar.gz*", "*.AppImage.tar.gz*", "*nsis.zip*" -ErrorAction SilentlyContinue
|
||||
|
||||
if ($updaterArtifacts.Count -ne 0) {
|
||||
Write-Error "PR builds must not generate updater artifacts."
|
||||
$updaterArtifacts | ForEach-Object { Write-Error $_.FullName }
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Upload artifact (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
@ -1125,7 +1055,7 @@ jobs:
|
||||
with:
|
||||
prerelease: true
|
||||
draft: false
|
||||
make_latest: true
|
||||
make_latest: false
|
||||
body: ${{ env.CHANGELOG }}
|
||||
name: "Release ${{ env.FORMATTED_VERSION }}"
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
28
AGENTS.md
28
AGENTS.md
@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
MindWork AI Studio is a cross-platform desktop application for interacting with Large Language Models (LLMs). The app uses a hybrid architecture combining a Rust Tauri runtime (for the native desktop shell) with a .NET Blazor Server web application (for the UI and business logic).
|
||||
|
||||
**Key Architecture Points:**
|
||||
- **Runtime:** Rust-based Tauri v1.8 application providing the native window, system integration, and IPC layer
|
||||
- **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer
|
||||
- **App:** .NET 9 Blazor Server application providing the UI and core functionality
|
||||
- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
|
||||
- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
|
||||
@ -18,7 +18,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
|
||||
### Prerequisites
|
||||
- .NET 9 SDK
|
||||
- Rust toolchain (stable)
|
||||
- Tauri v1.6.2 CLI: `cargo install --version 1.6.2 tauri-cli`
|
||||
- Tauri v2 CLI
|
||||
- Tauri prerequisites (platform-specific dependencies)
|
||||
- **Note:** Development on Linux is discouraged due to complex Tauri dependencies that vary by distribution
|
||||
|
||||
@ -49,7 +49,7 @@ Currently, no automated test suite exists in the repository.
|
||||
Key modules:
|
||||
- `app_window.rs` - Tauri window management, updater integration
|
||||
- `dotnet.rs` - Launches and manages the .NET sidecar process
|
||||
- `runtime_api.rs` - Rocket-based HTTPS API for .NET ↔ Rust communication
|
||||
- `runtime_api.rs` - Axum-based HTTPS API for .NET ↔ Rust communication
|
||||
- `certificate.rs` - Generates self-signed TLS certificates for secure IPC
|
||||
- `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager)
|
||||
- `clipboard.rs` - Cross-platform clipboard operations
|
||||
@ -112,12 +112,16 @@ Plugins can configure:
|
||||
- Chat templates
|
||||
- etc.
|
||||
|
||||
When adding configuration options, update:
|
||||
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs`: In method `TryProcessConfiguration` register new options.
|
||||
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs`: In method `LoadAll` check for leftover configuration.
|
||||
- The corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, when adding config options (in contrast to complex config. objects)
|
||||
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
|
||||
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option.
|
||||
Configuration plugins provide three kinds of values:
|
||||
- **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults.
|
||||
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
|
||||
- **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
|
||||
|
||||
When adding configuration plugin capabilities:
|
||||
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, process the setting in `PluginConfiguration.TryProcessConfiguration`, and check for leftover managed configuration in `PluginFactory.Loading.LoadAll`.
|
||||
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
|
||||
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
|
||||
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
|
||||
|
||||
## RAG (Retrieval-Augmented Generation)
|
||||
|
||||
@ -151,8 +155,8 @@ Multi-level confidence scheme allows users to control which providers see which
|
||||
## Dependencies and Frameworks
|
||||
|
||||
**Rust:**
|
||||
- Tauri 1.8 - Desktop application framework
|
||||
- Rocket - HTTPS API server
|
||||
- Tauri 2 - Desktop application framework
|
||||
- Axum - HTTPS API server
|
||||
- tokio - Async runtime
|
||||
- keyring - OS keyring integration
|
||||
- pdfium-render - PDF text extraction
|
||||
@ -187,6 +191,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
||||
- **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >`
|
||||
- **End of file formatting** - Do not append an extra empty line at the end of files.
|
||||
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
|
||||
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
|
||||
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
||||
- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing.
|
||||
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
||||
@ -195,6 +200,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
||||
- **Encryption** - Initialized before Rust service is marked ready
|
||||
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
|
||||
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
|
||||
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
|
||||
- **Empty lines** - Avoid adding extra empty lines at the end of files.
|
||||
|
||||
## Changelogs
|
||||
|
||||
11
README.md
11
README.md
@ -28,12 +28,11 @@ Since November 2024: Work on RAG (integration of your data and files) has begun.
|
||||
- [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] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~
|
||||
- [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
|
||||
- [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~
|
||||
- [ ] App: Implement the continuous process of vectorizing data
|
||||
- [ ] App: Implement the continuous process of vectorizing data (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
|
||||
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
|
||||
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
|
||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||
@ -79,6 +78,9 @@ Since March 2025: We have started developing the plugin system. There will be la
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
- v26.6.2: Expanded enterprise configuration options with chat defaults, custom introduction panels, trust settings for data security, and managed confidence levels; added auto-backups for app settings & the possibility to view managed profiles and chat templates.
|
||||
- v26.6.1: Increased enterprise configuration capacity for large organizations, broader Flatpak deployment support, startup and Linux package diagnostics, chat search across all workspaces, improved workspace workflows, better model discovery for self-hosted llama.cpp providers, and fixes for profile and chat template updates, workspace naming, and startup behavior.
|
||||
- v26.5.5: Released voice recording and transcription for all users; added support for multiple chats running at the same time, export options for profiles, chat templates, and ERI data sources, organization-managed ERI servers, and configurable request timeouts; upgraded the native runtime to Tauri v2.
|
||||
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
|
||||
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
|
||||
- v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection.
|
||||
@ -88,9 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la
|
||||
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
|
||||
- v0.9.45: Added chat templates to AI Studio, allowing you to create and use a library of system prompts for your chats.
|
||||
- v0.9.44: Added PDF import to the text summarizer, translation, and legal check assistants, allowing you to import PDF files and use them as input for the assistants.
|
||||
- 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.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@ -53,6 +53,9 @@ public sealed partial class CollectI18NKeysCommand
|
||||
foreach (var filePath in allFiles)
|
||||
{
|
||||
counter++;
|
||||
if(!this.IsSupportedSourceFile(filePath))
|
||||
continue;
|
||||
|
||||
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
@ -68,6 +71,9 @@ public sealed partial class CollectI18NKeysCommand
|
||||
continue;
|
||||
|
||||
var ns = this.DetermineNamespace(filePath);
|
||||
if(ns is null)
|
||||
throw new InvalidOperationException($"Could not determine the namespace for I18N source file '{filePath}'.");
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
var name = this.DetermineTypeName(filePath)
|
||||
@ -204,6 +210,10 @@ public sealed partial class CollectI18NKeysCommand
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private bool IsSupportedSourceFile(string filePath) =>
|
||||
filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
|
||||
filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private string? DetermineNamespace(string filePath)
|
||||
{
|
||||
@ -302,10 +312,10 @@ public sealed partial class CollectI18NKeysCommand
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||
[GeneratedRegex("""(?m)^\s*@namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||
private static partial Regex BlazorNamespaceRegex();
|
||||
|
||||
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||
[GeneratedRegex("""(?m)^\s*namespace\s+([a-zA-Z0-9_.]+)\s*[;{]""")]
|
||||
private static partial Regex CSharpNamespaceRegex();
|
||||
|
||||
[GeneratedRegex("""\bpartial\s+(?:class|struct|interface|record(?:\s+(?:class|struct))?)\s+([A-Za-z_][A-Za-z0-9_]*)""")]
|
||||
|
||||
@ -7,74 +7,95 @@ namespace Build.Commands;
|
||||
|
||||
public static class Pdfium
|
||||
{
|
||||
public static async Task InstallAsync(RID rid, string version)
|
||||
private static readonly HttpClient CLIENT = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
public static async Task InstallAsync(RID rid, string version, bool offline)
|
||||
{
|
||||
Console.Write($"- Installing Pdfium {version} for {rid.ToUserFriendlyName()} ...");
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var pdfiumTmpDownloadPath = Path.GetTempFileName();
|
||||
var pdfiumTmpExtractPath = Directory.CreateTempSubdirectory();
|
||||
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
|
||||
var library = GetLibraryPath(rid);
|
||||
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
|
||||
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using (var client = new HttpClient())
|
||||
if (offline)
|
||||
{
|
||||
var response = await client.GetAsync(pdfiumUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
if (File.Exists(pdfiumLibTargetPath))
|
||||
{
|
||||
Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}");
|
||||
Console.WriteLine(" offline mode enabled and library already exists, skipping download");
|
||||
return;
|
||||
}
|
||||
|
||||
await using var fileStream = File.Create(pdfiumTmpDownloadPath);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the downloaded file:
|
||||
//
|
||||
Console.Write(" extracting ...");
|
||||
await using(var tgzStream = File.Open(pdfiumTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
|
||||
await TarFile.ExtractToDirectoryAsync(uncompressedStream, pdfiumTmpExtractPath.FullName, true);
|
||||
}
|
||||
|
||||
//
|
||||
// Copy the library to the target directory:
|
||||
//
|
||||
Console.Write(" deploying ...");
|
||||
var library = GetLibraryPath(rid);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(library.Path))
|
||||
{
|
||||
Console.WriteLine($" failed to find the library path for {rid.ToUserFriendlyName()}");
|
||||
return;
|
||||
}
|
||||
|
||||
var pdfiumLibSourcePath = Path.Join(pdfiumTmpExtractPath.FullName, library.Path);
|
||||
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
|
||||
if (!File.Exists(pdfiumLibSourcePath))
|
||||
|
||||
var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries");
|
||||
var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp");
|
||||
var pdfiumLibArchivePath = library.Path.Replace('\\', '/');
|
||||
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library file '{pdfiumLibSourcePath}'");
|
||||
Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.Join(cwd, "resources", "libraries"));
|
||||
if (File.Exists(pdfiumLibTargetPath))
|
||||
File.Delete(pdfiumLibTargetPath);
|
||||
|
||||
File.Copy(pdfiumLibSourcePath, pdfiumLibTargetPath);
|
||||
|
||||
|
||||
//
|
||||
// Cleanup:
|
||||
// Extract the library from the downloaded file:
|
||||
//
|
||||
Console.Write(" cleaning up ...");
|
||||
File.Delete(pdfiumTmpDownloadPath);
|
||||
Directory.Delete(pdfiumTmpExtractPath.FullName, true);
|
||||
|
||||
Console.Write(" extracting ...");
|
||||
Directory.CreateDirectory(pdfiumLibTargetDirectory);
|
||||
|
||||
var foundLibrary = false;
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await response.Content.ReadAsStreamAsync();
|
||||
await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress);
|
||||
await using var tarReader = new TarReader(uncompressedStream);
|
||||
|
||||
while (await tarReader.GetNextEntryAsync() is { } entry)
|
||||
{
|
||||
if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (entry.DataStream == null)
|
||||
break;
|
||||
|
||||
await using var fileStream = File.Create(pdfiumLibTmpTargetPath);
|
||||
await entry.DataStream.CopyToAsync(fileStream);
|
||||
foundLibrary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundLibrary)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Write(" deploying ...");
|
||||
File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(pdfiumLibTmpTargetPath))
|
||||
File.Delete(pdfiumLibTmpTargetPath);
|
||||
}
|
||||
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public static class Qdrant
|
||||
{
|
||||
public static async Task InstallAsync(RID rid, string version)
|
||||
{
|
||||
Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ...");
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var qdrantTmpDownloadPath = Path.GetTempFileName();
|
||||
var qdrantTmpExtractPath = Directory.CreateTempSubdirectory();
|
||||
var qdrantUrl = GetQdrantDownloadUrl(rid, version);
|
||||
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var response = await client.GetAsync(qdrantUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}");
|
||||
return;
|
||||
}
|
||||
|
||||
await using var fileStream = File.Create(qdrantTmpDownloadPath);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the downloaded file:
|
||||
//
|
||||
Console.Write(" extracting ...");
|
||||
await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
if (rid == RID.WIN_X64)
|
||||
{
|
||||
using var archive = new ZipArchive(zStream, ZipArchiveMode.Read);
|
||||
archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress);
|
||||
await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Copy the database to the target directory:
|
||||
//
|
||||
Console.Write(" deploying ...");
|
||||
var database = GetDatabasePath(rid);
|
||||
if (string.IsNullOrWhiteSpace(database.Path))
|
||||
{
|
||||
Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}");
|
||||
return;
|
||||
}
|
||||
|
||||
var qdrantDbSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path);
|
||||
var qdrantDbTargetPath = Path.Join(cwd, "target", "databases", "qdrant",database.Filename);
|
||||
if (!File.Exists(qdrantDbSourcePath))
|
||||
{
|
||||
Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant"));
|
||||
if (File.Exists(qdrantDbTargetPath))
|
||||
File.Delete(qdrantDbTargetPath);
|
||||
|
||||
File.Copy(qdrantDbSourcePath, qdrantDbTargetPath);
|
||||
|
||||
//
|
||||
// Cleanup:
|
||||
//
|
||||
Console.Write(" cleaning up ...");
|
||||
File.Delete(qdrantTmpDownloadPath);
|
||||
Directory.Delete(qdrantTmpExtractPath.FullName, true);
|
||||
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
|
||||
private static Database GetDatabasePath(RID rid) => rid switch
|
||||
{
|
||||
RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
|
||||
RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
|
||||
|
||||
RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
|
||||
RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
|
||||
|
||||
RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"),
|
||||
RID.WIN_ARM64 => new("qdrant.exe", "qdrant-aarch64-pc-windows-msvc.exe"),
|
||||
|
||||
_ => new(string.Empty, string.Empty),
|
||||
};
|
||||
|
||||
private static string GetQdrantDownloadUrl(RID rid, string version)
|
||||
{
|
||||
var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-";
|
||||
return rid switch
|
||||
{
|
||||
RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz",
|
||||
RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz",
|
||||
|
||||
RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz",
|
||||
RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz",
|
||||
|
||||
RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
|
||||
RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,8 @@ public sealed partial class UpdateMetadataCommands
|
||||
[Command("release", Description = "Prepare & build the next release")]
|
||||
public async Task Release(
|
||||
[Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE,
|
||||
[Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null)
|
||||
[Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null,
|
||||
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
@ -42,7 +43,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
|
||||
// Build once to allow the Rust compiler to read the changed metadata
|
||||
// and to update all .NET artifacts:
|
||||
await this.Build();
|
||||
await this.Build(offline);
|
||||
|
||||
// Now, we update the web assets (which may were updated by the first build):
|
||||
new UpdateWebAssetsCommand().UpdateWebAssets();
|
||||
@ -53,7 +54,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
|
||||
// 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();
|
||||
await this.Build(offline);
|
||||
}
|
||||
|
||||
[Command("update-versions", Description = "The command will update the package versions in the metadata file")]
|
||||
@ -69,6 +70,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
await this.UpdateRustVersion();
|
||||
await this.UpdateMudBlazorVersion();
|
||||
await this.UpdateTauriVersion();
|
||||
await this.UpdateVectorStoreVersion();
|
||||
}
|
||||
|
||||
[Command("prepare", Description = "Prepare the metadata for the next release")]
|
||||
@ -126,6 +128,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
await this.UpdateRustVersion();
|
||||
await this.UpdateMudBlazorVersion();
|
||||
await this.UpdateTauriVersion();
|
||||
await this.UpdateVectorStoreVersion();
|
||||
await this.UpdateProjectCommitHash();
|
||||
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
|
||||
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs")));
|
||||
@ -134,7 +137,8 @@ public sealed partial class UpdateMetadataCommands
|
||||
}
|
||||
|
||||
[Command("build", Description = "Build MindWork AI Studio")]
|
||||
public async Task Build()
|
||||
public async Task Build(
|
||||
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
@ -147,12 +151,11 @@ public sealed partial class UpdateMetadataCommands
|
||||
|
||||
Console.WriteLine("==============================");
|
||||
await this.UpdateArchitecture(rid);
|
||||
await this.UpdateTauriVersion();
|
||||
await this.UpdateVectorStoreVersion();
|
||||
|
||||
var pdfiumVersion = await this.ReadPdfiumVersion();
|
||||
await Pdfium.InstallAsync(rid, pdfiumVersion);
|
||||
|
||||
var qdrantVersion = await this.ReadQdrantVersion();
|
||||
await Qdrant.InstallAsync(rid, qdrantVersion);
|
||||
await Pdfium.InstallAsync(rid, pdfiumVersion, Environment.IsOfflineBuildRequested(offline));
|
||||
|
||||
Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ...");
|
||||
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
|
||||
@ -367,16 +370,6 @@ public sealed partial class UpdateMetadataCommands
|
||||
return shortVersion;
|
||||
}
|
||||
|
||||
private async Task<string> ReadQdrantVersion()
|
||||
{
|
||||
const int QDRANT_VERSION_INDEX = 11;
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim();
|
||||
|
||||
return currentQdrantVersion;
|
||||
}
|
||||
|
||||
private async Task UpdateArchitecture(RID rid)
|
||||
{
|
||||
const int ARCHITECTURE_INDEX = 9;
|
||||
@ -529,7 +522,32 @@ public sealed partial class UpdateMetadataCommands
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
|
||||
private async Task UpdateVectorStoreVersion()
|
||||
{
|
||||
const int VECTOR_STORE_VERSION_INDEX = 11;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentVectorStoreVersion = lines[VECTOR_STORE_VERSION_INDEX].Trim();
|
||||
|
||||
var matches = await this.DetermineVersion("Qdrant Edge", Environment.GetRustRuntimeDirectory(), QdrantEdgeVersionRegex(), "cargo", "tree --depth 1");
|
||||
if (matches.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedVectorStoreVersion = matches[0].Groups["version"].Value;
|
||||
if(currentVectorStoreVersion == updatedVectorStoreVersion)
|
||||
{
|
||||
Console.WriteLine("- The vector store version is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Updated vector store version from {currentVectorStoreVersion} to {updatedVectorStoreVersion}.");
|
||||
lines[VECTOR_STORE_VERSION_INDEX] = updatedVectorStoreVersion;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task UpdateMudBlazorVersion()
|
||||
{
|
||||
const int MUD_BLAZOR_VERSION_INDEX = 6;
|
||||
@ -720,6 +738,9 @@ public sealed partial class UpdateMetadataCommands
|
||||
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
|
||||
private static partial Regex MudBlazorVersionRegex();
|
||||
|
||||
[GeneratedRegex("""qdrant-edge\s+v(?<version>[0-9.]+)""")]
|
||||
private static partial Regex QdrantEdgeVersionRegex();
|
||||
|
||||
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
|
||||
private static partial Regex TauriVersionRegex();
|
||||
|
||||
@ -731,4 +752,4 @@ public sealed partial class UpdateMetadataCommands
|
||||
|
||||
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
|
||||
private static partial Regex AppVersionRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ namespace Build.Tools;
|
||||
public static class Environment
|
||||
{
|
||||
public const string DOTNET_VERSION = "net9.0";
|
||||
public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE";
|
||||
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);
|
||||
@ -47,6 +48,19 @@ public static class Environment
|
||||
return Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
public static bool IsOfflineBuildRequested(bool offlineOption)
|
||||
{
|
||||
if (offlineOption)
|
||||
return true;
|
||||
|
||||
var environmentValue = global::System.Environment.GetEnvironmentVariable(BUILD_OFFLINE_ENVIRONMENT_VARIABLE);
|
||||
return environmentValue?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"1" or "true" or "yes" or "on" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
public static string? GetOS()
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<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/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERIV/@EntryIndexedValue">ERIV</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/=HF/@EntryIndexedValue">HF</s:String>
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
|
||||
{
|
||||
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.ProviderSettings.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>));
|
||||
|
||||
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.Confidence.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
|
||||
|
||||
private IReadOnlyList<Tools.Components> VisibleSendToAssistants => Enum.GetValues<AIStudio.Tools.Components>()
|
||||
.Where(this.CanSendToAssistant)
|
||||
@ -328,22 +328,40 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
this.isProcessing = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
if(manageCancellationLocally)
|
||||
try
|
||||
{
|
||||
this.CancellationTokenSource.Dispose();
|
||||
this.CancellationTokenSource = null;
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
|
||||
|
||||
// Return the AI response:
|
||||
return aiText.Text;
|
||||
}
|
||||
catch (ProviderRequestException e)
|
||||
{
|
||||
this.Logger.LogError(e, "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", this.Title, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
|
||||
|
||||
if (this.resultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text))
|
||||
{
|
||||
this.ChatThread?.Blocks.Remove(this.resultingContentBlock);
|
||||
this.resultingContentBlock = null;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Return the AI response:
|
||||
return aiText.Text;
|
||||
if(manageCancellationLocally)
|
||||
{
|
||||
this.CancellationTokenSource?.Dispose();
|
||||
this.CancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
|
||||
@ -10,6 +10,8 @@ using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Assistants.DocumentAnalysis;
|
||||
@ -437,10 +439,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
private ConfidenceLevel GetPolicyMinimumConfidenceLevel()
|
||||
{
|
||||
var minimumLevel = ConfidenceLevel.NONE;
|
||||
var llmSettings = this.SettingsManager.ConfigurationData.LLMProviders;
|
||||
var enforceGlobalMinimumConfidence = llmSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
|
||||
var confidenceSettings = this.SettingsManager.ConfigurationData.Confidence;
|
||||
var enforceGlobalMinimumConfidence = confidenceSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
|
||||
if (enforceGlobalMinimumConfidence)
|
||||
minimumLevel = llmSettings.GlobalMinimumConfidence;
|
||||
minimumLevel = confidenceSettings.GlobalMinimumConfidence;
|
||||
|
||||
if (this.selectedPolicy is not null && this.selectedPolicy.MinimumProviderConfidence > minimumLevel)
|
||||
minimumLevel = this.selectedPolicy.MinimumProviderConfidence;
|
||||
@ -747,16 +749,12 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return $$"""
|
||||
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
||||
["Id"] = "{{id}}",
|
||||
["PolicyName"] = "{{this.selectedPolicy.PolicyName.Trim()}}",
|
||||
["PolicyDescription"] = "{{this.selectedPolicy.PolicyDescription.Trim()}}",
|
||||
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyName.Trim())}},
|
||||
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyDescription.Trim())}},
|
||||
|
||||
["AnalysisRules"] = [===[
|
||||
{{this.selectedPolicy.AnalysisRules.Trim()}}
|
||||
]===],
|
||||
["AnalysisRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.AnalysisRules.Trim(), forceLongString: true)}},
|
||||
|
||||
["OutputRules"] = [===[
|
||||
{{this.selectedPolicy.OutputRules.Trim()}}
|
||||
]===],
|
||||
["OutputRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.OutputRules.Trim(), forceLongString: true)}},
|
||||
|
||||
-- Optional: minimum provider confidence required for this policy.
|
||||
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||
@T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")
|
||||
</MudJustifiedText>
|
||||
<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/>
|
||||
<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||
|
||||
@ -94,6 +94,8 @@ public sealed record ChatThread
|
||||
/// <returns>The prepared system prompt.</returns>
|
||||
public string PrepareSystemPrompt(SettingsManager settingsManager)
|
||||
{
|
||||
this.allowProfile = true;
|
||||
|
||||
//
|
||||
// Use the information from the chat template, if provided. Otherwise, use the default system prompt
|
||||
//
|
||||
@ -111,8 +113,8 @@ public sealed record ChatThread
|
||||
systemPromptTextWithChatTemplate = this.SystemPrompt;
|
||||
else
|
||||
{
|
||||
var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.SelectedChatTemplate);
|
||||
if(chatTemplate == null)
|
||||
var chatTemplate = settingsManager.GetChatTemplateById(this.SelectedChatTemplate);
|
||||
if(chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE)
|
||||
systemPromptTextWithChatTemplate = this.SystemPrompt;
|
||||
else
|
||||
{
|
||||
@ -168,8 +170,8 @@ public sealed record ChatThread
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.SelectedProfile);
|
||||
if(profile is null)
|
||||
var profile = settingsManager.GetProfileById(this.SelectedProfile);
|
||||
if(profile == Profile.NO_PROFILE)
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
@ -33,12 +34,13 @@ public static class ChatThreadExtensions
|
||||
return true;
|
||||
|
||||
//
|
||||
// Is the provider self-hosted?
|
||||
// Is the provider trusted for data-source security checks?
|
||||
//
|
||||
var isSelfHostedProvider = provider switch
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
var isTrustedProvider = provider switch
|
||||
{
|
||||
ProviderSelfHosted => true,
|
||||
AIStudio.Settings.Provider p => p.IsSelfHosted,
|
||||
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
|
||||
_ => false,
|
||||
};
|
||||
@ -46,12 +48,12 @@ public static class ChatThreadExtensions
|
||||
//
|
||||
// Check the chat data security against the selected provider:
|
||||
//
|
||||
return isSelfHostedProvider switch
|
||||
return isTrustedProvider switch
|
||||
{
|
||||
// The provider is self-hosted -- we can use any data source:
|
||||
// The provider is trusted -- we can use any data source:
|
||||
true => true,
|
||||
|
||||
// The provider is not self-hosted -- it depends on the data security of the chat thread:
|
||||
// The provider is not trusted -- it depends on the data security of the chat thread:
|
||||
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,59 +93,70 @@ public sealed class ContentText : IContent
|
||||
|
||||
// Start another thread by using a task to uncouple
|
||||
// the UI thread from the AI processing:
|
||||
await Task.Run(async () =>
|
||||
try
|
||||
{
|
||||
// We show the waiting animation until we get the first response:
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Stop the waiting animation:
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += contentStreamChunk;
|
||||
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
switch (settings.ConfigurationData.App.IsSavingEnergy)
|
||||
try
|
||||
{
|
||||
// Energy saving mode is off. We notify the UI
|
||||
// as fast as possible -- no matter the odds:
|
||||
case false:
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
|
||||
// Energy saving mode is on. We notify the UI
|
||||
// only when the time between two events is
|
||||
// greater than the minimum time:
|
||||
case true when now - last > MIN_TIME:
|
||||
last = now;
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
// We show the waiting animation until we get the first response:
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Stop the waiting animation:
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += contentStreamChunk;
|
||||
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
switch (settings.ConfigurationData.App.IsSavingEnergy)
|
||||
{
|
||||
// Energy saving mode is off. We notify the UI
|
||||
// as fast as possible -- no matter the odds:
|
||||
case false:
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
|
||||
// Energy saving mode is on. We notify the UI
|
||||
// only when the time between two events is
|
||||
// greater than the minimum time:
|
||||
case true when now - last > MIN_TIME:
|
||||
last = now;
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the waiting animation (in case the loop
|
||||
// was stopped, or no content was received):
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = false;
|
||||
}, token);
|
||||
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
finally
|
||||
{
|
||||
// Stop the waiting animation (in case the loop
|
||||
// was stopped, or no content was received):
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = false;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
}
|
||||
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
|
||||
@ -89,8 +89,10 @@ public static class IImageSourceExtensions
|
||||
|
||||
case ContentImageSource.URL:
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
|
||||
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||
var timeoutToken = timeoutTokenSource.Token;
|
||||
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);
|
||||
if(response.IsSuccessStatusCode)
|
||||
{
|
||||
// Read the length of the content:
|
||||
@ -101,7 +103,7 @@ public static class IImageSourceExtensions
|
||||
return (success: false, string.Empty);
|
||||
}
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(token);
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken);
|
||||
return (success: true, Convert.ToBase64String(bytes));
|
||||
}
|
||||
|
||||
|
||||
@ -52,29 +52,42 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
|
||||
<MudText Typo="Typo.body1" Inline="true">
|
||||
@T("Drag and drop files into the marked area or click here to attach documents: ")
|
||||
</MudText>
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => this.AddFilesManually())"
|
||||
Style="vertical-align: top; margin-top: -2px;"
|
||||
Size="Size.Small">
|
||||
@T("Add file")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@if (!this.Disabled)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
|
||||
<MudText Typo="Typo.body1" Inline="true">
|
||||
@T("Drag and drop files into the marked area or click here to attach documents: ")
|
||||
</MudText>
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => this.AddFilesManually())"
|
||||
Style="vertical-align: top; margin-top: -2px;"
|
||||
Size="Size.Small">
|
||||
@T("Add file")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<MudPaper Height="20em" Outlined="true" Class="@this.dragClass" Style="overflow-y: auto;">
|
||||
@foreach (var fileAttachment in this.DocumentPaths)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/>
|
||||
@if (this.Disabled)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
</div>
|
||||
<MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete">
|
||||
@T("Clear file list")
|
||||
</MudButton>
|
||||
@if (!this.Disabled)
|
||||
{
|
||||
<MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete">
|
||||
@T("Clear file list")
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
@ -14,16 +14,16 @@ using DialogOptions = Dialogs.DialogOptions;
|
||||
public partial class AttachDocuments : MSGComponentBase
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AttachDocuments).Namespace, nameof(AttachDocuments));
|
||||
|
||||
|
||||
[Parameter]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When true, pause catching dropped files. Default is false.
|
||||
/// </summary>
|
||||
@ -38,16 +38,19 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public Func<HashSet<FileAttachment>, Task> OnChange { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool UseSmallForm { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, validate media file types before attaching. Default is true. That means that
|
||||
/// the user cannot attach unsupported media file types when the provider or model does not
|
||||
@ -56,16 +59,16 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ValidateMediaFileTypes { get; set; } = true;
|
||||
|
||||
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider? Provider { get; set; }
|
||||
|
||||
|
||||
[Inject]
|
||||
private ILogger<AttachDocuments> Logger { get; set; } = null!;
|
||||
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@ -74,17 +77,17 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them.");
|
||||
|
||||
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private bool isDraggingOver;
|
||||
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
|
||||
|
||||
// Register this drop area:
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
|
||||
await base.OnInitializedAsync();
|
||||
@ -92,6 +95,9 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
if (this.Disabled && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
|
||||
return;
|
||||
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
@ -111,7 +117,7 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
{
|
||||
if(this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
|
||||
|
||||
if(this.numDropAreasAboveThis is 0)
|
||||
this.PauseCatchingDrops = false;
|
||||
}
|
||||
@ -122,40 +128,40 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
if(!this.isComponentHovered && !this.CatchAllDocuments)
|
||||
{
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.isDraggingOver = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
this.isDraggingOver = false;
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
this.isDraggingOver = false;
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
if(!this.isComponentHovered && !this.CatchAllDocuments)
|
||||
{
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name);
|
||||
@ -197,11 +203,14 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
#endregion
|
||||
|
||||
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full";
|
||||
|
||||
|
||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||
|
||||
|
||||
private async Task AddFilesManually()
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
// Ensure that Pandoc is installed and ready:
|
||||
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
|
||||
showSuccessMessage: false,
|
||||
@ -228,43 +237,49 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
|
||||
}
|
||||
|
||||
|
||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||
await this.OnChange(this.DocumentPaths);
|
||||
}
|
||||
|
||||
|
||||
private async Task OpenAttachmentsDialog()
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths);
|
||||
}
|
||||
|
||||
private async Task ClearAllFiles()
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
this.DocumentPaths.Clear();
|
||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||
await this.OnChange(this.DocumentPaths);
|
||||
}
|
||||
|
||||
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4";
|
||||
|
||||
|
||||
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
|
||||
|
||||
|
||||
private void OnMouseEnter(EventArgs _)
|
||||
{
|
||||
if(this.PauseCatchingDrops)
|
||||
if(this.Disabled || this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name);
|
||||
this.isComponentHovered = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
private void OnMouseLeave(EventArgs _)
|
||||
{
|
||||
if(this.PauseCatchingDrops)
|
||||
if(this.Disabled || this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name);
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
@ -273,6 +288,9 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
private async Task RemoveDocument(FileAttachment fileAttachment)
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
this.DocumentPaths.Remove(fileAttachment);
|
||||
|
||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||
|
||||
@ -13,6 +13,10 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (242, "v26.6.2, build 242 (2026-06-21 14:07 UTC)", "v26.6.2.md"),
|
||||
new (241, "v26.6.1, build 241 (2026-06-11 13:49 UTC)", "v26.6.1.md"),
|
||||
new (240, "v26.5.5, build 240 (2026-05-25 18:52 UTC)", "v26.5.5.md"),
|
||||
new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"),
|
||||
new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"),
|
||||
new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"),
|
||||
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@ref="@this.inputField"
|
||||
@bind-Text="@this.userInput"
|
||||
@bind-Text="@this.UserInput"
|
||||
Variant="Variant.Outlined"
|
||||
AutoGrow="@true"
|
||||
Lines="3"
|
||||
@ -68,7 +68,7 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.isStreaming)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@ -89,35 +89,35 @@
|
||||
@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">
|
||||
<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 || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@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">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="@(() => this.MoveChatToWorkspace())"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<MudTooltip Text="@T("Bold")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BOLD)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BOLD))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Italic")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Heading")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_HEADING)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_HEADING))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Bulleted List")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Code")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_CODE)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_CODE))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
@ -129,15 +129,15 @@
|
||||
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
|
||||
{
|
||||
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||
@if (this.IsCurrentChatStreaming)
|
||||
{
|
||||
<MudTooltip Text="@T("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>
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
@ -37,6 +38,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
[Parameter]
|
||||
public Workspaces? Workspaces { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public ChatComposerState ComposerState { get; set; } = new();
|
||||
|
||||
[Inject]
|
||||
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||
@ -47,6 +51,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
@ -58,29 +65,46 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private bool mustScrollToBottomAfterRender;
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
private byte scrollRenderCountdown;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private bool mustStoreChat;
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
private bool autoSaveEnabled;
|
||||
private bool previousInputForbidden = true;
|
||||
private Guid lastSeenChatId = Guid.Empty;
|
||||
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private Guid currentChatThreadId = Guid.Empty;
|
||||
private Guid loadedParameterChatId = Guid.Empty;
|
||||
private Guid loadedParameterWorkspaceId = Guid.Empty;
|
||||
private Guid foregroundChatId = Guid.Empty;
|
||||
private int workspaceHeaderSyncVersion;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private HashSet<FileAttachment> chatDocumentPaths = [];
|
||||
|
||||
// Unfortunately, we need the input field reference to blur the focus away. Without
|
||||
// this, we cannot clear the input field.
|
||||
private MudTextField<string> inputField = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the user's input in the chat interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property serves as a bridge between the chat component and the
|
||||
/// underlying composer state, allowing user input to be dynamically updated
|
||||
/// and managed. The setter also triggers state changes within the composer
|
||||
/// to track whether the user has drafted any input.
|
||||
/// </remarks>
|
||||
private string UserInput
|
||||
{
|
||||
get => this.ComposerState.UserInput;
|
||||
set => this.ComposerState.SetUserInput(value);
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Apply the filters for the message bus:
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED, Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
@ -91,15 +115,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Get the preselected chat template:
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(deferredInput))
|
||||
this.userInput = deferredInput;
|
||||
|
||||
// Apply template's file attachments, if any:
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.SetUserInput(deferredInput);
|
||||
|
||||
//
|
||||
// Check for deferred messages of the kind 'SEND_TO_CHAT',
|
||||
@ -117,6 +138,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.ChatThread.IncludeDateTime = true;
|
||||
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread.ChatId}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
// We know already that the chat thread is not null,
|
||||
@ -217,6 +239,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Select the correct provider:
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@ -242,6 +265,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
if(this.ChatThread is not null)
|
||||
{
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
@ -266,18 +290,54 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var inputForbidden = this.IsInputForbidden();
|
||||
if (!inputForbidden && this.previousInputForbidden)
|
||||
await this.inputField.FocusAsync();
|
||||
|
||||
this.previousInputForbidden = inputForbidden;
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
var incomingChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
if (incomingChatId != this.lastSeenChatId || this.Provider != this.lastSeenProvider)
|
||||
{
|
||||
this.lastSeenChatId = incomingChatId;
|
||||
this.lastSeenProvider = this.Provider;
|
||||
this.previousInputForbidden = true;
|
||||
}
|
||||
|
||||
await this.ApplyLoadedChatParameterAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ApplyLoadedChatParameterAsync()
|
||||
{
|
||||
var chatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
var workspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
|
||||
if (this.loadedParameterChatId == chatId && this.loadedParameterWorkspaceId == workspaceId)
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedParameterChatId = chatId;
|
||||
this.loadedParameterWorkspaceId = workspaceId;
|
||||
await this.LoadedChatChanged(notifyParent: false);
|
||||
}
|
||||
|
||||
private void MarkCurrentChatAsLoadedParameter()
|
||||
{
|
||||
this.loadedParameterChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
}
|
||||
|
||||
private async Task SyncWorkspaceHeaderWithChatThreadAsync()
|
||||
{
|
||||
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
|
||||
@ -333,7 +393,46 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
private async Task RefreshRenamedWorkspaceHeaderAsync(Guid workspaceId)
|
||||
{
|
||||
var currentChatThread = this.ChatThread;
|
||||
if (currentChatThread is null || currentChatThread.WorkspaceId != workspaceId)
|
||||
return;
|
||||
|
||||
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
|
||||
var chatThreadId = currentChatThread.ChatId;
|
||||
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
|
||||
|
||||
if (syncVersion != this.workspaceHeaderSyncVersion)
|
||||
return;
|
||||
|
||||
if (this.ChatThread is null
|
||||
|| this.ChatThread.ChatId != chatThreadId
|
||||
|| this.ChatThread.WorkspaceId != workspaceId)
|
||||
return;
|
||||
|
||||
this.currentChatThreadId = chatThreadId;
|
||||
this.currentWorkspaceId = workspaceId;
|
||||
this.PublishWorkspaceNameIfChanged(loadedWorkspaceName);
|
||||
}
|
||||
|
||||
private async Task SyncForegroundChatAsync()
|
||||
{
|
||||
var nextForegroundChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
if (this.foregroundChatId == nextForegroundChatId)
|
||||
return;
|
||||
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
|
||||
this.foregroundChatId = nextForegroundChatId;
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, true);
|
||||
}
|
||||
|
||||
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
private bool IsCurrentChatStreaming => this.ChatThread is not null && this.AIJobService.IsChatGenerationActive(this.ChatThread.ChatId);
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
|
||||
|
||||
@ -352,9 +451,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName);
|
||||
|
||||
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||
|
||||
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||
private string UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||
|
||||
private string UserInputClass => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||
|
||||
private void ApplyStandardDataSourceOptions()
|
||||
{
|
||||
@ -387,7 +486,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task ProfileWasChanged(Profile profile)
|
||||
{
|
||||
this.currentProfile = profile;
|
||||
this.currentProfile = this.SettingsManager.GetProfileById(profile.Id);
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
@ -401,14 +500,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.currentChatTemplate = chatTemplate;
|
||||
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
|
||||
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
|
||||
|
||||
// Apply template's file attachments (replaces existing):
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
@ -416,6 +513,42 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await this.StartNewChat(true);
|
||||
}
|
||||
|
||||
private void RefreshCurrentProfileAndChatTemplate()
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.GetProfileById(this.currentProfile.Id);
|
||||
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id);
|
||||
}
|
||||
|
||||
private async Task RefreshChatSelectionsAfterConfigurationChange()
|
||||
{
|
||||
var previousProvider = this.Provider;
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
var chatProviderId = this.ChatThread?.SelectedProvider;
|
||||
|
||||
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProviderId);
|
||||
if (this.Provider != previousProvider)
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentProfile = string.IsNullOrWhiteSpace(this.ChatThread.SelectedProfile)
|
||||
? this.SettingsManager.GetProfileById(this.currentProfile.Id)
|
||||
: this.SettingsManager.GetProfileById(this.ChatThread.SelectedProfile);
|
||||
|
||||
this.currentChatTemplate = string.IsNullOrWhiteSpace(this.ChatThread.SelectedChatTemplate)
|
||||
? this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id)
|
||||
: this.SettingsManager.GetChatTemplateById(this.ChatThread.SelectedChatTemplate);
|
||||
}
|
||||
|
||||
if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
}
|
||||
|
||||
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
|
||||
{
|
||||
if (this.ChatThread is null)
|
||||
@ -453,7 +586,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (!this.IsProviderSelected)
|
||||
return true;
|
||||
|
||||
if(this.isStreaming)
|
||||
if(this.IsCurrentChatStreaming)
|
||||
return true;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
@ -468,6 +601,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.hasUnsavedChanges = true;
|
||||
this.ComposerState.MarkUserDraft();
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
@ -499,7 +633,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.userInput = await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType);
|
||||
this.ComposerState.SetUserInput(await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType));
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
private void ComposerAttachmentsChanged(HashSet<FileAttachment> attachments)
|
||||
{
|
||||
if (!ReferenceEquals(this.ComposerState.FileAttachments, attachments))
|
||||
this.ComposerState.ReplaceFileAttachments(attachments);
|
||||
|
||||
this.ComposerState.MarkUserDraft();
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
@ -510,7 +653,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return;
|
||||
|
||||
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
// Blur the focus away from the input field to be able to clear it:
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
@ -527,17 +672,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
DataSourceOptions = this.earlyDataSourceOptions,
|
||||
Name = this.ExtractThreadName(this.userInput),
|
||||
Name = this.ExtractThreadName(this.ComposerState.UserInput),
|
||||
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
|
||||
};
|
||||
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.ComposerState.UserInput);
|
||||
|
||||
// Update provider, profile and chat template:
|
||||
this.ChatThread.SelectedProvider = this.Provider.Id;
|
||||
@ -554,14 +700,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
IContent? lastUserPrompt;
|
||||
if (!reuseLastUserPrompt)
|
||||
{
|
||||
var normalizedAttachments = this.chatDocumentPaths
|
||||
var normalizedAttachments = this.ComposerState.FileAttachments
|
||||
.Select(attachment => attachment.Normalize())
|
||||
.Where(attachment => attachment.IsValid)
|
||||
.ToList();
|
||||
|
||||
lastUserPrompt = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
Text = this.ComposerState.UserInput,
|
||||
FileAttachments = normalizedAttachments,
|
||||
};
|
||||
|
||||
@ -608,13 +754,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Clear the input field:
|
||||
await this.inputField.FocusAsync();
|
||||
|
||||
this.userInput = string.Empty;
|
||||
this.chatDocumentPaths.Clear();
|
||||
this.ComposerState.Clear();
|
||||
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
@ -624,38 +768,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
|
||||
using (this.cancellationTokenSource = new())
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
ChatThread = this.ChatThread!,
|
||||
AIText = aiText,
|
||||
LastUserPrompt = lastUserPrompt,
|
||||
ProviderSettings = this.Provider,
|
||||
IsForeground = true,
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
|
||||
// Update the UI:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
{
|
||||
if (this.cancellationTokenSource is not null)
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
if (this.ChatThread is not null)
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
@ -685,7 +814,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
//
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@ -718,9 +847,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
//
|
||||
// Reset our state:
|
||||
//
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
//
|
||||
// Reset the LLM provider considering the user's settings:
|
||||
@ -777,17 +906,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
};
|
||||
}
|
||||
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
|
||||
// Apply template's file attachments:
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
// Now, we have to reset the data source options as well:
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
|
||||
// Notify the parent component about the change:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
@ -796,7 +922,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@ -816,7 +942,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{ x => x.ConfirmText, T("Move chat") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
@ -829,25 +955,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
|
||||
this.ChatThread!.WorkspaceId = workspaceId;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SaveThread();
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
private async Task LoadedChatChanged(bool notifyParent = true)
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(this.ChatThread.ChatId) ?? this.ChatThread;
|
||||
this.loadedParameterChatId = this.ChatThread.ChatId;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
if (notifyParent)
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.loadedParameterChatId = Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = Guid.Empty;
|
||||
this.ClearWorkspaceHeaderState();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
}
|
||||
|
||||
@ -863,12 +999,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
this.ClearWorkspaceHeaderState();
|
||||
|
||||
this.ChatThread = null;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
@ -885,14 +1022,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Try to select the profile:
|
||||
if (!string.IsNullOrWhiteSpace(chatProfile))
|
||||
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile) ?? Profile.NO_PROFILE;
|
||||
this.currentProfile = this.SettingsManager.GetProfileById(chatProfile);
|
||||
|
||||
// Try to select the chat template:
|
||||
if (!string.IsNullOrWhiteSpace(chatChatTemplate))
|
||||
{
|
||||
var selectedTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate);
|
||||
this.currentChatTemplate = selectedTemplate ?? ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
}
|
||||
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatChatTemplate);
|
||||
}
|
||||
|
||||
private async Task ToggleWorkspaceOverlay()
|
||||
@ -939,7 +1073,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(lastBlockContent is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.hasUnsavedChanges = true;
|
||||
@ -956,13 +1090,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (block is not ContentText textBlock)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RestoreComposerFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.ComposerState.RestoreFromTextBlock(textBlock);
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
@ -982,9 +1121,32 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.autoSaveEnabled)
|
||||
await this.SaveThread();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_RENAMED:
|
||||
if (data is Guid workspaceId)
|
||||
await this.RefreshRenamedWorkspaceHeaderAsync(workspaceId);
|
||||
break;
|
||||
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.RefreshChatSelectionsAfterConfigurationChange();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
|
||||
await this.LoadedChatChanged();
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
if (data is AIJobSnapshot { Kind: AIJobKind.CHAT_GENERATION } snapshot && this.ChatThread?.ChatId == snapshot.SubjectId)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
||||
if (!snapshot.IsActive)
|
||||
{
|
||||
this.hasUnsavedChanges = false;
|
||||
this.previousInputForbidden = true;
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -996,8 +1158,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
return Task.FromResult((TResult?)(object)(this.hasUnsavedChanges || this.ComposerState.HasVisibleUserDraft));
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
@ -1015,21 +1180,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
if (this.cancellationTokenSource is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
|
||||
this.cancellationTokenSource.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
65
app/MindWork AI Studio/Components/ChatComposerState.cs
Normal file
65
app/MindWork AI Studio/Components/ChatComposerState.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public sealed class ChatComposerState
|
||||
{
|
||||
public string UserInput { get; private set; } = string.Empty;
|
||||
|
||||
public HashSet<FileAttachment> FileAttachments { get; } = [];
|
||||
|
||||
public bool HasUserDraft { get; private set; }
|
||||
|
||||
public bool HasComposerContent => !string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0;
|
||||
|
||||
public bool HasVisibleUserDraft => this.HasUserDraft && (!string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0);
|
||||
|
||||
public void ApplyTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.UserInput = chatTemplate.PredefinedUserPrompt;
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in chatTemplate.FileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void SetUserInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = !string.IsNullOrWhiteSpace(userInput);
|
||||
}
|
||||
|
||||
public void SetSystemInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void MarkUserDraft()
|
||||
{
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
|
||||
public void ReplaceFileAttachments(IEnumerable<FileAttachment> fileAttachments)
|
||||
{
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in fileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.UserInput = string.Empty;
|
||||
this.FileAttachments.Clear();
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void RestoreFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.UserInput = textBlock.Text;
|
||||
this.ReplaceFileAttachments(textBlock.FileAttachments);
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
<ActivatorContent>
|
||||
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
|
||||
{
|
||||
<MudButton IconSize="Size.Large" StartIcon="@Icons.Material.Filled.RateReview" IconColor="Color.Default">
|
||||
<MudButton IconSize="Size.Large" StartIcon="@this.ChatTemplateIcon(this.CurrentChatTemplate)" IconColor="Color.Default">
|
||||
@this.CurrentChatTemplate.GetSafeName()
|
||||
</MudButton>
|
||||
}
|
||||
@ -22,7 +22,7 @@
|
||||
<MudDivider/>
|
||||
@foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates())
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.RateReview" OnClick="@(async () => await this.SelectionChanged(chatTemplate))">
|
||||
<MudMenuItem Icon="@this.ChatTemplateIcon(chatTemplate)" OnClick="@(async () => await this.SelectionChanged(chatTemplate))">
|
||||
@chatTemplate.GetSafeName()
|
||||
</MudMenuItem>
|
||||
}
|
||||
|
||||
@ -11,13 +11,13 @@ public partial class ChatTemplateSelection : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public ChatTemplate CurrentChatTemplate { get; set; } = ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool CanChatThreadBeUsedForTemplate { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<ChatTemplate> CurrentChatTemplateChanged { get; set; }
|
||||
|
||||
@ -26,24 +26,42 @@ public partial class ChatTemplateSelection : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public string MarginRight { get; set; } = string.Empty;
|
||||
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
|
||||
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string ChatTemplateIcon(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (chatTemplate.IsEnterpriseConfiguration)
|
||||
return Icons.Material.Filled.Business;
|
||||
|
||||
return Icons.Material.Filled.RateReview;
|
||||
}
|
||||
|
||||
private async Task SelectionChanged(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.CurrentChatTemplate = chatTemplate;
|
||||
await this.CurrentChatTemplateChanged.InvokeAsync(chatTemplate);
|
||||
}
|
||||
|
||||
|
||||
private async Task OpenSettingsDialog()
|
||||
{
|
||||
var dialogParameters = new DialogParameters();
|
||||
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
|
||||
private async Task CreateNewChatTemplateFromChat()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<SettingsDialogChatTemplate>
|
||||
@ -53,4 +71,16 @@ public partial class ChatTemplateSelection : MSGComponentBase
|
||||
};
|
||||
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -56,10 +56,12 @@ public abstract partial class ConfigurationBase : MSGComponentBase
|
||||
|
||||
protected bool IsDisabled => this.Disabled() || this.IsLocked();
|
||||
|
||||
private string Classes => $"{this.GetClassForBase} {MARGIN_CLASS}";
|
||||
private string Classes => $"{this.GetClassForBase} {JUSTIFIED_HELP_CLASS} {MARGIN_CLASS}";
|
||||
|
||||
private protected virtual RenderFragment? Body => null;
|
||||
|
||||
private const string JUSTIFIED_HELP_CLASS = "configuration-help-justified";
|
||||
|
||||
private const string MARGIN_CLASS = "mb-6";
|
||||
|
||||
protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
27
app/MindWork AI Studio/Components/ConfigurationFile.razor
Normal file
27
app/MindWork AI Studio/Components/ConfigurationFile.razor
Normal file
@ -0,0 +1,27 @@
|
||||
@inherits ConfigurationBaseCore
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Immediate="@true"
|
||||
Underline="false"
|
||||
Class="flex-grow-1"
|
||||
/>
|
||||
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@this.IsDisabled"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
127
app/MindWork AI Studio/Components/ConfigurationFile.razor.cs
Normal file
127
app/MindWork AI Studio/Components/ConfigurationFile.razor.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
{
|
||||
/// <summary>
|
||||
/// The text used for the textfield.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string> Text { get; set; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action which is called when the text was changed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Action<string> TextUpdate { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// The icon to display next to the textfield.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.AttachFile;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Color IconColor { get; set; } = Color.Default;
|
||||
|
||||
/// <summary>
|
||||
/// The title of the file selection dialog.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string FileDialogTitle { get; set; } = "Select File";
|
||||
|
||||
/// <summary>
|
||||
/// The optional file type filter for the file selection dialog.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public FileTypeFilter[]? Filter { get; set; }
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||
{
|
||||
AutoReset = false
|
||||
};
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool Stretch => true;
|
||||
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.timer.Elapsed += async (_, _) => await this.InvokeAsync(async () => await this.OptionChanged(this.internalText));
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
this.internalText = this.Text();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void InternalUpdate(string text)
|
||||
{
|
||||
this.timer.Stop();
|
||||
this.internalText = text;
|
||||
this.timer.Start();
|
||||
}
|
||||
|
||||
private async Task OpenFileDialog()
|
||||
{
|
||||
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
|
||||
if (response.UserCancelled)
|
||||
return;
|
||||
|
||||
this.timer.Stop();
|
||||
this.internalText = response.SelectedFilePath;
|
||||
await this.OptionChanged(response.SelectedFilePath);
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
{
|
||||
this.TextUpdate(updatedText);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.timer.Stop();
|
||||
this.timer.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -41,9 +41,9 @@ public partial class ConfigurationMinConfidenceSelection : MSGComponentBase
|
||||
if (this.SelectedValue() is ConfidenceLevel.NONE)
|
||||
return ConfidenceLevel.NONE;
|
||||
|
||||
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
|
||||
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
|
||||
{
|
||||
var minimumLevel = this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence;
|
||||
var minimumLevel = this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence;
|
||||
if(this.SelectedValue() < minimumLevel)
|
||||
return minimumLevel;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@this.Icon" Color="@this.IconColor"/>
|
||||
<MudText Typo="Typo.body1" Class="flex-grow-1">
|
||||
@if (string.IsNullOrWhiteSpace(this.Shortcut()))
|
||||
@if (string.IsNullOrWhiteSpace(this.Data.Value()))
|
||||
{
|
||||
@T("No shortcut configured")
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@ -19,22 +18,10 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The current shortcut value.
|
||||
/// The shortcut binding data.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string> Shortcut { get; set; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action which is called when the shortcut was changed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Action<string> ShortcutUpdate { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier of the shortcut (used for conflict detection and registration).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Shortcut ShortcutId { get; init; }
|
||||
public ConfigurationShortcutData Data { get; set; } = ConfigurationShortcutData.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The icon to display.
|
||||
@ -60,10 +47,18 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
|
||||
|
||||
private string GetDisplayShortcut()
|
||||
{
|
||||
var shortcut = this.Shortcut();
|
||||
var shortcut = this.Data.Value();
|
||||
if (string.IsNullOrWhiteSpace(shortcut))
|
||||
return string.Empty;
|
||||
|
||||
var shortcutDisplayName = this.Data.DisplayName();
|
||||
var shortcutDisplaySource = this.Data.DisplaySource();
|
||||
if (!string.IsNullOrWhiteSpace(shortcutDisplayName)
|
||||
&& string.Equals(shortcutDisplaySource, shortcut, StringComparison.Ordinal))
|
||||
{
|
||||
return shortcutDisplayName;
|
||||
}
|
||||
|
||||
// Convert internal format to display format:
|
||||
return shortcut
|
||||
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
|
||||
@ -80,8 +75,8 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ShortcutDialog>
|
||||
{
|
||||
{ x => x.InitialShortcut, this.Shortcut() },
|
||||
{ x => x.ShortcutId, this.ShortcutId },
|
||||
{ x => x.InitialShortcut, this.Data.Value() },
|
||||
{ x => x.ShortcutId, this.Data.Id },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>(
|
||||
@ -93,9 +88,17 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
if (dialogResult.Data is string newShortcut)
|
||||
if (dialogResult.Data is ShortcutDialogResult shortcutResult)
|
||||
{
|
||||
this.ShortcutUpdate(newShortcut);
|
||||
this.Data.ValueUpdate(shortcutResult.Shortcut);
|
||||
this.Data.DisplayUpdate(shortcutResult.DisplayName, shortcutResult.DisplaySource);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
else if (dialogResult.Data is string newShortcut)
|
||||
{
|
||||
this.Data.ValueUpdate(newShortcut);
|
||||
this.Data.DisplayUpdate(string.Empty, string.Empty);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// UI binding data for a configurable keyboard shortcut.
|
||||
/// </summary>
|
||||
public sealed class ConfigurationShortcutData
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty shortcut binding.
|
||||
/// </summary>
|
||||
public static ConfigurationShortcutData Empty { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier of the shortcut, used for conflict detection and registration.
|
||||
/// </summary>
|
||||
public Shortcut Id { get; init; } = Shortcut.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The current shortcut value.
|
||||
/// </summary>
|
||||
public Func<string> Value { get; init; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the shortcut was changed.
|
||||
/// </summary>
|
||||
public Action<string> ValueUpdate { get; init; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// The optional user-facing shortcut label.
|
||||
/// </summary>
|
||||
public Func<string> DisplayName { get; init; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The canonical shortcut value the optional user-facing label belongs to.
|
||||
/// </summary>
|
||||
public Func<string> DisplaySource { get; init; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the user-facing shortcut label was changed.
|
||||
/// </summary>
|
||||
public Action<string, string> DisplayUpdate { get; init; } = (_, _) => { };
|
||||
}
|
||||
@ -37,6 +37,16 @@ public partial class ProfileSelection : MSGComponentBase
|
||||
private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText;
|
||||
|
||||
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string ProfileIcon(Profile profile)
|
||||
{
|
||||
@ -57,4 +67,16 @@ public partial class ProfileSelection : MSGComponentBase
|
||||
var dialogParameters = new DialogParameters();
|
||||
await this.DialogService.ShowAsync<SettingsDialogProfiles>(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -25,6 +25,16 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
private ILogger<ProviderSelection> Logger { get; init; } = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task SelectionChanged(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
@ -62,4 +72,16 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -3,9 +3,9 @@
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Policy" HeaderText="@T("Agent: Security Audit for external Assistants")">
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("This Agent audits newly installed or updated external Plugin-Assistant for security risks before they are activated and stores the latest audit card until the plugin manifest changes.")
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
<MudField Label="@T("Require a security audit before activating external Assistants?")" Variant="Variant.Outlined" Underline="false" Class="mb-6" InnerPadding="false">
|
||||
<MudSwitch T="bool" Value="@this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation" ValueChanged="@this.RequireAuditBeforeActivationChanged" Color="Color.Primary">
|
||||
@(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit"))
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="@T("Agent: Text Content Cleaner Options")">
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things, and attempts to convert relative links into absolute links so that they can be used.")
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
<ConfigurationOption OptionDescription="@T("Preselect text content cleaner options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
|
||||
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="@T("Agent: Data Source Selection Options")">
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Use Case: this agent is used to select the appropriate data sources for the current prompt.")
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
<ConfigurationOption OptionDescription="@T("Preselect data source selection options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
|
||||
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Assessment" HeaderText="@T("Agent: Retrieval Context Validation Options")">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.")
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
<ConfigurationOption OptionDescription="@T("Enable the retrieval context validation agent?")" LabelOn="@T("The validation agent is enabled")" LabelOff="@T("No validation is performed")" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation = updatedState)" OptionHelp="@T("When enabled, the retrieval context validation agent will check each retrieval context of any retrieval process, whether a context makes sense for the given prompt.")"/>
|
||||
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||
{
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
|
||||
@ -36,7 +37,7 @@
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationShortcut ShortcutId="Shortcut.VOICE_RECORDING_TOGGLE" OptionDescription="@T("Voice recording shortcut")" Shortcut="@(() => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording)" ShortcutUpdate="@(shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut)" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationShortcut Data="@this.VoiceRecordingShortcut" OptionDescription="@T("Voice recording shortcut")" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
@ -45,12 +46,12 @@
|
||||
@T("Enterprise Administration")
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Generate a 256-bit encryption secret for encrypting API keys in configuration plugins. Deploy this secret to client machines via Group Policy (Windows Registry) or environment variables. Providers can then be exported with encrypted API keys using the export buttons in the provider settings.")
|
||||
<MudLink Href="https://github.com/MindWorkAI/AI-Studio/blob/main/documentation/Enterprise%20IT.md" Target="_blank">
|
||||
@T("Read the Enterprise IT documentation for details.")
|
||||
</MudLink>
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Key"
|
||||
Variant="Variant.Filled"
|
||||
@ -58,5 +59,13 @@
|
||||
OnClick="@this.GenerateEncryptionSecret">
|
||||
@T("Generate an encryption secret and copy it to the clipboard")
|
||||
</MudButton>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mt-6 mb-3">
|
||||
@T("External HTTPS certificates")
|
||||
</MudText>
|
||||
|
||||
<ConfigurationOption OptionDescription="@T("Use additional root certificates for external HTTPS requests?")" LabelOn="@T("Additional root certificates are enabled")" LabelOff="@T("Additional root certificates are disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled = updatedState)" OptionHelp="@T("When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationFile OptionDescription="@T("Root certificate bundle path")" Icon="@Icons.Material.Filled.Folder" Text="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath = updatedText)" FileDialogTitle="@T("Select a root certificate bundle")" Filter="@([FileTypes.CERTIFICATE_BUNDLE])" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationText OptionDescription="@T("Allowed hosts for additional root certificates")" Icon="@Icons.Material.Filled.Dns" NumLines="3" Text="@this.GetExternalHttpCustomRootCertificateAllowedHostsText" TextUpdate="@this.UpdateExternalHttpCustomRootCertificateAllowedHosts" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
</ExpansionPanel>
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelApp : SettingsPanelBase
|
||||
{
|
||||
private ConfigurationShortcutData VoiceRecordingShortcut => new()
|
||||
{
|
||||
Id = Shortcut.VOICE_RECORDING_TOGGLE,
|
||||
Value = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording,
|
||||
ValueUpdate = shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut,
|
||||
DisplayName = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName,
|
||||
DisplaySource = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource,
|
||||
DisplayUpdate = this.UpdateShortcutVoiceRecordingDisplay,
|
||||
};
|
||||
|
||||
private async Task GenerateEncryptionSecret()
|
||||
{
|
||||
var secret = EnterpriseEncryption.GenerateSecret();
|
||||
@ -67,12 +78,38 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
return enabled;
|
||||
}
|
||||
|
||||
private string GetExternalHttpCustomRootCertificateAllowedHostsText()
|
||||
{
|
||||
return string.Join(Environment.NewLine, this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts.Order(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private bool AreExternalHttpCustomRootCertificateDetailsDisabled()
|
||||
{
|
||||
return !this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled;
|
||||
}
|
||||
|
||||
private void UpdateExternalHttpCustomRootCertificateAllowedHosts(string updatedText)
|
||||
{
|
||||
var patterns = updatedText
|
||||
.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts = patterns;
|
||||
}
|
||||
|
||||
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
|
||||
{
|
||||
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
|
||||
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
|
||||
}
|
||||
|
||||
private void UpdateShortcutVoiceRecordingDisplay(string displayName, string displaySource)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName = displayName;
|
||||
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource = displaySource;
|
||||
}
|
||||
|
||||
private async Task UpdateLangBehaviour(LangBehavior behavior)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.App.LanguageBehavior = behavior;
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Settings
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Security" HeaderText="@T("Provider Confidence")">
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
@T("Provider Confidence")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<ConfigurationOption OptionDescription="@T("Do you want to enforce an global minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
|
||||
@if(this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
|
||||
{
|
||||
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.GlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
|
||||
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ShowProviderConfidence, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ConfidenceScheme, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
|
||||
{
|
||||
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
<col style="width: 12em;"/>
|
||||
<col/>
|
||||
<col style="width: 22em;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Provider")</MudTh>
|
||||
<MudTh>@T("Description")</MudTh>
|
||||
<MudTh>@T("Confidence Level")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd Style="vertical-align: top;">
|
||||
@context.ToName()
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</MudTd>
|
||||
<MudTd Style="vertical-align: top;">
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsCustomConfidenceSchemeLocked()">
|
||||
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
|
||||
{
|
||||
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
|
||||
continue;
|
||||
|
||||
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
|
||||
@confidenceLevel.GetName()
|
||||
</MudMenuItem>
|
||||
}
|
||||
</MudMenu>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
}
|
||||
</ExpansionPanel>
|
||||
@ -0,0 +1,38 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelConfidence : SettingsPanelBase
|
||||
{
|
||||
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return level.GetName();
|
||||
|
||||
return T("Not yet configured");
|
||||
}
|
||||
|
||||
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return $"background-color: {level.GetColor(this.SettingsManager)};";
|
||||
|
||||
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
|
||||
}
|
||||
|
||||
private bool IsCustomConfidenceSchemeLocked()
|
||||
{
|
||||
return ManagedConfiguration.TryGet(x => x.Confidence, x => x.CustomConfidenceScheme, out var meta) && meta.IsLocked;
|
||||
}
|
||||
|
||||
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
|
||||
{
|
||||
if (this.IsCustomConfidenceSchemeLocked())
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme[llmProvider] = level;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits SettingsPanelProviderBase
|
||||
|
||||
@ -39,6 +40,12 @@
|
||||
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.")">
|
||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This embedding provider is managed by your organization.")">
|
||||
|
||||
@ -31,6 +31,12 @@
|
||||
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@T("This provider is trusted by your organization for data source security checks.")">
|
||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This provider is managed by your organization.")">
|
||||
@ -68,59 +74,4 @@
|
||||
}
|
||||
|
||||
<LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="mt-3" />
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
@T("LLM Provider Confidence")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<ConfigurationOption OptionDescription="@T("Do you want to enforce an app-wide minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.")"/>
|
||||
@if(this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
|
||||
{
|
||||
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence = selectedValue)"/>
|
||||
}
|
||||
|
||||
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")"/>
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")"/>
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
|
||||
{
|
||||
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
<col style="width: 12em;"/>
|
||||
<col/>
|
||||
<col style="width: 22em;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>@T("LLM Provider")</MudTh>
|
||||
<MudTh>@T("Description")</MudTh>
|
||||
<MudTh>@T("Confidence Level")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd Style="vertical-align: top;">
|
||||
@context.ToName()
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</MudTd>
|
||||
<MudTd Style="vertical-align: top;">
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
|
||||
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
|
||||
{
|
||||
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
|
||||
continue;
|
||||
|
||||
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
|
||||
@confidenceLevel.GetName()
|
||||
</MudMenuItem>
|
||||
}
|
||||
</MudMenu>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
}
|
||||
</ExpansionPanel>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@ -166,25 +165,4 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
|
||||
await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders);
|
||||
}
|
||||
|
||||
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return level.GetName();
|
||||
|
||||
return T("Not yet configured");
|
||||
}
|
||||
|
||||
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
|
||||
return $"background-color: {level.GetColor(this.SettingsManager)};";
|
||||
|
||||
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
|
||||
}
|
||||
|
||||
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[llmProvider] = level;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits SettingsPanelProviderBase
|
||||
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
|
||||
<PreviewBeta ApplyInnerScrollingFix="true"/>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
@T("Configured Transcription Providers")
|
||||
</MudText>
|
||||
@ -36,6 +36,12 @@
|
||||
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
||||
{
|
||||
<MudTooltip Text="@T("This transcription provider is trusted by your organization for data source security checks.")">
|
||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This transcription provider is managed by your organization.")">
|
||||
|
||||
@ -12,10 +12,16 @@ public class TreeItemData : ITreeItem
|
||||
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
public string DefaultIcon { get; init; } = string.Empty;
|
||||
|
||||
public TreeItemType Type { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public Guid ChatId { get; init; }
|
||||
|
||||
public Guid WorkspaceId { get; init; }
|
||||
|
||||
public bool Expandable { get; init; } = true;
|
||||
|
||||
public DateTimeOffset LastEditTime { get; init; }
|
||||
|
||||
@ -132,6 +132,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
}
|
||||
|
||||
var mimeTypes = GetPreferredMimeTypes(
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.WEBM).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
|
||||
@ -361,7 +362,18 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
|
||||
// Call the transcription API:
|
||||
this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.UsedLLMProvider, transcriptionProviderSettings.Model.ToString());
|
||||
var transcribedText = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
var transcriptionResult = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
if (!transcriptionResult.Success)
|
||||
{
|
||||
this.Logger.LogWarning("The transcription request failed.");
|
||||
var userMessage = string.IsNullOrWhiteSpace(transcriptionResult.ErrorMessage)
|
||||
? this.T("Unfortunately, there was an error communicating with the AI system.")
|
||||
: transcriptionResult.ErrorMessage;
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, userMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
var transcribedText = transcriptionResult.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transcribedText))
|
||||
{
|
||||
|
||||
@ -11,6 +11,36 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (this.SearchVisible)
|
||||
{
|
||||
<MudStack Class="mx-3 mt-2 mb-1" Spacing="1" Style="position: sticky; top: 0; z-index: 2; background-color: var(--mud-palette-background);">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
|
||||
<MudTextField T="string"
|
||||
Text="@this.searchText"
|
||||
TextChanged="@this.OnSearchTextChanged"
|
||||
Placeholder="@T("Search chats")"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Immediate="@true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"/>
|
||||
<MudTooltip Text="@T("Clear search")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Clear" Size="Size.Medium" Color="Color.Inherit" Disabled="@(string.IsNullOrWhiteSpace(this.searchText))" OnClick="@this.ClearSearchAsync"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
|
||||
<MudSwitch T="bool" Value="@this.includeThreadContents" ValueChanged="@this.IncludeThreadContentsChanged" Color="Color.Primary">
|
||||
@T("Search chat contents")
|
||||
</MudSwitch>
|
||||
@if (this.isSearchRunning)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="@true"/>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
|
||||
<ItemTemplate Context="item">
|
||||
@switch (item.Value)
|
||||
@ -24,7 +54,7 @@ else
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.LOADING)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<MudSkeleton Width="85%" Height="22px"/>
|
||||
</BodyContent>
|
||||
@ -32,10 +62,10 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
<MudText Style="@this.GetChatTreeItemTextStyle(treeItem)">
|
||||
@if (string.IsNullOrWhiteSpace(treeItem.Text))
|
||||
{
|
||||
@T("Empty chat")
|
||||
@ -48,15 +78,15 @@ else
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,28 +95,35 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
@if (!this.HasSearchQuery)
|
||||
{
|
||||
<div style="justify-self: end;">
|
||||
<MudTooltip Text="@this.GetAddChatToWorkspaceTooltip(treeItem.Text)" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddComment" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.AddChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
|
||||
@ -3,7 +3,7 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -18,6 +18,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
private ILogger<Workspaces> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
@ -28,20 +31,32 @@ public partial class Workspaces : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool ExpandRootNodes { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool SearchVisible { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<bool> SearchVisibleChanged { get; set; }
|
||||
|
||||
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1);
|
||||
private readonly List<TreeItemData<ITreeItem>> treeItems = [];
|
||||
private readonly HashSet<Guid> loadingWorkspaceChatLists = [];
|
||||
|
||||
private CancellationTokenSource? prefetchCancellationTokenSource;
|
||||
private CancellationTokenSource? searchCancellationTokenSource;
|
||||
private bool isInitialLoading = true;
|
||||
private bool isDisposed;
|
||||
private bool includeThreadContents;
|
||||
private bool isSearchRunning;
|
||||
private string searchText = string.Empty;
|
||||
private long searchRevision;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_CREATED ]);
|
||||
_ = this.LoadTreeItemsAsync(startPrefetch: true);
|
||||
}
|
||||
|
||||
@ -49,6 +64,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false)
|
||||
{
|
||||
var shouldRunSearch = false;
|
||||
await this.treeLoadingSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
@ -59,7 +75,11 @@ public partial class Workspaces : MSGComponentBase
|
||||
await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync();
|
||||
|
||||
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.BuildTreeItems(snapshot);
|
||||
if (this.HasSearchQuery)
|
||||
shouldRunSearch = true;
|
||||
else
|
||||
this.BuildTreeItems(snapshot);
|
||||
|
||||
this.isInitialLoading = false;
|
||||
}
|
||||
finally
|
||||
@ -67,12 +87,40 @@ public partial class Workspaces : MSGComponentBase
|
||||
this.treeLoadingSemaphore.Release();
|
||||
}
|
||||
|
||||
await this.SafeStateHasChanged();
|
||||
if (shouldRunSearch)
|
||||
await this.SearchWorkspaceItemsAsync();
|
||||
else
|
||||
await this.SafeStateHasChanged();
|
||||
|
||||
if (startPrefetch)
|
||||
await this.StartPrefetchAsync();
|
||||
}
|
||||
|
||||
private bool HasSearchQuery => this.SearchVisible && !string.IsNullOrWhiteSpace(this.searchText);
|
||||
|
||||
private string GetAddChatToWorkspaceTooltip(string workspaceName) => string.Format(T("Start a new chat in workspace '{0}'"), workspaceName);
|
||||
|
||||
private async Task<Func<string?, string?>> CreateWorkspaceNameValidationAsync(Guid excludedWorkspaceId = default, string? originalWorkspaceName = null)
|
||||
{
|
||||
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
return workspaceName =>
|
||||
{
|
||||
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(originalWorkspaceName) &&
|
||||
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(originalWorkspaceName), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
var nameExists = snapshot.Workspaces.Any(workspace =>
|
||||
workspace.WorkspaceId != excludedWorkspaceId &&
|
||||
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return nameExists ? T("There is already a workspace with this name. Please choose a different name.") : null;
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot)
|
||||
{
|
||||
this.treeItems.Clear();
|
||||
@ -111,7 +159,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime))
|
||||
temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
@ -136,7 +184,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (workspace.ChatsLoaded)
|
||||
{
|
||||
foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime))
|
||||
children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
children.Add(this.CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
}
|
||||
else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId))
|
||||
children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath));
|
||||
@ -192,7 +240,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
};
|
||||
}
|
||||
|
||||
private static TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
private TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
{
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
@ -204,13 +252,160 @@ public partial class Workspaces : MSGComponentBase
|
||||
Branch = branch,
|
||||
Text = chat.Name,
|
||||
Icon = icon,
|
||||
DefaultIcon = icon,
|
||||
Expandable = false,
|
||||
Path = chat.ChatPath,
|
||||
ChatId = chat.ChatId,
|
||||
WorkspaceId = chat.WorkspaceId,
|
||||
LastEditTime = chat.LastEditTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildSearchTreeItems(WorkspaceSearchSnapshot snapshot)
|
||||
{
|
||||
this.treeItems.Clear();
|
||||
|
||||
if (snapshot.Workspaces.Count == 0 && snapshot.TemporaryChats.Count == 0)
|
||||
{
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Depth = 0,
|
||||
Branch = WorkspaceBranch.NONE,
|
||||
Text = T("No chats found"),
|
||||
Icon = Icons.Material.Filled.Search,
|
||||
Expandable = false,
|
||||
Path = "search_empty",
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.Workspaces.Count > 0)
|
||||
{
|
||||
var workspaceChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var workspace in snapshot.Workspaces)
|
||||
workspaceChildren.Add(this.CreateSearchWorkspaceTreeItem(workspace));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = true,
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Depth = 0,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = T("Workspaces"),
|
||||
Icon = Icons.Material.Filled.Folder,
|
||||
Expandable = true,
|
||||
Path = "search_workspaces",
|
||||
Children = workspaceChildren,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.Workspaces.Count > 0 && snapshot.TemporaryChats.Count > 0)
|
||||
{
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeDivider(),
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.TemporaryChats.Count > 0)
|
||||
{
|
||||
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var temporaryChat in snapshot.TemporaryChats)
|
||||
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat.Chat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = true,
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Depth = 0,
|
||||
Branch = WorkspaceBranch.TEMPORARY_CHATS,
|
||||
Text = T("Disappearing Chats"),
|
||||
Icon = Icons.Material.Filled.Timer,
|
||||
Expandable = true,
|
||||
Path = "search_temp",
|
||||
Children = temporaryChatsChildren,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private TreeItemData<ITreeItem> CreateSearchWorkspaceTreeItem(WorkspaceSearchWorkspace workspace)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var chat in workspace.Chats)
|
||||
children.Add(this.CreateChatTreeItem(chat.Chat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = true,
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.WORKSPACE,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = workspace.Name,
|
||||
Icon = Icons.Material.Filled.Description,
|
||||
Expandable = true,
|
||||
Path = workspace.WorkspacePath,
|
||||
Children = children,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTreeItemIcon(TreeItemData treeItem)
|
||||
{
|
||||
if (treeItem.Type is not TreeItemType.CHAT)
|
||||
return treeItem.Icon;
|
||||
|
||||
var defaultIcon = string.IsNullOrWhiteSpace(treeItem.DefaultIcon) ? treeItem.Icon : treeItem.DefaultIcon;
|
||||
return this.GetChatTreeIcon(treeItem.ChatId, defaultIcon);
|
||||
}
|
||||
|
||||
private bool IsChatTreeItemBusy(TreeItemData treeItem)
|
||||
{
|
||||
return treeItem.Type is TreeItemType.CHAT && this.AIJobService.IsChatGenerationActive(treeItem.ChatId);
|
||||
}
|
||||
|
||||
private string GetChatTreeItemTextStyle(TreeItemData treeItem)
|
||||
{
|
||||
return this.IsCurrentChatTreeItem(treeItem) ? "justify-self: start; font-weight: 700;" : "justify-self: start;";
|
||||
}
|
||||
|
||||
private bool IsCurrentChatTreeItem(TreeItemData treeItem)
|
||||
{
|
||||
return treeItem.Type is TreeItemType.CHAT
|
||||
&& this.CurrentChatThread is not null
|
||||
&& treeItem.ChatId == this.CurrentChatThread.ChatId
|
||||
&& treeItem.WorkspaceId == this.CurrentChatThread.WorkspaceId;
|
||||
}
|
||||
|
||||
private string GetChatTreeIcon(Guid chatId, string defaultIcon)
|
||||
{
|
||||
var snapshot = this.AIJobService.TryGetChatSnapshot(chatId);
|
||||
if (snapshot is null || !snapshot.IsActive)
|
||||
return defaultIcon;
|
||||
|
||||
return snapshot.Status switch
|
||||
{
|
||||
AIJobStatus.WAITING_FOR_REMOTE => Icons.Material.Filled.HourglassTop,
|
||||
AIJobStatus.RUNNING => Icons.Material.Filled.ChangeCircle,
|
||||
_ => defaultIcon,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SafeStateHasChanged()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
@ -253,6 +448,106 @@ public partial class Workspaces : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleSearchAsync()
|
||||
{
|
||||
var searchVisible = !this.SearchVisible;
|
||||
this.SearchVisible = searchVisible;
|
||||
await this.SearchVisibleChanged.InvokeAsync(searchVisible);
|
||||
|
||||
if (this.SearchVisible)
|
||||
{
|
||||
await this.SafeStateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.CancelSearchAsync();
|
||||
this.searchText = string.Empty;
|
||||
this.isSearchRunning = false;
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task CancelSearchAsync()
|
||||
{
|
||||
this.searchRevision++;
|
||||
if (this.searchCancellationTokenSource is not null)
|
||||
{
|
||||
await this.searchCancellationTokenSource.CancelAsync();
|
||||
this.searchCancellationTokenSource.Dispose();
|
||||
this.searchCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearchTextChanged(string value)
|
||||
{
|
||||
this.searchText = value;
|
||||
if (string.IsNullOrWhiteSpace(this.searchText))
|
||||
{
|
||||
await this.CancelSearchAsync();
|
||||
this.isSearchRunning = false;
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.SearchWorkspaceItemsAsync();
|
||||
}
|
||||
|
||||
private async Task IncludeThreadContentsChanged(bool value)
|
||||
{
|
||||
this.includeThreadContents = value;
|
||||
if (this.HasSearchQuery)
|
||||
await this.SearchWorkspaceItemsAsync();
|
||||
}
|
||||
|
||||
private async Task ClearSearchAsync()
|
||||
{
|
||||
this.searchText = string.Empty;
|
||||
await this.CancelSearchAsync();
|
||||
this.isSearchRunning = false;
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task SearchWorkspaceItemsAsync()
|
||||
{
|
||||
await this.CancelSearchAsync();
|
||||
|
||||
var text = this.searchText;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
this.searchCancellationTokenSource = new CancellationTokenSource();
|
||||
var token = this.searchCancellationTokenSource.Token;
|
||||
var revision = ++this.searchRevision;
|
||||
|
||||
this.isSearchRunning = true;
|
||||
await this.SafeStateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = await WorkspaceBehaviour.SearchWorkspaceChatsAsync(text, this.includeThreadContents, token);
|
||||
if (this.isDisposed || token.IsCancellationRequested || revision != this.searchRevision)
|
||||
return;
|
||||
|
||||
this.BuildSearchTreeItems(snapshot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when the user keeps typing or hides the search row.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogWarning(ex, "Failed while searching workspace chats.");
|
||||
this.BuildSearchTreeItems(new([], []));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (revision == this.searchRevision)
|
||||
{
|
||||
this.isSearchRunning = false;
|
||||
await this.SafeStateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnWorkspaceClicked(TreeItemData treeItem)
|
||||
{
|
||||
if (treeItem.Type is not TreeItemType.WORKSPACE)
|
||||
@ -348,11 +643,13 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS);
|
||||
if (chat is not null)
|
||||
chat = this.AIJobService.TryGetLiveChatThread(chat.ChatId) ?? chat;
|
||||
|
||||
if (switchToChat)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
return chat;
|
||||
@ -371,6 +668,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
@ -398,7 +698,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,6 +706,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||
{
|
||||
@ -429,7 +731,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread.Name = chat.Name;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@ -452,6 +753,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
{ x => x.ConfirmColor, Color.Info },
|
||||
{ x => x.AllowEmptyInput, false },
|
||||
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
|
||||
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync(workspaceId, workspaceName) },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Rename Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
@ -460,9 +762,10 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
|
||||
var alteredWorkspaceName = (dialogResult.Data as string)!;
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8);
|
||||
await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName);
|
||||
if (!await WorkspaceBehaviour.RenameWorkspaceAsync(workspaceId, alteredWorkspaceName))
|
||||
return;
|
||||
|
||||
await this.SendMessage(Event.WORKSPACE_RENAMED, workspaceId);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
@ -477,6 +780,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
{ x => x.ConfirmColor, Color.Info },
|
||||
{ x => x.AllowEmptyInput, false },
|
||||
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
|
||||
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync() },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Add Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
@ -484,14 +788,10 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.NewGuid();
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
|
||||
var workspaceName = (dialogResult.Data as string)!;
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName);
|
||||
var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(workspaceName);
|
||||
if (!result.Success)
|
||||
return;
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
@ -525,6 +825,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
||||
{
|
||||
@ -533,7 +836,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
{ x => x.ConfirmText, T("Move chat") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
@ -549,7 +852,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@ -597,6 +899,16 @@ public partial class Workspaces : MSGComponentBase
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.ForceRefreshFromDiskAsync();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_CREATED:
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
break;
|
||||
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
await this.SafeStateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -606,9 +918,12 @@ public partial class Workspaces : MSGComponentBase
|
||||
this.prefetchCancellationTokenSource?.Cancel();
|
||||
this.prefetchCancellationTokenSource?.Dispose();
|
||||
this.prefetchCancellationTokenSource = null;
|
||||
this.searchCancellationTokenSource?.Cancel();
|
||||
this.searchCancellationTokenSource?.Dispose();
|
||||
this.searchCancellationTokenSource = null;
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("The name of the chat template is mandatory. Each chat template must have a unique name.")
|
||||
</MudJustifiedText>
|
||||
|
||||
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
@ -26,9 +26,10 @@
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.ValidateName"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-3">
|
||||
@T("System Prompt")
|
||||
</MudText>
|
||||
@ -47,16 +48,17 @@
|
||||
Class="mb-3"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Tell the AI your system prompt.")"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
/>
|
||||
|
||||
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.")
|
||||
</MudJustifiedText>
|
||||
<MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled">
|
||||
<MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled" Disabled="@this.IsReadOnly">
|
||||
@T("Use the default system prompt")
|
||||
</MudButton>
|
||||
<ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt"/>
|
||||
|
||||
<ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt" Disabled="@this.IsReadOnly"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Predefined User Input")
|
||||
</MudText>
|
||||
@ -77,6 +79,7 @@
|
||||
Class="mb-3"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Tell the AI your predefined user input.")"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@ -92,6 +95,7 @@
|
||||
UseSmallForm="false"
|
||||
CatchAllDocuments="true"
|
||||
ValidateMediaFileTypes="false"
|
||||
Disabled="@this.IsReadOnly"
|
||||
/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@ -100,8 +104,8 @@
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" />
|
||||
|
||||
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Example Conversation")
|
||||
</MudText>
|
||||
@ -129,18 +133,18 @@
|
||||
case ContentText textContent:
|
||||
<MudTextField AutoGrow="true" Value="@textContent.Text" Placeholder="@T("Enter a message")" ReadOnly="true" Variant="Variant.Text" Validation="@this.ValidateExampleTextMessage"/>
|
||||
break;
|
||||
|
||||
|
||||
case ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent:
|
||||
<MudImage Src="@imageContent.Source" Alt="@T("Image content")" Fluid="true" />
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
@T("Unsupported content type")
|
||||
break;
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@if (!this.isInlineEditOnGoing)
|
||||
@if (!this.isInlineEditOnGoing && !this.IsReadOnly)
|
||||
{
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudTooltip Text="@T("Add a new message below")">
|
||||
@ -153,22 +157,29 @@
|
||||
</RowTemplate>
|
||||
<RowEditingTemplate>
|
||||
<MudTd>
|
||||
<MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true">
|
||||
@foreach (var role in ChatRoles.ChatTemplateRoles())
|
||||
{
|
||||
<MudSelectItem Value="@role">
|
||||
@role.ToChatTemplateName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.IsReadOnly)
|
||||
{
|
||||
@context.Role.ToChatTemplateName()
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true">
|
||||
@foreach (var role in ChatRoles.ChatTemplateRoles())
|
||||
{
|
||||
<MudSelectItem Value="@role">
|
||||
@role.ToChatTemplateName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@switch(context.Content)
|
||||
{
|
||||
case ContentText textContent:
|
||||
<MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")"/>
|
||||
<MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")" ReadOnly="@this.IsReadOnly"/>
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Only text content is supported in the editing mode yet.")
|
||||
@ -182,8 +193,8 @@
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
</MudForm>
|
||||
|
||||
@if (!this.isInlineEditOnGoing)
|
||||
|
||||
@if (!this.isInlineEditOnGoing && !this.IsReadOnly)
|
||||
{
|
||||
<MudButton Class="mb-6" Color="Color.Primary" OnClick="@this.AddMessageToEnd" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled">
|
||||
@T("Add a message")
|
||||
@ -193,22 +204,31 @@
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
|
||||
@if (!this.isInlineEditOnGoing)
|
||||
@if (this.IsReadOnly)
|
||||
{
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if (this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
}
|
||||
else
|
||||
{
|
||||
@T("Add")
|
||||
}
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
|
||||
@if (!this.isInlineEditOnGoing)
|
||||
{
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if (this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
}
|
||||
else
|
||||
{
|
||||
@T("Add")
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -16,37 +16,40 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public uint DataNum { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The chat template's ID.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataId { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The chat template name chosen by the user.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// What is the system prompt?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataSystemPrompt { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// What is the predefined user prompt?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string PredefinedUserPrompt { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Should the dialog be in editing mode?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEditing { get; init; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool IsReadOnly { get; init; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyCollection<ContentBlock> ExampleConversation { get; init; } = [];
|
||||
|
||||
@ -55,23 +58,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public bool AllowProfileUsage { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
|
||||
[Parameter]
|
||||
public bool CreateFromExistingChatThread { get; set; }
|
||||
|
||||
[Parameter]
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? ExistingChatThread { get; set; }
|
||||
|
||||
|
||||
[Inject]
|
||||
private ILogger<ChatTemplateDialog> Logger { get; init; } = null!;
|
||||
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The list of used chat template names. We need this to check for uniqueness.
|
||||
/// </summary>
|
||||
private List<string> UsedNames { get; set; } = [];
|
||||
|
||||
|
||||
private bool dataIsValid;
|
||||
private List<ContentBlock> dataExampleConversation = [];
|
||||
private HashSet<FileAttachment> fileAttachments = [];
|
||||
@ -80,20 +83,20 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
private bool isInlineEditOnGoing;
|
||||
|
||||
private ContentBlock? messageEntryBeforeEdit;
|
||||
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
|
||||
// Load the used instance names:
|
||||
this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
@ -108,7 +111,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
this.dataExampleConversation = this.ExistingChatThread.Blocks.Select(n => n.DeepClone(true)).ToList();
|
||||
this.DataName = this.ExistingChatThread.Name;
|
||||
}
|
||||
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@ -118,7 +121,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
// We don't want to show validation errors when the user opens the dialog.
|
||||
if(!this.IsEditing && firstRender)
|
||||
this.form.ResetValidation();
|
||||
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
@ -128,28 +131,34 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
{
|
||||
Num = this.DataNum,
|
||||
Id = this.DataId,
|
||||
|
||||
|
||||
Name = this.DataName,
|
||||
SystemPrompt = this.DataSystemPrompt,
|
||||
PredefinedUserPrompt = this.PredefinedUserPrompt,
|
||||
ExampleConversation = this.dataExampleConversation,
|
||||
FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(),
|
||||
AllowProfileUsage = this.AllowProfileUsage,
|
||||
|
||||
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
|
||||
private void RemoveMessage(ContentBlock item)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
this.dataExampleConversation.Remove(item);
|
||||
}
|
||||
|
||||
private void AddMessageToEnd()
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
var newEntry = new ContentBlock
|
||||
{
|
||||
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
|
||||
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
|
||||
Content = new ContentText(),
|
||||
ContentType = ContentType.TEXT,
|
||||
HideFromUser = true,
|
||||
@ -161,6 +170,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
|
||||
private void AddMessageBelow(ContentBlock currentItem)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
var insertedEntry = new ContentBlock
|
||||
{
|
||||
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
|
||||
@ -169,7 +181,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
HideFromUser = true,
|
||||
Time = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
|
||||
// The rest of the method remains the same:
|
||||
var index = this.dataExampleConversation.IndexOf(currentItem);
|
||||
if (index >= 0)
|
||||
@ -177,71 +189,83 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
else
|
||||
this.dataExampleConversation.Add(insertedEntry);
|
||||
}
|
||||
|
||||
|
||||
private void BackupItem(object? element)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
this.isInlineEditOnGoing = true;
|
||||
this.messageEntryBeforeEdit = element switch
|
||||
{
|
||||
ContentBlock block => block.DeepClone(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetItem(object? element)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
this.isInlineEditOnGoing = false;
|
||||
switch (element)
|
||||
{
|
||||
case ContentBlock block:
|
||||
if (this.messageEntryBeforeEdit is null)
|
||||
return; // No backup to restore from
|
||||
|
||||
|
||||
block.Content = this.messageEntryBeforeEdit.Content?.DeepClone();
|
||||
block.Role = this.messageEntryBeforeEdit.Role;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private void CommitInlineEdit(object? element)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
this.isInlineEditOnGoing = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
await this.form.Validate();
|
||||
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
|
||||
// When an inline edit is ongoing, we cannot store the data:
|
||||
if (this.isInlineEditOnGoing)
|
||||
return;
|
||||
|
||||
|
||||
// Use the data model to store the chat template.
|
||||
// We just return this data to the parent component:
|
||||
var addedChatTemplateSettings = this.CreateChatTemplateSettings();
|
||||
|
||||
|
||||
if(this.IsEditing)
|
||||
this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'.");
|
||||
else
|
||||
this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'.");
|
||||
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings));
|
||||
}
|
||||
|
||||
|
||||
private string? ValidateExampleTextMessage(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return T("Please enter a message for the example conversation.");
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -249,20 +273,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return T("Please enter a name for the chat template.");
|
||||
|
||||
|
||||
if (name.Length > 40)
|
||||
return T("The chat template name must not exceed 40 characters.");
|
||||
|
||||
|
||||
// The instance name must be unique:
|
||||
var lowerName = name.ToLowerInvariant();
|
||||
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
|
||||
return T("The chat template name must be unique; the chosen name is already in use.");
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UseDefaultSystemPrompt()
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
this.DataSystemPrompt = SystemPrompts.DEFAULT;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name)
|
||||
</MudText>
|
||||
|
||||
<MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start">
|
||||
@foreach (var mode in this.availableUsernamePasswordModes)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetUsernamePasswordModeText(mode)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Export")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -0,0 +1,37 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceERIV1UsernamePasswordExportDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public DataSourceERI_V1 DataSource { get; set; }
|
||||
|
||||
private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes =
|
||||
[
|
||||
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD,
|
||||
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD
|
||||
];
|
||||
|
||||
private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
|
||||
|
||||
private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode);
|
||||
|
||||
private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch
|
||||
{
|
||||
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"),
|
||||
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"),
|
||||
|
||||
_ => T("User-managed username and password"),
|
||||
};
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1UsernamePasswordExportDialogResult(this.usernamePasswordMode)));
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode);
|
||||
@ -116,7 +116,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
// Load the secret:
|
||||
var requestedSecret = await this.RustService.GetSecret(this);
|
||||
var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE);
|
||||
if (requestedSecret.Success)
|
||||
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
else
|
||||
@ -169,6 +169,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
AuthMethod = this.dataAuthMethod,
|
||||
Username = this.dataUsername,
|
||||
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
SecurityPolicy = this.dataSecurityPolicy,
|
||||
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
||||
@ -323,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
if (!string.IsNullOrWhiteSpace(this.dataSecret))
|
||||
{
|
||||
// Store the secret in the OS secure storage:
|
||||
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret);
|
||||
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret, SecretStoreType.DATA_SOURCE);
|
||||
if (!storeResponse.Success)
|
||||
{
|
||||
this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue);
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.DataSource.Username" ClipboardTooltipSubject="@T("the username")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.effectiveUsername" ClipboardTooltipSubject="@T("the username")"/>
|
||||
}
|
||||
|
||||
<TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/>
|
||||
|
||||
@ -41,6 +41,7 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
private readonly List<string> dataIssues = [];
|
||||
|
||||
private string serverDescription = string.Empty;
|
||||
private string effectiveUsername = string.Empty;
|
||||
private ProviderType securityRequirements = ProviderType.NONE;
|
||||
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
||||
private RetrievalInfo selectedRetrievalInfo;
|
||||
@ -51,6 +52,27 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
|
||||
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
|
||||
|
||||
private async Task<(bool Success, DataSourceERI_V1 EffectiveDataSource)> CreateEffectiveDataSource()
|
||||
{
|
||||
this.effectiveUsername = this.DataSource.Username;
|
||||
if (this.DataSource is not { AuthMethod: AuthMethod.USERNAME_PASSWORD, UsernamePasswordMode: DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD })
|
||||
return (true, this.DataSource);
|
||||
|
||||
var osUsername = await this.RustService.ReadUserName();
|
||||
if (string.IsNullOrWhiteSpace(osUsername))
|
||||
{
|
||||
this.dataIssues.Add(T("Failed to read the user's username from the operating system."));
|
||||
return (false, this.DataSource);
|
||||
}
|
||||
|
||||
this.effectiveUsername = osUsername;
|
||||
return (true, this.DataSource with
|
||||
{
|
||||
Username = osUsername,
|
||||
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
private string RetrievalName(RetrievalInfo retrievalInfo)
|
||||
{
|
||||
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
|
||||
@ -91,15 +113,19 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
{
|
||||
this.IsOperationInProgress = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
var effectiveDataSourceResult = await this.CreateEffectiveDataSource();
|
||||
if (!effectiveDataSourceResult.Success)
|
||||
return;
|
||||
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, effectiveDataSourceResult.EffectiveDataSource);
|
||||
if(client is null)
|
||||
{
|
||||
this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService);
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService, cancellationToken: this.cts.Token);
|
||||
if (!loginResult.Successful)
|
||||
{
|
||||
this.dataIssues.Add(loginResult.Message);
|
||||
|
||||
@ -96,7 +96,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false;
|
||||
private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
|
||||
|
||||
private DataSourceLocalDirectory CreateDataSource() => new()
|
||||
{
|
||||
|
||||
@ -56,7 +56,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsy
|
||||
|
||||
private bool IsOperationInProgress { get; set; } = true;
|
||||
|
||||
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
|
||||
private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
|
||||
|
||||
private bool IsDirectoryAvailable => this.directoryInfo.Exists;
|
||||
|
||||
|
||||
@ -96,7 +96,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false;
|
||||
private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
|
||||
|
||||
private DataSourceLocalFile CreateDataSource() => new()
|
||||
{
|
||||
|
||||
@ -28,7 +28,7 @@ public partial class DataSourceLocalFileInfoDialog : MSGComponentBase
|
||||
private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE;
|
||||
private FileInfo fileInfo = null!;
|
||||
|
||||
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
|
||||
private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
|
||||
|
||||
private bool IsFileAvailable => this.fileInfo.Exists;
|
||||
|
||||
|
||||
@ -7,6 +7,12 @@ public static class DialogOptions
|
||||
CloseOnEscapeKey = true,
|
||||
FullWidth = true, MaxWidth = MaxWidth.Medium,
|
||||
};
|
||||
|
||||
public static readonly MudBlazor.DialogOptions FULLSCREEN_MANUAL_ESCAPE = new()
|
||||
{
|
||||
CloseOnEscapeKey = false,
|
||||
FullWidth = true, MaxWidth = MaxWidth.Medium,
|
||||
};
|
||||
|
||||
public static readonly MudBlazor.DialogOptions FULLSCREEN_NO_HEADER = new()
|
||||
{
|
||||
|
||||
@ -203,7 +203,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToName();
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataName;
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.ValidateName"
|
||||
Variant="Variant.Outlined"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
@ -44,8 +45,9 @@
|
||||
MaxLines="12"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
/>
|
||||
<ReadFileContent @bind-FileContent="@this.DataNeedToKnow"/>
|
||||
<ReadFileContent @bind-FileContent="@this.DataNeedToKnow" Disabled="@this.IsReadOnly"/>
|
||||
|
||||
<MudTextField
|
||||
T="string"
|
||||
@ -62,8 +64,9 @@
|
||||
Class="mt-10"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.")"
|
||||
ReadOnly="@this.IsReadOnly"
|
||||
/>
|
||||
<ReadFileContent @bind-FileContent="@this.DataActions"/>
|
||||
<ReadFileContent @bind-FileContent="@this.DataActions" Disabled="@this.IsReadOnly"/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3 mt-3">
|
||||
@T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.")
|
||||
@ -73,18 +76,27 @@
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
}
|
||||
else
|
||||
{
|
||||
@T("Add")
|
||||
}
|
||||
</MudButton>
|
||||
@if (this.IsReadOnly)
|
||||
{
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@T("Update")
|
||||
}
|
||||
else
|
||||
{
|
||||
@T("Add")
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -15,19 +15,19 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public uint DataNum { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The profile's ID.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataId { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The profile name chosen by the user.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// What should the LLM know about you?
|
||||
/// </summary>
|
||||
@ -39,27 +39,30 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataActions { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Should the dialog be in editing mode?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEditing { get; init; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool IsReadOnly { get; init; }
|
||||
|
||||
[Inject]
|
||||
private ILogger<ProviderDialog> Logger { get; init; } = null!;
|
||||
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The list of used profile names. We need this to check for uniqueness.
|
||||
/// </summary>
|
||||
private List<string> UsedNames { get; set; } = [];
|
||||
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataEditingPreviousName = string.Empty;
|
||||
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
|
||||
@ -70,7 +73,7 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
Name = this.DataName,
|
||||
NeedToKnow = this.DataNeedToKnow,
|
||||
Actions = this.DataActions,
|
||||
|
||||
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
@ -81,16 +84,16 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
|
||||
// Load the used instance names:
|
||||
this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
|
||||
}
|
||||
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@ -100,37 +103,40 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
// We don't want to show validation errors when the user opens the dialog.
|
||||
if(!this.IsEditing && firstRender)
|
||||
this.form.ResetValidation();
|
||||
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
await this.form.Validate();
|
||||
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
|
||||
// Use the data model to store the profile.
|
||||
// We just return this data to the parent component:
|
||||
var addedProfileSettings = this.CreateProfileSettings();
|
||||
|
||||
|
||||
if(this.IsEditing)
|
||||
this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'.");
|
||||
else
|
||||
this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'.");
|
||||
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedProfileSettings));
|
||||
}
|
||||
|
||||
|
||||
private string? ValidateNeedToKnow(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
|
||||
return T("Please enter what the LLM should know about you and/or what actions it should take.");
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -138,7 +144,7 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
|
||||
return T("Please enter what the LLM should know about you and/or what actions it should take.");
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -146,15 +152,15 @@ public partial class ProfileDialog : MSGComponentBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return T("Please enter a profile name.");
|
||||
|
||||
|
||||
if (name.Length > 40)
|
||||
return T("The profile name must not exceed 40 characters.");
|
||||
|
||||
|
||||
// The instance name must be unique:
|
||||
var lowerName = name.ToLowerInvariant();
|
||||
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
|
||||
return T("The profile name must be unique; the chosen name is already in use.");
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
@* ReSharper restore Asp.Entity *@
|
||||
}
|
||||
|
||||
@if (!this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost))
|
||||
@if (!this.IsLLMModelSelectionHidden)
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
|
||||
@ -104,6 +104,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
private string dataAPIKeyStorageIssue = string.Empty;
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
private string dataLoadingModelsIssue = string.Empty;
|
||||
private bool usesLegacySystemModelFallback;
|
||||
private bool showExpertSettings;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
@ -123,6 +124,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
GetUsedInstanceNames = () => this.UsedInstanceNames,
|
||||
GetHost = () => this.DataHost,
|
||||
IsModelProvidedManually = () => this.DataLLMProvider.IsLLMModelProvidedManually(),
|
||||
IsModelSelectionHidden = () => this.IsLLMModelSelectionHidden,
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,9 +134,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
// Determine the model based on the provider and host configuration:
|
||||
Model model;
|
||||
if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost))
|
||||
if (this.IsLLMModelSelectionHidden)
|
||||
{
|
||||
// Use system model placeholder for hosts that don't support model selection (e.g., llama.cpp):
|
||||
// Use system model placeholder for legacy hosts that don't support model selection:
|
||||
model = Model.SYSTEM_MODEL;
|
||||
}
|
||||
else if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE)
|
||||
@ -229,7 +231,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToName();
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataInstanceName;
|
||||
|
||||
@ -300,6 +302,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
this.dataManuallyModel = string.Empty;
|
||||
this.availableModels.Clear();
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
this.usesLegacySystemModelFallback = false;
|
||||
}
|
||||
|
||||
private async Task ReloadModels()
|
||||
@ -321,6 +324,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
this.availableModels.Clear();
|
||||
this.availableModels.AddRange(orderedModels);
|
||||
this.UpdateModelSelectionAfterLoading();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -334,6 +338,34 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
LLMProviders.SELF_HOSTED => T("(Optional) API Key"),
|
||||
_ => T("API Key"),
|
||||
};
|
||||
|
||||
private bool IsLLMModelSelectionHidden => this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost) ||
|
||||
this.DataLLMProvider is LLMProviders.SELF_HOSTED &&
|
||||
this.DataHost is Host.LLAMA_CPP &&
|
||||
this.usesLegacySystemModelFallback;
|
||||
|
||||
private void UpdateModelSelectionAfterLoading()
|
||||
{
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP)
|
||||
return;
|
||||
|
||||
this.usesLegacySystemModelFallback = this.availableModels.Count is 1 && this.availableModels[0].IsSystemModel;
|
||||
if (this.usesLegacySystemModelFallback)
|
||||
{
|
||||
this.DataModel = Model.SYSTEM_MODEL;
|
||||
return;
|
||||
}
|
||||
|
||||
var availableModel = this.availableModels.FirstOrDefault(model =>
|
||||
string.Equals(model.Id, this.DataModel.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (availableModel != default)
|
||||
{
|
||||
this.DataModel = availableModel;
|
||||
return;
|
||||
}
|
||||
|
||||
this.DataModel = this.availableModels.Count is 1 ? this.availableModels[0] : default;
|
||||
}
|
||||
|
||||
private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings;
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
protected RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
protected readonly List<ConfigurationSelectData<string>> AvailableLLMProviders = new();
|
||||
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();
|
||||
@ -62,6 +65,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.UpdateProviders();
|
||||
this.UpdateEmbeddingProviders();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -16,10 +16,10 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Provider selection when loading a chat and sending assistant results to chat")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetLoadingChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for loaded chats is selected and when assistant results are sent to chat.")"/>
|
||||
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")"/>
|
||||
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
|
||||
@ -33,9 +33,14 @@
|
||||
<MudTd>
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This template is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudTooltip Text="@T("This template is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("View")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="@(() => this.ViewChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -43,6 +48,28 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
|
||||
@ -6,24 +6,24 @@ namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
[Parameter]
|
||||
public bool CreateTemplateFromExistingChatThread { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? ExistingChatThread { get; set; }
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
if (this.CreateTemplateFromExistingChatThread)
|
||||
if (this.CreateTemplateFromExistingChatThread)
|
||||
await this.AddChatTemplate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private async Task AddChatTemplate()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ChatTemplateDialog>
|
||||
@ -41,21 +41,21 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
var addedChatTemplate = (ChatTemplate)dialogResult.Data!;
|
||||
addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ };
|
||||
|
||||
|
||||
this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate);
|
||||
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
|
||||
private async Task EditChatTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
|
||||
var dialogParameters = new DialogParameters<ChatTemplateDialog>
|
||||
{
|
||||
{ x => x.DataNum, chatTemplate.Num },
|
||||
@ -68,34 +68,115 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{ x => x.FileAttachments, chatTemplate.FileAttachments },
|
||||
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
var editedChatTemplate = (ChatTemplate)dialogResult.Data!;
|
||||
this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate;
|
||||
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ViewChatTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ChatTemplateDialog>
|
||||
{
|
||||
{ x => x.DataNum, chatTemplate.Num },
|
||||
{ x => x.DataId, chatTemplate.Id },
|
||||
{ x => x.DataName, chatTemplate.Name },
|
||||
{ x => x.DataSystemPrompt, chatTemplate.SystemPrompt },
|
||||
{ x => x.PredefinedUserPrompt, chatTemplate.PredefinedUserPrompt },
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.IsReadOnly, true },
|
||||
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
|
||||
{ x => x.FileAttachments, chatTemplate.FileAttachments },
|
||||
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
|
||||
};
|
||||
|
||||
await this.DialogService.ShowAsync<ChatTemplateDialog>(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
private async Task DeleteChatTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
|
||||
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportChatTemplateWithSharedAttachmentPaths(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
|
||||
}
|
||||
|
||||
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
if (chatTemplate.FileAttachments.Count == 0)
|
||||
{
|
||||
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
||||
if (pluginDirectoryResponse.UserCancelled)
|
||||
return;
|
||||
|
||||
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
|
||||
}
|
||||
|
||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export Chat Template"),
|
||||
issue,
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task CopyPackagedChatTemplateLuaToClipboard(ChatTemplate chatTemplate, string pluginDirectory)
|
||||
{
|
||||
if (!chatTemplate.TryExportAsConfigurationSectionWithPackagedAttachments(pluginDirectory, out var luaCode, out var issue))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export Chat Template"),
|
||||
issue,
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
}
|
||||
@ -38,12 +38,27 @@
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
</MudButton>
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This data source is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
@ -86,9 +87,106 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (dataSource is not DataSourceERI_V1 eriDataSource)
|
||||
return;
|
||||
|
||||
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var needsSecret = eriDataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
|
||||
if (!needsSecret)
|
||||
{
|
||||
var publicLuaCode = eriDataSource.ExportAsConfigurationSection();
|
||||
if (!string.IsNullOrWhiteSpace(publicLuaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, publicLuaCode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
|
||||
if (!secretResponse.Success)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
if (encryption?.IsAvailable != true)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Cannot export this ERI data source because no enterprise encryption secret is configured."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||
if (eriDataSource.AuthMethod is AuthMethod.TOKEN)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, T("This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Access Token?"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
else if (eriDataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<DataSourceERIV1UsernamePasswordExportDialog>
|
||||
{
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1UsernamePasswordExportDialog>(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1UsernamePasswordExportDialogResult exportResult)
|
||||
return;
|
||||
|
||||
usernamePasswordMode = exportResult.UsernamePasswordMode;
|
||||
}
|
||||
|
||||
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Cannot export this ERI data source because the authentication secret could not be encrypted."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var luaCode = eriDataSource.ExportAsConfigurationSection(
|
||||
encryptedSecret,
|
||||
usernamePasswordMode);
|
||||
if (string.IsNullOrWhiteSpace(luaCode))
|
||||
return;
|
||||
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task EditDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (dataSource.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
IDataSource? editedDataSource = null;
|
||||
switch (dataSource)
|
||||
{
|
||||
@ -151,6 +249,9 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
|
||||
private async Task DeleteDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (dataSource.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the data source '{0}' of type {1}?"), dataSource.Name, dataSource.Type.GetDisplayName()) },
|
||||
@ -174,7 +275,7 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
// All other auth methods require a secret, which we need to delete now:
|
||||
else
|
||||
{
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource);
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE);
|
||||
if (deleteSecretResponse.Success)
|
||||
applyChanges = true;
|
||||
}
|
||||
|
||||
@ -32,9 +32,14 @@
|
||||
<MudTd>
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This profile is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudTooltip Text="@T("This profile is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("View")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="() => this.ViewProfile(context)"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -42,6 +47,12 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
||||
</MudTooltip>
|
||||
|
||||
@ -10,21 +10,21 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Add Profile"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
var addedProfile = (Profile)dialogResult.Data!;
|
||||
addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ };
|
||||
|
||||
|
||||
this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile);
|
||||
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
|
||||
private async Task EditProfile(Profile profile)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ProfileDialog>
|
||||
@ -36,34 +36,63 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
|
||||
{ x => x.DataActions, profile.Actions },
|
||||
{ x => x.IsEditing, true },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Edit Profile"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
var editedProfile = (Profile)dialogResult.Data!;
|
||||
this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile;
|
||||
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ViewProfile(Profile profile)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ProfileDialog>
|
||||
{
|
||||
{ x => x.DataNum, profile.Num },
|
||||
{ x => x.DataId, profile.Id },
|
||||
{ x => x.DataName, profile.Name },
|
||||
{ x => x.DataNeedToKnow, profile.NeedToKnow },
|
||||
{ x => x.DataActions, profile.Actions },
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.IsReadOnly, true },
|
||||
};
|
||||
|
||||
await this.DialogService.ShowAsync<ProfileDialog>(T("View Profile"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
}
|
||||
|
||||
private async Task ExportProfile(Profile profile)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (profile == Profile.NO_PROFILE || profile.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var luaCode = profile.ExportAsConfigurationSection();
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task DeleteProfile(Profile profile)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the profile '{0}'?"), profile.Name) },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Profile"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
|
||||
this.SettingsManager.ConfigurationData.Profiles.Remove(profile);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
|
||||
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,7 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
|
||||
private string currentShortcut = string.Empty;
|
||||
private string originalShortcut = string.Empty;
|
||||
private string currentDisplayName = string.Empty;
|
||||
private string validationMessage = string.Empty;
|
||||
private Severity validationSeverity = Severity.Info;
|
||||
private bool hasValidationError;
|
||||
@ -115,6 +116,7 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
{
|
||||
this.UpdateModifiers(e);
|
||||
this.currentKey = null;
|
||||
this.currentDisplayName = string.Empty;
|
||||
this.UpdateShortcutString();
|
||||
return;
|
||||
}
|
||||
@ -123,10 +125,12 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
|
||||
// Get the key:
|
||||
this.currentKey = TranslateKeyCode(e.Code);
|
||||
this.currentDisplayName = this.BuildDisplayShortcut(e.Key);
|
||||
|
||||
// Validate: must have at least one modifier + a key
|
||||
if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta)
|
||||
{
|
||||
this.currentDisplayName = string.Empty;
|
||||
this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).");
|
||||
this.validationSeverity = Severity.Warning;
|
||||
this.hasValidationError = true;
|
||||
@ -216,6 +220,9 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
|
||||
private string GetDisplayShortcut()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this.currentDisplayName))
|
||||
return this.currentDisplayName;
|
||||
|
||||
// Convert internal format to display format:
|
||||
return this.currentShortcut
|
||||
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
|
||||
@ -225,6 +232,7 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
private void ClearShortcut()
|
||||
{
|
||||
this.currentShortcut = string.Empty;
|
||||
this.currentDisplayName = string.Empty;
|
||||
this.currentKey = null;
|
||||
this.hasCtrl = false;
|
||||
this.hasShift = false;
|
||||
@ -237,7 +245,17 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut));
|
||||
private void Confirm()
|
||||
{
|
||||
var displaySource = string.IsNullOrWhiteSpace(this.currentDisplayName)
|
||||
? string.Empty
|
||||
: this.currentShortcut;
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(new ShortcutDialogResult(
|
||||
this.currentShortcut,
|
||||
this.currentDisplayName,
|
||||
displaySource)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the key code represents a modifier key.
|
||||
@ -377,6 +395,36 @@ public partial class ShortcutDialog : MSGComponentBase
|
||||
_ => code,
|
||||
};
|
||||
|
||||
private string BuildDisplayShortcut(string? key)
|
||||
{
|
||||
var displayKey = GetDisplayKey(key);
|
||||
if (string.IsNullOrWhiteSpace(displayKey))
|
||||
return string.Empty;
|
||||
|
||||
var parts = new List<string>();
|
||||
|
||||
if (this.hasCtrl)
|
||||
parts.Add(OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
|
||||
|
||||
if (this.hasShift)
|
||||
parts.Add("Shift");
|
||||
|
||||
if (this.hasAlt)
|
||||
parts.Add("Alt");
|
||||
|
||||
parts.Add(displayKey);
|
||||
return string.Join("+", parts);
|
||||
}
|
||||
|
||||
private static string GetDisplayKey(string? key) => key switch
|
||||
{
|
||||
null or "" => string.Empty,
|
||||
" " => "Space",
|
||||
"Control" or "Shift" or "Alt" or "Meta" => string.Empty,
|
||||
_ when key.Length == 1 && key[0] >= 'a' && key[0] <= 'z' => key.ToUpperInvariant(),
|
||||
_ => key,
|
||||
};
|
||||
|
||||
private void HandleBlur()
|
||||
{
|
||||
// Re-focus the input field to keep capturing keys:
|
||||
|
||||
3
app/MindWork AI Studio/Dialogs/ShortcutDialogResult.cs
Normal file
3
app/MindWork AI Studio/Dialogs/ShortcutDialogResult.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct ShortcutDialogResult(string Shortcut, string DisplayName, string DisplaySource);
|
||||
@ -31,6 +31,9 @@ public partial class SingleInputDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public string EmptyInputErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Func<string?, string?>? AdditionalValidation { get; set; }
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
private MudForm form = null!;
|
||||
@ -52,8 +55,8 @@ public partial class SingleInputDialog : MSGComponentBase
|
||||
{
|
||||
if (!this.AllowEmptyInput && string.IsNullOrWhiteSpace(value))
|
||||
return string.IsNullOrWhiteSpace(this.EmptyInputErrorMessage) ? T("Please enter a value.") : this.EmptyInputErrorMessage;
|
||||
|
||||
return null;
|
||||
|
||||
return this.AdditionalValidation?.Invoke(value);
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
@ -218,7 +218,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToName();
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataName;
|
||||
|
||||
|
||||
@ -5,18 +5,57 @@
|
||||
@this.Message
|
||||
</MudText>
|
||||
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
|
||||
@foreach (var (workspaceName, workspaceId) in this.workspaces)
|
||||
@foreach (var workspace in this.workspaces)
|
||||
{
|
||||
<MudListItem Text="@workspaceName" Icon="@Icons.Material.Filled.Description" Value="@workspaceId" />
|
||||
<MudListItem Text="@workspace.Name" Icon="@Icons.Material.Filled.Description" Value="@workspace.WorkspaceId" />
|
||||
}
|
||||
</MudList>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info">
|
||||
@this.ConfirmText
|
||||
</MudButton>
|
||||
<MudStack Style="width: 100%;" Spacing="2">
|
||||
<MudDivider/>
|
||||
|
||||
@if (this.showCreateWorkspaceForm)
|
||||
{
|
||||
<MudForm @ref="this.createWorkspaceForm">
|
||||
<MudTextField T="string"
|
||||
@ref="@this.newWorkspaceNameField"
|
||||
@bind-Text="@this.newWorkspaceName"
|
||||
Variant="Variant.Outlined"
|
||||
AutoGrow="@false"
|
||||
Lines="1"
|
||||
Label="@T("Workspace name")"
|
||||
AutoFocus="@true"
|
||||
Immediate="@true"
|
||||
Disabled="@this.isCreatingWorkspace"
|
||||
OnKeyDown="@this.HandleNewWorkspaceNameKeyDown"
|
||||
Validation="@this.ValidateNewWorkspaceName" />
|
||||
</MudForm>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton StartIcon="@Icons.Material.Filled.LibraryAdd" Variant="Variant.Filled" OnClick="@this.ShowCreateWorkspaceForm">
|
||||
@T("Create new workspace")
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
<MudStack Row="@true" Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="2">
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
@if (this.showCreateWorkspaceForm)
|
||||
{
|
||||
<MudButton OnClick="@this.CreateWorkspaceAsync" Variant="Variant.Filled" Color="Color.Info" Disabled="@this.isCreatingWorkspace">
|
||||
@T("Add workspace")
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info" Disabled="@(this.selectedWorkspace == Guid.Empty)">
|
||||
@this.ConfirmText
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -1,14 +1,20 @@
|
||||
using AIStudio.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class WorkspaceSelectionDialog : MSGComponentBase
|
||||
{
|
||||
private readonly record struct WorkspaceSelectionItem(Guid WorkspaceId, string Name);
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
@ -18,8 +24,18 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public string ConfirmText { get; set; } = "OK";
|
||||
|
||||
private readonly Dictionary<string, Guid> workspaces = new();
|
||||
private readonly List<WorkspaceSelectionItem> workspaces = [];
|
||||
private readonly string escapeHandlerId = $"workspace-selection-dialog-{Guid.NewGuid():N}";
|
||||
private MudForm? createWorkspaceForm;
|
||||
private MudTextField<string>? newWorkspaceNameField;
|
||||
private DotNetObjectReference<WorkspaceSelectionDialog>? dotNetReference;
|
||||
private Guid selectedWorkspace;
|
||||
private string newWorkspaceName = string.Empty;
|
||||
private bool isCreatingWorkspace;
|
||||
private bool showCreateWorkspaceForm;
|
||||
private bool shouldFocusNewWorkspaceName;
|
||||
private string? createWorkspaceError;
|
||||
private string? createWorkspaceErrorName;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
@ -29,15 +45,156 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
|
||||
|
||||
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
foreach (var workspace in snapshot.Workspaces)
|
||||
this.workspaces[workspace.Name] = workspace.WorkspaceId;
|
||||
this.workspaces.Add(new(workspace.WorkspaceId, workspace.Name));
|
||||
|
||||
this.StateHasChanged();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
this.dotNetReference = DotNetObjectReference.Create(this);
|
||||
await this.JsRuntime.InvokeVoidAsync("registerEscapeHandler", this.escapeHandlerId, this.dotNetReference);
|
||||
}
|
||||
|
||||
if (this.shouldFocusNewWorkspaceName && this.newWorkspaceNameField is not null)
|
||||
{
|
||||
this.shouldFocusNewWorkspaceName = false;
|
||||
await this.newWorkspaceNameField.FocusAsync();
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
private string? ValidateNewWorkspaceName(string? workspaceName)
|
||||
{
|
||||
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
|
||||
return T("Please enter a workspace name.");
|
||||
|
||||
if (this.IsWorkspaceNameExisting(normalizedWorkspaceName))
|
||||
return T("There is already a workspace with this name. Please choose a different name.");
|
||||
|
||||
if (this.createWorkspaceError is not null && string.Equals(this.createWorkspaceErrorName, normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
|
||||
return this.createWorkspaceError;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsWorkspaceNameExisting(string normalizedWorkspaceName)
|
||||
{
|
||||
return this.workspaces.Any(workspace =>
|
||||
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task HandleNewWorkspaceNameKeyDown(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
var key = keyEvent.Key.ToLowerInvariant();
|
||||
var code = keyEvent.Code.ToLowerInvariant();
|
||||
if (key is not "enter" && code is not "enter" and not "numpadenter")
|
||||
return;
|
||||
|
||||
if (keyEvent is { AltKey: true } or { CtrlKey: true } or { MetaKey: true })
|
||||
return;
|
||||
|
||||
await this.CreateWorkspaceAsync();
|
||||
}
|
||||
|
||||
private void ShowCreateWorkspaceForm()
|
||||
{
|
||||
this.createWorkspaceError = null;
|
||||
this.createWorkspaceErrorName = null;
|
||||
this.newWorkspaceName = string.Empty;
|
||||
this.showCreateWorkspaceForm = true;
|
||||
this.shouldFocusNewWorkspaceName = true;
|
||||
}
|
||||
|
||||
private async Task CreateWorkspaceAsync()
|
||||
{
|
||||
if (this.createWorkspaceForm is null)
|
||||
return;
|
||||
|
||||
this.createWorkspaceError = null;
|
||||
this.createWorkspaceErrorName = null;
|
||||
await this.createWorkspaceForm.Validate();
|
||||
if (!this.createWorkspaceForm.IsValid)
|
||||
return;
|
||||
|
||||
this.isCreatingWorkspace = true;
|
||||
try
|
||||
{
|
||||
var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(this.newWorkspaceName);
|
||||
if (!result.Success)
|
||||
{
|
||||
this.createWorkspaceError = T("There is already a workspace with this name. Please choose a different name.");
|
||||
this.createWorkspaceErrorName = WorkspaceBehaviour.NormalizeWorkspaceName(this.newWorkspaceName);
|
||||
await this.createWorkspaceForm.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.workspaces.Add(new(result.Workspace.WorkspaceId, result.Workspace.Name));
|
||||
this.selectedWorkspace = result.Workspace.WorkspaceId;
|
||||
this.newWorkspaceName = string.Empty;
|
||||
this.createWorkspaceForm?.ResetValidation();
|
||||
this.showCreateWorkspaceForm = false;
|
||||
await this.SendMessage(Event.WORKSPACE_CREATED, result.Workspace.WorkspaceId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isCreatingWorkspace = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
if (!this.showCreateWorkspaceForm)
|
||||
{
|
||||
this.MudDialog.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createWorkspaceError = null;
|
||||
this.createWorkspaceErrorName = null;
|
||||
this.newWorkspaceName = string.Empty;
|
||||
this.createWorkspaceForm?.ResetValidation();
|
||||
this.showCreateWorkspaceForm = false;
|
||||
this.shouldFocusNewWorkspaceName = false;
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task HandleEscapeKeyAsync()
|
||||
{
|
||||
await this.InvokeAsync(() =>
|
||||
{
|
||||
this.Cancel();
|
||||
this.StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace));
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = this.JsRuntime.InvokeVoidAsync("unregisterEscapeHandler", this.escapeHandlerId).AsTask();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore JS cleanup errors while the dialog is being disposed.
|
||||
}
|
||||
|
||||
this.dotNetReference?.Dispose();
|
||||
this.dotNetReference = null;
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
<MudNavMenu>
|
||||
@foreach (var navBarItem in this.navItems)
|
||||
{
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">
|
||||
@navBarItem.Name
|
||||
</MudNavLink>
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -26,6 +27,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
@ -54,6 +58,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
private MudThemeProvider themeProvider = null!;
|
||||
private bool useDarkMode;
|
||||
private bool startupCompleted;
|
||||
private bool settingsWriteProtectionWarningShown;
|
||||
private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1);
|
||||
|
||||
private IReadOnlyCollection<NavBarItem> navItems = [];
|
||||
@ -83,7 +88,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
// Read the user language from Rust:
|
||||
//
|
||||
var userLanguage = await this.RustService.ReadUserLanguage();
|
||||
var userName = await this.RustService.ReadUserName();
|
||||
this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language.");
|
||||
this.Logger.LogInformation($"The OS says '{userName}' is the username.");
|
||||
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
@ -94,7 +101,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
[
|
||||
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
||||
Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
|
||||
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED,
|
||||
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
|
||||
Event.CHAT_GENERATION_CHANGED,
|
||||
]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
@ -120,6 +128,39 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
|
||||
#endregion
|
||||
|
||||
private void ShowSettingsWriteProtectionWarning()
|
||||
{
|
||||
if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown)
|
||||
return;
|
||||
|
||||
this.settingsWriteProtectionWarningShown = true;
|
||||
var reason = this.SettingsManager.SettingsWriteBlockReason;
|
||||
var message = reason switch
|
||||
{
|
||||
SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."),
|
||||
SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
|
||||
SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
|
||||
SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."),
|
||||
SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."),
|
||||
_ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."),
|
||||
};
|
||||
message = $"{message} {T("Reason")}: {reason}";
|
||||
|
||||
this.Snackbar.Add(message, Severity.Warning, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.WarningAmber;
|
||||
config.IconSize = Size.Large;
|
||||
config.VisibleStateDuration = 32_000;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.Action = T("Check for updates");
|
||||
config.ActionVariant = Variant.Filled;
|
||||
config.OnClick = async _ =>
|
||||
{
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#region Implementation of ILang
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -184,6 +225,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
this.LoadNavItems();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_SUCCESS:
|
||||
if (data is DataSuccessMessage success)
|
||||
success.Show(this.Snackbar);
|
||||
@ -262,6 +310,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
I18N.Init(this.Lang);
|
||||
this.ShowSettingsWriteProtectionWarning();
|
||||
this.LoadNavItems();
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
@ -294,7 +343,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
|
||||
|
||||
yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true);
|
||||
yield return new(T("Chat"), Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
|
||||
yield return new(T("Chat"), this.AIJobService.HasActiveJobs ? Icons.Material.Filled.Chat : Icons.Material.Outlined.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
|
||||
yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false);
|
||||
|
||||
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
|
||||
|
||||
@ -50,12 +50,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.15" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.17" />
|
||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
||||
<PackageReference Include="Qdrant.Client" Version="1.17.0" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.5.3" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.5.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -88,7 +87,7 @@
|
||||
<MetaAppCommitHash>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ])</MetaAppCommitHash>
|
||||
<MetaArchitecture>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ])</MetaArchitecture>
|
||||
<MetaPdfiumVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ])</MetaPdfiumVersion>
|
||||
<MetaQdrantVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaQdrantVersion>
|
||||
<MetaVectorStoreVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaVectorStoreVersion>
|
||||
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
|
||||
@ -116,8 +115,8 @@
|
||||
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataLibraries">
|
||||
<_Parameter1>$(MetaPdfiumVersion)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataDatabases">
|
||||
<_Parameter1>$(MetaQdrantVersion)</_Parameter1>
|
||||
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataVectorStore">
|
||||
<_Parameter1>$(MetaVectorStoreVersion)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -51,13 +51,16 @@
|
||||
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Hide your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Size="Size.Medium" Icon="@this.WorkspaceSidebarToggleIcon" Class="me-1" OnClick="@(() => this.ToggleWorkspaceSidebar())"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</HeaderContent>
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
@ -77,10 +80,13 @@
|
||||
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</HeaderContent>
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
@ -89,6 +95,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
</EndContent>
|
||||
@ -115,6 +122,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
</MudStack>
|
||||
@ -125,6 +133,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
}
|
||||
@ -146,11 +155,14 @@
|
||||
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
|
||||
</MudTooltip>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Error" Size="Size.Medium" OnClick="@(() => this.ToggleWorkspacesOverlay())"/>
|
||||
</MudStack>
|
||||
</MudDrawerHeader>
|
||||
<MudDrawerContainer Class="ml-6">
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
|
||||
</MudDrawerContainer>
|
||||
</MudDrawer>
|
||||
}
|
||||
|
||||
@ -23,9 +23,11 @@ public partial class Chat : MSGComponentBase
|
||||
private ChatThread? chatThread;
|
||||
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
|
||||
private bool workspaceOverlayVisible;
|
||||
private bool workspaceSearchVisible;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Workspaces? workspaces;
|
||||
private double splitterPosition = 30;
|
||||
private readonly ChatComposerState composerState = new();
|
||||
|
||||
private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
@ -50,6 +52,10 @@ public partial class Chat : MSGComponentBase
|
||||
|
||||
private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
|
||||
|
||||
private string WorkspaceSearchIcon => this.workspaceSearchVisible ? Icons.Material.Filled.SearchOff : Icons.Material.Filled.Search;
|
||||
|
||||
private string WorkspaceSearchTooltip => this.workspaceSearchVisible ? T("Hide search") : T("Search your workspaces");
|
||||
|
||||
private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
||||
|| this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
|
||||
@ -106,6 +112,14 @@ public partial class Chat : MSGComponentBase
|
||||
await this.workspaces.ForceRefreshFromDiskAsync();
|
||||
}
|
||||
|
||||
private async Task ToggleWorkspaceSearch()
|
||||
{
|
||||
if (this.workspaces is null)
|
||||
return;
|
||||
|
||||
await this.workspaces.ToggleSearchAsync();
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override void DisposeResources()
|
||||
|
||||
@ -8,36 +8,52 @@
|
||||
</MudText>
|
||||
|
||||
<InnerScrolling>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
<MudExpansionPanels @key="@this.expansionPanelsRenderKey" Class="mb-3" MultiExpansion="@false">
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true">
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
@T("Welcome to MindWork AI Studio!")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
|
||||
@T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Here's what makes MindWork AI Studio stand out:")
|
||||
</MudText>
|
||||
<MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
|
||||
</MudText>
|
||||
</ExpansionPanel>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION, isExpanded))">
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
@T("Welcome to MindWork AI Studio!")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
|
||||
@T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Here's what makes MindWork AI Studio stand out:")
|
||||
</MudText>
|
||||
<MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
|
||||
</MudText>
|
||||
</ExpansionPanel>
|
||||
}
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
|
||||
@foreach (var introduction in this.introductions)
|
||||
{
|
||||
<ExpansionPanel @key="@introduction.Id" HeaderIcon="@Icons.Material.Filled.Info" HeaderText="@introduction.Title" IsExpanded="@this.IsPanelExpanded(IntroductionPanelId(introduction))" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(IntroductionPanelId(introduction), isExpanded))">
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Version"): @introduction.VersionText
|
||||
</MudText>
|
||||
<MudJustifiedMarkdown Value="@introduction.Markdown" />
|
||||
</ExpansionPanel>
|
||||
}
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_LAST_CHANGELOG)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_LAST_CHANGELOG, isExpanded))">
|
||||
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_VISION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_VISION, isExpanded))">
|
||||
<Vision/>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
|
||||
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</ExpansionPanel>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide)
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_QUICK_START_GUIDE)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_QUICK_START_GUIDE, isExpanded))">
|
||||
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</ExpansionPanel>
|
||||
}
|
||||
|
||||
</MudExpansionPanels>
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -18,13 +19,25 @@ public partial class Home : MSGComponentBase
|
||||
private string LastChangeContent { get; set; } = string.Empty;
|
||||
|
||||
private TextItem[] itemsAdvantages = [];
|
||||
|
||||
private List<DataIntroduction> introductions = [];
|
||||
|
||||
private string expandedPanelId = string.Empty;
|
||||
private int expansionPanelsRenderKey;
|
||||
|
||||
private const string PANEL_ID_BUILT_IN_INTRODUCTION = "built-in-introduction";
|
||||
private const string PANEL_ID_LAST_CHANGELOG = "last-changelog";
|
||||
private const string PANEL_ID_VISION = "vision";
|
||||
private const string PANEL_ID_QUICK_START_GUIDE = "quick-start-guide";
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
this.InitializeAdvantagesItems();
|
||||
this.RefreshIntroductionPanels();
|
||||
this.EnsureDefaultExpandedPanel();
|
||||
|
||||
// Read the last change content asynchronously
|
||||
// without blocking the UI thread:
|
||||
@ -69,6 +82,14 @@ public partial class Home : MSGComponentBase
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.InitializeAdvantagesItems();
|
||||
this.RefreshIntroductionPanels();
|
||||
this.EnsureDefaultExpandedPanel();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.RefreshIntroductionPanels();
|
||||
this.EnsureDefaultExpandedPanel();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
@ -76,6 +97,42 @@ public partial class Home : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
private void RefreshIntroductionPanels()
|
||||
{
|
||||
this.introductions = PluginFactory.GetIntroductions().ToList();
|
||||
}
|
||||
|
||||
private string GetDefaultExpandedPanelId()
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
|
||||
return PANEL_ID_BUILT_IN_INTRODUCTION;
|
||||
|
||||
var firstIntroduction = this.introductions.FirstOrDefault();
|
||||
return firstIntroduction is not null
|
||||
? IntroductionPanelId(firstIntroduction)
|
||||
: PANEL_ID_LAST_CHANGELOG;
|
||||
}
|
||||
|
||||
private void EnsureDefaultExpandedPanel()
|
||||
{
|
||||
this.expandedPanelId = this.GetDefaultExpandedPanelId();
|
||||
this.expansionPanelsRenderKey++;
|
||||
}
|
||||
|
||||
private bool IsPanelExpanded(string panelId) => string.Equals(this.expandedPanelId, panelId, StringComparison.Ordinal);
|
||||
|
||||
private Task SetPanelExpanded(string panelId, bool isExpanded)
|
||||
{
|
||||
if (isExpanded)
|
||||
this.expandedPanelId = panelId;
|
||||
else if (this.IsPanelExpanded(panelId))
|
||||
this.expandedPanelId = string.Empty;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string IntroductionPanelId(DataIntroduction introduction) => $"introduction:{introduction.Id}";
|
||||
|
||||
private async Task ReadLastChangeAsync()
|
||||
{
|
||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
||||
|
||||
@ -21,11 +21,11 @@
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionRust"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage">
|
||||
<MudText Typo="Typo.body1">
|
||||
@this.VersionDatabase
|
||||
@this.VersionVectorStore
|
||||
</MudText>
|
||||
<MudCollapse Expanded="@this.showDatabaseDetails">
|
||||
<MudCollapse Expanded="@this.showVectorStoreDetails">
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
@foreach (var item in this.databaseDisplayInfo)
|
||||
@foreach (var item in this.vectorStoreDisplayInfo)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
@ -35,11 +35,11 @@
|
||||
}
|
||||
</MudText>
|
||||
</MudCollapse>
|
||||
<MudButton StartIcon="@(this.showDatabaseDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
<MudButton StartIcon="@(this.showVectorStoreDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
OnClick="@this.ToggleDatabaseDetails">
|
||||
@(this.showDatabaseDetails ? T("Hide Details") : T("Show Details"))
|
||||
OnClick="@this.ToggleVectorStoreDetails">
|
||||
@(this.showVectorStoreDetails ? T("Hide Details") : T("Show Details"))
|
||||
</MudButton>
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/>
|
||||
@ -47,6 +47,27 @@
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.AccountCircle" Text="@this.OSUserName"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudText Typo="Typo.body1">
|
||||
@this.WorkingDirectory
|
||||
</MudText>
|
||||
<MudCopyClipboardButton TooltipMessage="@(T("Copies the working directory to the clipboard"))" StringContent="@this.runtimeInfo.WorkingDirectory"/>
|
||||
</div>
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.InsertDriveFile">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudText Typo="Typo.body1">
|
||||
@this.ExecutablePath
|
||||
</MudText>
|
||||
<MudCopyClipboardButton TooltipMessage="@(T("Copies the executable path to the clipboard"))" StringContent="@this.runtimeInfo.ExecutablePath"/>
|
||||
</div>
|
||||
</MudListItem>
|
||||
@if (OperatingSystem.IsLinux())
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.LinuxPackageType"/>
|
||||
}
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||
@switch (HasAnyActiveEnvironment)
|
||||
{
|
||||
@ -88,18 +109,7 @@
|
||||
{
|
||||
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
|
||||
HeaderText="@T("Waiting for the configuration plugin...")"
|
||||
Items="@([
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||
env.ConfigurationId.ToString(),
|
||||
T("Copies the config ID to the clipboard")),
|
||||
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||
env.ConfigurationServerUrl,
|
||||
T("Copies the server URL to the clipboard"),
|
||||
"margin-top: 4px;")
|
||||
])"/>
|
||||
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
|
||||
}
|
||||
|
||||
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
|
||||
@ -129,41 +139,13 @@
|
||||
{
|
||||
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
|
||||
HeaderText="@T("Waiting for the configuration plugin...")"
|
||||
Items="@([
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||
env.ConfigurationId.ToString(),
|
||||
T("Copies the config ID to the clipboard")),
|
||||
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||
env.ConfigurationServerUrl,
|
||||
T("Copies the server URL to the clipboard"),
|
||||
"margin-top: 4px;")
|
||||
])"/>
|
||||
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
|
||||
continue;
|
||||
}
|
||||
|
||||
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
|
||||
HeaderText="@matchingPlugin.Name"
|
||||
Items="@([
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||
env.ConfigurationId.ToString(),
|
||||
T("Copies the config ID to the clipboard")),
|
||||
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||
env.ConfigurationServerUrl,
|
||||
T("Copies the server URL to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
|
||||
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
|
||||
matchingPlugin.Id.ToString(),
|
||||
T("Copies the configuration plugin ID to the clipboard"),
|
||||
"margin-top: 4px;")
|
||||
])"
|
||||
Items="@this.BuildEnterpriseConfigurationItems(env, matchingPlugin)"
|
||||
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
|
||||
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
|
||||
}
|
||||
@ -185,9 +167,32 @@
|
||||
</MudButton>
|
||||
}
|
||||
</MudListItem>
|
||||
@if (ExternalHttpClientTimeout.CustomRootCertificateState.IsEnabled)
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Security">
|
||||
<MudText Typo="Typo.body1">
|
||||
@(ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable
|
||||
? T("External HTTPS custom root certificates are active.")
|
||||
: T("External HTTPS custom root certificates are configured but not active."))
|
||||
</MudText>
|
||||
<MudCollapse Expanded="@this.showExternalHttpCustomRootCertificateDetails">
|
||||
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Security"
|
||||
HeaderText="@T("External HTTPS custom root certificates")"
|
||||
Items="@this.BuildExternalHttpCustomRootCertificateItems()"
|
||||
ShowWarning="@(!ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable)"
|
||||
WarningText="@this.ExternalHttpCustomRootCertificateWarningText"/>
|
||||
</MudCollapse>
|
||||
<MudButton StartIcon="@(this.showExternalHttpCustomRootCertificateDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
OnClick="@this.ToggleExternalHttpCustomRootCertificateDetails">
|
||||
@(this.showExternalHttpCustomRootCertificateDetails ? T("Hide Details") : T("Show Details"))
|
||||
</MudButton>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
<MudStack Row="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@(() => this.CheckForUpdate())">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@this.CheckForUpdate">
|
||||
@T("Check for updates")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Default" StartIcon="@Icons.Material.Filled.Download" OnClick="@(async () => await this.ShowPandocDialog())">
|
||||
@ -278,13 +283,19 @@
|
||||
<ThirdPartyComponent Name="CodeBeam.MudBlazor.Extensions" Developer="Mehmet Can Karagöz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions/blob/dev/LICENSE" RepositoryUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions" UseCase="@T("This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.")"/>
|
||||
<ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="@T("The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.")"/>
|
||||
<ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/>
|
||||
<ThirdPartyComponent Name="Qdrant" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
|
||||
|
||||
@if (OperatingSystem.IsLinux())
|
||||
{
|
||||
<ThirdPartyComponent Name="GStreamer" Developer="GStreamer contributors & Open Source Community" LicenseName="LGPL-2.1" LicenseUrl="https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html" RepositoryUrl="https://gitlab.freedesktop.org/gstreamer/gstreamer" UseCase="@T("Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.")"/>
|
||||
}
|
||||
|
||||
<ThirdPartyComponent Name="Qdrant Edge" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
|
||||
<ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/>
|
||||
<ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/>
|
||||
<ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/>
|
||||
<ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/>
|
||||
<ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.")"/>
|
||||
<ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.")"/>
|
||||
<ThirdPartyComponent Name="keyring-core" Developer="Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/open-source-cooperative/keyring-core/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/open-source-cooperative/keyring-core" UseCase="@T("AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.")"/>
|
||||
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/>
|
||||
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/>
|
||||
<ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/>
|
||||
@ -301,8 +312,9 @@
|
||||
<ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
|
||||
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
|
||||
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/>
|
||||
<ThirdPartyComponent Name="whoami" Developer="Ardaku Systems, Jeryn Aldaron Lau, Chase Johnson & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ardaku/whoami/blob/stable/LICENSE_MIT" RepositoryUrl="https://github.com/ardaku/whoami" UseCase="@T("This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.")"/>
|
||||
<ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/>
|
||||
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.")"/>
|
||||
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders in runtime tests and supporting filesystem operations.")"/>
|
||||
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" />
|
||||
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="@T("We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
|
||||
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
|
||||
|
||||
@ -4,6 +4,7 @@ using AIStudio.Components;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.Databases.VectorStore;
|
||||
using AIStudio.Tools.Metadata;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
@ -29,17 +30,19 @@ public partial class Information : MSGComponentBase
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private DatabaseClient DatabaseClient { get; init; } = null!;
|
||||
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
|
||||
private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!;
|
||||
private static readonly MetaDataDatabasesAttribute META_DATA_DATABASES = ASSEMBLY.GetCustomAttribute<MetaDataDatabasesAttribute>()!;
|
||||
private static readonly MetaDataVectorStoreAttribute META_DATA_VECTOR_STORE = ASSEMBLY.GetCustomAttribute<MetaDataVectorStoreAttribute>()!;
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
|
||||
|
||||
private string osLanguage = string.Empty;
|
||||
private string osUserName = string.Empty;
|
||||
private RuntimeInfoResponse runtimeInfo;
|
||||
|
||||
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})";
|
||||
|
||||
@ -49,6 +52,22 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'";
|
||||
|
||||
private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'";
|
||||
|
||||
private string WorkingDirectory => $"{T("Working directory")}: {this.runtimeInfo.WorkingDirectory}";
|
||||
|
||||
private string ExecutablePath => $"{T("Executable path")}: {this.runtimeInfo.ExecutablePath}";
|
||||
|
||||
private string LinuxPackageType => $"{T("Linux package")}: {this.LinuxPackageTypeDisplayName}";
|
||||
|
||||
private string LinuxPackageTypeDisplayName => this.runtimeInfo.LinuxPackageType switch
|
||||
{
|
||||
"appimage" => "AppImage",
|
||||
"flatpak" => "Flatpak",
|
||||
"unknown" => T("unknown"),
|
||||
_ => T("not applicable")
|
||||
};
|
||||
|
||||
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
||||
|
||||
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
||||
@ -59,9 +78,21 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
|
||||
|
||||
private string VersionDatabase => this.DatabaseClient.IsAvailable
|
||||
? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"
|
||||
: $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}";
|
||||
private string VersionVectorStore
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.vectorStore is null)
|
||||
return $"{T("Vector store")}: {T("checking availability")}";
|
||||
|
||||
return this.vectorStore.Status switch
|
||||
{
|
||||
DatabaseClientStatus.AVAILABLE => $"{T("Vector store version")}: {this.vectorStore.Name} v{META_DATA_VECTOR_STORE.VectorStoreVersion}",
|
||||
DatabaseClientStatus.STARTING => $"{T("Vector store")}: {this.vectorStore.Name} - {T("starting")}",
|
||||
_ => $"{T("Vector store")}: {this.vectorStore.Name} - {T("not available")}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string versionPandoc = TB("Determine Pandoc version, please wait...");
|
||||
private PandocInstallation pandocInstallation;
|
||||
@ -70,7 +101,8 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private bool showEnterpriseConfigDetails;
|
||||
|
||||
private bool showDatabaseDetails;
|
||||
private bool showVectorStoreDetails;
|
||||
private bool showExternalHttpCustomRootCertificateDetails;
|
||||
|
||||
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
|
||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||
@ -80,12 +112,13 @@ public partial class Information : MSGComponentBase
|
||||
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
||||
|
||||
private List<MandatoryInfoPanelData> mandatoryInfoPanels = [];
|
||||
|
||||
private sealed record DatabaseDisplayInfo(string Label, string Value);
|
||||
|
||||
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
|
||||
|
||||
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
||||
|
||||
private sealed record VectorStoreDisplayInfo(string Label, string Value);
|
||||
private readonly List<VectorStoreDisplayInfo> vectorStoreDisplayInfo = new();
|
||||
private DatabaseClient? vectorStore;
|
||||
private CancellationTokenSource? vectorStoreRefreshCancellationTokenSource;
|
||||
|
||||
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
|
||||
|
||||
@ -128,12 +161,13 @@ public partial class Information : MSGComponentBase
|
||||
this.RefreshEnterpriseConfigurationState();
|
||||
|
||||
this.osLanguage = await this.RustService.ReadUserLanguage();
|
||||
this.osUserName = await this.RustService.ReadUserName();
|
||||
this.runtimeInfo = await this.RustService.GetRuntimeInfo();
|
||||
this.logPaths = await this.RustService.GetLogPaths();
|
||||
|
||||
await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo())
|
||||
{
|
||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||
}
|
||||
await this.RefreshVectorStoreInfo(CancellationToken.None);
|
||||
if (this.vectorStore?.Status is DatabaseClientStatus.STARTING)
|
||||
this.StartShortVectorStoreRefreshLoop();
|
||||
|
||||
// Determine the Pandoc version may take some time, so we start it here
|
||||
// without waiting for the result:
|
||||
@ -231,10 +265,78 @@ public partial class Information : MSGComponentBase
|
||||
{
|
||||
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
|
||||
}
|
||||
|
||||
private void ToggleDatabaseDetails()
|
||||
|
||||
private void ToggleExternalHttpCustomRootCertificateDetails()
|
||||
{
|
||||
this.showDatabaseDetails = !this.showDatabaseDetails;
|
||||
this.showExternalHttpCustomRootCertificateDetails = !this.showExternalHttpCustomRootCertificateDetails;
|
||||
}
|
||||
|
||||
private void ToggleVectorStoreDetails()
|
||||
{
|
||||
this.showVectorStoreDetails = !this.showVectorStoreDetails;
|
||||
}
|
||||
|
||||
private async Task RefreshVectorStoreInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
|
||||
this.vectorStore = refreshedClient;
|
||||
this.vectorStoreDisplayInfo.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.vectorStore = new NoVectorStoreClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
|
||||
await foreach (var (label, value) in this.vectorStore.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartShortVectorStoreRefreshLoop()
|
||||
{
|
||||
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
|
||||
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
|
||||
this.vectorStoreRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = this.vectorStoreRefreshCancellationTokenSource.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
const int MAX_TRIES = 12;
|
||||
for (var attempt = 0; attempt < MAX_TRIES; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
await this.InvokeAsync(async () =>
|
||||
{
|
||||
await this.RefreshVectorStoreInfo(cancellationToken);
|
||||
this.StateHasChanged();
|
||||
});
|
||||
|
||||
if (this.vectorStore?.Status is not DatabaseClientStatus.STARTING)
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||
@ -244,11 +346,139 @@ public partial class Information : MSGComponentBase
|
||||
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ConfigInfoRowItem> BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null)
|
||||
{
|
||||
var items = new List<ConfigInfoRowItem>
|
||||
{
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Enterprise configuration ID:")} {environment.ConfigurationId}",
|
||||
environment.ConfigurationId.ToString(),
|
||||
T("Copies the config ID to the clipboard")),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration server:")} {environment.ConfigurationServerUrl}",
|
||||
environment.ConfigurationServerUrl,
|
||||
T("Copies the server URL to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration source:")} {environment.Source}",
|
||||
environment.Source,
|
||||
T("Copies the configuration source to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(environment.SourceDetail))
|
||||
{
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration origin:")} {environment.SourceDetail}",
|
||||
environment.SourceDetail,
|
||||
T("Copies the configuration origin to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
}
|
||||
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration slot:")} {environment.Slot}",
|
||||
environment.Slot,
|
||||
T("Copies the configuration slot to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
|
||||
if (plugin is not null)
|
||||
{
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration plugin ID:")} {plugin.Id}",
|
||||
plugin.Id.ToString(),
|
||||
T("Copies the configuration plugin ID to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
|
||||
{
|
||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||
}
|
||||
|
||||
private string ExternalHttpCustomRootCertificateWarningText
|
||||
{
|
||||
get
|
||||
{
|
||||
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
|
||||
return string.IsNullOrWhiteSpace(state.Issue)
|
||||
? T("The configured root certificates could not be used.")
|
||||
: state.Issue;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<ConfigInfoRowItem> BuildExternalHttpCustomRootCertificateItems()
|
||||
{
|
||||
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
|
||||
var items = new List<ConfigInfoRowItem>
|
||||
{
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Status:")} {(state.IsUsable ? T("active") : T("not active"))}",
|
||||
state.IsUsable ? T("active") : T("not active"),
|
||||
T("Copies the status to the clipboard")),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration source:")} {state.Source}",
|
||||
state.Source,
|
||||
T("Copies the configuration source to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Certificate bundle:")} {state.BundlePath}",
|
||||
state.BundlePath,
|
||||
T("Copies the certificate bundle path to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Loaded root certificates:")} {state.CertificateCount}",
|
||||
state.CertificateCount.ToString(),
|
||||
T("Copies the number of loaded root certificates to the clipboard"),
|
||||
"margin-top: 4px;")
|
||||
};
|
||||
|
||||
if (state.AllowedHostPatterns.Count == 0)
|
||||
{
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||
T("Allowed hosts: none configured"),
|
||||
string.Empty,
|
||||
T("Copies the allowed host configuration to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var allowedHostPattern in state.AllowedHostPatterns)
|
||||
{
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Dns,
|
||||
$"{T("Allowed host:")} {allowedHostPattern}",
|
||||
allowedHostPattern,
|
||||
T("Copies the allowed host pattern to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var fingerprint in state.CertificateFingerprints)
|
||||
{
|
||||
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Fingerprint,
|
||||
$"{T("Root certificate fingerprint:")} {fingerprint}",
|
||||
fingerprint,
|
||||
T("Copies the root certificate fingerprint to the clipboard"),
|
||||
"margin-top: 4px;"));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
|
||||
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
private async Task CopyStartupLogPath()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
<InnerScrolling>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
<SettingsPanelConfidence/>
|
||||
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders"/>
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
|
||||
@ -10,6 +10,7 @@ namespace AIStudio.Pages;
|
||||
|
||||
public partial class Writer : MSGComponentBase
|
||||
{
|
||||
private static readonly ILogger<Writer> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Writer>();
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
|
||||
|
||||
@ -106,22 +107,38 @@ public partial class Writer : MSGComponentBase
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
var aiBlock = new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
});
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(aiBlock);
|
||||
|
||||
this.isStreaming = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.suggestion = aiText.Text;
|
||||
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.suggestion = aiText.Text;
|
||||
}
|
||||
catch (ProviderRequestException e)
|
||||
{
|
||||
LOGGER.LogError(e, "The provider request failed for writer suggestions. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", e.StatusCode, e.ReasonPhrase, e.ResponseBody);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
|
||||
this.suggestion = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(aiText.Text))
|
||||
this.chatThread?.Blocks.Remove(aiBlock);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void AcceptEntireSuggestion()
|
||||
|
||||
@ -136,6 +136,54 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- ERI v1 data sources for retrieval-augmented generation:
|
||||
CONFIG["DATA_SOURCES"] = {}
|
||||
|
||||
-- Example: ERI v1 data source with a shared access token.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "TOKEN",
|
||||
-- ["Token"] = "ENC:v1:<base64-encoded encrypted token>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
-- Example: ERI v1 data source with a shared username and password.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "USERNAME_PASSWORD",
|
||||
-- ["UsernamePasswordMode"] = "SHARED_USERNAME_AND_PASSWORD",
|
||||
-- ["Username"] = "<shared username>",
|
||||
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
-- Example: ERI v1 data source using the user's username and a shared password.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "USERNAME_PASSWORD",
|
||||
-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD",
|
||||
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
CONFIG["SETTINGS"] = {}
|
||||
|
||||
-- Configure the update check interval:
|
||||
@ -156,12 +204,16 @@ CONFIG["SETTINGS"] = {}
|
||||
-- but users can still choose another start page in the app settings.
|
||||
-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true
|
||||
|
||||
-- Configure whether the quick start guide is shown on the welcome page.
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false
|
||||
|
||||
-- Configure whether the built-in introduction is shown on the welcome page.
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShowIntroduction"] = false
|
||||
|
||||
-- Configure the user permission to add providers:
|
||||
-- Allowed values are: true, false
|
||||
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
|
||||
|
||||
-- Configure whether administration settings are visible in the UI:
|
||||
-- Allowed values are: true, false
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
|
||||
|
||||
-- Configure the visibility of preview features:
|
||||
@ -172,9 +224,9 @@ CONFIG["SETTINGS"] = {}
|
||||
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
|
||||
|
||||
-- Configure the enabled preview features:
|
||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026.
|
||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" }
|
||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||
-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
|
||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
|
||||
|
||||
-- Configure the preselected provider.
|
||||
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||
@ -186,6 +238,32 @@ CONFIG["SETTINGS"] = {}
|
||||
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
|
||||
-- CONFIG["SETTINGS"]["DataApp.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
-- Configure chat-specific preselected options.
|
||||
-- This must be enabled for the chat-specific provider, profile, and chat template to take effect.
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions"] = true
|
||||
--
|
||||
-- Configure the preselected provider for chats.
|
||||
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
|
||||
--
|
||||
-- Configure the preselected profile for chats.
|
||||
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
|
||||
-- Please note: using an empty string ("") means chats will use the app default profile.
|
||||
-- Please note: using "00000000-0000-0000-0000-000000000000" means chats will use no profile.
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
|
||||
--
|
||||
-- Configure the preselected chat template for chats.
|
||||
-- It must be one of the chat template IDs defined in CONFIG["CHAT_TEMPLATES"].
|
||||
-- Please note: using an empty string ("") or "00000000-0000-0000-0000-000000000000" means chats will use no chat template.
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate"] = "00000000-0000-0000-0000-000000000000"
|
||||
--
|
||||
-- Allow users to change any configured chat default locally.
|
||||
-- Allowed values are: true, false
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate.AllowUserOverride"] = true
|
||||
|
||||
-- Configure the transcription provider for voice-to-text functionality.
|
||||
-- It must be one of the transcription provider IDs defined in CONFIG["TRANSCRIPTION_PROVIDERS"].
|
||||
-- Without a selected transcription provider, dictation and transcription features will be disabled.
|
||||
@ -212,6 +290,89 @@ CONFIG["SETTINGS"] = {}
|
||||
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
|
||||
|
||||
-- Configure the HTTP timeout for external requests, in seconds.
|
||||
-- The default is 3600 (1 hour).
|
||||
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
|
||||
|
||||
-- Configure additional root certificates for external HTTPS requests.
|
||||
--
|
||||
-- This is intended for managed Linux/Flatpak deployments where organization-internal
|
||||
-- HTTPS certificates chain to a private root CA that is not visible inside the sandbox.
|
||||
-- The file must be a PEM bundle with one or more root CA certificates and must be
|
||||
-- readable by AI Studio.
|
||||
--
|
||||
-- IMPORTANT: A configuration plugin cannot fix the very first download of that same
|
||||
-- configuration plugin. For bootstrapping enterprise configuration downloads, deploy
|
||||
-- the equivalent environment variables before AI Studio starts:
|
||||
--
|
||||
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
|
||||
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
|
||||
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;data.example.org
|
||||
--
|
||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
|
||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
|
||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
|
||||
|
||||
-- Configure provider confidence settings.
|
||||
-- These settings apply to LLM providers, embedding providers, and transcription providers.
|
||||
--
|
||||
-- Configure a predefined confidence scheme.
|
||||
-- Allowed values are: TRUST_ALL, TRUST_USA_EUROPE, TRUST_USA, TRUST_EUROPE, TRUST_ASIA, LOCAL_TRUST_ONLY, CUSTOM
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme"] = "TRUST_EUROPE"
|
||||
--
|
||||
-- Configure whether users can still change the confidence scheme locally.
|
||||
-- Allowed values are: true, false
|
||||
-- When set to true, the configured confidence scheme becomes the organization default,
|
||||
-- but users can still choose another scheme in the app settings.
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme.AllowUserOverride"] = true
|
||||
--
|
||||
-- Configure whether confidence levels are shown in the UI.
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.ShowProviderConfidence"] = true
|
||||
--
|
||||
-- Configure an app-wide minimum confidence level.
|
||||
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence"] = true
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence"] = "MEDIUM"
|
||||
--
|
||||
-- Configure whether users can change the app-wide minimum confidence level locally.
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence.AllowUserOverride"] = false
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence.AllowUserOverride"] = false
|
||||
--
|
||||
-- Configure a custom confidence scheme.
|
||||
-- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM.
|
||||
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
|
||||
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
|
||||
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme"] = {
|
||||
-- ["OPEN_AI"] = "MODERATE",
|
||||
-- ["ANTHROPIC"] = "MODERATE",
|
||||
-- ["MISTRAL"] = "HIGH",
|
||||
-- ["GOOGLE"] = "LOW",
|
||||
-- ["X"] = "LOW",
|
||||
-- ["DEEP_SEEK"] = "LOW",
|
||||
-- ["ALIBABA_CLOUD"] = "LOW",
|
||||
-- ["PERPLEXITY"] = "MODERATE",
|
||||
-- ["OPEN_ROUTER"] = "MODERATE",
|
||||
-- ["FIREWORKS"] = "MODERATE",
|
||||
-- ["GROQ"] = "MODERATE",
|
||||
-- ["HUGGINGFACE"] = "MODERATE",
|
||||
-- ["SELF_HOSTED"] = "HIGH",
|
||||
-- ["HELMHOLTZ"] = "HIGH",
|
||||
-- ["GWDG"] = "HIGH",
|
||||
-- }
|
||||
--
|
||||
-- Configure whether users can change the custom confidence scheme locally.
|
||||
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme.AllowUserOverride"] = false
|
||||
--
|
||||
-- Configure provider instances trusted by your organization for data-source security checks.
|
||||
-- These IDs may refer to LLM providers, embedding providers, or transcription providers
|
||||
-- defined in this configuration. Trusted providers are treated like self-hosted providers
|
||||
-- only for data-source security checks and related local data warnings.
|
||||
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = {
|
||||
-- "00000000-0000-0000-0000-000000000000",
|
||||
-- "00000000-0000-0000-0000-000000000001",
|
||||
-- }
|
||||
|
||||
-- Example chat templates for this configuration:
|
||||
CONFIG["CHAT_TEMPLATES"] = {}
|
||||
|
||||
@ -246,7 +407,8 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
||||
-- ["AllowProfileUsage"] = true,
|
||||
-- -- Optional: Pre-attach files that will be automatically included when using this template.
|
||||
-- -- These files will be loaded when the user selects this chat template.
|
||||
-- -- Note: File paths must be absolute paths and accessible to all users.
|
||||
-- -- Note: File paths can be absolute paths that are accessible to all users, or relative paths
|
||||
-- -- inside this plugin folder, for example "attachments/00000000-0000-0000-0000-000000000001/Guidelines.pdf".
|
||||
-- ["FileAttachments"] = {
|
||||
-- "G:\\Company\\Documents\\Guidelines.pdf",
|
||||
-- "G:\\Company\\Documents\\CompanyPolicies.docx"
|
||||
@ -263,6 +425,26 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- Introduction texts shown as expansion panels on the welcome page:
|
||||
CONFIG["INTRODUCTIONS"] = {}
|
||||
|
||||
-- An example introduction:
|
||||
-- CONFIG["INTRODUCTIONS"][#CONFIG["INTRODUCTIONS"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Title"] = "Welcome to Your Organization's AI Studio",
|
||||
-- ["Version"] = "1",
|
||||
-- ["Index"] = 1,
|
||||
-- ["Markdown"] = [===[
|
||||
-- ## Getting Started
|
||||
--
|
||||
-- This AI Studio installation is managed by your organization.
|
||||
-- Please use the preconfigured providers and follow your internal
|
||||
-- AI usage guidelines.
|
||||
--
|
||||
-- Further information is available in the [internal wiki](https://example.org/wiki).
|
||||
-- ]===]
|
||||
-- }
|
||||
|
||||
-- Mandatory infos that users must explicitly accept before using AI Studio:
|
||||
-- AI Studio asks users again when Version, Title, or Markdown change.
|
||||
-- Changing Version additionally allows the UI to communicate that a new version is available.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.Databases.Qdrant;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -28,7 +28,7 @@ internal sealed class Program
|
||||
public static string API_TOKEN = null!;
|
||||
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||
public static ILoggerFactory LOGGER_FACTORY = null!;
|
||||
public static DatabaseClient DATABASE_CLIENT = null!;
|
||||
public static DatabaseClientProvider DATABASE_CLIENT_PROVIDER = null!;
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
@ -87,48 +87,6 @@ internal sealed class Program
|
||||
return;
|
||||
}
|
||||
|
||||
var qdrantInfo = await rust.GetQdrantInfo();
|
||||
DatabaseClient databaseClient;
|
||||
if (!qdrantInfo.IsAvailable)
|
||||
{
|
||||
Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'.");
|
||||
databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (qdrantInfo.Path == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant path from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortHttp == 0)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortGrpc == 0)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.Fingerprint == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.ApiToken == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant API token from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
||||
{
|
||||
@ -171,6 +129,7 @@ internal sealed class Program
|
||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||
builder.Services.AddSingleton<SettingsManager>();
|
||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||
builder.Services.AddSingleton<AIJobService>();
|
||||
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
|
||||
builder.Services.AddSingleton<DataSourceService>();
|
||||
builder.Services.AddScoped<PandocAvailabilityService>();
|
||||
@ -183,7 +142,7 @@ internal sealed class Program
|
||||
builder.Services.AddHostedService<UpdateService>();
|
||||
builder.Services.AddHostedService<TemporaryChatService>();
|
||||
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
|
||||
builder.Services.AddSingleton(databaseClient);
|
||||
builder.Services.AddSingleton<DatabaseClientProvider>();
|
||||
builder.Services.AddHostedService<GlobalShortcutService>();
|
||||
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
||||
|
||||
@ -242,10 +201,7 @@ internal sealed class Program
|
||||
|
||||
RUST_SERVICE = rust;
|
||||
ENCRYPTION = encryption;
|
||||
|
||||
var databaseLogger = app.Services.GetRequiredService<ILogger<DatabaseClient>>();
|
||||
databaseClient.SetLogger(databaseLogger);
|
||||
DATABASE_CLIENT = databaseClient;
|
||||
DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>();
|
||||
|
||||
programLogger.LogInformation("Initialize internal file system.");
|
||||
app.Use(Redirect.HandlerContentAsync);
|
||||
@ -283,7 +239,7 @@ internal sealed class Program
|
||||
await serverTask;
|
||||
|
||||
RUST_SERVICE.Dispose();
|
||||
DATABASE_CLIENT.Dispose();
|
||||
DATABASE_CLIENT_PROVIDER.Dispose();
|
||||
PluginFactory.Dispose();
|
||||
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.AlibabaCloud;
|
||||
|
||||
public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/", LOGGER)
|
||||
public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, new Uri("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
|
||||
{
|
||||
private static readonly ILogger<ProviderAlibabaCloud> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAlibabaCloud>();
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.ALIBABA_CLOUD.ToName();
|
||||
public override string Id => LLMProviders.ALIBABA_CLOUD.ToSecretId();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "AlibabaCloud";
|
||||
@ -60,15 +60,15 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||
var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||
}
|
||||
|
||||
|
||||
@ -8,14 +8,14 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "https://api.anthropic.com/v1/", LOGGER)
|
||||
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, new Uri("https://api.anthropic.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
|
||||
{
|
||||
private static readonly ILogger<ProviderAnthropic> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAnthropic>();
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.ANTHROPIC.ToName();
|
||||
public override string Id => LLMProviders.ANTHROPIC.ToSecretId();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Anthropic";
|
||||
@ -27,7 +27,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
@ -93,7 +93,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
@ -116,9 +116,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
@ -179,6 +179,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
{
|
||||
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
},
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user