mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 17:16:28 +00:00
Merge branch 'main' into Fehlermeldung-.doc-Datei
This commit is contained in:
commit
5f8afbf869
171
.github/workflows/build-and-release.yml
vendored
171
.github/workflows/build-and-release.yml
vendored
@ -12,6 +12,10 @@ on:
|
|||||||
- synchronize
|
- synchronize
|
||||||
- reopened
|
- 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:
|
env:
|
||||||
RETENTION_INTERMEDIATE_ASSETS: 1
|
RETENTION_INTERMEDIATE_ASSETS: 1
|
||||||
RETENTION_RELEASE_ASSETS: 30
|
RETENTION_RELEASE_ASSETS: 30
|
||||||
@ -37,6 +41,8 @@ jobs:
|
|||||||
id: determine
|
id: determine
|
||||||
env:
|
env:
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
PR_ACTION: ${{ github.event.action }}
|
||||||
|
ACTION_LABEL_NAME: ${{ github.event.label.name }}
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
|
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
|
||||||
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@ -55,6 +61,11 @@ jobs:
|
|||||||
is_internal_pr=true
|
is_internal_pr=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
has_run_pipeline_label=false
|
||||||
|
if [[ " $PR_LABELS " == *" run-pipeline "* ]]; then
|
||||||
|
has_run_pipeline_label=true
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$REF" == refs/tags/v* ]]; then
|
if [[ "$REF" == refs/tags/v* ]]; then
|
||||||
is_release=true
|
is_release=true
|
||||||
build_enabled=true
|
build_enabled=true
|
||||||
@ -65,13 +76,21 @@ jobs:
|
|||||||
build_enabled=true
|
build_enabled=true
|
||||||
artifact_retention_days=7
|
artifact_retention_days=7
|
||||||
skip_reason=""
|
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_labeled_pr=true
|
||||||
is_pr_build=true
|
is_pr_build=true
|
||||||
build_enabled=true
|
build_enabled=true
|
||||||
artifact_retention_days=3
|
artifact_retention_days=3
|
||||||
skip_reason=""
|
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."
|
skip_reason="Build disabled: PR does not have the required 'run-pipeline' label."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -220,29 +239,29 @@ jobs:
|
|||||||
rust_target: 'aarch64-apple-darwin'
|
rust_target: 'aarch64-apple-darwin'
|
||||||
dotnet_runtime: 'osx-arm64'
|
dotnet_runtime: 'osx-arm64'
|
||||||
dotnet_name_postfix: '-aarch64-apple-darwin'
|
dotnet_name_postfix: '-aarch64-apple-darwin'
|
||||||
tauri_bundle: 'dmg,updater'
|
tauri_bundle: 'dmg,app,updater'
|
||||||
tauri_bundle_pr: 'dmg'
|
tauri_bundle_pr: 'dmg'
|
||||||
|
|
||||||
- platform: 'macos-latest' # for Intel-based macOS
|
- platform: 'macos-latest' # for Intel-based macOS
|
||||||
rust_target: 'x86_64-apple-darwin'
|
rust_target: 'x86_64-apple-darwin'
|
||||||
dotnet_runtime: 'osx-x64'
|
dotnet_runtime: 'osx-x64'
|
||||||
dotnet_name_postfix: '-x86_64-apple-darwin'
|
dotnet_name_postfix: '-x86_64-apple-darwin'
|
||||||
tauri_bundle: 'dmg,updater'
|
tauri_bundle: 'dmg,app,updater'
|
||||||
tauri_bundle_pr: 'dmg'
|
tauri_bundle_pr: 'dmg'
|
||||||
|
|
||||||
- platform: 'ubuntu-22.04' # for x86-based Linux
|
- platform: 'ubuntu-22.04' # for x86-based Linux
|
||||||
rust_target: 'x86_64-unknown-linux-gnu'
|
rust_target: 'x86_64-unknown-linux-gnu'
|
||||||
dotnet_runtime: 'linux-x64'
|
dotnet_runtime: 'linux-x64'
|
||||||
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
||||||
tauri_bundle: 'appimage,deb,updater'
|
tauri_bundle: 'appimage,updater'
|
||||||
tauri_bundle_pr: 'appimage,deb'
|
tauri_bundle_pr: 'appimage'
|
||||||
|
|
||||||
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
|
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
|
||||||
rust_target: 'aarch64-unknown-linux-gnu'
|
rust_target: 'aarch64-unknown-linux-gnu'
|
||||||
dotnet_runtime: 'linux-arm64'
|
dotnet_runtime: 'linux-arm64'
|
||||||
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
|
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
|
||||||
tauri_bundle: 'appimage,deb,updater'
|
tauri_bundle: 'appimage,updater'
|
||||||
tauri_bundle_pr: 'appimage,deb'
|
tauri_bundle_pr: 'appimage'
|
||||||
|
|
||||||
- platform: 'windows-latest' # for x86-based Windows
|
- platform: 'windows-latest' # for x86-based Windows
|
||||||
rust_target: 'x86_64-pc-windows-msvc'
|
rust_target: 'x86_64-pc-windows-msvc'
|
||||||
@ -685,11 +704,9 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/bin
|
|
||||||
~/.cargo/git/db/
|
~/.cargo/git/db/
|
||||||
~/.cargo/registry/index/
|
~/.cargo/registry/index/
|
||||||
~/.cargo/registry/cache/
|
~/.cargo/registry/cache/
|
||||||
~/.rustup/toolchains
|
|
||||||
runtime/target
|
runtime/target
|
||||||
|
|
||||||
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
|
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
|
||||||
@ -700,41 +717,63 @@ jobs:
|
|||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.rust_target }}
|
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)
|
- name: Setup dependencies (Ubuntu-specific, x86)
|
||||||
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils
|
||||||
|
|
||||||
- name: Setup dependencies (Ubuntu-specific, ARM)
|
- name: Setup dependencies (Ubuntu-specific, ARM)
|
||||||
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils
|
||||||
|
|
||||||
- name: Setup Tauri (Unix)
|
- name: Setup Tauri (Unix)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
if ! cargo tauri --version > /dev/null 2>&1; then
|
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
|
||||||
cargo install --version 1.6.2 tauri-cli
|
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 --root "$HOME/.cargo-tauri-cli"
|
||||||
else
|
else
|
||||||
echo "Tauri is already installed"
|
echo "Tauri CLI v2 is already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Tauri (Windows)
|
- name: Setup Tauri (Windows)
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
if (-not (cargo tauri --version 2>$null)) {
|
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
|
||||||
cargo install --version 1.6.2 tauri-cli
|
$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 --root "$env:USERPROFILE\.cargo-tauri-cli"
|
||||||
} else {
|
} else {
|
||||||
Write-Output "Tauri is already installed"
|
Write-Output "Tauri CLI v2 is already installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Delete previous artifact, which may exist due to caching (macOS)
|
- name: Delete previous artifact, which may exist due to caching (macOS)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
run: |
|
run: |
|
||||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
dmg_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/dmg"
|
||||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
|
macos_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/macos"
|
||||||
|
|
||||||
|
if [ -d "$dmg_dir" ]; then
|
||||||
|
find "$dmg_dir" -maxdepth 1 -name 'MindWork AI Studio_*.dmg' -delete
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$macos_dir" ]; then
|
||||||
|
find "$macos_dir" -maxdepth 1 -name '*.app' -exec rm -rf {} +
|
||||||
|
find "$macos_dir" -maxdepth 1 -name '*.app.tar.gz*' -delete
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
|
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
|
||||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
||||||
@ -748,16 +787,11 @@ jobs:
|
|||||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
|
||||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
|
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
|
||||||
run: |
|
|
||||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
|
|
||||||
|
|
||||||
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
|
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
||||||
run: |
|
run: |
|
||||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage
|
||||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz*
|
||||||
|
|
||||||
- name: Build Tauri project (Unix)
|
- name: Build Tauri project (Unix)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
@ -766,17 +800,39 @@ jobs:
|
|||||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
bundles="${{ matrix.tauri_bundle }}"
|
bundles="${{ matrix.tauri_bundle }}"
|
||||||
|
tauri_config_args=()
|
||||||
|
|
||||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
||||||
echo "Running PR test build without updater bundle signing"
|
echo "Running PR test build without updater bundle signing"
|
||||||
bundles="${{ matrix.tauri_bundle_pr }}"
|
bundles="${{ matrix.tauri_bundle_pr }}"
|
||||||
|
tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||||
else
|
else
|
||||||
export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
||||||
export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd runtime
|
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)
|
||||||
|
app_update_signature_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz.sig' | wc -l)
|
||||||
|
|
||||||
|
if [ "$app_update_archive_count" -eq 0 ] || [ "$app_update_signature_count" -eq 0 ]; then
|
||||||
|
echo "Expected macOS updater artifacts were not generated."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build Tauri project (Windows)
|
- name: Build Tauri project (Windows)
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
@ -785,17 +841,29 @@ jobs:
|
|||||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$bundles = "${{ matrix.tauri_bundle }}"
|
$bundles = "${{ matrix.tauri_bundle }}"
|
||||||
|
$tauriConfigArgs = @()
|
||||||
|
|
||||||
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
||||||
Write-Output "Running PR test build without updater bundle signing"
|
Write-Output "Running PR test build without updater bundle signing"
|
||||||
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
||||||
|
$tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||||
} else {
|
} else {
|
||||||
$env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
||||||
$env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
cd runtime
|
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)
|
- name: Upload artifact (macOS)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
@ -804,7 +872,7 @@ jobs:
|
|||||||
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
|
||||||
path: |
|
path: |
|
||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
|
runtime/target/${{ matrix.rust_target }}/release/bundle/macos/*.app.tar.gz*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||||
|
|
||||||
@ -830,24 +898,14 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||||
|
|
||||||
- name: Upload artifact (Linux - Debian Package)
|
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
|
|
||||||
path: |
|
|
||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
|
||||||
|
|
||||||
- name: Upload artifact (Linux - AppImage)
|
- name: Upload artifact (Linux - AppImage)
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
|
||||||
path: |
|
path: |
|
||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
|
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage
|
||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||||
|
|
||||||
@ -883,14 +941,14 @@ jobs:
|
|||||||
# Find and process files in the artifacts directory:
|
# Find and process files in the artifacts directory:
|
||||||
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
|
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
|
||||||
|
|
||||||
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
|
||||||
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
|
||||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
|
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
|
||||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
||||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig"
|
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig"
|
||||||
|
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
||||||
|
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
||||||
else
|
else
|
||||||
TARGET_NAME="$(basename "$FILE")"
|
TARGET_NAME="$(basename "$FILE")"
|
||||||
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
|
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
|
||||||
@ -941,9 +999,9 @@ jobs:
|
|||||||
platform="linux-x86_64"
|
platform="linux-x86_64"
|
||||||
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
||||||
platform="linux-aarch64"
|
platform="linux-aarch64"
|
||||||
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"x64-setup"* ]]; then
|
||||||
platform="windows-x86_64"
|
platform="windows-x86_64"
|
||||||
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"arm64-setup"* ]]; then
|
||||||
platform="windows-aarch64"
|
platform="windows-aarch64"
|
||||||
else
|
else
|
||||||
echo "Platform not recognized: '$sig_file'"
|
echo "Platform not recognized: '$sig_file'"
|
||||||
@ -1007,6 +1065,13 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
for platform in darwin-aarch64 darwin-x86_64 linux-aarch64 linux-x86_64 windows-aarch64 windows-x86_64; do
|
||||||
|
if ! jq -e --arg platform "$platform" '.platforms[$platform]' $GITHUB_WORKSPACE/release/assets/latest.json > /dev/null; then
|
||||||
|
echo "The generated latest.json is missing platform '$platform'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
- name: Show all release assets
|
- name: Show all release assets
|
||||||
run: ls -Rlhat $GITHUB_WORKSPACE/release/assets
|
run: ls -Rlhat $GITHUB_WORKSPACE/release/assets
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -169,3 +169,6 @@ orleans.codegen.cs
|
|||||||
|
|
||||||
# Ignore GitHub Copilot migration files:
|
# Ignore GitHub Copilot migration files:
|
||||||
**/copilot.data.migration.*.xml
|
**/copilot.data.migration.*.xml
|
||||||
|
|
||||||
|
# Tauri generated schemas/manifests
|
||||||
|
/runtime/gen/
|
||||||
|
|||||||
@ -49,7 +49,7 @@ Currently, no automated test suite exists in the repository.
|
|||||||
Key modules:
|
Key modules:
|
||||||
- `app_window.rs` - Tauri window management, updater integration
|
- `app_window.rs` - Tauri window management, updater integration
|
||||||
- `dotnet.rs` - Launches and manages the .NET sidecar process
|
- `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
|
- `certificate.rs` - Generates self-signed TLS certificates for secure IPC
|
||||||
- `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager)
|
- `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager)
|
||||||
- `clipboard.rs` - Cross-platform clipboard operations
|
- `clipboard.rs` - Cross-platform clipboard operations
|
||||||
@ -152,7 +152,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
|||||||
|
|
||||||
**Rust:**
|
**Rust:**
|
||||||
- Tauri 1.8 - Desktop application framework
|
- Tauri 1.8 - Desktop application framework
|
||||||
- Rocket - HTTPS API server
|
- Axum - HTTPS API server
|
||||||
- tokio - Async runtime
|
- tokio - Async runtime
|
||||||
- keyring - OS keyring integration
|
- keyring - OS keyring integration
|
||||||
- pdfium-render - PDF text extraction
|
- pdfium-render - PDF text extraction
|
||||||
@ -187,6 +187,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 >`
|
- **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.
|
- **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.
|
- **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
|
- **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.
|
- **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
|
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
||||||
|
|||||||
@ -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: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
- [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 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))~~
|
- [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))~~
|
- [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 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: 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))~~
|
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="UserContentModel">
|
<component name="UserContentModel">
|
||||||
<attachedFolders />
|
<attachedFolders>
|
||||||
|
<Path>../../mindwork-ai-studio</Path>
|
||||||
|
</attachedFolders>
|
||||||
<explicitIncludes />
|
<explicitIncludes />
|
||||||
<explicitExcludes />
|
<explicitExcludes />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@ -245,7 +245,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
Console.WriteLine("- Start building the Rust runtime ...");
|
Console.WriteLine("- Start building the Rust runtime ...");
|
||||||
|
|
||||||
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
|
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true);
|
||||||
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||||
var foundRustIssue = false;
|
var foundRustIssue = false;
|
||||||
foreach (var buildOutputLine in rustBuildOutputLines)
|
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||||
|
|||||||
@ -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/=AI/@EntryIndexedValue">AI</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=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/=FNV/@EntryIndexedValue">FNV</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
||||||
|
|||||||
@ -10,6 +10,8 @@ using AIStudio.Settings.DataModel;
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.DocumentAnalysis;
|
namespace AIStudio.Assistants.DocumentAnalysis;
|
||||||
@ -747,16 +749,12 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
return $$"""
|
return $$"""
|
||||||
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
||||||
["Id"] = "{{id}}",
|
["Id"] = "{{id}}",
|
||||||
["PolicyName"] = "{{this.selectedPolicy.PolicyName.Trim()}}",
|
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyName.Trim())}},
|
||||||
["PolicyDescription"] = "{{this.selectedPolicy.PolicyDescription.Trim()}}",
|
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyDescription.Trim())}},
|
||||||
|
|
||||||
["AnalysisRules"] = [===[
|
["AnalysisRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.AnalysisRules.Trim(), forceLongString: true)}},
|
||||||
{{this.selectedPolicy.AnalysisRules.Trim()}}
|
|
||||||
]===],
|
|
||||||
|
|
||||||
["OutputRules"] = [===[
|
["OutputRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.OutputRules.Trim(), forceLongString: true)}},
|
||||||
{{this.selectedPolicy.OutputRules.Trim()}}
|
|
||||||
]===],
|
|
||||||
|
|
||||||
-- Optional: minimum provider confidence required for this policy.
|
-- Optional: minimum provider confidence required for this policy.
|
||||||
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||||
|
|||||||
@ -2644,6 +2644,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
|||||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
||||||
|
|
||||||
|
-- seconds
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||||
|
|
||||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||||
|
|
||||||
@ -2692,6 +2695,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
|||||||
-- Spellchecking is enabled
|
-- Spellchecking is enabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||||
|
|
||||||
|
-- Request timeout
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
|
||||||
|
|
||||||
-- App Options
|
-- App Options
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
||||||
|
|
||||||
@ -2719,6 +2725,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
|||||||
-- Select a transcription provider
|
-- Select a transcription provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||||
|
|
||||||
|
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
|
||||||
|
|
||||||
-- Navigation bar behavior
|
-- Navigation bar behavior
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
||||||
|
|
||||||
@ -3631,6 +3640,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
|||||||
-- Maximum matches per query
|
-- Maximum matches per query
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||||
|
|
||||||
-- Open web link, show more information
|
-- Open web link, show more information
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
||||||
|
|
||||||
@ -3682,6 +3694,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
|||||||
-- Embeddings
|
-- Embeddings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
||||||
|
|
||||||
|
-- Use the same username and password for all users
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users"
|
||||||
|
|
||||||
|
-- Username and password mode
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode"
|
||||||
|
|
||||||
|
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"
|
||||||
|
|
||||||
|
-- User-managed username and password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password"
|
||||||
|
|
||||||
|
-- Export
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export"
|
||||||
|
|
||||||
|
-- Read each user's username from the operating system and share one password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password"
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
-- Describe what data this directory contains to help the AI select it.
|
-- Describe what data this directory contains to help the AI select it.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
||||||
|
|
||||||
@ -4717,6 +4750,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
|||||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
||||||
|
|
||||||
|
-- Copy attachments into plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
||||||
|
|
||||||
@ -4726,6 +4762,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
|||||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
||||||
|
|
||||||
|
-- Use shared attachment paths
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Use shared attachment paths"
|
||||||
|
|
||||||
-- No chat templates configured yet.
|
-- No chat templates configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||||
|
|
||||||
@ -4744,6 +4783,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
|||||||
-- This template is managed by your organization.
|
-- This template is managed by your organization.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
||||||
|
|
||||||
|
-- Select configuration plugin folder
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Select configuration plugin folder"
|
||||||
|
|
||||||
-- Edit Chat Template
|
-- Edit Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
||||||
|
|
||||||
@ -4756,6 +4798,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
|
|||||||
-- Delete Chat Template
|
-- Delete Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
||||||
|
|
||||||
|
-- Export Chat Template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- Which programming language should be preselected for added contexts?
|
-- Which programming language should be preselected for added contexts?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
||||||
|
|
||||||
@ -4810,6 +4858,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419
|
|||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete"
|
||||||
|
|
||||||
|
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted."
|
||||||
|
|
||||||
-- External (ERI)
|
-- External (ERI)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
||||||
|
|
||||||
@ -4840,6 +4894,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
|||||||
-- Embedding
|
-- Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
||||||
|
|
||||||
|
-- This data source is managed by your organization.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization."
|
||||||
|
|
||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
||||||
|
|
||||||
@ -4864,21 +4921,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566
|
|||||||
-- No data sources configured yet.
|
-- No data sources configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet."
|
||||||
|
|
||||||
|
-- Export Access Token?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?"
|
||||||
|
|
||||||
|
-- Export ERI Data Source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "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."
|
||||||
|
|
||||||
-- Configured Data Sources
|
-- Configured Data Sources
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
||||||
|
|
||||||
-- Add ERI v1 Data Source
|
-- Add ERI v1 Data Source
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured."
|
||||||
|
|
||||||
-- External Data (ERI-Server v1)
|
-- External Data (ERI-Server v1)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"
|
||||||
|
|
||||||
-- Local Directory
|
-- Local Directory
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- When enabled, you can preselect some ERI server options.
|
-- When enabled, you can preselect some ERI server options.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
||||||
|
|
||||||
@ -5164,6 +5239,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
|||||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- Preselect the target language
|
-- Preselect the target language
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
||||||
|
|
||||||
@ -6019,18 +6097,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update
|
|||||||
-- Vision
|
-- Vision
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||||
|
|
||||||
-- 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.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "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."
|
|
||||||
|
|
||||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||||
|
|
||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||||
|
|
||||||
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."
|
|
||||||
|
|
||||||
-- Copies the following to the clipboard
|
-- Copies the following to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
|
||||||
|
|
||||||
@ -6112,6 +6184,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
|||||||
-- Explanation
|
-- Explanation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||||
|
|
||||||
|
-- checking availability
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||||
|
|
||||||
-- 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.
|
-- 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.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "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."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "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."
|
||||||
|
|
||||||
@ -6133,6 +6208,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
|
|||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "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."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "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."
|
||||||
|
|
||||||
-- Update Pandoc
|
-- Update Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
||||||
|
|
||||||
@ -6157,6 +6238,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w
|
|||||||
-- 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!
|
-- 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!
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "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!"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "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!"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "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."
|
||||||
|
|
||||||
-- Motivation
|
-- Motivation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||||
|
|
||||||
@ -6166,6 +6250,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
|
|||||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
||||||
|
|
||||||
|
-- Username provided by the OS
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
|
||||||
|
|
||||||
-- this version does not met the requirements
|
-- this version does not met the requirements
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
||||||
|
|
||||||
@ -6187,6 +6274,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
|
|||||||
-- Database
|
-- Database
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "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."
|
||||||
|
|
||||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
||||||
|
|
||||||
@ -6205,6 +6295,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
|
|||||||
-- Used .NET SDK
|
-- Used .NET SDK
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||||
|
|
||||||
|
-- starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||||
|
|
||||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
||||||
|
|
||||||
@ -6226,6 +6319,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi
|
|||||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "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."
|
||||||
|
|
||||||
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."
|
||||||
|
|
||||||
@ -6367,6 +6463,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
|
|||||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
||||||
|
|
||||||
|
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
|
||||||
|
|
||||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
||||||
|
|
||||||
@ -6472,9 +6571,27 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T39077128
|
|||||||
-- Model as configured by whisper.cpp
|
-- Model as configured by whisper.cpp
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is not a text message.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message."
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."
|
||||||
|
|
||||||
|
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Please select a valid configuration plugin folder. The folder must contain a plugin.lua file."
|
||||||
|
|
||||||
|
-- Cannot package the chat template attachments. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Cannot package the chat template attachments. The issue was: {0}"
|
||||||
|
|
||||||
|
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Cannot package the attachment '{0}' because the file does not exist."
|
||||||
|
|
||||||
-- Use no chat template
|
-- Use no chat template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is empty.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Cannot export this chat template because example message {0} is empty."
|
||||||
|
|
||||||
-- Navigation never expands, but there are tooltips
|
-- Navigation never expands, but there are tooltips
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
||||||
|
|
||||||
@ -6670,8 +6787,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
|||||||
-- Unknown preview feature
|
-- Unknown preview feature
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
||||||
|
|
||||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
-- Transcription: Convert recordings and audio files into text
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text"
|
||||||
|
|
||||||
-- Use no data sources, when sending an assistant result to a chat
|
-- Use no data sources, when sending an assistant result to a chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
||||||
@ -6838,6 +6955,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
|||||||
-- Reason
|
-- Reason
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
||||||
|
|
||||||
|
-- Starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||||
|
|
||||||
-- Unavailable
|
-- Unavailable
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
||||||
|
|
||||||
@ -6922,6 +7042,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile
|
|||||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||||
|
|
||||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
||||||
|
|
||||||
@ -6967,6 +7090,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
|
|||||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
||||||
|
|
||||||
|
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||||
|
|
||||||
|
-- Pandoc doesn't seem to be installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
|
||||||
|
|
||||||
-- Was not able to validate the Pandoc installation.
|
-- Was not able to validate the Pandoc installation.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
||||||
|
|
||||||
@ -6988,20 +7117,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
|
|||||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive was not found.
|
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
|
||||||
|
|
||||||
-- Pandoc is not available on the system or the process had issues.
|
-- Pandoc is not available on the system or the process had issues.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
|
||||||
|
|
||||||
-- It seems that Pandoc is not installed.
|
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
|
||||||
|
|
||||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
-- AI Studio couldn't install Pandoc.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||||
|
|
||||||
-- Pandoc is required for Microsoft Word export.
|
-- Pandoc is required for Microsoft Word export.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||||
@ -7492,6 +7621,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||||
|
|
||||||
|
-- Failed to store the secret data due to an API issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||||
|
|
||||||
-- Failed to delete the secret data due to an API issue.
|
-- Failed to delete the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
<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.")
|
@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>
|
</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>
|
<MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText>
|
||||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
|||||||
@ -89,8 +89,10 @@ public static class IImageSourceExtensions
|
|||||||
|
|
||||||
case ContentImageSource.URL:
|
case ContentImageSource.URL:
|
||||||
{
|
{
|
||||||
using var httpClient = new HttpClient();
|
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient();
|
||||||
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||||
|
var timeoutToken = timeoutTokenSource.Token;
|
||||||
|
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);
|
||||||
if(response.IsSuccessStatusCode)
|
if(response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// Read the length of the content:
|
// Read the length of the content:
|
||||||
@ -101,7 +103,7 @@ public static class IImageSourceExtensions
|
|||||||
return (success: false, string.Empty);
|
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));
|
return (success: true, Convert.ToBase64String(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,10 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
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"),
|
||||||
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
||||||
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
||||||
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
||||||
|
|||||||
@ -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.")"/>
|
<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("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.")"/>
|
<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("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("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.")"/>
|
<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.")"/>
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||||
{
|
{
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
|
||||||
<PreviewBeta ApplyInnerScrollingFix="true"/>
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-3">
|
<MudText Typo="Typo.h4" Class="mb-3">
|
||||||
@T("Configured Transcription Providers")
|
@T("Configured Transcription Providers")
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|||||||
@ -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)
|
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
||||||
{
|
{
|
||||||
// Load the secret:
|
// Load the secret:
|
||||||
var requestedSecret = await this.RustService.GetSecret(this);
|
var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE);
|
||||||
if (requestedSecret.Success)
|
if (requestedSecret.Success)
|
||||||
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
|
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||||
else
|
else
|
||||||
@ -169,6 +169,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
|||||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||||
AuthMethod = this.dataAuthMethod,
|
AuthMethod = this.dataAuthMethod,
|
||||||
Username = this.dataUsername,
|
Username = this.dataUsername,
|
||||||
|
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED,
|
||||||
Type = DataSourceType.ERI_V1,
|
Type = DataSourceType.ERI_V1,
|
||||||
SecurityPolicy = this.dataSecurityPolicy,
|
SecurityPolicy = this.dataSecurityPolicy,
|
||||||
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
||||||
@ -323,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
|||||||
if (!string.IsNullOrWhiteSpace(this.dataSecret))
|
if (!string.IsNullOrWhiteSpace(this.dataSecret))
|
||||||
{
|
{
|
||||||
// Store the secret in the OS secure storage:
|
// 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)
|
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);
|
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)
|
@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")"/>
|
<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 readonly List<string> dataIssues = [];
|
||||||
|
|
||||||
private string serverDescription = string.Empty;
|
private string serverDescription = string.Empty;
|
||||||
|
private string effectiveUsername = string.Empty;
|
||||||
private ProviderType securityRequirements = ProviderType.NONE;
|
private ProviderType securityRequirements = ProviderType.NONE;
|
||||||
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
||||||
private RetrievalInfo selectedRetrievalInfo;
|
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 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)
|
private string RetrievalName(RetrievalInfo retrievalInfo)
|
||||||
{
|
{
|
||||||
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
|
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
|
||||||
@ -92,14 +114,18 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
|||||||
this.IsOperationInProgress = true;
|
this.IsOperationInProgress = true;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
|
||||||
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
|
var effectiveDataSourceResult = await this.CreateEffectiveDataSource();
|
||||||
|
if (!effectiveDataSourceResult.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var client = ERIClientFactory.Get(ERIVersion.V1, effectiveDataSourceResult.EffectiveDataSource);
|
||||||
if(client is null)
|
if(client is null)
|
||||||
{
|
{
|
||||||
this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported."));
|
this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginResult = await client.AuthenticateAsync(this.RustService);
|
var loginResult = await client.AuthenticateAsync(this.RustService, cancellationToken: this.cts.Token);
|
||||||
if (!loginResult.Successful)
|
if (!loginResult.Successful)
|
||||||
{
|
{
|
||||||
this.dataIssues.Add(loginResult.Message);
|
this.dataIssues.Add(loginResult.Message);
|
||||||
|
|||||||
@ -19,6 +19,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
protected RustService RustService { get; init; } = null!;
|
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>> AvailableLLMProviders = new();
|
||||||
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();
|
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,28 @@
|
|||||||
<MudTooltip Text="@T("Edit")">
|
<MudTooltip Text="@T("Edit")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
||||||
</MudTooltip>
|
</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")">
|
<MudTooltip Text="@T("Delete")">
|
||||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|||||||
@ -98,4 +98,66 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
|
|
||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
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>
|
<MudTd>
|
||||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
<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)"/>
|
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/>
|
||||||
|
@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)">
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||||
@T("Edit")
|
@T("Edit")
|
||||||
</MudButton>
|
</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)">
|
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||||
@T("Delete")
|
@T("Delete")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
namespace AIStudio.Dialogs.Settings;
|
namespace AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
@ -87,8 +88,105 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
|||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
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)
|
private async Task EditDataSource(IDataSource dataSource)
|
||||||
{
|
{
|
||||||
|
if (dataSource.IsEnterpriseConfiguration)
|
||||||
|
return;
|
||||||
|
|
||||||
IDataSource? editedDataSource = null;
|
IDataSource? editedDataSource = null;
|
||||||
switch (dataSource)
|
switch (dataSource)
|
||||||
{
|
{
|
||||||
@ -151,6 +249,9 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
|||||||
|
|
||||||
private async Task DeleteDataSource(IDataSource dataSource)
|
private async Task DeleteDataSource(IDataSource dataSource)
|
||||||
{
|
{
|
||||||
|
if (dataSource.IsEnterpriseConfiguration)
|
||||||
|
return;
|
||||||
|
|
||||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
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()) },
|
{ 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:
|
// All other auth methods require a secret, which we need to delete now:
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource);
|
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE);
|
||||||
if (deleteSecretResponse.Success)
|
if (deleteSecretResponse.Success)
|
||||||
applyChanges = true;
|
applyChanges = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,12 @@
|
|||||||
<MudTooltip Text="@T("Edit")">
|
<MudTooltip Text="@T("Edit")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
||||||
</MudTooltip>
|
</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")">
|
<MudTooltip Text="@T("Delete")">
|
||||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|||||||
@ -49,6 +49,19 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
|
|||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private async Task DeleteProfile(Profile profile)
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||||
|
|||||||
@ -83,7 +83,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
// Read the user language from Rust:
|
// Read the user language from Rust:
|
||||||
//
|
//
|
||||||
var userLanguage = await this.RustService.ReadUserLanguage();
|
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 '{userLanguage}' is the user language.");
|
||||||
|
this.Logger.LogInformation($"The OS says '{userName}' is the username.");
|
||||||
|
|
||||||
// Ensure that all settings are loaded:
|
// Ensure that all settings are loaded:
|
||||||
await this.SettingsManager.LoadSettings();
|
await this.SettingsManager.LoadSettings();
|
||||||
|
|||||||
@ -50,12 +50,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
|
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
<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.16" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||||
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
||||||
<PackageReference Include="Qdrant.Client" Version="1.17.0" />
|
<PackageReference Include="Qdrant.Client" Version="1.18.1" />
|
||||||
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
||||||
<PackageReference Include="LuaCSharp" Version="0.5.3" />
|
<PackageReference Include="LuaCSharp" Version="0.5.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
|
<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.Memory" Text="@TauriVersion"/>
|
||||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
|
<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.Business">
|
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||||
@switch (HasAnyActiveEnvironment)
|
@switch (HasAnyActiveEnvironment)
|
||||||
{
|
{
|
||||||
@ -279,10 +280,12 @@
|
|||||||
<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="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="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.")"/>
|
<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.")"/>
|
||||||
<ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/>
|
<ThirdPartyComponent Name="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="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="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="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="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.")"/>
|
<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.")"/>
|
||||||
@ -299,6 +302,7 @@
|
|||||||
<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" 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="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="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="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 for saving the certificate and private key for communication with Qdrant.")"/>
|
||||||
<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="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.")" />
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public partial class Information : MSGComponentBase
|
|||||||
private ISnackbar Snackbar { get; init; } = null!;
|
private ISnackbar Snackbar { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private DatabaseClient DatabaseClient { get; init; } = null!;
|
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
|
||||||
|
|
||||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||||
@ -40,6 +40,7 @@ public partial class Information : MSGComponentBase
|
|||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
|
||||||
|
|
||||||
private string osLanguage = string.Empty;
|
private string osLanguage = string.Empty;
|
||||||
|
private string osUserName = string.Empty;
|
||||||
|
|
||||||
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()})";
|
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 +50,8 @@ public partial class Information : MSGComponentBase
|
|||||||
|
|
||||||
private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'";
|
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 VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
||||||
|
|
||||||
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
||||||
@ -59,9 +62,21 @@ public partial class Information : MSGComponentBase
|
|||||||
|
|
||||||
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
|
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
|
||||||
|
|
||||||
private string VersionDatabase => this.DatabaseClient.IsAvailable
|
private string VersionDatabase
|
||||||
? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"
|
{
|
||||||
: $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}";
|
get
|
||||||
|
{
|
||||||
|
if (this.databaseClient is null)
|
||||||
|
return $"{T("Database")}: {T("checking availability")}";
|
||||||
|
|
||||||
|
return this.databaseClient.Status switch
|
||||||
|
{
|
||||||
|
DatabaseClientStatus.AVAILABLE => $"{T("Database version")}: {this.databaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}",
|
||||||
|
DatabaseClientStatus.STARTING => $"{T("Database")}: {this.databaseClient.Name} - {T("starting")}",
|
||||||
|
_ => $"{T("Database")}: {this.databaseClient.Name} - {T("not available")}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string versionPandoc = TB("Determine Pandoc version, please wait...");
|
private string versionPandoc = TB("Determine Pandoc version, please wait...");
|
||||||
private PandocInstallation pandocInstallation;
|
private PandocInstallation pandocInstallation;
|
||||||
@ -86,6 +101,8 @@ public partial class Information : MSGComponentBase
|
|||||||
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
|
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
|
||||||
|
|
||||||
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
||||||
|
private DatabaseClient? databaseClient;
|
||||||
|
private CancellationTokenSource? databaseRefreshCancellationTokenSource;
|
||||||
|
|
||||||
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
|
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
|
||||||
|
|
||||||
@ -128,12 +145,12 @@ public partial class Information : MSGComponentBase
|
|||||||
this.RefreshEnterpriseConfigurationState();
|
this.RefreshEnterpriseConfigurationState();
|
||||||
|
|
||||||
this.osLanguage = await this.RustService.ReadUserLanguage();
|
this.osLanguage = await this.RustService.ReadUserLanguage();
|
||||||
|
this.osUserName = await this.RustService.ReadUserName();
|
||||||
this.logPaths = await this.RustService.GetLogPaths();
|
this.logPaths = await this.RustService.GetLogPaths();
|
||||||
|
|
||||||
await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo())
|
await this.RefreshDatabaseInfo(CancellationToken.None);
|
||||||
{
|
if (this.databaseClient?.Status is DatabaseClientStatus.STARTING)
|
||||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
this.StartShortDatabaseRefreshLoop();
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the Pandoc version may take some time, so we start it here
|
// Determine the Pandoc version may take some time, so we start it here
|
||||||
// without waiting for the result:
|
// without waiting for the result:
|
||||||
@ -237,6 +254,69 @@ public partial class Information : MSGComponentBase
|
|||||||
this.showDatabaseDetails = !this.showDatabaseDetails;
|
this.showDatabaseDetails = !this.showDatabaseDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RefreshDatabaseInfo(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
|
||||||
|
this.databaseClient = refreshedClient;
|
||||||
|
this.databaseDisplayInfo.Clear();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||||
|
{
|
||||||
|
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.databaseClient = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
|
||||||
|
await foreach (var (label, value) in this.databaseClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||||
|
{
|
||||||
|
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartShortDatabaseRefreshLoop()
|
||||||
|
{
|
||||||
|
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||||
|
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||||
|
this.databaseRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
var cancellationToken = this.databaseRefreshCancellationTokenSource.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.RefreshDatabaseInfo(cancellationToken);
|
||||||
|
this.StateHasChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.databaseClient?.Status is not DatabaseClientStatus.STARTING)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||||
{
|
{
|
||||||
return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId)
|
return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId)
|
||||||
@ -249,6 +329,13 @@ public partial class Information : MSGComponentBase
|
|||||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void DisposeResources()
|
||||||
|
{
|
||||||
|
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||||
|
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||||
|
base.DisposeResources();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CopyStartupLogPath()
|
private async Task CopyStartupLogPath()
|
||||||
{
|
{
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||||
|
|||||||
@ -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"] = {}
|
CONFIG["SETTINGS"] = {}
|
||||||
|
|
||||||
-- Configure the update check interval:
|
-- Configure the update check interval:
|
||||||
@ -172,9 +220,9 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
|
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
|
||||||
|
|
||||||
-- Configure the enabled preview features:
|
-- 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
|
-- 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, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026.
|
-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
|
||||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" }
|
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
|
||||||
|
|
||||||
-- Configure the preselected provider.
|
-- Configure the preselected provider.
|
||||||
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||||
@ -212,6 +260,10 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
|
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
|
-- 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
|
||||||
|
|
||||||
-- Example chat templates for this configuration:
|
-- Example chat templates for this configuration:
|
||||||
CONFIG["CHAT_TEMPLATES"] = {}
|
CONFIG["CHAT_TEMPLATES"] = {}
|
||||||
|
|
||||||
@ -246,7 +298,8 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
|||||||
-- ["AllowProfileUsage"] = true,
|
-- ["AllowProfileUsage"] = true,
|
||||||
-- -- Optional: Pre-attach files that will be automatically included when using this template.
|
-- -- 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.
|
-- -- 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"] = {
|
-- ["FileAttachments"] = {
|
||||||
-- "G:\\Company\\Documents\\Guidelines.pdf",
|
-- "G:\\Company\\Documents\\Guidelines.pdf",
|
||||||
-- "G:\\Company\\Documents\\CompanyPolicies.docx"
|
-- "G:\\Company\\Documents\\CompanyPolicies.docx"
|
||||||
|
|||||||
@ -2646,6 +2646,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
|||||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Möchten Sie eines ihrer Profile als Standard für die gesamte App festlegen? Wenn Sie einem Assistenten ein anderes Profil zuweisen, hat dieses immer Vorrang."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Möchten Sie eines ihrer Profile als Standard für die gesamte App festlegen? Wenn Sie einem Assistenten ein anderes Profil zuweisen, hat dieses immer Vorrang."
|
||||||
|
|
||||||
|
-- seconds
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "Sekunden"
|
||||||
|
|
||||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert."
|
||||||
|
|
||||||
@ -2694,6 +2697,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
|||||||
-- Spellchecking is enabled
|
-- Spellchecking is enabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
|
||||||
|
|
||||||
|
-- Request timeout
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Zeitüberschreitung bei der Anfrage"
|
||||||
|
|
||||||
-- App Options
|
-- App Options
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App-Einstellungen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App-Einstellungen"
|
||||||
|
|
||||||
@ -2721,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
|||||||
-- Select a transcription provider
|
-- Select a transcription provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus"
|
||||||
|
|
||||||
|
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "Wie lange AI Studio auf externe HTTP-Anfragen wartet, z. B. an KI-Anbieter, Einbettungen, Transkription, ERI-Datenquellen und Downloads von Enterprise-Konfigurationen."
|
||||||
|
|
||||||
-- Navigation bar behavior
|
-- Navigation bar behavior
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste"
|
||||||
|
|
||||||
@ -3633,6 +3642,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
|||||||
-- Maximum matches per query
|
-- Maximum matches per query
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximale Treffer pro Abfrage"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximale Treffer pro Abfrage"
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Der Benutzername des Nutzers konnte nicht aus dem Betriebssystem gelesen werden."
|
||||||
|
|
||||||
-- Open web link, show more information
|
-- Open web link, show more information
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Weblink öffnen & mehr Informationen anzeigen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Weblink öffnen & mehr Informationen anzeigen"
|
||||||
|
|
||||||
@ -3684,6 +3696,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
|||||||
-- Embeddings
|
-- Embeddings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Einbettungen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Einbettungen"
|
||||||
|
|
||||||
|
-- Use the same username and password for all users
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Für alle Benutzer denselben Benutzernamen und dasselbe Passwort verwenden"
|
||||||
|
|
||||||
|
-- Username and password mode
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Modus für den Benutzernamen und das Passwort"
|
||||||
|
|
||||||
|
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "Wie soll AI Studio die Konfiguration von Benutzername und Passwort für die ERI-v1-Datenquelle „{0}“ exportieren?"
|
||||||
|
|
||||||
|
-- User-managed username and password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "Vom Benutzer verwaltete Anmeldedaten (Benutzername und Passwort)"
|
||||||
|
|
||||||
|
-- Export
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Exportieren"
|
||||||
|
|
||||||
|
-- Read each user's username from the operating system and share one password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Den Benutzernamen jedes Benutzers aus dem Betriebssystem auslesen und ein Passwort teilen."
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Abbrechen"
|
||||||
|
|
||||||
-- Describe what data this directory contains to help the AI select it.
|
-- Describe what data this directory contains to help the AI select it.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Beschreiben Sie, welche Daten dieses Verzeichnis enthält, um der KI bei der Auswahl zu helfen."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Beschreiben Sie, welche Daten dieses Verzeichnis enthält, um der KI bei der Auswahl zu helfen."
|
||||||
|
|
||||||
@ -4719,6 +4752,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
|||||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Passen Sie ihre KI-Erfahrung mit Chat-Vorlagen an. Egal, ob Sie mit Prompt-Engineering experimentieren, einfach einen eigenen System-Prompt im normalen Chat verwenden oder einen spezialisierten Assistenten erstellen möchten – mit Chat-Vorlagen haben Sie die volle Kontrolle. Ähnlich wie in den Playgrounds gängiger KI-Anbieter können Sie eigene System-Prompts festlegen und bei unterstützenden Anbietern auch Assistenten-Prompts nutzen."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Passen Sie ihre KI-Erfahrung mit Chat-Vorlagen an. Egal, ob Sie mit Prompt-Engineering experimentieren, einfach einen eigenen System-Prompt im normalen Chat verwenden oder einen spezialisierten Assistenten erstellen möchten – mit Chat-Vorlagen haben Sie die volle Kontrolle. Ähnlich wie in den Playgrounds gängiger KI-Anbieter können Sie eigene System-Prompts festlegen und bei unterstützenden Anbietern auch Assistenten-Prompts nutzen."
|
||||||
|
|
||||||
|
-- Copy attachments into plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Anhänge in das Plugin kopieren"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Löschen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Löschen"
|
||||||
|
|
||||||
@ -4728,6 +4764,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
|||||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Hinweis: Diese fortgeschrittene Funktion richtet sich an Nutzer, die mit den Grundlagen des Prompt Engineerings vertraut sind. Außerdem müssen Sie selbst sicherstellen, dass Ihr gewählter Anbieter die Verwendung von Assistenten-Prompts unterstützt."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Hinweis: Diese fortgeschrittene Funktion richtet sich an Nutzer, die mit den Grundlagen des Prompt Engineerings vertraut sind. Außerdem müssen Sie selbst sicherstellen, dass Ihr gewählter Anbieter die Verwendung von Assistenten-Prompts unterstützt."
|
||||||
|
|
||||||
|
-- Use shared attachment paths
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Gemeinsame Pfade für Anhänge verwenden"
|
||||||
|
|
||||||
-- No chat templates configured yet.
|
-- No chat templates configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert."
|
||||||
|
|
||||||
@ -4746,6 +4785,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
|||||||
-- This template is managed by your organization.
|
-- This template is managed by your organization.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet."
|
||||||
|
|
||||||
|
-- Select configuration plugin folder
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Konfigurationsordner für Plugins auswählen"
|
||||||
|
|
||||||
-- Edit Chat Template
|
-- Edit Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten"
|
||||||
|
|
||||||
@ -4755,9 +4797,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38241
|
|||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Aktionen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Aktionen"
|
||||||
|
|
||||||
|
-- Export
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3898821075"] = "Exportieren"
|
||||||
|
|
||||||
-- Delete Chat Template
|
-- Delete Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Chat-Vorlage löschen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Chat-Vorlage löschen"
|
||||||
|
|
||||||
|
-- Export Chat Template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Chat-Vorlage exportieren"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Konfiguration exportieren"
|
||||||
|
|
||||||
-- Which programming language should be preselected for added contexts?
|
-- Which programming language should be preselected for added contexts?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Welche Programmiersprache soll für hinzugefügte Kontexte vorausgewählt werden?"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Welche Programmiersprache soll für hinzugefügte Kontexte vorausgewählt werden?"
|
||||||
|
|
||||||
@ -4812,6 +4863,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419
|
|||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Löschen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Löschen"
|
||||||
|
|
||||||
|
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos-/SSO-ERI-Datenquellen können noch nicht exportiert werden. Bitte konfigurieren Sie diese manuell im Konfigurations-Plugin."
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da das Authentifizierungsgeheimnis nicht verschlüsselt werden konnte."
|
||||||
|
|
||||||
-- External (ERI)
|
-- External (ERI)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "Extern (ERI)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "Extern (ERI)"
|
||||||
|
|
||||||
@ -4842,6 +4899,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
|||||||
-- Embedding
|
-- Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Einbettung"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Einbettung"
|
||||||
|
|
||||||
|
-- This data source is managed by your organization.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "Diese Datenquelle wird von Ihrer Organisation verwaltet."
|
||||||
|
|
||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Bearbeiten"
|
||||||
|
|
||||||
@ -4866,21 +4926,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566
|
|||||||
-- No data sources configured yet.
|
-- No data sources configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "Noch keine Datenquellen konfiguriert."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "Noch keine Datenquellen konfiguriert."
|
||||||
|
|
||||||
|
-- Export Access Token?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Zugriffstoken exportieren?"
|
||||||
|
|
||||||
|
-- Export ERI Data Source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "ERI-Datenquelle exportieren"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Aktionen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Aktionen"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "Für diese ERI-Datenquelle ist ein Zugriffstoken konfiguriert. Möchten Sie das verschlüsselte Zugriffstoken in den Export aufnehmen? Hinweis: Der Empfänger benötigt dasselbe Geheimnis für die Verschlüsselung, um das Zugriffstoken verwenden zu können."
|
||||||
|
|
||||||
-- Configured Data Sources
|
-- Configured Data Sources
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Konfigurierte Datenquellen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Konfigurierte Datenquellen"
|
||||||
|
|
||||||
-- Add ERI v1 Data Source
|
-- Add ERI v1 Data Source
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "ERI v1 Datenquelle hinzufügen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "ERI v1 Datenquelle hinzufügen"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Geheimnis für die Verschlüsselung konfiguriert ist."
|
||||||
|
|
||||||
-- External Data (ERI-Server v1)
|
-- External Data (ERI-Server v1)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "Externe Daten (ERI-Server v1)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "Externe Daten (ERI-Server v1)"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Authentifizierungsgeheimnis konfiguriert ist. Das Problem war: {0}"
|
||||||
|
|
||||||
-- Local Directory
|
-- Local Directory
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Lokaler Ordner"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Lokaler Ordner"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Konfiguration exportieren"
|
||||||
|
|
||||||
-- When enabled, you can preselect some ERI server options.
|
-- When enabled, you can preselect some ERI server options.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "Wenn aktiviert, können Sie einige ERI-Serveroptionen vorauswählen."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "Wenn aktiviert, können Sie einige ERI-Serveroptionen vorauswählen."
|
||||||
|
|
||||||
@ -5166,6 +5244,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
|||||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Sind Sie Projektleiter in einer Forschungseinrichtung? Dann möchten Sie vielleicht ein Profil für ihre Projektmanagement-Aktivitäten anlegen, eines für ihre wissenschaftliche Arbeit und ein weiteres Profil, wenn Sie Programmcode schreiben müssen. In diesen Profilen können Sie festhalten, wie viel Erfahrung Sie haben oder welche Methoden Sie bevorzugen oder nicht gerne verwenden. Später können Sie dann auswählen, wann und wo Sie jedes Profil nutzen möchten."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Sind Sie Projektleiter in einer Forschungseinrichtung? Dann möchten Sie vielleicht ein Profil für ihre Projektmanagement-Aktivitäten anlegen, eines für ihre wissenschaftliche Arbeit und ein weiteres Profil, wenn Sie Programmcode schreiben müssen. In diesen Profilen können Sie festhalten, wie viel Erfahrung Sie haben oder welche Methoden Sie bevorzugen oder nicht gerne verwenden. Später können Sie dann auswählen, wann und wo Sie jedes Profil nutzen möchten."
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Konfiguration exportieren"
|
||||||
|
|
||||||
-- Preselect the target language
|
-- Preselect the target language
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Zielsprache vorwählen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Zielsprache vorwählen"
|
||||||
|
|
||||||
@ -6021,18 +6102,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Nach Updates suc
|
|||||||
-- Vision
|
-- Vision
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||||
|
|
||||||
-- 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.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "Um ein beliebiges LLM nutzen zu können, muss jeder User seinen sogenannten API-Schlüssel für jeden LLM-Anbieter speichern. Dieser Schlüssel muss sicher aufbewahrt werden – ähnlich wie ein Passwort. Die sicherste Methode hierfür bieten Betriebssysteme wie macOS, Windows und Linux: Sie verfügen über Mechanismen, solche Daten – sofern vorhanden – auf spezieller Sicherheits-Hardware zu speichern. Da dies derzeit in .NET nicht möglich ist, verwenden wir diese Rust-Bibliothek."
|
|
||||||
|
|
||||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
|
||||||
|
|
||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
|
||||||
|
|
||||||
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust."
|
|
||||||
|
|
||||||
-- Copies the following to the clipboard
|
-- Copies the following to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage"
|
||||||
|
|
||||||
@ -6114,6 +6189,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Verwendete .NET-
|
|||||||
-- Explanation
|
-- Explanation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung"
|
||||||
|
|
||||||
|
-- checking availability
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "Verfügbarkeit wird geprüft"
|
||||||
|
|
||||||
-- 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.
|
-- 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.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software."
|
||||||
|
|
||||||
@ -6135,6 +6213,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen
|
|||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Der Axum-Server führt den internen Axum-Dienst über eine sichere lokale Verbindung aus. Dadurch kann AI Studio die Kommunikation zwischen der Rust-Laufzeitumgebung und der Benutzeroberfläche schützen."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls hilft dabei, die interne Verbindung zwischen der Benutzeroberfläche der App und der Rust-Laufzeitumgebung abzusichern. Dadurch wird die lokale Kommunikation geschützt, die AI Studio während der Ausführung benötigt."
|
||||||
|
|
||||||
-- Update Pandoc
|
-- Update Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Pandoc aktualisieren"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Pandoc aktualisieren"
|
||||||
|
|
||||||
@ -6159,6 +6243,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird m
|
|||||||
-- 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!
|
-- 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!
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio speichert vertrauliche Daten wie API-Schlüssel im sicheren Speicher Ihres Betriebssystems. Die Bibliothek keyring-core übernimmt dies, indem sie eine Verbindung zum macOS-Schlüsselbund, zur Windows-Anmeldeinformationsverwaltung und zum Linux Secret Service herstellt."
|
||||||
|
|
||||||
-- Motivation
|
-- Motivation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||||
|
|
||||||
@ -6168,6 +6255,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar
|
|||||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
|
||||||
|
|
||||||
|
-- Username provided by the OS
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername"
|
||||||
|
|
||||||
-- this version does not met the requirements
|
-- this version does not met the requirements
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
|
||||||
|
|
||||||
@ -6189,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen"
|
|||||||
-- Database
|
-- Database
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "Diese Bibliothek wird von der Rust-Laufzeitumgebung verwendet, um den Benutzernamen des aktuellen Benutzers auszulesen, z. B. wenn ein von einer Organisation verwalteter ERI-Server den OS-Benutzernamen für die Authentifizierung verwendet."
|
||||||
|
|
||||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen."
|
||||||
|
|
||||||
@ -6207,6 +6300,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bib
|
|||||||
-- Used .NET SDK
|
-- Used .NET SDK
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK"
|
||||||
|
|
||||||
|
-- starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "wird gestartet"
|
||||||
|
|
||||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden."
|
||||||
|
|
||||||
@ -6228,6 +6324,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Bereitgestellt vo
|
|||||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden diese Bibliothek, um PowerPoint-Dateien lesen zu können. So ist es möglich, Inhalte aus Folien in Prompts einzufügen und PowerPoint-Dateien in RAG-Prozessen zu berücksichtigen. Wir danken Nils Kruthoff für seine Arbeit an diesem Rust-Crate."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden diese Bibliothek, um PowerPoint-Dateien lesen zu können. So ist es möglich, Inhalte aus Folien in Prompts einzufügen und PowerPoint-Dateien in RAG-Prozessen zu berücksichtigen. Wir danken Nils Kruthoff für seine Arbeit an diesem Rust-Crate."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum wird verwendet, um den kleinen internen Dienst bereitzustellen, der die Rust-Laufzeitumgebung mit der Benutzeroberfläche der App verbindet. So können beide Teile von AI Studio Informationen austauschen, während die App läuft."
|
||||||
|
|
||||||
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend."
|
||||||
|
|
||||||
@ -6369,6 +6468,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Ihre Regieanweisungen"
|
|||||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Server ist möglicherweise nicht erreichbar oder hat Probleme. Die Nachricht des Anbieters lautet: „{2}“"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Server ist möglicherweise nicht erreichbar oder hat Probleme. Die Nachricht des Anbieters lautet: „{2}“"
|
||||||
|
|
||||||
|
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "Die Anfrage an den LLM-Anbieter „{0}“ (Typ={1}) hat nach {2} während „{3}“ das Zeitlimit überschritten. Bitte versuchen Sie es erneut oder prüfen Sie, ob der Anbieter noch antwortet."
|
||||||
|
|
||||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Beim Versuch, die Antwort des LLM-Anbieters '{0}' zu streamen, sind Probleme aufgetreten. Die Meldung lautet: '{1}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Beim Versuch, die Antwort des LLM-Anbieters '{0}' zu streamen, sind Probleme aufgetreten. Die Meldung lautet: '{1}'"
|
||||||
|
|
||||||
@ -6474,9 +6576,27 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T39077128
|
|||||||
-- Model as configured by whisper.cpp
|
-- Model as configured by whisper.cpp
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is not a text message.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} keine Textnachricht ist."
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Diese Chat-Vorlage kann nicht exportiert werden, da die Beispielnachricht {0} eine Rolle verwendet, die von Konfigurations-Plugins nicht unterstützt wird."
|
||||||
|
|
||||||
|
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Bitte wählen Sie einen gültigen Konfigurations-Plug-in-Ordner aus. Der Ordner muss eine Datei „plugin.lua“ enthalten."
|
||||||
|
|
||||||
|
-- Cannot package the chat template attachments. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Die Anhänge der Chat-Vorlage können nicht verpackt werden. Das Problem war: {0}"
|
||||||
|
|
||||||
|
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Der Anhang „{0}“ kann nicht gepackt werden, da die Datei nicht existiert."
|
||||||
|
|
||||||
-- Use no chat template
|
-- Use no chat template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is empty.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} leer ist."
|
||||||
|
|
||||||
-- Navigation never expands, but there are tooltips
|
-- Navigation never expands, but there are tooltips
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Die Navigationsleiste wird nie ausgeklappt, aber es gibt Tooltips"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Die Navigationsleiste wird nie ausgeklappt, aber es gibt Tooltips"
|
||||||
|
|
||||||
@ -6672,8 +6792,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
|||||||
-- Unknown preview feature
|
-- Unknown preview feature
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unbekannte Vorschau-Funktion"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unbekannte Vorschau-Funktion"
|
||||||
|
|
||||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
-- Transcription: Convert recordings and audio files into text
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transkription: Vorschau unseres Sprache-zu-Text-Systems, mit dem Sie Aufnahmen und Audiodateien in Text transkribieren können"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transkription: Aufnahmen und Audiodateien in Text umwandeln"
|
||||||
|
|
||||||
-- Use no data sources, when sending an assistant result to a chat
|
-- Use no data sources, when sending an assistant result to a chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Keine Datenquellen vorauswählen, wenn ein Ergebnis von einem Assistenten an einen neuen Chat gesendet wird"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Keine Datenquellen vorauswählen, wenn ein Ergebnis von einem Assistenten an einen neuen Chat gesendet wird"
|
||||||
@ -6840,6 +6960,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
|||||||
-- Reason
|
-- Reason
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund"
|
||||||
|
|
||||||
|
-- Starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Wird gestartet"
|
||||||
|
|
||||||
-- Unavailable
|
-- Unavailable
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar"
|
||||||
|
|
||||||
@ -6924,6 +7047,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Authe
|
|||||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Die Sicherheitsanforderungen konnten nicht abgerufen werden: Die Anfrage wurde entweder vom Benutzer abgebrochen oder ist aufgrund eines Zeitüberschreitungsfehlers fehlgeschlagen."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Die Sicherheitsanforderungen konnten nicht abgerufen werden: Die Anfrage wurde entweder vom Benutzer abgebrochen oder ist aufgrund eines Zeitüberschreitungsfehlers fehlgeschlagen."
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Der Benutzername konnte nicht aus dem Betriebssystem ausgelesen werden."
|
||||||
|
|
||||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Die Sicherheitsanforderungen konnten wegen eines Problems nicht abgerufen werden: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Die Sicherheitsanforderungen konnten wegen eines Problems nicht abgerufen werden: {0}"
|
||||||
|
|
||||||
@ -6969,6 +7095,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Fehler
|
|||||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben."
|
||||||
|
|
||||||
|
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde."
|
||||||
|
|
||||||
|
-- Pandoc doesn't seem to be installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc scheint nicht installiert zu sein."
|
||||||
|
|
||||||
-- Was not able to validate the Pandoc installation.
|
-- Was not able to validate the Pandoc installation.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Die Pandoc-Installation konnte nicht überprüft werden."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Die Pandoc-Installation konnte nicht überprüft werden."
|
||||||
|
|
||||||
@ -6990,20 +7122,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} ist insta
|
|||||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} ist installiert, entspricht aber nicht der benötigten Version (v{1})."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} ist installiert, entspricht aber nicht der benötigten Version (v{1})."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive was not found.
|
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc wurde nicht erfolgreich installiert, da das Archiv nicht gefunden wurde."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio konnte Pandoc nicht installieren, da der Archivtyp unbekannt ist."
|
||||||
|
|
||||||
-- Pandoc is not available on the system or the process had issues.
|
-- Pandoc is not available on the system or the process had issues.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem System nicht verfügbar oder der Vorgang ist auf Probleme gestoßen."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem System nicht verfügbar oder der Vorgang ist auf Probleme gestoßen."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc wurde nicht erfolgreich installiert, da der Archivtyp unbekannt ist."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio konnte Pandoc nicht installieren, da die ausführbare Datei im Archiv nicht gefunden wurde."
|
||||||
|
|
||||||
-- It seems that Pandoc is not installed.
|
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pandoc nicht installiert ist."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio konnte die neueste Pandoc-Version nicht finden und installiert stattdessen Version {0}."
|
||||||
|
|
||||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
-- AI Studio couldn't install Pandoc.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pandoc nicht installieren."
|
||||||
|
|
||||||
-- Pandoc is required for Microsoft Word export.
|
-- Pandoc is required for Microsoft Word export.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt."
|
||||||
@ -7494,6 +7626,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
||||||
|
|
||||||
|
-- Failed to store the secret data due to an API issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems."
|
||||||
|
|
||||||
-- Failed to delete the secret data due to an API issue.
|
-- Failed to delete the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen."
|
||||||
|
|
||||||
|
|||||||
@ -2646,6 +2646,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
|||||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
||||||
|
|
||||||
|
-- seconds
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||||
|
|
||||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||||
|
|
||||||
@ -2694,6 +2697,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
|||||||
-- Spellchecking is enabled
|
-- Spellchecking is enabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||||
|
|
||||||
|
-- Request timeout
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
|
||||||
|
|
||||||
-- App Options
|
-- App Options
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
||||||
|
|
||||||
@ -2721,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
|||||||
-- Select a transcription provider
|
-- Select a transcription provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||||
|
|
||||||
|
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
|
||||||
|
|
||||||
-- Navigation bar behavior
|
-- Navigation bar behavior
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
||||||
|
|
||||||
@ -3633,6 +3642,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
|||||||
-- Maximum matches per query
|
-- Maximum matches per query
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||||
|
|
||||||
-- Open web link, show more information
|
-- Open web link, show more information
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
||||||
|
|
||||||
@ -3684,6 +3696,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
|||||||
-- Embeddings
|
-- Embeddings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
||||||
|
|
||||||
|
-- Use the same username and password for all users
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users"
|
||||||
|
|
||||||
|
-- Username and password mode
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode"
|
||||||
|
|
||||||
|
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"
|
||||||
|
|
||||||
|
-- User-managed username and password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password"
|
||||||
|
|
||||||
|
-- Export
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export"
|
||||||
|
|
||||||
|
-- Read each user's username from the operating system and share one password
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password"
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
-- Describe what data this directory contains to help the AI select it.
|
-- Describe what data this directory contains to help the AI select it.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
||||||
|
|
||||||
@ -4719,6 +4752,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
|||||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
||||||
|
|
||||||
|
-- Copy attachments into plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
||||||
|
|
||||||
@ -4728,6 +4764,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
|||||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
||||||
|
|
||||||
|
-- Use shared attachment paths
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Use shared attachment paths"
|
||||||
|
|
||||||
-- No chat templates configured yet.
|
-- No chat templates configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||||
|
|
||||||
@ -4746,6 +4785,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
|||||||
-- This template is managed by your organization.
|
-- This template is managed by your organization.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
||||||
|
|
||||||
|
-- Select configuration plugin folder
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Select configuration plugin folder"
|
||||||
|
|
||||||
-- Edit Chat Template
|
-- Edit Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
||||||
|
|
||||||
@ -4755,9 +4797,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38241
|
|||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Actions"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Actions"
|
||||||
|
|
||||||
|
-- Export
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3898821075"] = "Export"
|
||||||
|
|
||||||
-- Delete Chat Template
|
-- Delete Chat Template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
||||||
|
|
||||||
|
-- Export Chat Template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- Which programming language should be preselected for added contexts?
|
-- Which programming language should be preselected for added contexts?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
||||||
|
|
||||||
@ -4812,6 +4863,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419
|
|||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete"
|
||||||
|
|
||||||
|
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted."
|
||||||
|
|
||||||
-- External (ERI)
|
-- External (ERI)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
||||||
|
|
||||||
@ -4842,6 +4899,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
|||||||
-- Embedding
|
-- Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
||||||
|
|
||||||
|
-- This data source is managed by your organization.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization."
|
||||||
|
|
||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
||||||
|
|
||||||
@ -4866,21 +4926,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566
|
|||||||
-- No data sources configured yet.
|
-- No data sources configured yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet."
|
||||||
|
|
||||||
|
-- Export Access Token?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?"
|
||||||
|
|
||||||
|
-- Export ERI Data Source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "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."
|
||||||
|
|
||||||
-- Configured Data Sources
|
-- Configured Data Sources
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
||||||
|
|
||||||
-- Add ERI v1 Data Source
|
-- Add ERI v1 Data Source
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured."
|
||||||
|
|
||||||
-- External Data (ERI-Server v1)
|
-- External Data (ERI-Server v1)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
||||||
|
|
||||||
|
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"
|
||||||
|
|
||||||
-- Local Directory
|
-- Local Directory
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- When enabled, you can preselect some ERI server options.
|
-- When enabled, you can preselect some ERI server options.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
||||||
|
|
||||||
@ -5166,6 +5244,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
|||||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
||||||
|
|
||||||
|
-- Export configuration
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Export configuration"
|
||||||
|
|
||||||
-- Preselect the target language
|
-- Preselect the target language
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
||||||
|
|
||||||
@ -6021,18 +6102,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update
|
|||||||
-- Vision
|
-- Vision
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||||
|
|
||||||
-- 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.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "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."
|
|
||||||
|
|
||||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||||
|
|
||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||||
|
|
||||||
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."
|
|
||||||
|
|
||||||
-- Copies the following to the clipboard
|
-- Copies the following to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
|
||||||
|
|
||||||
@ -6114,6 +6189,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
|||||||
-- Explanation
|
-- Explanation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||||
|
|
||||||
|
-- checking availability
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||||
|
|
||||||
-- 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.
|
-- 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.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "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."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "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."
|
||||||
|
|
||||||
@ -6135,6 +6213,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
|
|||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "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."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "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."
|
||||||
|
|
||||||
-- Update Pandoc
|
-- Update Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
||||||
|
|
||||||
@ -6159,6 +6243,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w
|
|||||||
-- 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!
|
-- 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!
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "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!"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "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!"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "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."
|
||||||
|
|
||||||
-- Motivation
|
-- Motivation
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||||
|
|
||||||
@ -6168,6 +6255,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
|
|||||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
||||||
|
|
||||||
|
-- Username provided by the OS
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
|
||||||
|
|
||||||
-- this version does not met the requirements
|
-- this version does not met the requirements
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
||||||
|
|
||||||
@ -6189,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
|
|||||||
-- Database
|
-- Database
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "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."
|
||||||
|
|
||||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
||||||
|
|
||||||
@ -6207,6 +6300,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
|
|||||||
-- Used .NET SDK
|
-- Used .NET SDK
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||||
|
|
||||||
|
-- starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||||
|
|
||||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
||||||
|
|
||||||
@ -6228,6 +6324,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi
|
|||||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "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."
|
||||||
|
|
||||||
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."
|
||||||
|
|
||||||
@ -6369,6 +6468,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
|
|||||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
||||||
|
|
||||||
|
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
|
||||||
|
|
||||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
||||||
|
|
||||||
@ -6474,9 +6576,27 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T39077128
|
|||||||
-- Model as configured by whisper.cpp
|
-- Model as configured by whisper.cpp
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is not a text message.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message."
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."
|
||||||
|
|
||||||
|
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Please select a valid configuration plugin folder. The folder must contain a plugin.lua file."
|
||||||
|
|
||||||
|
-- Cannot package the chat template attachments. The issue was: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Cannot package the chat template attachments. The issue was: {0}"
|
||||||
|
|
||||||
|
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Cannot package the attachment '{0}' because the file does not exist."
|
||||||
|
|
||||||
-- Use no chat template
|
-- Use no chat template
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||||
|
|
||||||
|
-- Cannot export this chat template because example message {0} is empty.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Cannot export this chat template because example message {0} is empty."
|
||||||
|
|
||||||
-- Navigation never expands, but there are tooltips
|
-- Navigation never expands, but there are tooltips
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
||||||
|
|
||||||
@ -6672,8 +6792,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
|||||||
-- Unknown preview feature
|
-- Unknown preview feature
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
||||||
|
|
||||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
-- Transcription: Convert recordings and audio files into text
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text"
|
||||||
|
|
||||||
-- Use no data sources, when sending an assistant result to a chat
|
-- Use no data sources, when sending an assistant result to a chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
||||||
@ -6840,6 +6960,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
|||||||
-- Reason
|
-- Reason
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
||||||
|
|
||||||
|
-- Starting
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||||
|
|
||||||
-- Unavailable
|
-- Unavailable
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
||||||
|
|
||||||
@ -6924,6 +7047,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile
|
|||||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
||||||
|
|
||||||
|
-- Failed to read the user's username from the operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||||
|
|
||||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
||||||
|
|
||||||
@ -6969,6 +7095,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
|
|||||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
||||||
|
|
||||||
|
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||||
|
|
||||||
|
-- Pandoc doesn't seem to be installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
|
||||||
|
|
||||||
-- Was not able to validate the Pandoc installation.
|
-- Was not able to validate the Pandoc installation.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
||||||
|
|
||||||
@ -6990,20 +7122,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
|
|||||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive was not found.
|
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
|
||||||
|
|
||||||
-- Pandoc is not available on the system or the process had issues.
|
-- Pandoc is not available on the system or the process had issues.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
||||||
|
|
||||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
|
||||||
|
|
||||||
-- It seems that Pandoc is not installed.
|
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
|
||||||
|
|
||||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
-- AI Studio couldn't install Pandoc.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||||
|
|
||||||
-- Pandoc is required for Microsoft Word export.
|
-- Pandoc is required for Microsoft Word export.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||||
@ -7494,6 +7626,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||||
|
|
||||||
|
-- Failed to store the secret data due to an API issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||||
|
|
||||||
-- Failed to delete the secret data due to an API issue.
|
-- Failed to delete the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ using AIStudio.Agents;
|
|||||||
using AIStudio.Agents.AssistantAudit;
|
using AIStudio.Agents.AssistantAudit;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Tools.Databases;
|
using AIStudio.Tools.Databases;
|
||||||
using AIStudio.Tools.Databases.Qdrant;
|
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
using AIStudio.Tools.PluginSystem.Assistants;
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
@ -28,7 +27,7 @@ internal sealed class Program
|
|||||||
public static string API_TOKEN = null!;
|
public static string API_TOKEN = null!;
|
||||||
public static IServiceProvider SERVICE_PROVIDER = null!;
|
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||||
public static ILoggerFactory LOGGER_FACTORY = 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()
|
public static async Task Main()
|
||||||
{
|
{
|
||||||
@ -87,48 +86,6 @@ internal sealed class Program
|
|||||||
return;
|
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();
|
var builder = WebApplication.CreateBuilder();
|
||||||
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
||||||
{
|
{
|
||||||
@ -183,7 +140,7 @@ internal sealed class Program
|
|||||||
builder.Services.AddHostedService<UpdateService>();
|
builder.Services.AddHostedService<UpdateService>();
|
||||||
builder.Services.AddHostedService<TemporaryChatService>();
|
builder.Services.AddHostedService<TemporaryChatService>();
|
||||||
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
|
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
|
||||||
builder.Services.AddSingleton(databaseClient);
|
builder.Services.AddSingleton<DatabaseClientProvider>();
|
||||||
builder.Services.AddHostedService<GlobalShortcutService>();
|
builder.Services.AddHostedService<GlobalShortcutService>();
|
||||||
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
||||||
|
|
||||||
@ -242,10 +199,7 @@ internal sealed class Program
|
|||||||
|
|
||||||
RUST_SERVICE = rust;
|
RUST_SERVICE = rust;
|
||||||
ENCRYPTION = encryption;
|
ENCRYPTION = encryption;
|
||||||
|
DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>();
|
||||||
var databaseLogger = app.Services.GetRequiredService<ILogger<DatabaseClient>>();
|
|
||||||
databaseClient.SetLogger(databaseLogger);
|
|
||||||
DATABASE_CLIENT = databaseClient;
|
|
||||||
|
|
||||||
programLogger.LogInformation("Initialize internal file system.");
|
programLogger.LogInformation("Initialize internal file system.");
|
||||||
app.Use(Redirect.HandlerContentAsync);
|
app.Use(Redirect.HandlerContentAsync);
|
||||||
@ -283,7 +237,7 @@ internal sealed class Program
|
|||||||
await serverTask;
|
await serverTask;
|
||||||
|
|
||||||
RUST_SERVICE.Dispose();
|
RUST_SERVICE.Dispose();
|
||||||
DATABASE_CLIENT.Dispose();
|
DATABASE_CLIENT_PROVIDER.Dispose();
|
||||||
PluginFactory.Dispose();
|
PluginFactory.Dispose();
|
||||||
programLogger.LogInformation("The AI Studio server was stopped.");
|
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The HTTP client to use it for all requests.
|
/// The HTTP client to use it for all requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly HttpClient HttpClient = new();
|
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger to use.
|
/// The logger to use.
|
||||||
@ -136,6 +136,23 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
|
|
||||||
protected static ModelLoadResult FailedModelLoadResult(ModelLoadFailureReason failureReason, string? technicalDetails = null) => ModelLoadResult.Failure(failureReason, technicalDetails);
|
protected static ModelLoadResult FailedModelLoadResult(ModelLoadFailureReason failureReason, string? technicalDetails = null) => ModelLoadResult.Failure(failureReason, technicalDetails);
|
||||||
|
|
||||||
|
protected bool IsTimeoutException(Exception exception, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ExternalHttpClientTimeout.IsTimeoutException(exception, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Task SendTimeoutError(string action) => MessageBus.INSTANCE.SendError(new(
|
||||||
|
Icons.Material.Filled.HourglassTop,
|
||||||
|
string.Format(
|
||||||
|
TB("The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."),
|
||||||
|
this.InstanceName,
|
||||||
|
this.Provider,
|
||||||
|
ExternalHttpClientTimeout.GetTimeoutDescription(),
|
||||||
|
action)));
|
||||||
|
|
||||||
protected async Task<string?> GetModelLoadingSecretKey(SecretStoreType storeType, string? apiKeyProvisional = null, bool isTryingSecret = false) => apiKeyProvisional switch
|
protected async Task<string?> GetModelLoadingSecretKey(SecretStoreType storeType, string? apiKeyProvisional = null, bool isTryingSecret = false) => apiKeyProvisional switch
|
||||||
{
|
{
|
||||||
not null => apiKeyProvisional,
|
not null => apiKeyProvisional,
|
||||||
@ -175,6 +192,8 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
else if (!string.IsNullOrWhiteSpace(secretKey))
|
else if (!string.IsNullOrWhiteSpace(secretKey))
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using var response = await this.HttpClient.SendAsync(request, token);
|
using var response = await this.HttpClient.SendAsync(request, token);
|
||||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
@ -196,17 +215,26 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, e.Message);
|
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("loading the available models");
|
||||||
|
this.logger.LogError(e, "Timed out while loading models from provider '{ProviderInstanceName}' (provider={ProviderType}).", this.InstanceName, this.Provider);
|
||||||
|
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a request and handles rate limiting by exponential backoff.
|
/// Sends a request and handles rate limiting by exponential backoff.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestBuilder">A function that builds the request.</param>
|
/// <param name="requestBuilder">A function that builds the request.</param>
|
||||||
/// <param name="token">The cancellation token.</param>
|
/// <param name="userCancellationToken">The user cancellation token.</param>
|
||||||
|
/// <param name="requestCancellationToken">The token to use for the HTTP request.</param>
|
||||||
/// <returns>The status object of the request.</returns>
|
/// <returns>The status object of the request.</returns>
|
||||||
private async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken token = default)
|
private async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken userCancellationToken = default, CancellationToken requestCancellationToken = default)
|
||||||
{
|
{
|
||||||
const int MAX_RETRIES = 6;
|
const int MAX_RETRIES = 6;
|
||||||
const double RETRY_DELAY_SECONDS = 4;
|
const double RETRY_DELAY_SECONDS = 4;
|
||||||
|
var effectiveCancellationToken = requestCancellationToken.CanBeCanceled ? requestCancellationToken : userCancellationToken;
|
||||||
|
|
||||||
var retry = 0;
|
var retry = 0;
|
||||||
var response = default(HttpResponseMessage);
|
var response = default(HttpResponseMessage);
|
||||||
@ -223,14 +251,25 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
// Please notice: We do not dispose the response here. The caller is responsible
|
// Please notice: We do not dispose the response here. The caller is responsible
|
||||||
// for disposing the response object. This is important because the response
|
// for disposing the response object. This is important because the response
|
||||||
// object is used to read the stream.
|
// object is used to read the stream.
|
||||||
var nextResponse = await this.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
HttpResponseMessage nextResponse;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nextResponse = await this.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, effectiveCancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e) when (this.IsTimeoutException(e, userCancellationToken))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("waiting for the chat response");
|
||||||
|
this.logger.LogError(e, "Timed out while sending a streaming request to provider '{ProviderInstanceName}' (provider={ProviderType}).", this.InstanceName, this.Provider);
|
||||||
|
return new HttpRateLimitedStreamResult(false, true, e.Message, response);
|
||||||
|
}
|
||||||
|
|
||||||
if (nextResponse.IsSuccessStatusCode)
|
if (nextResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
response = nextResponse;
|
response = nextResponse;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorBody = await nextResponse.Content.ReadAsStringAsync(token);
|
var errorBody = await nextResponse.Content.ReadAsStringAsync(effectiveCancellationToken);
|
||||||
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
|
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||||
@ -296,7 +335,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
timeSeconds = 90;
|
timeSeconds = 90;
|
||||||
|
|
||||||
this.logger.LogDebug("Failed request with status code {ResponseStatusCode} (message = '{ErrorMessage}'). Retrying in {TimeSeconds:0.00} seconds.", nextResponse.StatusCode, errorMessage, timeSeconds);
|
this.logger.LogDebug("Failed request with status code {ResponseStatusCode} (message = '{ErrorMessage}'). Retrying in {TimeSeconds:0.00} seconds.", nextResponse.StatusCode, errorMessage, timeSeconds);
|
||||||
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token);
|
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), effectiveCancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
||||||
@ -323,10 +362,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
||||||
|
|
||||||
StreamReader? streamReader = null;
|
StreamReader? streamReader = null;
|
||||||
|
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||||
|
var timeoutToken = timeoutTokenSource.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Send the request using exponential backoff:
|
// Send the request using exponential backoff:
|
||||||
var responseData = await this.SendRequest(requestBuilder, token);
|
var responseData = await this.SendRequest(requestBuilder, token, timeoutToken);
|
||||||
if(responseData.IsFailedAfterAllRetries)
|
if(responseData.IsFailedAfterAllRetries)
|
||||||
{
|
{
|
||||||
this.logger.LogError($"The {providerName} chat completion failed: {responseData.ErrorMessage}");
|
this.logger.LogError($"The {providerName} chat completion failed: {responseData.ErrorMessage}");
|
||||||
@ -334,16 +375,28 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open the response stream:
|
// Open the response stream:
|
||||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
|
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(timeoutToken);
|
||||||
|
|
||||||
// Add a stream reader to read the stream, line by line:
|
// Add a stream reader to read the stream, line by line:
|
||||||
streamReader = new StreamReader(providerStream);
|
streamReader = new StreamReader(providerStream);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("The user canceled the chat completion request for {ProviderName} '{ProviderInstanceName}' before the response stream was opened.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else if (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("opening the chat response stream");
|
||||||
|
this.logger.LogError(e, "Timed out while opening the chat completion stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||||
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (streamReader is null)
|
if (streamReader is null)
|
||||||
yield break;
|
yield break;
|
||||||
@ -379,15 +432,31 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
string? line;
|
string? line;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
line = await streamReader.ReadLineAsync(token);
|
line = await streamReader.ReadLineAsync(timeoutToken);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("The user canceled the chat completion stream for {ProviderName} '{ProviderInstanceName}' while reading the next chunk.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else if (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("reading the chat response stream");
|
||||||
|
this.logger.LogError(e, "Timed out while reading the chat stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line is null)
|
||||||
|
break;
|
||||||
|
|
||||||
// Skip empty lines:
|
// Skip empty lines:
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
continue;
|
continue;
|
||||||
@ -487,10 +556,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
||||||
|
|
||||||
StreamReader? streamReader = null;
|
StreamReader? streamReader = null;
|
||||||
|
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||||
|
var timeoutToken = timeoutTokenSource.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Send the request using exponential backoff:
|
// Send the request using exponential backoff:
|
||||||
var responseData = await this.SendRequest(requestBuilder, token);
|
var responseData = await this.SendRequest(requestBuilder, token, timeoutToken);
|
||||||
if(responseData.IsFailedAfterAllRetries)
|
if(responseData.IsFailedAfterAllRetries)
|
||||||
{
|
{
|
||||||
this.logger.LogError($"The {providerName} responses call failed: {responseData.ErrorMessage}");
|
this.logger.LogError($"The {providerName} responses call failed: {responseData.ErrorMessage}");
|
||||||
@ -498,16 +569,28 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open the response stream:
|
// Open the response stream:
|
||||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
|
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(timeoutToken);
|
||||||
|
|
||||||
// Add a stream reader to read the stream, line by line:
|
// Add a stream reader to read the stream, line by line:
|
||||||
streamReader = new StreamReader(providerStream);
|
streamReader = new StreamReader(providerStream);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("The user canceled the responses request for {ProviderName} '{ProviderInstanceName}' before the response stream was opened.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else if (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("opening the chat response stream");
|
||||||
|
this.logger.LogError(e, "Timed out while opening the responses stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||||
this.logger.LogError($"Failed to stream responses from {providerName} '{this.InstanceName}': {e.Message}");
|
this.logger.LogError($"Failed to stream responses from {providerName} '{this.InstanceName}': {e.Message}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (streamReader is null)
|
if (streamReader is null)
|
||||||
yield break;
|
yield break;
|
||||||
@ -543,15 +626,31 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
string? line;
|
string? line;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
line = await streamReader.ReadLineAsync(token);
|
line = await streamReader.ReadLineAsync(timeoutToken);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("The user canceled the responses stream for {ProviderName} '{ProviderInstanceName}' while reading the next chunk.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else if (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("reading the chat response stream");
|
||||||
|
this.logger.LogError(e, "Timed out while reading the responses stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line is null)
|
||||||
|
break;
|
||||||
|
|
||||||
// Skip empty lines:
|
// Skip empty lines:
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
continue;
|
continue;
|
||||||
@ -784,6 +883,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
if (this.IsTimeoutException(e, token))
|
||||||
|
await this.SendTimeoutError("transcribing audio");
|
||||||
|
|
||||||
this.logger.LogError("Failed to perform transcription request: '{Message}'.", e.Message);
|
this.logger.LogError("Failed to perform transcription request: '{Message}'.", e.Message);
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@ -859,6 +961,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
if (this.IsTimeoutException(e, token))
|
||||||
|
await this.SendTimeoutError("creating embeddings");
|
||||||
|
|
||||||
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,6 +135,9 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
if (this.IsTimeoutException(e, token))
|
||||||
|
await this.SendTimeoutError("creating embeddings");
|
||||||
|
|
||||||
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,6 +125,8 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
|||||||
if (string.IsNullOrWhiteSpace(secretKey))
|
if (string.IsNullOrWhiteSpace(secretKey))
|
||||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, "No API key available for model loading.");
|
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, "No API key available for model loading.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
@ -152,4 +154,11 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
|||||||
return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message);
|
return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("loading the available models");
|
||||||
|
LOGGER.LogError(e, "Timed out while loading models from Helmholtz provider '{ProviderInstanceName}'.", this.InstanceName);
|
||||||
|
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -173,6 +173,8 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
|||||||
{
|
{
|
||||||
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, true);
|
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
if(secretKey is not null)
|
if(secretKey is not null)
|
||||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
@ -187,4 +189,11 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
|||||||
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
|
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
|
||||||
.Select(n => new Provider.Model(n.Id, null)));
|
.Select(n => new Provider.Model(n.Id, null)));
|
||||||
}
|
}
|
||||||
|
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||||
|
{
|
||||||
|
await this.SendTimeoutError("loading the available models");
|
||||||
|
LOGGER.LogError(e, "Timed out while loading models from self-hosted provider '{ProviderInstanceName}'.", this.InstanceName);
|
||||||
|
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,11 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
using Lua;
|
using SharedTools;
|
||||||
|
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
@ -17,6 +21,8 @@ public record ChatTemplate(
|
|||||||
bool IsEnterpriseConfiguration = false,
|
bool IsEnterpriseConfiguration = false,
|
||||||
Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject
|
Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject
|
||||||
{
|
{
|
||||||
|
private const string ATTACHMENTS_DIRECTORY = "attachments";
|
||||||
|
|
||||||
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false)
|
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -74,7 +80,7 @@ public record ChatTemplate(
|
|||||||
return this.SystemPrompt;
|
return this.SystemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
|
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
||||||
{
|
{
|
||||||
template = NO_CHAT_TEMPLATE;
|
template = NO_CHAT_TEMPLATE;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||||
@ -103,7 +109,7 @@ public record ChatTemplate(
|
|||||||
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
|
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
|
||||||
allowProfileUsage = allow;
|
allowProfileUsage = allow;
|
||||||
|
|
||||||
var fileAttachments = ParseFileAttachments(idx, table);
|
var fileAttachments = ParseFileAttachments(idx, table, pluginPath);
|
||||||
|
|
||||||
template = new ChatTemplate
|
template = new ChatTemplate
|
||||||
{
|
{
|
||||||
@ -169,7 +175,7 @@ public record ChatTemplate(
|
|||||||
return exampleConversation;
|
return exampleConversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table)
|
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table, string pluginPath)
|
||||||
{
|
{
|
||||||
var fileAttachments = new List<FileAttachment>();
|
var fileAttachments = new List<FileAttachment>();
|
||||||
if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead<LuaTable>(out var fileAttTable))
|
if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead<LuaTable>(out var fileAttTable))
|
||||||
@ -185,9 +191,227 @@ public record ChatTemplate(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileAttachments.Add(FileAttachment.FromPath(filePath));
|
if (TryResolveFileAttachmentPath(idx, attachmentNum, filePath, pluginPath, out var resolvedFilePath))
|
||||||
|
fileAttachments.Add(FileAttachment.FromPath(resolvedFilePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileAttachments;
|
return fileAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveFileAttachmentPath(int idx, int attachmentNum, string filePath, string pluginPath, out string resolvedFilePath)
|
||||||
|
{
|
||||||
|
resolvedFilePath = filePath;
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning("The FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} is empty.", attachmentNum, idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Path.IsPathFullyQualified(filePath))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginPath))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} cannot be resolved because the plugin path is unknown.", attachmentNum, idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginRoot = Path.GetFullPath(pluginPath);
|
||||||
|
var relativePath = filePath
|
||||||
|
.Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.Replace('\\', Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
if (relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == ".."))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} contains '..' path segments and will be ignored.", attachmentNum, idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var combinedPath = Path.GetFullPath(Path.Combine(pluginRoot, relativePath));
|
||||||
|
var pluginRootWithSeparator = pluginRoot.EndsWith(Path.DirectorySeparatorChar)
|
||||||
|
? pluginRoot
|
||||||
|
: pluginRoot + Path.DirectorySeparatorChar;
|
||||||
|
var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||||
|
if (!combinedPath.StartsWith(pluginRootWithSeparator, comparison))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} points outside of the plugin folder and will be ignored.", attachmentNum, idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedFilePath = combinedPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryExportAsConfigurationSection(out string luaCode, out string issue) => this.TryExportAsConfigurationSection(null, Guid.NewGuid().ToString(), out luaCode, out issue);
|
||||||
|
|
||||||
|
private bool TryExportAsConfigurationSection(IReadOnlyList<string>? fileAttachmentPaths, string exportId, out string luaCode, out string issue)
|
||||||
|
{
|
||||||
|
luaCode = string.Empty;
|
||||||
|
issue = string.Empty;
|
||||||
|
if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.TryExportAsConfigurationSection(fileAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryExportAsConfigurationSection(IReadOnlyList<string>? fileAttachmentPaths, string exportId, string exampleConversationLua, out string luaCode, out string issue)
|
||||||
|
{
|
||||||
|
issue = string.Empty;
|
||||||
|
var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths);
|
||||||
|
luaCode = $$"""
|
||||||
|
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||||
|
["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}",
|
||||||
|
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||||
|
["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}},
|
||||||
|
["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}},
|
||||||
|
["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}},
|
||||||
|
["FileAttachments"] = {{fileAttachmentsLua}},
|
||||||
|
["ExampleConversation"] = {{exampleConversationLua}},
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryExportAsConfigurationSectionWithPackagedAttachments(string pluginDirectory, out string luaCode, out string issue)
|
||||||
|
{
|
||||||
|
luaCode = string.Empty;
|
||||||
|
issue = string.Empty;
|
||||||
|
var exportId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.FileAttachments.Count == 0)
|
||||||
|
return this.TryExportAsConfigurationSection(null, exportId, exampleConversationLua, out luaCode, out issue);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginDirectory) || !File.Exists(Path.Combine(pluginDirectory, "plugin.lua")))
|
||||||
|
{
|
||||||
|
issue = TB("Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourcePaths = new List<string>();
|
||||||
|
foreach (var attachment in this.FileAttachments)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(attachment.FilePath) || !File.Exists(attachment.FilePath))
|
||||||
|
{
|
||||||
|
issue = string.Format(TB("Cannot package the attachment '{0}' because the file does not exist."), attachment.FileName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePaths.Add(attachment.FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetDirectory = Path.Combine(pluginDirectory, ATTACHMENTS_DIRECTORY, exportId);
|
||||||
|
var relativeAttachmentPaths = new List<string>();
|
||||||
|
var usedFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
foreach (var sourcePath in sourcePaths)
|
||||||
|
{
|
||||||
|
var targetFileName = CreateUniqueAttachmentFileName(sourcePath, usedFileNames);
|
||||||
|
var targetPath = Path.Combine(targetDirectory, targetFileName);
|
||||||
|
File.Copy(sourcePath, targetPath, overwrite: false);
|
||||||
|
relativeAttachmentPaths.Add($"{ATTACHMENTS_DIRECTORY}/{exportId}/{targetFileName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(targetDirectory))
|
||||||
|
Directory.Delete(targetDirectory, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep the original packaging error as the user-facing issue.
|
||||||
|
}
|
||||||
|
|
||||||
|
issue = string.Format(TB("Cannot package the chat template attachments. The issue was: {0}"), e.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.TryExportAsConfigurationSection(relativeAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryBuildExampleConversationLua(out string luaTable, out string issue)
|
||||||
|
{
|
||||||
|
luaTable = "{}";
|
||||||
|
issue = string.Empty;
|
||||||
|
if (this.ExampleConversation.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("{");
|
||||||
|
for (var i = 0; i < this.ExampleConversation.Count; i++)
|
||||||
|
{
|
||||||
|
var block = this.ExampleConversation[i];
|
||||||
|
if (block.Role is not ChatRole.USER and not ChatRole.AI)
|
||||||
|
{
|
||||||
|
issue = string.Format(TB("Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."), i + 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.Content is not ContentText textContent)
|
||||||
|
{
|
||||||
|
issue = string.Format(TB("Cannot export this chat template because example message {0} is not a text message."), i + 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(textContent.Text))
|
||||||
|
{
|
||||||
|
issue = string.Format(TB("Cannot export this chat template because example message {0} is empty."), i + 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine($" [\"Role\"] = \"{block.Role}\",");
|
||||||
|
builder.AppendLine($" [\"Content\"] = {LuaTools.ToLuaStringLiteral(textContent.Text)},");
|
||||||
|
builder.AppendLine(" },");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" }");
|
||||||
|
luaTable = builder.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths)
|
||||||
|
{
|
||||||
|
var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList();
|
||||||
|
if (paths.Count == 0)
|
||||||
|
return "{}";
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("{");
|
||||||
|
foreach (var path in paths)
|
||||||
|
builder.AppendLine($" \"{LuaTools.EscapeLuaString(path)}\",");
|
||||||
|
|
||||||
|
builder.Append(" }");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateUniqueAttachmentFileName(string sourcePath, HashSet<string> usedFileNames)
|
||||||
|
{
|
||||||
|
var fileName = SanitizeFileName(Path.GetFileName(sourcePath));
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
fileName = "attachment";
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(fileName);
|
||||||
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
|
||||||
|
var candidate = fileName;
|
||||||
|
var counter = 2;
|
||||||
|
while (!usedFileNames.Add(candidate))
|
||||||
|
candidate = $"{nameWithoutExtension}-{counter++}{extension}";
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string fileName)
|
||||||
|
{
|
||||||
|
foreach (var invalidChar in Path.GetInvalidFileNameChars())
|
||||||
|
fileName = fileName.Replace(invalidChar, '_');
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +94,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
|
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP timeout in seconds for external HTTP clients.
|
||||||
|
/// </summary>
|
||||||
|
public int HttpClientTimeoutSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HttpClientTimeoutSeconds, ExternalHttpClientTimeout.DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should the user be allowed to add providers?
|
/// Should the user be allowed to add providers?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
public enum DataSourceERIUsernamePasswordMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user manages the username and password locally.
|
||||||
|
/// </summary>
|
||||||
|
USER_MANAGED,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The username and password are shared by all users and provided by configuration.
|
||||||
|
/// </summary>
|
||||||
|
SHARED_USERNAME_AND_PASSWORD,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The username is read from the operating system, and the password is shared by all users.
|
||||||
|
/// </summary>
|
||||||
|
OS_USERNAME_SHARED_PASSWORD,
|
||||||
|
}
|
||||||
@ -4,11 +4,15 @@ using AIStudio.Assistants.ERI;
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Tools.ERIClient;
|
using AIStudio.Tools.ERIClient;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
using AIStudio.Tools.RAG;
|
using AIStudio.Tools.RAG;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
using ChatThread = AIStudio.Chat.ChatThread;
|
using ChatThread = AIStudio.Chat.ChatThread;
|
||||||
using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
|
using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings.DataModel;
|
namespace AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
@ -17,6 +21,8 @@ namespace AIStudio.Settings.DataModel;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct DataSourceERI_V1 : IERIDataSource
|
public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||||
{
|
{
|
||||||
|
private static readonly ILogger<DataSourceERI_V1> LOGGER = Program.LOGGER_FACTORY.CreateLogger<DataSourceERI_V1>();
|
||||||
|
|
||||||
public DataSourceERI_V1()
|
public DataSourceERI_V1()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -45,9 +51,18 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Username { get; init; } = string.Empty;
|
public string Username { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; } = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEnterpriseConfiguration { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ERIVersion Version { get; init; } = ERIVersion.V1;
|
public ERIVersion Version { get; init; } = ERIVersion.V1;
|
||||||
|
|
||||||
@ -82,7 +97,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
|||||||
|
|
||||||
Thread = await thread.ToERIChatThread(token),
|
Thread = await thread.ToERIChatThread(token),
|
||||||
MaxMatches = this.MaxMatches,
|
MaxMatches = this.MaxMatches,
|
||||||
RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId,
|
RetrievalProcessId = this.SelectedRetrievalId,
|
||||||
Parameters = null, // The ERI server selects useful default parameters
|
Parameters = null, // The ERI server selects useful default parameters
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -139,4 +154,240 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
|||||||
logger.LogWarning($"Was not able to authenticate with the ERI data source '{this.Name}'. Message: {authResponse.Message}");
|
logger.LogWarning($"Was not able to authenticate with the ERI data source '{this.Name}'. Message: {authResponse.Message}");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataSourceERI_V1 dataSource)
|
||||||
|
{
|
||||||
|
dataSource = default;
|
||||||
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("Type", out var typeValue) || !typeValue.TryRead<string>(out var typeText) || !Enum.TryParse<DataSourceType>(typeText, true, out var type) || type is not DataSourceType.ERI_V1)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a supported data source type. Only ERI_V1 is supported. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname) || string.IsNullOrWhiteSpace(hostname))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("Port", out var portValue) || !portValue.TryRead<int>(out var port) || port is < 1 or > 65535)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid port. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("AuthMethod", out var authMethodValue) || !authMethodValue.TryRead<string>(out var authMethodText) || !Enum.TryParse<AuthMethod>(authMethodText, true, out var authMethod))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid auth method. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("SecurityPolicy", out var securityPolicyValue) || !securityPolicyValue.TryRead<string>(out var securityPolicyText) || !Enum.TryParse<DataSourceSecurity>(securityPolicyText, true, out var securityPolicy))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid security policy. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityPolicy is DataSourceSecurity.NOT_SPECIFIED)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} must specify a security policy. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("SelectedRetrievalId", out var selectedRetrievalIdValue) || !selectedRetrievalIdValue.TryRead<string>(out var selectedRetrievalId) || string.IsNullOrWhiteSpace(selectedRetrievalId))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} must specify a selected retrieval ID. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!table.TryGetValue("MaxMatches", out var maxMatchesValue) || !maxMatchesValue.TryRead<int>(out var maxMatches) || maxMatches is < 1 or > ushort.MaxValue)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid maximum number of matches. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = string.Empty;
|
||||||
|
var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||||
|
if (table.TryGetValue("UsernamePasswordMode", out var usernamePasswordModeValue) && usernamePasswordModeValue.TryRead<string>(out var usernamePasswordModeText))
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse(usernamePasswordModeText, true, out usernamePasswordMode))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid username/password mode. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} uses the user-managed username/password mode. This mode is not allowed in configuration plugins. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMethod is AuthMethod.USERNAME_PASSWORD)
|
||||||
|
{
|
||||||
|
if (!table.TryGetValue("UsernamePasswordMode", out _) || usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} must specify an organization-managed username/password mode. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD &&
|
||||||
|
(!table.TryGetValue("Username", out var usernameValue) || !usernameValue.TryRead<string>(out username) || string.IsNullOrWhiteSpace(username)))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} must specify a username. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource = new DataSourceERI_V1
|
||||||
|
{
|
||||||
|
Num = 0,
|
||||||
|
Id = id.ToString(),
|
||||||
|
Name = name,
|
||||||
|
Type = DataSourceType.ERI_V1,
|
||||||
|
Hostname = CleanHostname(hostname),
|
||||||
|
Port = port,
|
||||||
|
AuthMethod = authMethod,
|
||||||
|
Username = username,
|
||||||
|
UsernamePasswordMode = usernamePasswordMode,
|
||||||
|
SecurityPolicy = securityPolicy,
|
||||||
|
Version = ERIVersion.V1,
|
||||||
|
SelectedRetrievalId = selectedRetrievalId,
|
||||||
|
MaxMatches = (ushort)maxMatches,
|
||||||
|
IsEnterpriseConfiguration = true,
|
||||||
|
EnterpriseConfigurationPluginId = configPluginId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return TryQueueEnterpriseSecret(idx, table, configPluginId, dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports the ERI v1 data source configuration as a Lua configuration section.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedSecret">Optional encrypted token or password to include in the export.</param>
|
||||||
|
/// <param name="usernamePasswordMode">The organization-managed username/password mode to export.</param>
|
||||||
|
/// <returns>A Lua configuration section string.</returns>
|
||||||
|
public string ExportAsConfigurationSection(string? encryptedSecret = null, DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||||
|
{
|
||||||
|
var secretLine = string.Empty;
|
||||||
|
var usernamePasswordModeLine = string.Empty;
|
||||||
|
var usernameLine = string.Empty;
|
||||||
|
|
||||||
|
switch (this.AuthMethod)
|
||||||
|
{
|
||||||
|
case AuthMethod.TOKEN:
|
||||||
|
secretLine = CreateSecretLine("Token", encryptedSecret);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AuthMethod.USERNAME_PASSWORD:
|
||||||
|
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||||
|
usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
|
||||||
|
|
||||||
|
usernamePasswordModeLine = $"""
|
||||||
|
["UsernamePasswordMode"] = "{usernamePasswordMode}",
|
||||||
|
""";
|
||||||
|
|
||||||
|
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD)
|
||||||
|
{
|
||||||
|
var username = string.IsNullOrWhiteSpace(this.Username) ? "<shared username>" : this.Username;
|
||||||
|
usernameLine = $"""
|
||||||
|
["Username"] = "{LuaTools.EscapeLuaString(username)}",
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
secretLine = CreateSecretLine("Password", encryptedSecret);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $$"""
|
||||||
|
CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||||
|
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||||
|
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
||||||
|
["Type"] = "ERI_V1",
|
||||||
|
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||||
|
["Port"] = {{this.Port}},
|
||||||
|
["AuthMethod"] = "{{this.AuthMethod}}",
|
||||||
|
{{usernamePasswordModeLine}}
|
||||||
|
{{usernameLine}}
|
||||||
|
{{secretLine}}
|
||||||
|
["SecurityPolicy"] = "{{this.SecurityPolicy}}",
|
||||||
|
["SelectedRetrievalId"] = "{{LuaTools.EscapeLuaString(this.SelectedRetrievalId)}}",
|
||||||
|
["MaxMatches"] = {{this.MaxMatches}},
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryQueueEnterpriseSecret(int idx, LuaTable table, Guid configPluginId, DataSourceERI_V1 dataSource)
|
||||||
|
{
|
||||||
|
var secretFieldName = dataSource.AuthMethod switch
|
||||||
|
{
|
||||||
|
AuthMethod.TOKEN => "Token",
|
||||||
|
AuthMethod.USERNAME_PASSWORD => "Password",
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(secretFieldName))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!table.TryGetValue(secretFieldName, out var secretValue) || !secretValue.TryRead<string>(out var encryptedSecret) || string.IsNullOrWhiteSpace(encryptedSecret))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid encrypted {secretFieldName}. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnterpriseEncryption.IsEncrypted(encryptedSecret))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} contains a plaintext {secretFieldName}. Only encrypted secrets (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryption = PluginFactory.EnterpriseEncryption;
|
||||||
|
if (encryption?.IsAvailable != true)
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"The configured data source {idx} contains an encrypted {secretFieldName}, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encryption.TryDecrypt(encryptedSecret, out var decryptedSecret))
|
||||||
|
{
|
||||||
|
LOGGER.LogWarning($"Failed to decrypt the {secretFieldName} for data source {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingEnterpriseSecrets.Add(new(
|
||||||
|
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{dataSource.Id}",
|
||||||
|
dataSource.Name,
|
||||||
|
decryptedSecret,
|
||||||
|
SecretStoreType.DATA_SOURCE));
|
||||||
|
LOGGER.LogDebug($"Successfully decrypted the {secretFieldName} for data source {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSecretLine(string fieldName, string? encryptedSecret)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(encryptedSecret))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return $"""
|
||||||
|
["{fieldName}"] = "{LuaTools.EscapeLuaString(encryptedSecret)}",
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanHostname(string hostname)
|
||||||
|
{
|
||||||
|
var cleanedHostname = hostname.Trim();
|
||||||
|
return cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -36,6 +36,12 @@ public readonly record struct DataSourceLocalDirectory : IInternalDataSource
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEnterpriseConfiguration { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ushort MaxMatches { get; init; } = 10;
|
public ushort MaxMatches { get; init; } = 10;
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,12 @@ public readonly record struct DataSourceLocalFile : IInternalDataSource
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEnterpriseConfiguration { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ushort MaxMatches { get; init; } = 10;
|
public ushort MaxMatches { get; init; } = 10;
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ public static class PreviewFeaturesExtensions
|
|||||||
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
|
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
|
||||||
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
|
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
|
||||||
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
|
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
|
||||||
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"),
|
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
|
||||||
|
|
||||||
_ => TB("Unknown preview feature")
|
_ => TB("Unknown preview feature")
|
||||||
};
|
};
|
||||||
@ -33,6 +33,7 @@ public static class PreviewFeaturesExtensions
|
|||||||
PreviewFeatures.PRE_READ_PDF_2025 => true,
|
PreviewFeatures.PRE_READ_PDF_2025 => true,
|
||||||
PreviewFeatures.PRE_PLUGINS_2025 => true,
|
PreviewFeatures.PRE_PLUGINS_2025 => true,
|
||||||
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
|
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
|
||||||
|
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true,
|
||||||
|
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,7 +12,6 @@ public static class PreviewVisibilityExtensions
|
|||||||
if (visibility >= PreviewVisibility.BETA)
|
if (visibility >= PreviewVisibility.BETA)
|
||||||
{
|
{
|
||||||
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
|
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
|
||||||
features.Add(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibility >= PreviewVisibility.ALPHA)
|
if (visibility >= PreviewVisibility.ALPHA)
|
||||||
|
|||||||
@ -3,9 +3,10 @@ using System.Text.Json.Serialization;
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
using Lua;
|
using SharedTools;
|
||||||
|
|
||||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
using AIStudio.Tools.RAG;
|
using AIStudio.Tools.RAG;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
@ -13,23 +14,8 @@ namespace AIStudio.Settings;
|
|||||||
[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))]
|
[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))]
|
||||||
[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))]
|
[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))]
|
||||||
[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
|
[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
|
||||||
public interface IDataSource
|
public interface IDataSource : IConfigurationObject
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The number of the data source.
|
|
||||||
/// </summary>
|
|
||||||
public uint Num { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The unique identifier of the data source.
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the data source.
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Which type of data source is this?
|
/// Which type of data source is this?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Assistants.ERI;
|
using AIStudio.Assistants.ERI;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
@ -25,6 +26,11 @@ public interface IERIDataSource : IExternalDataSource
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How username/password authentication should obtain the username.
|
||||||
|
/// </summary>
|
||||||
|
public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ERI specification to use.
|
/// The ERI specification to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ public interface IExternalDataSource : IDataSource, ISecretId
|
|||||||
#region Implementation of ISecretId
|
#region Implementation of ISecretId
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
string ISecretId.SecretId => this.Id;
|
string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
string ISecretId.SecretName => this.Name;
|
string ISecretId.SecretName => this.Name;
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
using Lua;
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
@ -132,4 +135,20 @@ public record Profile(
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports the profile configuration as a Lua configuration section.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A Lua configuration section string.</returns>
|
||||||
|
public string ExportAsConfigurationSection()
|
||||||
|
{
|
||||||
|
return $$"""
|
||||||
|
CONFIG["PROFILES"][#CONFIG["PROFILES"]+1] = {
|
||||||
|
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||||
|
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||||
|
["NeedToKnow"] = {{LuaTools.ToLuaStringLiteral(this.NeedToKnow)}},
|
||||||
|
["Actions"] = {{LuaTools.ToLuaStringLiteral(this.Actions)}},
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,9 +4,10 @@ using AIStudio.Provider;
|
|||||||
using AIStudio.Provider.HuggingFace;
|
using AIStudio.Provider.HuggingFace;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
using Lua;
|
using SharedTools;
|
||||||
|
|
||||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,10 @@ using System.Text.Json.Serialization;
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
using Lua;
|
using SharedTools;
|
||||||
|
|
||||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||||
|
using LuaTable = Lua.LuaTable;
|
||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,11 @@ public abstract class DatabaseClient(string name, string path)
|
|||||||
{
|
{
|
||||||
public string Name => name;
|
public string Name => name;
|
||||||
|
|
||||||
public virtual bool IsAvailable => true;
|
public virtual string CacheKey => name;
|
||||||
|
|
||||||
|
public virtual DatabaseClientStatus Status => DatabaseClientStatus.AVAILABLE;
|
||||||
|
|
||||||
|
public bool IsAvailable => this.Status is DatabaseClientStatus.AVAILABLE;
|
||||||
|
|
||||||
private string Path => path;
|
private string Path => path;
|
||||||
|
|
||||||
|
|||||||
180
app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs
Normal file
180
app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using AIStudio.Tools.Databases.Qdrant;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Databases;
|
||||||
|
|
||||||
|
public sealed class DatabaseClientProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<DatabaseRole, DatabaseClient> clients = new();
|
||||||
|
private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new();
|
||||||
|
private readonly Lock locksLock = new();
|
||||||
|
private readonly ILogger<DatabaseClientProvider> logger = loggerFactory.CreateLogger<DatabaseClientProvider>();
|
||||||
|
private readonly ILogger<DatabaseClient> databaseClientLogger = loggerFactory.CreateLogger<DatabaseClient>();
|
||||||
|
|
||||||
|
public async Task<DatabaseClient> GetClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var databaseLock = this.GetLock(databaseRole);
|
||||||
|
await databaseLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.clients.TryGetValue(databaseRole, out var cachedClient) && cachedClient.IsAvailable)
|
||||||
|
return cachedClient;
|
||||||
|
|
||||||
|
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||||
|
return this.CacheIfAvailable(databaseRole, client);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
databaseLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DatabaseClient> RefreshClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var databaseLock = this.GetLock(databaseRole);
|
||||||
|
await databaseLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||||
|
return this.CacheIfAvailable(databaseRole, client);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
databaseLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseClient CacheIfAvailable(DatabaseRole databaseRole, DatabaseClient client)
|
||||||
|
{
|
||||||
|
if (!client.IsAvailable)
|
||||||
|
return client;
|
||||||
|
|
||||||
|
if (this.clients.TryGetValue(databaseRole, out var cachedClient))
|
||||||
|
{
|
||||||
|
if (IsSameClient(cachedClient, client))
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedClient.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients[databaseRole] = client;
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SemaphoreSlim GetLock(DatabaseRole databaseRole)
|
||||||
|
{
|
||||||
|
lock (this.locksLock)
|
||||||
|
{
|
||||||
|
if (this.locks.TryGetValue(databaseRole, out var databaseLock))
|
||||||
|
return databaseLock;
|
||||||
|
|
||||||
|
databaseLock = new SemaphoreSlim(1, 1);
|
||||||
|
this.locks[databaseRole] = databaseLock;
|
||||||
|
return databaseLock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DatabaseClient> CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch
|
||||||
|
{
|
||||||
|
DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken),
|
||||||
|
_ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var qdrantInfo = await rustService.GetQdrantInfo(cancellationToken);
|
||||||
|
if (qdrantInfo.Status is QdrantStatus.STARTING)
|
||||||
|
{
|
||||||
|
return this.CreateNoDatabaseClient(
|
||||||
|
"Qdrant",
|
||||||
|
"Qdrant is starting. Details will appear shortly.",
|
||||||
|
DatabaseClientStatus.STARTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!qdrantInfo.IsAvailable || qdrantInfo.Status is QdrantStatus.UNAVAILABLE)
|
||||||
|
{
|
||||||
|
var reason = qdrantInfo.UnavailableReason ?? "unknown";
|
||||||
|
this.logger.LogWarning("Qdrant is not available. Starting without vector database. Reason: '{Reason}'.", reason);
|
||||||
|
return this.CreateNoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasValidQdrantConnectionInfo(qdrantInfo, out var invalidReason))
|
||||||
|
return this.CreateNoDatabaseClient("Qdrant", invalidReason, DatabaseClientStatus.UNAVAILABLE);
|
||||||
|
|
||||||
|
var client = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||||
|
client.SetLogger(this.databaseClientLogger);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.CheckAvailabilityAsync();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
this.logger.LogWarning(e, "Qdrant reported as available by Rust, but the health check failed.");
|
||||||
|
return this.CreateNoDatabaseClient("Qdrant", e.Message, DatabaseClientStatus.STARTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasValidQdrantConnectionInfo(QdrantInfo qdrantInfo, out string invalidReason)
|
||||||
|
{
|
||||||
|
if (qdrantInfo.Path == string.Empty)
|
||||||
|
{
|
||||||
|
invalidReason = "Failed to get the Qdrant path from Rust.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qdrantInfo.PortHttp == 0)
|
||||||
|
{
|
||||||
|
invalidReason = "Failed to get the Qdrant HTTP port from Rust.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qdrantInfo.PortGrpc == 0)
|
||||||
|
{
|
||||||
|
invalidReason = "Failed to get the Qdrant gRPC port from Rust.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qdrantInfo.Fingerprint == string.Empty)
|
||||||
|
{
|
||||||
|
invalidReason = "Failed to get the Qdrant fingerprint from Rust.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qdrantInfo.ApiToken == string.Empty)
|
||||||
|
{
|
||||||
|
invalidReason = "Failed to get the Qdrant API token from Rust.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidReason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoDatabaseClient CreateNoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status)
|
||||||
|
{
|
||||||
|
var client = new NoDatabaseClient(name, unavailableReason, status);
|
||||||
|
client.SetLogger(this.databaseClientLogger);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSameClient(DatabaseClient left, DatabaseClient right) =>
|
||||||
|
left.IsAvailable
|
||||||
|
&& right.IsAvailable
|
||||||
|
&& left.CacheKey == right.CacheKey;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var client in this.clients.Values)
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
foreach (var databaseLock in this.locks.Values)
|
||||||
|
databaseLock.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Tools.Databases;
|
||||||
|
|
||||||
|
public enum DatabaseClientStatus
|
||||||
|
{
|
||||||
|
STARTING,
|
||||||
|
AVAILABLE,
|
||||||
|
UNAVAILABLE,
|
||||||
|
}
|
||||||
6
app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs
Normal file
6
app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Databases;
|
||||||
|
|
||||||
|
public enum DatabaseRole
|
||||||
|
{
|
||||||
|
VECTOR_STORE,
|
||||||
|
}
|
||||||
@ -2,15 +2,19 @@ using AIStudio.Tools.PluginSystem;
|
|||||||
|
|
||||||
namespace AIStudio.Tools.Databases;
|
namespace AIStudio.Tools.Databases;
|
||||||
|
|
||||||
public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty)
|
public sealed class NoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : DatabaseClient(name, string.Empty)
|
||||||
{
|
{
|
||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient));
|
||||||
|
|
||||||
public override bool IsAvailable => false;
|
public override DatabaseClientStatus Status => status;
|
||||||
|
|
||||||
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
|
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
|
||||||
{
|
{
|
||||||
yield return (TB("Status"), TB("Unavailable"));
|
yield return (TB("Status"), status switch
|
||||||
|
{
|
||||||
|
DatabaseClientStatus.STARTING => TB("Starting"),
|
||||||
|
_ => TB("Unavailable")
|
||||||
|
});
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(unavailableReason))
|
if (!string.IsNullOrWhiteSpace(unavailableReason))
|
||||||
yield return (TB("Reason"), unavailableReason);
|
yield return (TB("Reason"), unavailableReason);
|
||||||
|
|||||||
@ -27,6 +27,8 @@ public class QdrantClientImplementation : DatabaseClient
|
|||||||
this.GrpcClient = this.CreateQdrantClient();
|
this.GrpcClient = this.CreateQdrantClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string CacheKey => $"{this.Name}:{this.HttpPort}:{this.GrpcPort}:{this.Fingerprint}";
|
||||||
|
|
||||||
private const string IP_ADDRESS = "localhost";
|
private const string IP_ADDRESS = "localhost";
|
||||||
|
|
||||||
private QdrantClient CreateQdrantClient()
|
private QdrantClient CreateQdrantClient()
|
||||||
@ -47,6 +49,11 @@ public class QdrantClientImplementation : DatabaseClient
|
|||||||
return $"v{operation.Version}";
|
return $"v{operation.Version}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CheckAvailabilityAsync()
|
||||||
|
{
|
||||||
|
await this.GrpcClient.HealthAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> GetCollectionsAmount()
|
private async Task<string> GetCollectionsAmount()
|
||||||
{
|
{
|
||||||
var operation = await this.GrpcClient.ListCollectionsAsync();
|
var operation = await this.GrpcClient.ListCollectionsAsync();
|
||||||
|
|||||||
@ -23,10 +23,7 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protected readonly HttpClient HttpClient = new()
|
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(new Uri($"{dataSource.Hostname}:{dataSource.Port}"));
|
||||||
{
|
|
||||||
BaseAddress = new Uri($"{dataSource.Hostname}:{dataSource.Port}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
protected string SecurityToken = string.Empty;
|
protected string SecurityToken = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.ERIClient.DataModel;
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
@ -102,10 +103,23 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
|||||||
}
|
}
|
||||||
|
|
||||||
case AuthMethod.USERNAME_PASSWORD:
|
case AuthMethod.USERNAME_PASSWORD:
|
||||||
|
if (this.DataSource.UsernamePasswordMode is DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD)
|
||||||
|
{
|
||||||
|
username = await rustService.ReadUserName();
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Successful = false,
|
||||||
|
Message = TB("Failed to read the user's username from the operating system.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string password;
|
string password;
|
||||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||||
{
|
{
|
||||||
var passwordResponse = await rustService.GetSecret(this.DataSource);
|
var passwordResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
|
||||||
if (!passwordResponse.Success)
|
if (!passwordResponse.Success)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
@ -159,7 +173,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
|||||||
string token;
|
string token;
|
||||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||||
{
|
{
|
||||||
var tokenResponse = await rustService.GetSecret(this.DataSource);
|
var tokenResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
|
||||||
if (!tokenResponse.Success)
|
if (!tokenResponse.Success)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
|
|||||||
81
app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs
Normal file
81
app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides utility methods to standardize the management of HTTP client timeouts
|
||||||
|
/// across various components in the application.
|
||||||
|
/// </summary>
|
||||||
|
public static class ExternalHttpClientTimeout
|
||||||
|
{
|
||||||
|
public const int MIN_HTTP_CLIENT_TIMEOUT_SECONDS = 120;
|
||||||
|
public const int MAX_HTTP_CLIENT_TIMEOUT_SECONDS = 3600;
|
||||||
|
public const int DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS = 3600;
|
||||||
|
|
||||||
|
private static readonly Lazy<SettingsManager> SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>());
|
||||||
|
|
||||||
|
public static HttpClient CreateHttpClient(Uri? baseAddress = null)
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
Configure(httpClient, baseAddress);
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetTimeoutDescription()
|
||||||
|
{
|
||||||
|
var timeout = GetTimeout();
|
||||||
|
|
||||||
|
if (timeout.TotalHours >= 1 && timeout.TotalMinutes % 60 == 0)
|
||||||
|
{
|
||||||
|
var hours = (int)timeout.TotalHours;
|
||||||
|
return hours == 1 ? "1 hour" : $"{hours} hours";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout.TotalMinutes >= 1 && timeout.TotalSeconds % 60 == 0)
|
||||||
|
{
|
||||||
|
var minutes = (int)timeout.TotalMinutes;
|
||||||
|
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = (int)timeout.TotalSeconds;
|
||||||
|
return seconds == 1 ? "1 second" : $"{seconds} seconds";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CancellationTokenSource CreateTimeoutTokenSource(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
timeoutTokenSource.CancelAfter(GetTimeout());
|
||||||
|
return timeoutTokenSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsTimeoutException(Exception exception, CancellationToken userCancellationToken = default)
|
||||||
|
{
|
||||||
|
if (userCancellationToken.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (exception is TimeoutException)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (exception is OperationCanceledException)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return exception.InnerException is not null && IsTimeoutException(exception.InnerException, userCancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetTimeout()
|
||||||
|
{
|
||||||
|
var seconds = SETTINGS_MANAGER.Value.ConfigurationData.App.HttpClientTimeoutSeconds;
|
||||||
|
if (seconds <= 0)
|
||||||
|
seconds = DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS;
|
||||||
|
|
||||||
|
seconds = Math.Clamp(seconds, MIN_HTTP_CLIENT_TIMEOUT_SECONDS, MAX_HTTP_CLIENT_TIMEOUT_SECONDS);
|
||||||
|
return TimeSpan.FromSeconds(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Configure(HttpClient httpClient, Uri? baseAddress = null)
|
||||||
|
{
|
||||||
|
httpClient.Timeout = GetTimeout();
|
||||||
|
if (baseAddress is not null)
|
||||||
|
httpClient.BaseAddress = baseAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +0,0 @@
|
|||||||
namespace AIStudio.Tools;
|
|
||||||
|
|
||||||
public static class LuaTools
|
|
||||||
{
|
|
||||||
public static string EscapeLuaString(string? value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return value
|
|
||||||
.Replace("\\", "\\\\")
|
|
||||||
.Replace("\"", "\\\"")
|
|
||||||
.Replace("\r", "\\r")
|
|
||||||
.Replace("\n", "\\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -35,12 +35,13 @@ public static partial class Pandoc
|
|||||||
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
|
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
|
||||||
|
|
||||||
private static readonly HttpClient WEB_CLIENT = new();
|
private static readonly HttpClient WEB_CLIENT = new();
|
||||||
|
private static readonly SemaphoreSlim INSTALLATION_LOCK = new(1, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares a Pandoc process by using the Pandoc process builder.
|
/// Prepares a Pandoc process by using the Pandoc process builder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The Pandoc process builder with default settings.</returns>
|
/// <returns>The Pandoc process builder with default settings.</returns>
|
||||||
public static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
|
private static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir.
|
/// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir.
|
||||||
@ -145,12 +146,12 @@ public static partial class Pandoc
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (showMessages)
|
if (showMessages)
|
||||||
await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("It seems that Pandoc is not installed.")));
|
await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("Pandoc doesn't seem to be installed.")));
|
||||||
|
|
||||||
if(shouldLog)
|
if(shouldLog)
|
||||||
LOG.LogError(e, "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH.");
|
LOG.LogError(e, "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH.");
|
||||||
|
|
||||||
return new(false, TB("It seems that Pandoc is not installed."), false, string.Empty, false);
|
return new(false, TB("Pandoc doesn't seem to be installed."), false, string.Empty, false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -165,76 +166,230 @@ public static partial class Pandoc
|
|||||||
/// <returns>None</returns>
|
/// <returns>None</returns>
|
||||||
public static async Task InstallAsync(RustService rustService)
|
public static async Task InstallAsync(RustService rustService)
|
||||||
{
|
{
|
||||||
|
await INSTALLATION_LOCK.WaitAsync();
|
||||||
|
|
||||||
var latestVersion = await FetchLatestVersionAsync();
|
var latestVersion = await FetchLatestVersionAsync();
|
||||||
var installDir = await GetPandocDataFolder(rustService);
|
var installDir = await GetPandocDataFolder(rustService);
|
||||||
ClearFolder(installDir);
|
var installParentDir = Path.GetDirectoryName(installDir) ?? Path.GetTempPath();
|
||||||
|
var stagingDir = Path.Combine(installParentDir, $"pandoc-install-{Guid.NewGuid():N}");
|
||||||
|
var pandocTempDownloadFile = Path.GetTempFileName();
|
||||||
|
|
||||||
LOG.LogInformation("Trying to install Pandoc v{0} to '{1}'...", latestVersion, installDir);
|
LOG.LogInformation("Trying to install Pandoc v{0} to '{1}'...", latestVersion, installDir);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(installDir))
|
if (!Directory.Exists(installParentDir))
|
||||||
Directory.CreateDirectory(installDir);
|
Directory.CreateDirectory(installParentDir);
|
||||||
|
|
||||||
// Create a temporary file to download the archive to:
|
|
||||||
var pandocTempDownloadFile = Path.GetTempFileName();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Download the latest Pandoc archive from GitHub:
|
// Download the latest Pandoc archive from GitHub:
|
||||||
//
|
//
|
||||||
var uri = await GenerateArchiveUriAsync();
|
var uri = GenerateArchiveUri(latestVersion);
|
||||||
var response = await WEB_CLIENT.GetAsync(uri);
|
if (string.IsNullOrWhiteSpace(uri))
|
||||||
|
{
|
||||||
|
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown.")));
|
||||||
|
LOG.LogError("Pandoc was not installed, no archive is available for architecture '{Architecture}'.", CPU_ARCHITECTURE.ToUserFriendlyName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var response = await WEB_CLIENT.GetAsync(uri);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive was not found.")));
|
||||||
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
|
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download the archive to the temporary file:
|
// Download the archive to the temporary file:
|
||||||
await using var tempFileStream = File.Create(pandocTempDownloadFile);
|
await using (var tempFileStream = File.Create(pandocTempDownloadFile))
|
||||||
|
{
|
||||||
await response.Content.CopyToAsync(tempFileStream);
|
await response.Content.CopyToAsync(tempFileStream);
|
||||||
|
await tempFileStream.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(stagingDir);
|
||||||
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir);
|
await RunWithRetriesAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
ZipFile.ExtractToDirectory(pandocTempDownloadFile, stagingDir, true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
"extracting the Pandoc ZIP archive");
|
||||||
}
|
}
|
||||||
else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
|
else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await RunWithRetriesAsync(
|
||||||
|
async () =>
|
||||||
{
|
{
|
||||||
await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
|
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
|
||||||
await TarFile.ExtractToDirectoryAsync(uncompressedStream, installDir, true);
|
await TarFile.ExtractToDirectoryAsync(uncompressedStream, stagingDir, true);
|
||||||
|
},
|
||||||
|
"extracting the Pandoc TAR archive");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive type is unknown.")));
|
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown.")));
|
||||||
LOG.LogError("Pandoc was not installed, the archive is unknown: url='{0}'", uri);
|
LOG.LogError("Pandoc was not installed, the archive is unknown: url='{0}'", uri);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Delete(pandocTempDownloadFile);
|
var stagedPandocExecutable = FindExecutableInDirectory(stagingDir, PandocProcessBuilder.PandocExecutableName);
|
||||||
|
if (string.IsNullOrWhiteSpace(stagedPandocExecutable))
|
||||||
|
{
|
||||||
|
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the executable was not found in the archive.")));
|
||||||
|
LOG.LogError("Pandoc was not installed, the executable was not found in the extracted archive: '{StagingDir}'.", stagingDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.LogInformation("Found Pandoc executable in downloaded archive: '{Executable}'.", stagedPandocExecutable);
|
||||||
|
|
||||||
|
await ReplaceInstallationDirectoryAsync(stagingDir, installDir);
|
||||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} was installed successfully."), latestVersion)));
|
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} was installed successfully."), latestVersion)));
|
||||||
LOG.LogInformation("Pandoc v{0} was installed successfully.", latestVersion);
|
LOG.LogInformation("Pandoc v{0} was installed successfully.", latestVersion);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc.")));
|
||||||
LOG.LogError(ex, "An error occurred while installing Pandoc.");
|
LOG.LogError(ex, "An error occurred while installing Pandoc.");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDeleteFile(pandocTempDownloadFile);
|
||||||
|
|
||||||
|
if (Directory.Exists(stagingDir))
|
||||||
|
await TryDeleteFolderAsync(stagingDir);
|
||||||
|
|
||||||
|
INSTALLATION_LOCK.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ClearFolder(string path)
|
private static async Task ReplaceInstallationDirectoryAsync(string stagingDir, string installDir)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(path))
|
var backupDir = $"{installDir}.backup-{Guid.NewGuid():N}";
|
||||||
|
var hasBackup = false;
|
||||||
|
var stagingWasMoved = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(installDir))
|
||||||
|
{
|
||||||
|
await MoveDirectoryWithRetriesAsync(installDir, backupDir, "moving the previous Pandoc installation to backup");
|
||||||
|
hasBackup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await MoveDirectoryWithRetriesAsync(stagingDir, installDir, "moving the new Pandoc installation into place");
|
||||||
|
stagingWasMoved = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (hasBackup && !stagingWasMoved && !Directory.Exists(installDir) && Directory.Exists(backupDir))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MoveDirectoryWithRetriesAsync(backupDir, installDir, "restoring the previous Pandoc installation");
|
||||||
|
hasBackup = false;
|
||||||
|
}
|
||||||
|
catch (Exception rollbackEx)
|
||||||
|
{
|
||||||
|
LOG.LogError(rollbackEx, "Error restoring previous Pandoc installation directory. Keeping backup directory at: '{BackupDir}'.", backupDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.LogError(ex, "Error replacing pandoc installation directory.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (hasBackup && stagingWasMoved && Directory.Exists(backupDir))
|
||||||
|
await TryDeleteFolderAsync(backupDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindExecutableInDirectory(string rootDirectory, string executableName)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(rootDirectory))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var rootExecutablePath = Path.Combine(rootDirectory, executableName);
|
||||||
|
if (File.Exists(rootExecutablePath))
|
||||||
|
return rootExecutablePath;
|
||||||
|
|
||||||
|
foreach (var subdirectory in Directory.GetDirectories(rootDirectory, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var pandocPath = Path.Combine(subdirectory, executableName);
|
||||||
|
if (File.Exists(pandocPath))
|
||||||
|
return pandocPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task MoveDirectoryWithRetriesAsync(string sourceDir, string destinationDir, string operationName)
|
||||||
|
{
|
||||||
|
await RunWithRetriesAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Directory.Move(sourceDir, destinationDir);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
operationName,
|
||||||
|
maxAttempts: 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RunWithRetriesAsync(Func<Task> operation, string operationName, int maxAttempts = 4)
|
||||||
|
{
|
||||||
|
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await operation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < maxAttempts && ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
LOG.LogWarning(ex, "Error while {OperationName}; retrying attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, maxAttempts);
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteFile(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(path, true);
|
File.Delete(path);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LOG.LogError(ex, "Error clearing pandoc installation directory.");
|
LOG.LogWarning(ex, "Was not able to delete temporary Pandoc archive: '{Path}'.", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TryDeleteFolderAsync(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RunWithRetriesAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
$"deleting temporary Pandoc directory '{path}'",
|
||||||
|
maxAttempts: 3);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LOG.LogWarning(ex, "Was not able to delete temporary Pandoc directory: '{Path}'.", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +403,7 @@ public static partial class Pandoc
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage);
|
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage);
|
||||||
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString())));
|
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString())));
|
||||||
return FALLBACK_VERSION.ToString();
|
return FALLBACK_VERSION.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +412,7 @@ public static partial class Pandoc
|
|||||||
if (!versionMatch.Success)
|
if (!versionMatch.Success)
|
||||||
{
|
{
|
||||||
LOG.LogError("The latest version regex returned nothing: {0}", versionMatch.Groups.ToString());
|
LOG.LogError("The latest version regex returned nothing: {0}", versionMatch.Groups.ToString());
|
||||||
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString())));
|
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString())));
|
||||||
return FALLBACK_VERSION.ToString();
|
return FALLBACK_VERSION.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,6 +427,11 @@ public static partial class Pandoc
|
|||||||
public static async Task<string> GenerateArchiveUriAsync()
|
public static async Task<string> GenerateArchiveUriAsync()
|
||||||
{
|
{
|
||||||
var version = await FetchLatestVersionAsync();
|
var version = await FetchLatestVersionAsync();
|
||||||
|
return GenerateArchiveUri(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateArchiveUri(string version)
|
||||||
|
{
|
||||||
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
|
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
|
||||||
return CPU_ARCHITECTURE switch
|
return CPU_ARCHITECTURE switch
|
||||||
{
|
{
|
||||||
|
|||||||
@ -220,6 +220,17 @@ public sealed class PandocProcessBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName))
|
||||||
|
{
|
||||||
|
if (!File.Exists(candidate))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (shouldLog)
|
||||||
|
LOGGER.LogInformation("Found system Pandoc installation at: '{Path}'.", candidate);
|
||||||
|
|
||||||
|
return new(candidate, false);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
|
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
|
||||||
//
|
//
|
||||||
@ -238,4 +249,59 @@ public sealed class PandocProcessBuilder
|
|||||||
/// Reads the os platform to determine the used executable name.
|
/// Reads the os platform to determine the used executable name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc";
|
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc";
|
||||||
|
|
||||||
|
private static IEnumerable<string> SystemPandocExecutableCandidates(string executableName)
|
||||||
|
{
|
||||||
|
var candidates = new List<string>();
|
||||||
|
|
||||||
|
switch (CPU_ARCHITECTURE)
|
||||||
|
{
|
||||||
|
case RID.WIN_X64 or RID.WIN_ARM64:
|
||||||
|
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Pandoc", executableName);
|
||||||
|
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Pandoc", executableName);
|
||||||
|
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Pandoc", executableName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RID.OSX_X64 or RID.OSX_ARM64:
|
||||||
|
AddCandidate(candidates, "/opt/homebrew/bin", executableName);
|
||||||
|
AddCandidate(candidates, "/usr/local/bin", executableName);
|
||||||
|
AddCandidate(candidates, "/usr/bin", executableName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RID.LINUX_X64 or RID.LINUX_ARM64:
|
||||||
|
AddCandidate(candidates, "/usr/local/bin", executableName);
|
||||||
|
AddCandidate(candidates, "/usr/bin", executableName);
|
||||||
|
AddCandidate(candidates, "/snap/bin", executableName);
|
||||||
|
|
||||||
|
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
AddCandidate(candidates, homeDirectory, ".local", "bin", executableName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pathDirectory in GetPathDirectories())
|
||||||
|
AddCandidate(candidates, pathDirectory, executableName);
|
||||||
|
|
||||||
|
var comparer = CPU_ARCHITECTURE is RID.WIN_X64 or RID.WIN_ARM64
|
||||||
|
? StringComparer.OrdinalIgnoreCase
|
||||||
|
: StringComparer.Ordinal;
|
||||||
|
return candidates.Distinct(comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetPathDirectories()
|
||||||
|
{
|
||||||
|
var pathValue = Environment.GetEnvironmentVariable("PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(pathValue))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (var pathDirectory in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
yield return pathDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddCandidate(List<string> candidates, params string[] pathParts)
|
||||||
|
{
|
||||||
|
if (pathParts.Any(string.IsNullOrWhiteSpace))
|
||||||
|
return;
|
||||||
|
|
||||||
|
candidates.Add(Path.Combine(pathParts));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -14,36 +14,3 @@ public sealed record PendingEnterpriseApiKey(
|
|||||||
string SecretName,
|
string SecretName,
|
||||||
string ApiKey,
|
string ApiKey,
|
||||||
SecretStoreType StoreType);
|
SecretStoreType StoreType);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Static container for pending API keys during plugin loading.
|
|
||||||
/// </summary>
|
|
||||||
public static class PendingEnterpriseApiKeys
|
|
||||||
{
|
|
||||||
private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = [];
|
|
||||||
private static readonly Lock LOCK = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a pending API key to the list.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The pending API key to add.</param>
|
|
||||||
public static void Add(PendingEnterpriseApiKey key)
|
|
||||||
{
|
|
||||||
lock (LOCK)
|
|
||||||
PENDING_KEYS.Add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets and clears all pending API keys.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A list of all pending API keys.</returns>
|
|
||||||
public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear()
|
|
||||||
{
|
|
||||||
lock (LOCK)
|
|
||||||
{
|
|
||||||
var keys = PENDING_KEYS.ToList();
|
|
||||||
PENDING_KEYS.Clear();
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static container for pending API keys during plugin loading.
|
||||||
|
/// </summary>
|
||||||
|
public static class PendingEnterpriseApiKeys
|
||||||
|
{
|
||||||
|
private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = [];
|
||||||
|
private static readonly Lock LOCK = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a pending API key to the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The pending API key to add.</param>
|
||||||
|
public static void Add(PendingEnterpriseApiKey key)
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
PENDING_KEYS.Add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and clears all pending API keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of all pending API keys.</returns>
|
||||||
|
public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear()
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
{
|
||||||
|
var keys = PENDING_KEYS.ToList();
|
||||||
|
PENDING_KEYS.Clear();
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pending enterprise secret that needs to be stored in the OS keyring.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SecretId">The secret ID.</param>
|
||||||
|
/// <param name="SecretName">The secret name.</param>
|
||||||
|
/// <param name="SecretData">The decrypted secret data.</param>
|
||||||
|
/// <param name="StoreType">The type of secret store to use.</param>
|
||||||
|
public sealed record PendingEnterpriseSecret(
|
||||||
|
string SecretId,
|
||||||
|
string SecretName,
|
||||||
|
string SecretData,
|
||||||
|
SecretStoreType StoreType);
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static container for pending enterprise secrets during plugin loading.
|
||||||
|
/// </summary>
|
||||||
|
public static class PendingEnterpriseSecrets
|
||||||
|
{
|
||||||
|
private static readonly List<PendingEnterpriseSecret> PENDING_SECRETS = [];
|
||||||
|
private static readonly Lock LOCK = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a pending enterprise secret to the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="secret">The pending enterprise secret to add.</param>
|
||||||
|
public static void Add(PendingEnterpriseSecret secret)
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
PENDING_SECRETS.Add(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and clears all pending enterprise secrets.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of all pending enterprise secrets.</returns>
|
||||||
|
public static IReadOnlyList<PendingEnterpriseSecret> GetAndClear()
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
{
|
||||||
|
var secrets = PENDING_SECRETS.ToList();
|
||||||
|
PENDING_SECRETS.Clear();
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,12 +39,43 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
{
|
{
|
||||||
// Store any decrypted API keys from enterprise configuration in the OS keyring:
|
// Store any decrypted API keys from enterprise configuration in the OS keyring:
|
||||||
await StoreEnterpriseApiKeysAsync();
|
await StoreEnterpriseApiKeysAsync();
|
||||||
|
await StoreEnterpriseSecretsAsync();
|
||||||
|
|
||||||
await SETTINGS_MANAGER.StoreSettings();
|
await SETTINGS_MANAGER.StoreSettings();
|
||||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores any pending enterprise secrets in the OS keyring.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task StoreEnterpriseSecretsAsync()
|
||||||
|
{
|
||||||
|
var pendingSecrets = PendingEnterpriseSecrets.GetAndClear();
|
||||||
|
if (pendingSecrets.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG.LogInformation($"Storing {pendingSecrets.Count} enterprise secret(s) in the OS keyring.");
|
||||||
|
var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
||||||
|
foreach (var pendingSecret in pendingSecrets)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var secretId = new TemporarySecretId(pendingSecret.SecretId, pendingSecret.SecretName);
|
||||||
|
var result = await rustService.SetSecret(secretId, pendingSecret.SecretData, pendingSecret.StoreType);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
LOG.LogDebug($"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring.");
|
||||||
|
else
|
||||||
|
LOG.LogWarning($"Failed to store enterprise secret for '{pendingSecret.SecretName}': {result.Issue}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LOG.LogError(ex, $"Exception while storing enterprise secret for '{pendingSecret.SecretName}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores any pending enterprise API keys in the OS keyring.
|
/// Stores any pending enterprise API keys in the OS keyring.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -141,6 +172,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Config: global voice recording shortcut
|
// Config: global voice recording shortcut
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: timeout for external HTTP requests
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HttpClientTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Handle configured LLM providers:
|
// Handle configured LLM providers:
|
||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||||
|
|
||||||
@ -151,7 +185,10 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||||
|
|
||||||
// Handle configured chat templates:
|
// Handle configured chat templates:
|
||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath);
|
||||||
|
|
||||||
|
// Handle configured data sources:
|
||||||
|
PluginConfigurationObject.TryParseDataSources(mainTable, this.Id, ref this.configObjects, dryRun);
|
||||||
|
|
||||||
// Handle configured profiles:
|
// Handle configured profiles:
|
||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.PROFILE, x => x.Profiles, x => x.NextProfileNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.PROFILE, x => x.Profiles, x => x.NextProfileNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||||
|
|||||||
@ -52,6 +52,7 @@ public sealed record PluginConfigurationObject
|
|||||||
/// This parameter is passed by reference.</param>
|
/// This parameter is passed by reference.</param>
|
||||||
/// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
|
/// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
|
||||||
/// are not persisted.</param>
|
/// are not persisted.</param>
|
||||||
|
/// <param name="pluginPath">An optional parameter specifying the file path of the plugin, used for relative paths in the Lua table.</param>
|
||||||
/// <returns>Returns true if parsing succeeds and configuration objects are added
|
/// <returns>Returns true if parsing succeeds and configuration objects are added
|
||||||
/// to the list; otherwise, false.</returns>
|
/// to the list; otherwise, false.</returns>
|
||||||
public static bool TryParse<TClass>(
|
public static bool TryParse<TClass>(
|
||||||
@ -61,7 +62,8 @@ public sealed record PluginConfigurationObject
|
|||||||
LuaTable mainTable,
|
LuaTable mainTable,
|
||||||
Guid configPluginId,
|
Guid configPluginId,
|
||||||
ref List<PluginConfigurationObject> configObjects,
|
ref List<PluginConfigurationObject> configObjects,
|
||||||
bool dryRun
|
bool dryRun,
|
||||||
|
string pluginPath = ""
|
||||||
) where TClass : ConfigurationBaseObject
|
) where TClass : ConfigurationBaseObject
|
||||||
{
|
{
|
||||||
var luaTableName = configObjectType switch
|
var luaTableName = configObjectType switch
|
||||||
@ -104,7 +106,7 @@ public sealed record PluginConfigurationObject
|
|||||||
var (wasParsingSuccessful, configObject) = configObjectType switch
|
var (wasParsingSuccessful, configObject) = configObjectType switch
|
||||||
{
|
{
|
||||||
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
|
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
|
||||||
PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
|
PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
|
||||||
PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject),
|
PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject),
|
||||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
||||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
|
||||||
@ -162,6 +164,87 @@ public sealed record PluginConfigurationObject
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses configured data sources from a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mainTable">The Lua table containing entries to parse into data sources.</param>
|
||||||
|
/// <param name="configPluginId">The unique identifier of the plugin associated with the data sources.</param>
|
||||||
|
/// <param name="configObjects">The list to populate with the parsed configuration objects.</param>
|
||||||
|
/// <param name="dryRun">Specifies whether to perform the operation as a dry run.</param>
|
||||||
|
/// <returns>True if the table was present and processed; otherwise false.</returns>
|
||||||
|
public static bool TryParseDataSources(
|
||||||
|
LuaTable mainTable,
|
||||||
|
Guid configPluginId,
|
||||||
|
ref List<PluginConfigurationObject> configObjects,
|
||||||
|
bool dryRun)
|
||||||
|
{
|
||||||
|
const string LUA_TABLE_NAME = "DATA_SOURCES";
|
||||||
|
if (!mainTable.TryGetValue(LUA_TABLE_NAME, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
||||||
|
{
|
||||||
|
LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, configPluginId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedObjects = SETTINGS_MANAGER.ConfigurationData.DataSources;
|
||||||
|
var numberObjects = luaTable.ArrayLength;
|
||||||
|
ThreadSafeRandom? random = null;
|
||||||
|
for (var i = 1; i <= numberObjects; i++)
|
||||||
|
{
|
||||||
|
var luaObjectTableValue = luaTable[i];
|
||||||
|
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
||||||
|
{
|
||||||
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject))
|
||||||
|
{
|
||||||
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
configObjects.Add(new()
|
||||||
|
{
|
||||||
|
ConfigPluginId = configPluginId,
|
||||||
|
Id = Guid.Parse(configObject.Id),
|
||||||
|
Type = PluginConfigurationObjectType.DATA_SOURCE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dryRun)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
|
||||||
|
if (objectIndex > -1)
|
||||||
|
{
|
||||||
|
var existingObject = storedObjects[objectIndex];
|
||||||
|
configObject = configObject with { Num = existingObject.Num };
|
||||||
|
storedObjects[objectIndex] = configObject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IncrementDataSourceNum() is { Success: true, UpdatedValue: var nextNum })
|
||||||
|
{
|
||||||
|
configObject = configObject with { Num = nextNum };
|
||||||
|
storedObjects.Add(configObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
random ??= new ThreadSafeRandom();
|
||||||
|
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
|
||||||
|
storedObjects.Add(configObject);
|
||||||
|
LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
static IncrementResult<uint> IncrementDataSourceNum()
|
||||||
|
{
|
||||||
|
return ((Expression<Func<Data, uint>>)(x => x.NextDataSourceNum)).TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -171,13 +254,15 @@ public sealed record PluginConfigurationObject
|
|||||||
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
||||||
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
||||||
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
||||||
|
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
||||||
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
|
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
|
||||||
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
|
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
|
||||||
PluginConfigurationObjectType configObjectType,
|
PluginConfigurationObjectType configObjectType,
|
||||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||||
IList<IAvailablePlugin> availablePlugins,
|
IList<IAvailablePlugin> availablePlugins,
|
||||||
IList<PluginConfigurationObject> configObjectList,
|
IList<PluginConfigurationObject> configObjectList,
|
||||||
SecretStoreType? secretStoreType = null) where TClass : IConfigurationObject
|
SecretStoreType? secretStoreType = null,
|
||||||
|
bool deleteSecret = false) where TClass : IConfigurationObject
|
||||||
{
|
{
|
||||||
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
|
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
|
||||||
var leftOverObjects = new List<TClass>();
|
var leftOverObjects = new List<TClass>();
|
||||||
@ -220,7 +305,15 @@ public sealed record PluginConfigurationObject
|
|||||||
configuredObjects.Remove(item);
|
configuredObjects.Remove(item);
|
||||||
|
|
||||||
// Delete the API key from the OS keyring if the removed object has one:
|
// Delete the API key from the OS keyring if the removed object has one:
|
||||||
if(secretStoreType is not null && item is ISecretId secretId)
|
if(deleteSecret && item is ISecretId regularSecretId)
|
||||||
|
{
|
||||||
|
var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE);
|
||||||
|
if (deleteResult.Success)
|
||||||
|
LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring.");
|
||||||
|
else
|
||||||
|
LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}");
|
||||||
|
}
|
||||||
|
else if(secretStoreType is not null && item is ISecretId secretId)
|
||||||
{
|
{
|
||||||
var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value);
|
var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value);
|
||||||
if (deleteResult.Success)
|
if (deleteResult.Success)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public static partial class PluginFactory
|
|||||||
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
|
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
|
||||||
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
|
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
|
||||||
|
|
||||||
using var http = new HttpClient();
|
using var http = ExternalHttpClientTimeout.CreateHttpClient();
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
|
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
|
||||||
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
@ -52,7 +52,7 @@ public static partial class PluginFactory
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LockHotReloadAsync();
|
await LockHotReloadAsync();
|
||||||
using var httpClient = new HttpClient();
|
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient();
|
||||||
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
|
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -174,6 +174,10 @@ public static partial class PluginFactory
|
|||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
// Check data sources:
|
||||||
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check chat templates:
|
// Check chat templates:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
@ -241,6 +245,10 @@ public static partial class PluginFactory
|
|||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
// Check for the external HTTP client timeout:
|
||||||
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HttpClientTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check if audit is required before it can be activated
|
// Check if audit is required before it can be activated
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
@ -318,7 +326,11 @@ public static partial class PluginFactory
|
|||||||
return new PluginLanguage(isInternal, state, type);
|
return new PluginLanguage(isInternal, state, type);
|
||||||
|
|
||||||
case PluginType.CONFIGURATION:
|
case PluginType.CONFIGURATION:
|
||||||
var configPlug = new PluginConfiguration(isInternal, state, type);
|
var configPlug = new PluginConfiguration(isInternal, state, type)
|
||||||
|
{
|
||||||
|
PluginPath = pluginPath ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
await configPlug.InitializeAsync(true);
|
await configPlug.InitializeAsync(true);
|
||||||
return configPlug;
|
return configPlug;
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ namespace AIStudio.Tools.Rust;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FileTypes
|
public static class FileTypes
|
||||||
{
|
{
|
||||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter));
|
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileTypes).Namespace, nameof(FileTypes));
|
||||||
|
|
||||||
public static readonly FileTypeFilter SOURCE_LIKE_FILE_NAMES = FileTypeFilter.Leaf(TB("Source like"),
|
public static readonly FileTypeFilter SOURCE_LIKE_FILE_NAMES = FileTypeFilter.Leaf(TB("Source like"),
|
||||||
"Dockerfile", "Containerfile", "Jenkinsfile", "Makefile", "GNUmakefile", "Procfile", "Vagrantfile",
|
"Dockerfile", "Containerfile", "Jenkinsfile", "Makefile", "GNUmakefile", "Procfile", "Vagrantfile",
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct QdrantInfo
|
public readonly record struct QdrantInfo
|
||||||
{
|
{
|
||||||
|
public QdrantStatus Status { get; init; }
|
||||||
|
|
||||||
public bool IsAvailable { get; init; }
|
public bool IsAvailable { get; init; }
|
||||||
|
|
||||||
public string? UnavailableReason { get; init; }
|
public string? UnavailableReason { get; init; }
|
||||||
|
|||||||
8
app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs
Normal file
8
app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
public enum QdrantStatus
|
||||||
|
{
|
||||||
|
STARTING,
|
||||||
|
AVAILABLE,
|
||||||
|
UNAVAILABLE,
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
namespace AIStudio.Tools;
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the type of secret store used for API keys.
|
/// Represents the type of secret store used for API keys and other secrets.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Different provider types use different prefixes for storing API keys.
|
/// Different provider and secret types use different prefixes for storing secrets.
|
||||||
/// This prevents collisions when the same instance name is used across
|
/// This prevents collisions when the same instance name is used across
|
||||||
/// different provider types (e.g., LLM, Embedding, Transcription).
|
/// different provider types (e.g., LLM, Embedding, Transcription).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
@ -29,4 +29,9 @@ public enum SecretStoreType
|
|||||||
/// Image provider secrets. Uses the "image::" prefix.
|
/// Image provider secrets. Uses the "image::" prefix.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IMAGE_PROVIDER,
|
IMAGE_PROVIDER,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data source secrets. Uses the "data-source::" prefix.
|
||||||
|
/// </summary>
|
||||||
|
DATA_SOURCE,
|
||||||
}
|
}
|
||||||
@ -9,12 +9,14 @@ public static class SecretStoreTypeExtensions
|
|||||||
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
|
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="type">The SecretStoreType enum value.</param>
|
/// <param name="type">The SecretStoreType enum value.</param>
|
||||||
/// <returns>>The corresponding prefix string.</returns>
|
/// <returns>The corresponding prefix string.</returns>
|
||||||
public static string Prefix(this SecretStoreType type) => type switch
|
public static string Prefix(this SecretStoreType type) => type switch
|
||||||
{
|
{
|
||||||
SecretStoreType.LLM_PROVIDER => "provider",
|
SecretStoreType.LLM_PROVIDER => "provider",
|
||||||
SecretStoreType.EMBEDDING_PROVIDER => "embedding",
|
SecretStoreType.EMBEDDING_PROVIDER => "embedding",
|
||||||
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
||||||
|
SecretStoreType.IMAGE_PROVIDER => "image",
|
||||||
|
SecretStoreType.DATA_SOURCE => "data-source",
|
||||||
|
|
||||||
_ => "provider",
|
_ => "provider",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -200,7 +200,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
{
|
{
|
||||||
logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins.");
|
logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins.");
|
||||||
PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret);
|
PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret);
|
||||||
await this.RemoveEnterpriseManagedApiKeysAsync();
|
await this.RemoveEnterpriseManagedSecretsAsync();
|
||||||
await PluginFactory.LoadAll();
|
await PluginFactory.LoadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,34 +249,36 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
return serverUrl.Trim().TrimEnd('/');
|
return serverUrl.Trim().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveEnterpriseManagedApiKeysAsync()
|
private async Task RemoveEnterpriseManagedSecretsAsync()
|
||||||
{
|
{
|
||||||
var secretTargets = GetEnterpriseManagedSecretTargets();
|
var secretTargets = GetEnterpriseManagedSecretTargets();
|
||||||
if (secretTargets.Count == 0)
|
if (secretTargets.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogInformation("No enterprise-managed API keys are currently known in the settings. No keyring cleanup is required.");
|
logger.LogInformation("No enterprise-managed secrets are currently known in the settings. No keyring cleanup is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("Removing {SecretCount} enterprise-managed API key(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count);
|
logger.LogInformation("Removing {SecretCount} enterprise-managed secret(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count);
|
||||||
foreach (var target in secretTargets)
|
foreach (var target in secretTargets)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deleteResult = await rustService.DeleteAPIKey(target, target.StoreType);
|
var deleteResult = target.StoreType is SecretStoreType.DATA_SOURCE
|
||||||
|
? await rustService.DeleteSecret(target, target.StoreType)
|
||||||
|
: await rustService.DeleteAPIKey(target, target.StoreType);
|
||||||
if (deleteResult.Success)
|
if (deleteResult.Success)
|
||||||
{
|
{
|
||||||
if (deleteResult.WasEntryFound)
|
if (deleteResult.WasEntryFound)
|
||||||
logger.LogInformation("Successfully deleted enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
logger.LogInformation("Successfully deleted enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName);
|
||||||
else
|
else
|
||||||
logger.LogInformation("Enterprise-managed API key '{SecretName}' was already absent from the OS keyring.", target.SecretName);
|
logger.LogInformation("Enterprise-managed secret '{SecretName}' was already absent from the OS keyring.", target.SecretName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
logger.LogWarning("Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue);
|
logger.LogWarning("Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.LogWarning(e, "Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
logger.LogWarning(e, "Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,6 +291,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets);
|
AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets);
|
||||||
AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets);
|
AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets);
|
||||||
AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets);
|
AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets);
|
||||||
|
AddEnterpriseManagedSecretTargets(configurationData.DataSources.OfType<IExternalDataSource>(), SecretStoreType.DATA_SOURCE, secretTargets);
|
||||||
|
|
||||||
return secretTargets.ToList();
|
return secretTargets.ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,9 +185,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
return new(shortcut, isEnabled, false);
|
return new(shortcut, isEnabled, false);
|
||||||
|
|
||||||
var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording;
|
var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording;
|
||||||
var fallbackEnabled =
|
var fallbackEnabled = !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider);
|
||||||
settingsSnapshot.App.EnabledPreviewFeatures.Contains(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026) &&
|
|
||||||
!string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider);
|
|
||||||
|
|
||||||
if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut))
|
if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut))
|
||||||
return new(shortcut, isEnabled, false);
|
return new(shortcut, isEnabled, false);
|
||||||
|
|||||||
@ -4,13 +4,27 @@ namespace AIStudio.Tools.Services;
|
|||||||
|
|
||||||
public sealed partial class RustService
|
public sealed partial class RustService
|
||||||
{
|
{
|
||||||
public async Task<QdrantInfo> GetQdrantInfo()
|
public async Task<QdrantInfo> GetQdrantInfo(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
var response = await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token);
|
cts.CancelAfter(TimeSpan.FromSeconds(45));
|
||||||
return response;
|
|
||||||
|
return await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if(this.logger is not null)
|
||||||
|
this.logger.LogWarning("Fetching Qdrant info from Rust service was cancelled by caller.");
|
||||||
|
else
|
||||||
|
Console.WriteLine("Fetching Qdrant info from Rust service was cancelled by caller.");
|
||||||
|
|
||||||
|
return new QdrantInfo
|
||||||
|
{
|
||||||
|
Status = QdrantStatus.UNAVAILABLE,
|
||||||
|
UnavailableReason = "Operation cancelled by caller."
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -19,7 +33,11 @@ public sealed partial class RustService
|
|||||||
else
|
else
|
||||||
Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'.");
|
Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'.");
|
||||||
|
|
||||||
return default;
|
return new QdrantInfo
|
||||||
|
{
|
||||||
|
Status = QdrantStatus.UNAVAILABLE,
|
||||||
|
UnavailableReason = e.Message
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,8 +6,11 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||||
{
|
{
|
||||||
PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory);
|
var encodedTitle = Uri.EscapeDataString(title);
|
||||||
var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions);
|
var result = initialDirectory is null
|
||||||
|
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
||||||
|
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||||
|
|||||||
@ -32,4 +32,35 @@ public sealed partial class RustService
|
|||||||
this.userLanguageLock.Release();
|
this.userLanguageLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> ReadUserName(bool forceRequest = false)
|
||||||
|
{
|
||||||
|
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName))
|
||||||
|
return this.cachedUserName;
|
||||||
|
|
||||||
|
await this.userNameLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName))
|
||||||
|
return this.cachedUserName;
|
||||||
|
|
||||||
|
var response = await this.http.GetAsync("/system/username");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
this.logger!.LogError($"Failed to read the user name from Rust: '{response.StatusCode}'");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userName = (await response.Content.ReadAsStringAsync()).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(userName))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
this.cachedUserName = userName;
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.userNameLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -13,7 +13,16 @@ public sealed partial class RustService
|
|||||||
var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
this.logger?.LogError(
|
||||||
|
"Failed to read arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'",
|
||||||
|
response.StatusCode,
|
||||||
|
response.ReasonPhrase,
|
||||||
|
path,
|
||||||
|
responseBody);
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
var resultBuilder = new StringBuilder();
|
var resultBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
|||||||
@ -4,26 +4,34 @@ namespace AIStudio.Tools.Services;
|
|||||||
|
|
||||||
public sealed partial class RustService
|
public sealed partial class RustService
|
||||||
{
|
{
|
||||||
|
private static string SecretKey(ISecretId secretId, SecretStoreType storeType) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}";
|
||||||
|
|
||||||
|
private static string LegacySecretKey(ISecretId secretId) => $"secret::{secretId.SecretId}::{secretId.SecretName}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to get the secret data for the given secret ID.
|
/// Try to get the secret data for the given secret ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="secretId">The secret ID to get the data for.</param>
|
/// <param name="secretId">The secret ID to get the data for.</param>
|
||||||
|
/// <param name="storeType">The secret store type.</param>
|
||||||
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
|
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
|
||||||
/// <returns>The requested secret.</returns>
|
/// <returns>The requested secret.</returns>
|
||||||
public async Task<RequestedSecret> GetSecret(ISecretId secretId, bool isTrying = false)
|
public async Task<RequestedSecret> GetSecret(ISecretId secretId, SecretStoreType storeType, bool isTrying = false)
|
||||||
{
|
{
|
||||||
var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying);
|
var secretKey = SecretKey(secretId, storeType);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
var secret = await this.GetSecretByKey(secretKey, isTrying || storeType is SecretStoreType.DATA_SOURCE);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (secret.Success || storeType is not SecretStoreType.DATA_SOURCE)
|
||||||
|
return secret;
|
||||||
|
|
||||||
|
var legacySecretKey = LegacySecretKey(secretId);
|
||||||
|
var legacySecret = await this.GetSecretByKey(legacySecretKey, isTrying: true);
|
||||||
|
if (legacySecret.Success)
|
||||||
{
|
{
|
||||||
if(!isTrying)
|
this.logger!.LogDebug($"Successfully retrieved the legacy data source secret for '{legacySecretKey}'.");
|
||||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
return legacySecret;
|
||||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
|
||||||
if (!secret.Success && !isTrying)
|
if (!secret.Success && !isTrying)
|
||||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
this.logger!.LogError($"Failed to get the secret data for '{secretKey}': '{secret.Issue}'");
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
@ -33,21 +41,26 @@ public sealed partial class RustService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="secretId">The secret ID to store the data for.</param>
|
/// <param name="secretId">The secret ID to store the data for.</param>
|
||||||
/// <param name="secretData">The data to store.</param>
|
/// <param name="secretData">The data to store.</param>
|
||||||
|
/// <param name="storeType">The secret store type.</param>
|
||||||
/// <returns>The store secret response.</returns>
|
/// <returns>The store secret response.</returns>
|
||||||
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData)
|
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData, SecretStoreType storeType)
|
||||||
{
|
{
|
||||||
|
var secretKey = SecretKey(secretId, storeType);
|
||||||
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
||||||
var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret);
|
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedSecret);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new StoreSecretResponse(false, TB("Failed to get the secret data due to an API issue."));
|
return new StoreSecretResponse(false, TB("Failed to store the secret data due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to store the secret data for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
|
if (state.Success && storeType is SecretStoreType.DATA_SOURCE)
|
||||||
|
await this.DeleteSecretByKey(LegacySecretKey(secretId));
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -56,20 +69,48 @@ public sealed partial class RustService
|
|||||||
/// Tries to delete the secret data for the given secret ID.
|
/// Tries to delete the secret data for the given secret ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="secretId">The secret ID to delete the data for.</param>
|
/// <param name="secretId">The secret ID to delete the data for.</param>
|
||||||
|
/// <param name="storeType">The secret store type.</param>
|
||||||
/// <returns>The delete secret response.</returns>
|
/// <returns>The delete secret response.</returns>
|
||||||
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId)
|
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId, SecretStoreType storeType)
|
||||||
{
|
{
|
||||||
var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false);
|
var deleteResult = await this.DeleteSecretByKey(SecretKey(secretId, storeType));
|
||||||
|
if (storeType is not SecretStoreType.DATA_SOURCE || !deleteResult.Success)
|
||||||
|
return deleteResult;
|
||||||
|
|
||||||
|
var legacyDeleteResult = await this.DeleteSecretByKey(LegacySecretKey(secretId));
|
||||||
|
if (!legacyDeleteResult.Success)
|
||||||
|
return legacyDeleteResult;
|
||||||
|
|
||||||
|
return deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<RequestedSecret> GetSecretByKey(string secretKey, bool isTrying)
|
||||||
|
{
|
||||||
|
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
||||||
|
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||||
|
if (!result.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if(!isTrying)
|
||||||
|
this.logger!.LogError($"Failed to get the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
|
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DeleteSecretResponse> DeleteSecretByKey(string secretKey)
|
||||||
|
{
|
||||||
|
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")};
|
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")};
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
|
|
||||||
private readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||||
|
|
||||||
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
||||||
{
|
{
|
||||||
@ -31,6 +32,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
private ILogger<RustService>? logger;
|
private ILogger<RustService>? logger;
|
||||||
private Encryption? encryptor;
|
private Encryption? encryptor;
|
||||||
private string? cachedUserLanguage;
|
private string? cachedUserLanguage;
|
||||||
|
private string? cachedUserName;
|
||||||
|
|
||||||
private readonly string apiPort;
|
private readonly string apiPort;
|
||||||
private readonly string certificateFingerprint;
|
private readonly string certificateFingerprint;
|
||||||
@ -91,6 +93,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
{
|
{
|
||||||
this.http.Dispose();
|
this.http.Dispose();
|
||||||
this.userLanguageLock.Dispose();
|
this.userLanguageLock.Dispose();
|
||||||
|
this.userNameLock.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,24 +22,28 @@
|
|||||||
},
|
},
|
||||||
"LuaCSharp": {
|
"LuaCSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[0.5.3, )",
|
"requested": "[0.5.5, )",
|
||||||
"resolved": "0.5.3",
|
"resolved": "0.5.5",
|
||||||
"contentHash": "qpgmCaNx08+eiWOmz7U/mXOH8DXUyLW8fsCukKjN8hVled2y4HrapsZlmrnIf9iaNfEQusUR/8d1M2XX6NIzbQ=="
|
"contentHash": "IL44DCbMtEafyiy8DzHFd/f+1pXuDUVFJMCJPAu8vQHNfO3ADSoWSOKMg9Py1za/ZE1K0gs0jll1viInoN+19Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"LuaCSharp.Annotations": "0.5.5",
|
||||||
|
"LuaCSharp.SourceGenerator": "0.5.5"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.FileProviders.Embedded": {
|
"Microsoft.Extensions.FileProviders.Embedded": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.15, )",
|
"requested": "[9.0.16, )",
|
||||||
"resolved": "9.0.15",
|
"resolved": "9.0.16",
|
||||||
"contentHash": "XFlI3ZISL344QdPLtaXG0yPyjkHQR82DYXrJa9aF00Qeu7dDnFxwFgP/ItkkyiLjAe/NSj6vksxOdnelXGT1vQ==",
|
"contentHash": "QRlSWz7zEplBxETrySKK3qpPm/7NPaRGnUpEXQNP3k6Ht2KdVy59JcoUPXlNGnNE3tJd3ycXfMeWqxBG6SyV0w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.15"
|
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.NET.ILLink.Tasks": {
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.15, )",
|
"requested": "[9.0.16, )",
|
||||||
"resolved": "9.0.15",
|
"resolved": "9.0.16",
|
||||||
"contentHash": "EejcbfCMR77Dthy77qxRbEShmzLApHZUPqXMBVQK+A0pNrRThkaHoGGMGvbq/gTkC/waKcDEgjBkbaejB58Wtw=="
|
"contentHash": "ccPBYGLPJt8DeJTUzQ0JzOh/iuUAgnjayU63PokVywAhUOx+dzDKSPTL7AG94U/VpvNXflTT2AjsFAIF1+bXBw=="
|
||||||
},
|
},
|
||||||
"MudBlazor": {
|
"MudBlazor": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
@ -64,9 +68,9 @@
|
|||||||
},
|
},
|
||||||
"Qdrant.Client": {
|
"Qdrant.Client": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.17.0, )",
|
"requested": "[1.18.1, )",
|
||||||
"resolved": "1.17.0",
|
"resolved": "1.18.1",
|
||||||
"contentHash": "QFNtVu4Kiz6NHAAi2UQk+Ia64/qyX1NMecQGIBGnKqFOlpnxI3OCCBRBKXWGPk/c+4vAmR3Dj+cQ9apqX0zU8A==",
|
"contentHash": "eBwFLihGMvN02/jr/BNdcop2XmtA10y8VMOclVZ7K2H8yheAhl7jbkf7I8e4X3RYpT+cAxgcalP4xmOhgs4KJg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Google.Protobuf": "3.31.0",
|
"Google.Protobuf": "3.31.0",
|
||||||
"Grpc.Net.Client": "2.71.0"
|
"Grpc.Net.Client": "2.71.0"
|
||||||
@ -113,6 +117,16 @@
|
|||||||
"Grpc.Core.Api": "2.71.0"
|
"Grpc.Core.Api": "2.71.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"LuaCSharp.Annotations": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "0.5.5",
|
||||||
|
"contentHash": "5VcwcTNGCY5YXLz2BRko5/Z0YGd6MZqNsnnfPOsGHHpAtqWPFbD0vtOZR4jUqaQLtQUvl2+WRfmIOhp6L2S0rw=="
|
||||||
|
},
|
||||||
|
"LuaCSharp.SourceGenerator": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "0.5.5",
|
||||||
|
"contentHash": "2xHKGc1bYXTsmSzZCNmKkuAU6A+1azulNiPY/ICKBSHIgEPMNRQ7JS6PvAClrHe6bk8SKcC/fbba6igtDzDaAw=="
|
||||||
|
},
|
||||||
"Markdig": {
|
"Markdig": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "0.41.3",
|
"resolved": "0.41.3",
|
||||||
@ -182,10 +196,10 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "9.0.15",
|
"resolved": "9.0.16",
|
||||||
"contentHash": "yzWilnNU/MvHINapPhY6iFAeApZnhToXbEBplORucn01hFc1F6ZaKt0V9dHYpUMun8WR9cSnq1ky35FWREVZbA==",
|
"contentHash": "/YLSWDs+p0Y4+UGPoWI3uUNq7R5/f/8zw8XeViuhfSTGnPowoqbllBE9aR4TteFgNfIH4IHkhUwSlhMLB0aL8g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "9.0.15"
|
"Microsoft.Extensions.Primitives": "9.0.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Localization": {
|
"Microsoft.Extensions.Localization": {
|
||||||
@ -223,8 +237,8 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Extensions.Primitives": {
|
"Microsoft.Extensions.Primitives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "9.0.15",
|
"resolved": "9.0.16",
|
||||||
"contentHash": "WRPJ9kpIwsOcghRT0tduIqiz7CDv7WsnL4kTJavtHS4j5AW++4LlR63oOSTL2o/zLR4T1z0/FQMgrnsPJ5bpQQ=="
|
"contentHash": "w5RE1MR0lnAElsRJaFd2POIXl/H62aBKmfX8ibYmRmbk0JB9V/9jR0VD5NxiP1ETWpnDAnPguTSe7fF/FdsHEQ=="
|
||||||
},
|
},
|
||||||
"Microsoft.JSInterop": {
|
"Microsoft.JSInterop": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
|||||||
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md
Normal file
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# v26.5.1, build 236 (2026-05-06 13:06 UTC)
|
||||||
|
- Changed the preview update path for a controlled prerelease test. Please do not install this prerelease manually. Production versions such as v26.4.1 will ignore this update. We are using this prerelease to test the clean update path for the migration from the Tauri v1 framework to the Tauri v2 framework. After a successful test, this prerelease will be removed.
|
||||||
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md
Normal file
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# v26.5.2, build 237 (2026-05-06 16:38 UTC)
|
||||||
|
- Updated the underlying Tauri framework from version 1 to the latest version 2. Please do not install this prerelease manually. Production versions such as v26.4.1 will ignore this update. We are using this prerelease to test the clean update path for the migration from the Tauri v1 framework to the Tauri v2 framework. After a successful test, this prerelease will be removed.
|
||||||
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md
Normal file
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# v26.5.3, build 238 (2026-05-13 09:50 UTC)
|
||||||
|
- Migrated away from Rocket to Axum for our internal IPC API. Please do not install this prerelease manually. Production versions, such as v26.4.1, will ignore this update. We are using this prerelease to test the clean update path. After a successful test, this prerelease will be removed.
|
||||||
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md
Normal file
2
app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# v26.5.4, build 239 (2026-05-13 11:58 UTC)
|
||||||
|
- Migrated away from Rocket to Axum for our internal IPC API. Please do not install this prerelease manually. Production versions, such as v26.4.1, will ignore this update. We are using this prerelease to test the clean update path. After a successful test, this prerelease will be removed.
|
||||||
20
app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md
Normal file
20
app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# v26.5.5, build 240 (2026-05-xx xx:xx UTC)
|
||||||
|
- Released the voice recording and transcription for all users. You no longer need to enable a preview feature to configure transcription providers, select a transcription provider, or use dictation.
|
||||||
|
- Added export options for profiles and chat templates, including an option to package chat template attachments into configuration plugins.
|
||||||
|
- Added support for organization-managed ERI servers in configuration plugins, so admins can preconfigure external data sources for users.
|
||||||
|
- Added an export option for ERI server data sources, so admins can create configuration plugin snippets without writing the Lua code manually.
|
||||||
|
- Added an option to configure the timeout setting for all requests. This is useful when you have a slow network connection, or you have to work with slow AI servers. It is also possible to configure this timeout for an entire organization using configuration plugins.
|
||||||
|
- Added the username to the information page to make organization support easier when users share their screen.
|
||||||
|
- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base.
|
||||||
|
- Improved the Pandoc management and detection process to make it more reliable.
|
||||||
|
- Improved the Qdrant startup and vector database initialization, so AI Studio can start more reliably while the local vector database is still starting.
|
||||||
|
- Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency.
|
||||||
|
- Fixed an issue where the spellchecking setting was not applied to all text fields in the slide builder assistant.
|
||||||
|
- Fixed missing translations for file type names in file selection dialogs.
|
||||||
|
- Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system.
|
||||||
|
- Upgraded Rust to v1.95.0.
|
||||||
|
- Upgraded .NET to v9.0.16.
|
||||||
|
- Upgraded Tauri to v2.11.1.
|
||||||
|
- Upgraded PDFium to v148.0.7763.0.
|
||||||
|
- Upgraded Qdrant to v1.18.0.
|
||||||
|
- Upgraded other dependencies as well.
|
||||||
@ -2,9 +2,49 @@ namespace SharedTools;
|
|||||||
|
|
||||||
public static class LuaTools
|
public static class LuaTools
|
||||||
{
|
{
|
||||||
public static string EscapeLuaString(string value)
|
public static string EscapeLuaString(string? value)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
// Replace backslashes with double backslashes and escape double quotes:
|
// Replace backslashes with double backslashes and escape double quotes:
|
||||||
return value.Replace("\\", @"\\").Replace("\"", "\\\"");
|
return value
|
||||||
|
.Replace("\\", @"\\")
|
||||||
|
.Replace("\"", "\\\"")
|
||||||
|
.Replace("\r", "\\r")
|
||||||
|
.Replace("\n", "\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToLuaStringLiteral(string? value, bool forceLongString = false, int longStringLengthThreshold = 80)
|
||||||
|
{
|
||||||
|
value ??= string.Empty;
|
||||||
|
if (!forceLongString &&
|
||||||
|
value.Length <= longStringLengthThreshold &&
|
||||||
|
!value.Contains('\n') &&
|
||||||
|
!value.Contains('\r'))
|
||||||
|
return $"\"{EscapeLuaString(value)}\"";
|
||||||
|
|
||||||
|
return $"{CreateLongStringOpeningDelimiter(value)}{value}{CreateLongStringClosingDelimiter(value)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateLongStringOpeningDelimiter(string value)
|
||||||
|
{
|
||||||
|
var equals = CreateLongStringEquals(value);
|
||||||
|
return $"[{equals}[";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateLongStringClosingDelimiter(string value)
|
||||||
|
{
|
||||||
|
var equals = CreateLongStringEquals(value);
|
||||||
|
return $"]{equals}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateLongStringEquals(string value)
|
||||||
|
{
|
||||||
|
var equalsCount = 3;
|
||||||
|
while (value.Contains($"]{new string('=', equalsCount)}]"))
|
||||||
|
equalsCount++;
|
||||||
|
|
||||||
|
return new string('=', equalsCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ Therefore, we cannot provide a static list here that is valid for all Linux syst
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
1. Install the [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0).
|
1. Install the [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0).
|
||||||
2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest stable version.
|
2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest stable version.
|
||||||
3. Met the prerequisites for building [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/). Node.js is **not** required, though.
|
3. Meet the prerequisites for building [Tauri](https://v2.tauri.app/start/prerequisites/). Node.js is **not** required, though.
|
||||||
4. The core team uses [JetBrains](https://www.jetbrains.com/) [Rider](https://www.jetbrains.com/rider/) and [RustRover](https://www.jetbrains.com/rust/) for development. Both IDEs are free to use for open-source projects for non-commercial use. They are available for macOS, Linux, and Windows systems. Profiles are provided for these IDEs, so you can get started right away. However, you can also use a different IDE.
|
4. The core team uses [JetBrains](https://www.jetbrains.com/) [Rider](https://www.jetbrains.com/rider/) and [RustRover](https://www.jetbrains.com/rust/) for development. Both IDEs are free to use for open-source projects for non-commercial use. They are available for macOS, Linux, and Windows systems. Profiles are provided for these IDEs, so you can get started right away. However, you can also use a different IDE.
|
||||||
4. Clone the repository.
|
4. Clone the repository.
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ Therefore, we cannot provide a static list here that is valid for all Linux syst
|
|||||||
Regardless of whether you want to build the app locally for yourself (not trusting the pre-built binaries) or test your changes before creating a PR, you have to run the following commands at least once:
|
Regardless of whether you want to build the app locally for yourself (not trusting the pre-built binaries) or test your changes before creating a PR, you have to run the following commands at least once:
|
||||||
|
|
||||||
1. Open a terminal.
|
1. Open a terminal.
|
||||||
2. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`.
|
2. Install the Tauri CLI by running `cargo install tauri-cli --version 2.11.0 --locked`.
|
||||||
3. Navigate to the `/app/Build` directory within the repository.
|
3. Navigate to the `/app/Build` directory within the repository.
|
||||||
4. Run `dotnet run build` to build the entire app.
|
4. Run `dotnet run build` to build the entire app.
|
||||||
|
|
||||||
|
|||||||
@ -84,4 +84,4 @@ We have to figure out if you have an Intel/AMD or a modern ARM system on your Li
|
|||||||
2. Open a terminal and navigate to the Downloads folder: `cd Downloads`.
|
2. Open a terminal and navigate to the Downloads folder: `cd Downloads`.
|
||||||
3. Make the AppImage executable: `chmod +x mind-work-ai-studio_amd64.AppImage`.
|
3. Make the AppImage executable: `chmod +x mind-work-ai-studio_amd64.AppImage`.
|
||||||
4. You might want to move the AppImage to a more convenient location, e.g., your home directory: `mv mind-work-ai-studio_amd64.AppImage ~/`.
|
4. You might want to move the AppImage to a more convenient location, e.g., your home directory: `mv mind-work-ai-studio_amd64.AppImage ~/`.
|
||||||
4. Now you can run the AppImage from your file manager (double-click) or the terminal: `./mind-work-ai-studio_amd64.AppImage`.
|
5. Now you can run the AppImage from your file manager (double-click) or the terminal: `./mind-work-ai-studio_amd64.AppImage`.
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
20
metadata.txt
20
metadata.txt
@ -1,12 +1,12 @@
|
|||||||
26.4.1
|
26.5.4
|
||||||
2026-04-17 17:25:44 UTC
|
2026-05-13 11:58:02 UTC
|
||||||
235
|
239
|
||||||
9.0.116 (commit fb4af7e1b3)
|
9.0.117 (commit 6e241a69c1)
|
||||||
9.0.15 (commit 4250c8399a)
|
9.0.16 (commit a1e6809fb8)
|
||||||
1.93.1 (commit 01f6ddf75)
|
1.95.0 (commit 59807616e)
|
||||||
8.15.0
|
8.15.0
|
||||||
1.8.3
|
2.11.1
|
||||||
c6ed7e3c0ce, release
|
0089849e0c3, release
|
||||||
osx-arm64
|
osx-arm64
|
||||||
144.0.7543.0
|
148.0.7763.0
|
||||||
1.17.1
|
1.18.0
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user