mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-02 05:47:21 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a755082c0f
|
||
|
|
22b5d16c72
|
||
|
|
620afd1611
|
||
|
|
7b65d96cd8
|
||
|
|
eafc134ab8
|
||
|
|
fc7399e81b | ||
|
|
faae2951f7 | ||
|
|
64dc621a7d | ||
|
|
9532664a25 | ||
|
|
43d202310b | ||
|
|
652d3a509f | ||
|
|
8a4443ab4b | ||
|
|
4522c52cba | ||
|
|
2c2ad0dfde | ||
|
|
eef475581c | ||
|
|
aeba1fbccc | ||
|
|
e0337a99bc | ||
|
|
c2b62c6dbd | ||
|
|
1c8b6ef774 | ||
|
|
bca728ba2c | ||
|
|
2a8a08dd8d | ||
|
|
cea6e2a355 |
No files matched your search
@@ -21,6 +21,10 @@ once it is done: that is our signal to take over. The automated check waits unti
|
|||||||
|
|
||||||
<!-- For example: ran `dotnet run verify`, ran the app locally, and tried out … -->
|
<!-- For example: ran `dotnet run verify`, ran the app locally, and tried out … -->
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
|
||||||
|
<!-- Does this change something users or IT departments notice, such as a new feature, a changed default, or a new setting for configuration plugins? Then describe what the wiki should say about it; the maintainers update the wiki when the release comes out. Otherwise, write "none". -->
|
||||||
|
|
||||||
## Required statements
|
## Required statements
|
||||||
|
|
||||||
<!-- Please tick each box yourself: these are statements you make personally, see "Licensing of your contribution" in CONTRIBUTING.md. Do not change their wording; the automated check compares it word by word. -->
|
<!-- Please tick each box yourself: these are statements you make personally, see "Licensing of your contribution" in CONTRIBUTING.md. Do not change their wording; the automated check compares it word by word. -->
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ env:
|
|||||||
SSIGN_VERSION: 0.1.7
|
SSIGN_VERSION: 0.1.7
|
||||||
SSIGN_SHA256: 4ee5389a74ddbeb67e18fe8c8b963734ca13de7b24beffbe430c3f427ecc5e40
|
SSIGN_SHA256: 4ee5389a74ddbeb67e18fe8c8b963734ca13de7b24beffbe430c3f427ecc5e40
|
||||||
|
|
||||||
|
# The Tauri CLI which bundles the app, exactly this version on every platform. Keep it at the minor
|
||||||
|
# version of the tauri crate in runtime/Cargo.toml. The version also names the cache of the CLI, so
|
||||||
|
# changing it installs the new CLI once on each platform and caches it from then on:
|
||||||
|
TAURI_CLI_VERSION: "2.12.4"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
determine_run_mode:
|
determine_run_mode:
|
||||||
name: Determine run mode
|
name: Determine run mode
|
||||||
@@ -334,7 +339,7 @@ jobs:
|
|||||||
-y \
|
-y \
|
||||||
flathub \
|
flathub \
|
||||||
"org.freedesktop.Sdk//${FLATPAK_FREEDESKTOP_VERSION}" \
|
"org.freedesktop.Sdk//${FLATPAK_FREEDESKTOP_VERSION}" \
|
||||||
"org.freedesktop.Sdk.Extension.dotnet9//${FLATPAK_FREEDESKTOP_VERSION}"
|
"org.freedesktop.Sdk.Extension.dotnet10//${FLATPAK_FREEDESKTOP_VERSION}"
|
||||||
|
|
||||||
tools_dir="$RUNNER_TEMP/flatpak-sync-tools"
|
tools_dir="$RUNNER_TEMP/flatpak-sync-tools"
|
||||||
mkdir -p "$tools_dir"
|
mkdir -p "$tools_dir"
|
||||||
@@ -777,13 +782,18 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
# Full debug information takes up most of the space which this job's Rust build fills, and with
|
||||||
|
# it most of its cache. Line tables alone keep file and line in the backtrace of a failing test:
|
||||||
|
env:
|
||||||
|
CARGO_PROFILE_DEV_DEBUG: line-tables-only
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Read the toolchain versions from the metadata
|
- name: Read the toolchain versions from the metadata
|
||||||
run: |
|
run: |
|
||||||
# The .NET SDK version. The format is '9.0.205 (commit 3e1383b780)',
|
# The .NET SDK version. The format is '10.0.114 (commit 0427b9abbe)',
|
||||||
# so we extract the version number alone:
|
# so we extract the version number alone:
|
||||||
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||||
|
|
||||||
@@ -800,8 +810,6 @@ jobs:
|
|||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||||
cache: true
|
|
||||||
cache-dependency-path: 'app/MindWork AI Studio/packages.lock.json'
|
|
||||||
|
|
||||||
- name: Pin the .NET SDK from the metadata
|
- name: Pin the .NET SDK from the metadata
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -815,23 +823,61 @@ jobs:
|
|||||||
used_dotnet_sdk_version=$(dotnet --version)
|
used_dotnet_sdk_version=$(dotnet --version)
|
||||||
echo ".NET SDK in use: '${used_dotnet_sdk_version}'"
|
echo ".NET SDK in use: '${used_dotnet_sdk_version}'"
|
||||||
|
|
||||||
- name: Cache Rust
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/git/db/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
runtime/target
|
|
||||||
|
|
||||||
key: verify-linux-x64-rust-${{ env.RUST_VERSION }}
|
|
||||||
|
|
||||||
- name: Setup Rust (stable)
|
- name: Setup Rust (stable)
|
||||||
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
|
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
|
# The cache below puts the version of every installed toolchain into its key, also of the stable
|
||||||
|
# one which comes with the runner image. That version changes with the image, independently of
|
||||||
|
# RUST_VERSION, and each change would start a new cache. Therefore, only our toolchain stays:
|
||||||
|
- name: Remove the other Rust toolchains
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for toolchain in $(rustup toolchain list --quiet | tr -d '\r'); do
|
||||||
|
if [[ "${toolchain}" != "${RUST_VERSION}-"* ]]; then
|
||||||
|
rustup toolchain uninstall "${toolchain}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rustup toolchain list
|
||||||
|
|
||||||
|
# Without a key of our own, the cache below would start from the latest cache of the same
|
||||||
|
# toolchain whenever the dependencies change. The action then keeps the old version of each
|
||||||
|
# updated crate next to its new one: a single update of Tauri let the caches grow by a fifth to
|
||||||
|
# a third. Also, GitHub dropped the new cache of this job right away, since build_main still
|
||||||
|
# read its old caches afterwards. Therefore, a hash of Cargo.toml and Cargo.lock goes into the
|
||||||
|
# key, so that only the very same dependencies restore a cache. The version of our own crate
|
||||||
|
# stays out of the hash, since every release changes it; in both files, it directly follows the
|
||||||
|
# name of the crate:
|
||||||
|
- name: Hash the Rust dependencies
|
||||||
|
id: rust_dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
hash=$(sed '/^name = "mindwork-ai-studio"/{n;d;}' runtime/Cargo.toml runtime/Cargo.lock | git hash-object --stdin)
|
||||||
|
echo "hash=${hash}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Hash of the Rust dependencies: '${hash}'"
|
||||||
|
|
||||||
|
# The key of this cache includes the version of rustc and the hash from above, so the cache has
|
||||||
|
# to come after both steps. A new toolchain or a change to the dependencies leads to a new
|
||||||
|
# cache, which starts empty. Before the cache is saved, the action removes everything which is
|
||||||
|
# no dependency, our own crate among it.
|
||||||
|
# Only pushes to main save a cache: pull requests and releases read it alone, so they cannot
|
||||||
|
# crowd out the other caches, since GitHub keeps at most 10 GB of them per repository.
|
||||||
|
#
|
||||||
|
# This job and build_main never share their caches, because they have no artifact in common:
|
||||||
|
# this job compiles for the tests and Clippy in the debug profile, build_main compiles in the
|
||||||
|
# release profile for one target triple each. Nothing here installs binaries with cargo, and
|
||||||
|
# the Tauri CLI of build_main has a cache of its own, so ~/.cargo/bin stays out:
|
||||||
|
- name: Cache Rust
|
||||||
|
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||||
|
with:
|
||||||
|
workspaces: runtime -> target
|
||||||
|
key: ${{ steps.rust_dependencies.outputs.hash }}
|
||||||
|
cache-bin: false
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Setup dependencies (Ubuntu-specific)
|
- name: Setup dependencies (Ubuntu-specific)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -1064,8 +1110,6 @@ jobs:
|
|||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||||
cache: true
|
|
||||||
cache-dependency-path: 'app/MindWork AI Studio/packages.lock.json'
|
|
||||||
|
|
||||||
- name: Pin the .NET SDK from the metadata
|
- name: Pin the .NET SDK from the metadata
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1250,28 +1294,50 @@ jobs:
|
|||||||
cd publish/dotnet
|
cd publish/dotnet
|
||||||
mv mindworkAIStudio.exe "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer${{ matrix.dotnet_name_postfix }}"
|
mv mindworkAIStudio.exe "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer${{ matrix.dotnet_name_postfix }}"
|
||||||
|
|
||||||
- name: Cache Rust
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/git/db/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
runtime/target
|
|
||||||
|
|
||||||
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
|
|
||||||
|
|
||||||
- name: Setup Rust (stable)
|
- name: Setup Rust (stable)
|
||||||
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
|
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.rust_target }}
|
targets: ${{ matrix.rust_target }}
|
||||||
|
|
||||||
|
# The same step of the verify job explains why the other toolchains have to go:
|
||||||
|
- name: Remove the other Rust toolchains
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for toolchain in $(rustup toolchain list --quiet | tr -d '\r'); do
|
||||||
|
if [[ "${toolchain}" != "${RUST_VERSION}-"* ]]; then
|
||||||
|
rustup toolchain uninstall "${toolchain}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rustup toolchain list
|
||||||
|
|
||||||
|
# The same step of the verify job explains why the cache needs this hash:
|
||||||
|
- name: Hash the Rust dependencies
|
||||||
|
id: rust_dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
hash=$(sed '/^name = "mindwork-ai-studio"/{n;d;}' runtime/Cargo.toml runtime/Cargo.lock | git hash-object --stdin)
|
||||||
|
echo "hash=${hash}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Hash of the Rust dependencies: '${hash}'"
|
||||||
|
|
||||||
|
# The same step of the verify job explains how this cache works, and why the two jobs keep
|
||||||
|
# separate caches. The action tells the caches apart by the job name and the runner. Both are
|
||||||
|
# the same for the two macOS builds, and for the two Windows builds as well, so the key holds
|
||||||
|
# the runtime identifier, which gives each platform a cache of its own, next to the hash:
|
||||||
|
- name: Cache Rust
|
||||||
|
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||||
|
with:
|
||||||
|
workspaces: runtime -> target
|
||||||
|
key: ${{ matrix.dotnet_runtime }}-${{ steps.rust_dependencies.outputs.hash }}
|
||||||
|
cache-bin: false
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Cache Tauri CLI
|
- name: Cache Tauri CLI
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo-tauri-cli
|
path: ~/.cargo-tauri-cli
|
||||||
key: tauri-cli-v2-${{ runner.os }}-${{ runner.arch }}
|
key: tauri-cli-${{ env.TAURI_CLI_VERSION }}-${{ 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')
|
||||||
@@ -1291,10 +1357,12 @@ jobs:
|
|||||||
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
|
||||||
export PATH="$HOME/.cargo-tauri-cli/bin:$PATH"
|
export PATH="$HOME/.cargo-tauri-cli/bin:$PATH"
|
||||||
|
|
||||||
if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then
|
# The cached CLI counts only when it is exactly the version of TAURI_CLI_VERSION; otherwise,
|
||||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$HOME/.cargo-tauri-cli"
|
# that version gets installed:
|
||||||
|
if ! cargo tauri --version 2>/dev/null | grep -Fxq "tauri-cli ${TAURI_CLI_VERSION}"; then
|
||||||
|
cargo install tauri-cli --version "=${TAURI_CLI_VERSION}" --locked --force --root "$HOME/.cargo-tauri-cli"
|
||||||
else
|
else
|
||||||
echo "Tauri CLI v2 is already installed"
|
echo "Tauri CLI ${TAURI_CLI_VERSION} is already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Tauri (Windows)
|
- name: Setup Tauri (Windows)
|
||||||
@@ -1303,11 +1371,13 @@ jobs:
|
|||||||
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
|
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
|
||||||
$env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH"
|
$env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH"
|
||||||
|
|
||||||
|
# The cached CLI counts only when it is exactly the version of TAURI_CLI_VERSION; otherwise,
|
||||||
|
# that version gets installed:
|
||||||
$tauriVersion = cargo tauri --version 2>$null
|
$tauriVersion = cargo tauri --version 2>$null
|
||||||
if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') {
|
if ($tauriVersion -ne "tauri-cli $($env:TAURI_CLI_VERSION)") {
|
||||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli"
|
cargo install tauri-cli --version "=$($env:TAURI_CLI_VERSION)" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli"
|
||||||
} else {
|
} else {
|
||||||
Write-Output "Tauri CLI v2 is already installed"
|
Write-Output "Tauri CLI $($env:TAURI_CLI_VERSION) is already installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Install ssign (Windows)
|
- name: Install ssign (Windows)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Ignore any startup.env file:
|
# Ignore any startup.env file:
|
||||||
startup.env
|
startup.env
|
||||||
|
|
||||||
|
# Ignore the boot link of development builds:
|
||||||
|
dev-boot-link.txt
|
||||||
|
|
||||||
# Ignore pdfium library:
|
# Ignore pdfium library:
|
||||||
libpdfium.dylib
|
libpdfium.dylib
|
||||||
libpdfium.so
|
libpdfium.so
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
|
|||||||
|
|
||||||
**Key Architecture Points:**
|
**Key Architecture Points:**
|
||||||
- **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer
|
- **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer
|
||||||
- **App:** .NET 9 Blazor Server application providing the UI and core functionality
|
- **App:** .NET 10 Blazor Server application providing the UI and core functionality
|
||||||
- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
|
- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
|
||||||
- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
|
- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
|
||||||
- **Plugin System:** Lua-based plugin system for language packs, configuration, assistants, and model knowledge
|
- **Plugin System:** Lua-based plugin system for language packs, configuration, assistants, and model knowledge
|
||||||
@@ -89,7 +89,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
|
|||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- .NET 9 SDK
|
- .NET 10 SDK
|
||||||
- Rust toolchain (stable)
|
- Rust toolchain (stable)
|
||||||
- Tauri v2 CLI
|
- Tauri v2 CLI
|
||||||
- Tauri prerequisites (platform-specific dependencies)
|
- Tauri prerequisites (platform-specific dependencies)
|
||||||
@@ -114,7 +114,7 @@ failures (for reference: https://github.com/openai/codex/issues/4915). This appl
|
|||||||
Instead, use the JetBrains IDE MCP servers. They execute in the IDE process, which runs outside the
|
Instead, use the JetBrains IDE MCP servers. They execute in the IDE process, which runs outside the
|
||||||
agent sandbox:
|
agent sandbox:
|
||||||
|
|
||||||
- `rider` for the .NET solution at `app/MindWork AI Studio.sln`
|
- `rider` for the .NET solution at `app/MindWork AI Studio.slnx`
|
||||||
- `rustrover` for the Rust runtime at `runtime/`
|
- `rustrover` for the Rust runtime at `runtime/`
|
||||||
|
|
||||||
Pass the `rootFolder` parameter on every call, e.g. the absolute path of the `app` directory for Rider
|
Pass the `rootFolder` parameter on every call, e.g. the absolute path of the `app` directory for Rider
|
||||||
@@ -169,7 +169,7 @@ again yourself when `metadata.txt` changes during the session, e.g., after a mer
|
|||||||
**Building and testing** happen in the shell:
|
**Building and testing** happen in the shell:
|
||||||
|
|
||||||
- The quality gate, the same as in the pipeline: `cd app/Build && dotnet run verify`. It compiles both
|
- The quality gate, the same as in the pipeline: `cd app/Build && dotnet run verify`. It compiles both
|
||||||
the .NET app and the Rust runtime on its way. Narrower checks: `dotnet build "app/MindWork AI Studio.sln"`,
|
the .NET app and the Rust runtime on its way. Narrower checks: `dotnet build "app/MindWork AI Studio.slnx"`,
|
||||||
`dotnet test` in `app/Tests`, and `cargo test` or `cargo clippy --all-targets -- -D warnings` in `runtime/`.
|
`dotnet test` in `app/Tests`, and `cargo test` or `cargo clippy --all-targets -- -D warnings` in `runtime/`.
|
||||||
- A cold build of the Rust runtime takes several minutes: run the gate and cargo in the background, or
|
- A cold build of the Rust runtime takes several minutes: run the gate and cargo in the background, or
|
||||||
give them the longest timeout available.
|
give them the longest timeout available.
|
||||||
@@ -199,6 +199,23 @@ again yourself when `metadata.txt` changes during the session, e.g., after a mer
|
|||||||
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
||||||
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
|
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
|
||||||
|
The documentation for users and IT departments lives in the wiki, https://github.com/MindWorkAI/AI-Studio/wiki:
|
||||||
|
the user guide in US English and German, the Enterprise IT guides, and pages for developers. The files in
|
||||||
|
`documentation/` are meant for contributors.
|
||||||
|
|
||||||
|
The maintainers write the wiki, and they update it when a release is published, not before: a page which
|
||||||
|
describes a version nobody can install yet misleads users and IT departments. Contributors need neither
|
||||||
|
access to the wiki nor a copy of it.
|
||||||
|
|
||||||
|
- A change which users or IT departments notice, such as a new assistant, a changed default, or a new
|
||||||
|
capability of configuration plugins, gets the section "Wiki" in the description of its pull request:
|
||||||
|
what changes for users or IT departments, which settings or configuration keys are new or changed, and
|
||||||
|
which pages of the wiki it concerns, if you know them. Write "none" when nobody notices the change.
|
||||||
|
- Leave the wiki itself to the maintainers. They carry these sections into it for the release, see
|
||||||
|
"Release Process" in `app/Build/AGENTS.md`.
|
||||||
|
|
||||||
## Changelogs
|
## Changelogs
|
||||||
Changelogs are located in `app/MindWork AI Studio/wwwroot/changelog/` with filenames `vX.Y.Z.md`. These changelogs are meant to be for normal end-users
|
Changelogs are located in `app/MindWork AI Studio/wwwroot/changelog/` with filenames `vX.Y.Z.md`. These changelogs are meant to be for normal end-users
|
||||||
and should be written in a non-technical way, focusing on user-facing changes and improvements. Additionally, changes made regarding the plugin system
|
and should be written in a non-technical way, focusing on user-facing changes and improvements. Additionally, changes made regarding the plugin system
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
|
|||||||
</h3>
|
</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
|
- v26.10.1: Added your mailboxes as a data source in the prototype preview, so the AI can search and read your e-mails from any IMAP provider; added backups of all your data in a single file, for example, to move to a new computer; added the SWOT Analysis Assistant, and the Slide Assistant now creates PowerPoint files in the design of your own template; added a "Send to" menu to every chat message, so you can continue with an assistant or in a new chat; you can now import providers, chat templates, and more that a colleague exported, and chat templates can bring their own provider, profile, and workspace; added support for Mistral Large 4; AI Studio is now signed for macOS and Windows, so macOS opens it like any other app and Windows shows who published it; expanded enterprise IT support with prohibited providers, a prompt for your personal API key on the start page, and organization-wide management of mailboxes, backups, and imports; and many, many more features, changes, and fixes.
|
||||||
- v26.9.1: Added local RAG as a beta feature, so the AI can answer from the documents in your own folders and files; added Semantic Search, so a model searches your data sources itself whenever a question calls for it; added tools that AI models can use on their own, starting with Web Search, Read Web Page, and Search Confluence, available in chats, assistants, batch runs, document analysis policies, chat templates, and assistant plugins; added a token count below the message field and knowledge about each model's context window; added support for OpenAI's GPT-6 Astra, Sol, and Luna, and model plugins for organizations that run their own models; you can now copy an entire chat or roll it back to an earlier answer; and expanded enterprise IT support with organization-wide management of tools and of how data sources are searched.
|
- v26.9.1: Added local RAG as a beta feature, so the AI can answer from the documents in your own folders and files; added Semantic Search, so a model searches your data sources itself whenever a question calls for it; added tools that AI models can use on their own, starting with Web Search, Read Web Page, and Search Confluence, available in chats, assistants, batch runs, document analysis policies, chat templates, and assistant plugins; added a token count below the message field and knowledge about each model's context window; added support for OpenAI's GPT-6 Astra, Sol, and Luna, and model plugins for organizations that run their own models; you can now copy an entire chat or roll it back to an earlier answer; and expanded enterprise IT support with organization-wide management of tools and of how data sources are searched.
|
||||||
- v26.8.2: Added protection against prompt injection, so hidden instructions in documents, web pages, and retrieved content are removed before a model reads them; added IONOS' AI Model Hub and LiteLLM as providers, along with speech-to-text and embeddings for Hugging Face, Helmholtz Blablador, GroqCloud, and GWDG SAIA; added knowledge about the latest AI models like Claude Opus 5 & Sonnet 5, Gemini 3.6 & 3.7, and Grok 4, and corrected the abilities shown for many models across all providers; added provider logos throughout the app; AI answers can now be exported as Word, OpenDocument, LaTeX, Markdown, or a webpage, with tables saved separately as spreadsheets; greatly reduced memory usage when working with large documents; and expanded enterprise rollouts to cover every kind of plugin.
|
- v26.8.2: Added protection against prompt injection, so hidden instructions in documents, web pages, and retrieved content are removed before a model reads them; added IONOS' AI Model Hub and LiteLLM as providers, along with speech-to-text and embeddings for Hugging Face, Helmholtz Blablador, GroqCloud, and GWDG SAIA; added knowledge about the latest AI models like Claude Opus 5 & Sonnet 5, Gemini 3.6 & 3.7, and Grok 4, and corrected the abilities shown for many models across all providers; added provider logos throughout the app; AI answers can now be exported as Word, OpenDocument, LaTeX, Markdown, or a webpage, with tables saved separately as spreadsheets; greatly reduced memory usage when working with large documents; and expanded enterprise rollouts to cover every kind of plugin.
|
||||||
- v26.8.1: Added Hetzner's EU-hosted inference API as a provider, along with support for the latest open-source models like DeepSeek V4, GLM 5.2, Kimi K2.7 & K3, and Qwen 3.6 & 3.8; added the Visual Briefing Assistant as a preview feature and the Batch Processing Assistant to process entire folders of documents in one run; you can now share, import, and delete plugins; greatly improved working with files, including much better Word and OpenDocument support; expanded enterprise IT support with configuration priorities, test configurations before rollout, and policies for plugin sharing and imports.
|
- v26.8.1: Added Hetzner's EU-hosted inference API as a provider, along with support for the latest open-source models like DeepSeek V4, GLM 5.2, Kimi K2.7 & K3, and Qwen 3.6 & 3.8; added the Visual Briefing Assistant as a preview feature and the Batch Processing Assistant to process entire folders of documents in one run; you can now share, import, and delete plugins; greatly improved working with files, including much better Word and OpenDocument support; expanded enterprise IT support with configuration priorities, test configurations before rollout, and policies for plugin sharing and imports.
|
||||||
@@ -22,7 +23,6 @@ Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
|
|||||||
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
|
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
|
||||||
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
|
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
|
||||||
- v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection.
|
- v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection.
|
||||||
- v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
|
|||||||
|
|
||||||
MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provides a unified user interface for interaction with Large Language Models (LLM). AI Studio also offers so-called assistants, where prompting is not necessary. You can think of AI Studio like an email program: you bring your own API key for the LLM of your choice and can then use these AI systems with AI Studio. Whether you want to use Google Gemini, OpenAI GPT-6, or even your own local AI models.
|
MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provides a unified user interface for interaction with Large Language Models (LLM). AI Studio also offers so-called assistants, where prompting is not necessary. You can think of AI Studio like an email program: you bring your own API key for the LLM of your choice and can then use these AI systems with AI Studio. Whether you want to use Google Gemini, OpenAI GPT-6, or even your own local AI models.
|
||||||
|
|
||||||
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](https://github.com/MindWorkAI/AI-Studio/wiki/Installing-AI-Studio).
|
||||||
|
|
||||||
**Key advantages:**
|
**Key advantages:**
|
||||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||||
@@ -59,15 +59,16 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
|||||||
- For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
|
- For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
|
||||||
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
||||||
- **Tools**: AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work.
|
- **Tools**: AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work.
|
||||||
|
- **Your own data**: The AI can answer from your own documents. Add folders and files from your computer, or your organization's data through [ERI](https://github.com/MindWorkAI/ERI), as data sources, and the AI searches them whenever a question calls for it and names the passages it used. AI Studio keeps the index on your computer, and you choose the embedding provider, including one you host yourself. As a prototype, the AI can also search and read your mailboxes. Both are preview features, which you enable in the app settings first.
|
||||||
- **Voice input**: Dictate your messages instead of typing them, and have your recordings as well as audio and video files transcribed. You choose the transcription provider, including one you host yourself.
|
- **Voice input**: Dictate your messages instead of typing them, and have your recordings as well as audio and video files transcribed. You choose the transcription provider, including one you host yourself.
|
||||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
||||||
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
||||||
- **Privacy**: You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
|
- **Privacy**: You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
|
||||||
- **Enterprise IT**: Your IT department can manage AI Studio centrally: roll out providers, assistants, chat templates, and tools, set defaults, or lock settings for the whole organization. [Here's how it works.](documentation/Enterprise%20IT.md)
|
- **Enterprise IT**: Your IT department can manage AI Studio centrally: roll out providers, assistants, chat templates, and tools, set defaults, or lock settings for the whole organization. [Here's how it works.](https://github.com/MindWorkAI/AI-Studio/wiki/Enterprise-IT)
|
||||||
- **Flexibility**: Choose the provider and model best suited for your current task.
|
- **Flexibility**: Choose the provider and model best suited for your current task.
|
||||||
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
|
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
|
||||||
|
|
||||||
## **Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
## **Ready to get started 🤩?** [Download the appropriate setup for your operating system here](https://github.com/MindWorkAI/AI-Studio/wiki/Installing-AI-Studio).
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
@@ -99,14 +100,14 @@ Your support, whether big or small, keeps the wheels turning and is deeply appre
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
|
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
|
||||||
- **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
|
- **Agent mode:** You'll hand larger tasks to the AI. It plans the steps, asks for your approval, and works through them with the tools you allow, e.g., across your documents, mailboxes, and the web.
|
||||||
- **Integration of enterprise data:** It will soon be possible to integrate data from the corporate network using a specified interface ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). This will likely require development work by the organization in question.
|
- **Your own tools:** Developers and IT departments will be able to define their own tools as plugins, so the AI can use, for example, an internal database or a specialist application of your organization.
|
||||||
|
- **Notes and tasks:** You'll keep your notes and manage your tasks in AI Studio, together with the AI.
|
||||||
- **Useful assistants:** We'll develop more assistants for everyday tasks.
|
- **Useful assistants:** We'll develop more assistants for everyday tasks.
|
||||||
- **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
|
- **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
|
||||||
- **Specific requirements:** Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas.
|
- **Voice control:** You can already dictate your messages. Next, we want the AI to answer with its voice (text-to-speech), and later on, to hold a natural conversation with you.
|
||||||
- **Voice control:** You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
|
|
||||||
- **Content creation:** There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers.
|
- **Content creation:** There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers.
|
||||||
- **Email monitoring:** You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
|
- **Email monitoring:** You can already connect your mailboxes and let the AI search them in your chats, as a prototype. Next, the AI will watch your inboxes and notify you of important events, e.g., mails from certain senders or on certain topics.
|
||||||
- **Browser usage:** We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser.
|
- **Browser usage:** We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser.
|
||||||
|
|
||||||
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.
|
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.
|
||||||
@@ -150,7 +151,7 @@ Did you find a security vulnerability? Please follow our [security policy](SECUR
|
|||||||
</h2>
|
</h2>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
Do you want to manage AI Studio centrally from your IT department? Yes, that’s possible. [Here’s how it works.](documentation/Enterprise%20IT.md)
|
Do you want to manage AI Studio centrally from your IT department? Yes, that’s possible. [Here’s how it works.](https://github.com/MindWorkAI/AI-Studio/wiki/Enterprise-IT)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="[2] Start .NET Server" type="DotNetProject" factoryName=".NET Project">
|
<configuration default="false" name="[2] Start .NET Server" type="DotNetProject" factoryName=".NET Project">
|
||||||
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net9.0/osx-arm64/mindworkAIStudio" />
|
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net10.0/osx-arm64/mindworkAIStudio" />
|
||||||
<option name="PROGRAM_PARAMETERS" value="" />
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net9.0" />
|
<option name="PROJECT_TFM" value="net10.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
|
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
|
||||||
|
|||||||
+7
-4
@@ -35,10 +35,12 @@ Key structure:
|
|||||||
- **Sending content on** - Content which leaves one place for another, such as the result of an assistant sent to another assistant or a text sent into the chat, travels as a `TextLaunchPayload` through `AssistantLaunchService`, together with the restrictions of where it was written (`ChatRestrictions`, see `ChatThread.CaptureRestrictions`). Never hand over a bare string: the destination would let any provider or tool see content which its source kept from them. Build the payload with `AssistantLaunchService.CreateTextPayloadAsync`, which filters what an AI wrote for prompt injections before it reaches the next model as a message of the user. An assistant which allows sending to it overrides `AssistantBase.ApplyTextLaunchPayload`, and `SendToReceiverTests` checks that every one does; the restrictions themselves are applied centrally in `AssistantBase`.
|
- **Sending content on** - Content which leaves one place for another, such as the result of an assistant sent to another assistant or a text sent into the chat, travels as a `TextLaunchPayload` through `AssistantLaunchService`, together with the restrictions of where it was written (`ChatRestrictions`, see `ChatThread.CaptureRestrictions`). Never hand over a bare string: the destination would let any provider or tool see content which its source kept from them. Build the payload with `AssistantLaunchService.CreateTextPayloadAsync`, which filters what an AI wrote for prompt injections before it reaches the next model as a message of the user. An assistant which allows sending to it overrides `AssistantBase.ApplyTextLaunchPayload`, and `SendToReceiverTests` checks that every one does; the restrictions themselves are applied centrally in `AssistantBase`.
|
||||||
- **JSON in model answers** - Read the JSON part of a model answer with `ModelAnswerJson`, never with a scanner of your own. It skips the prose and code fences around the JSON but checks nothing, so broken JSON reaches the deserializer unchanged and its message can go back to the model.
|
- **JSON in model answers** - Read the JSON part of a model answer with `ModelAnswerJson`, never with a scanner of your own. It skips the prose and code fences around the JSON but checks nothing, so broken JSON reaches the deserializer unchanged and its message can go back to the model.
|
||||||
- **Long menus** - MudBlazor keeps a `MudSelect` inside the window by itself, but never a `MudMenu`: a menu taller than the window runs past its edge, and its lower entries cannot be reached. Give every `MudMenu` whose entries come from data, such as the assistants or the profiles, `MaxHeight="@MenuDefaults.MAX_HEIGHT"`, so that it scrolls.
|
- **Long menus** - MudBlazor keeps a `MudSelect` inside the window by itself, but never a `MudMenu`: a menu taller than the window runs past its edge, and its lower entries cannot be reached. Give every `MudMenu` whose entries come from data, such as the assistants or the profiles, `MaxHeight="@MenuDefaults.MAX_HEIGHT"`, so that it scrolls.
|
||||||
- **Growing popovers** - MudBlazor places an open popover only when it opens, when the window changes, and when its component renders again. Content which unfolds later, such as a `MudCollapse` or an `ExpansionPanel` inside a `MudPopover`, would therefore grow out of the window. `wwwroot/app.js` places every open popover again whenever its size changes, so such content needs no code of its own. This relies on `window.mudpopoverHelper.placePopoverByNode`, which is internal to MudBlazor: check that it still exists with every new MudBlazor version.
|
- **Growing popovers** - Since version 9, MudBlazor places an open popover again whenever its content changes size. Content which unfolds later, such as a `MudCollapse` or an `ExpansionPanel` inside a `MudPopover`, therefore stays inside the window and needs no code of its own.
|
||||||
- **Encryption** - Initialized before Rust service is marked ready
|
- **Encryption** - Initialized before Rust service is marked ready
|
||||||
|
- **Access to the app server** - Only the app window gets into the Blazor server. `AppAccessMiddleware` runs first in `Program.cs`, right after the host filter, and refuses every request without the session cookie of the window or with an origin other than the app's own. Add no exception for any path, not even for static files: `AppAccessPipelineTests` checks the order in `Program.cs`. Since nothing but the window holds the cookie, the app never sends a request to its own server: read files below `wwwroot/` through `WebRootFiles`. The system browser gets no cookie either, so never open a URL of the app server there; offer a save dialog through `RustService` instead, as the ERI Server assistant does for its specification. `runtime/AGENTS.md` describes the runtime side.
|
||||||
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
||||||
- **Static files and the browser cache** - When the developer starts the app from Rider, the embedded browser of Tauri keeps the static files under `wwwroot/`, such as `app.css` or a script, in its cache. The app then still shows the old file, and a change seems not to work. Whenever you changed such a file, tell the developer to clear that cache through the developer tools of the embedded browser before testing. Users of a release are not affected.
|
- **Boot link** - Once the .NET server is ready, a development build writes `dev-boot-link.txt` into the repository root. Opening that link hands another browser the session cookie. Open it only when the developer asks for it: the development build shows the data of the installed app.
|
||||||
|
- **Static files and the browser cache** - When the developer starts the app from Rider, the embedded browser of Tauri keeps the static files under `wwwroot/`, such as `app.css` or a script, in its cache. The app then still shows the old file, and a change seems not to work. Whenever you changed such a file, tell the developer to clear that cache through the developer tools of the embedded browser before testing, and only the cache: the website data hold the session cookie, and without it the window shows "This window cannot access AI Studio. Please restart AI Studio." until the .NET server starts again. Users of a release are not affected.
|
||||||
- **Production environment** - Runtime launches .NET sidecar with environment variables
|
- **Production environment** - Runtime launches .NET sidecar with environment variables
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
@@ -154,12 +156,13 @@ and the rules which are easy to break.
|
|||||||
## Enterprise IT Support
|
## Enterprise IT Support
|
||||||
|
|
||||||
AI Studio supports centralized configuration for enterprise environments:
|
AI Studio supports centralized configuration for enterprise environments:
|
||||||
- **Registry (Windows)** or **environment variables** (all platforms) specify configuration server URL and ID
|
- **Registry (Windows)**, **policy files**, or **environment variables** (all platforms) specify configuration server URL and ID
|
||||||
- Configuration downloaded as ZIP containing Lua plugin
|
- Configuration downloaded as ZIP containing Lua plugin
|
||||||
- Checks for updates every ~16 minutes via ETag
|
- Checks for updates every ~16 minutes via ETag
|
||||||
- Allows IT departments to pre-configure providers, settings, and chat templates
|
- Allows IT departments to pre-configure providers, settings, and chat templates
|
||||||
|
|
||||||
**Documentation:** `documentation/Enterprise IT.md`
|
**Documentation:** the Enterprise IT section of the wiki, https://github.com/MindWorkAI/AI-Studio/wiki/Enterprise-IT;
|
||||||
|
see "Wiki" in the root `AGENTS.md`.
|
||||||
|
|
||||||
## Provider Confidence System
|
## Provider Confidence System
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ changelog entries.
|
|||||||
6. After PR merge, maintainer creates git tag: `vX.Y.Z`
|
6. After PR merge, maintainer creates git tag: `vX.Y.Z`
|
||||||
7. GitHub Actions builds release binaries for all platforms
|
7. GitHub Actions builds release binaries for all platforms
|
||||||
8. Binaries uploaded to GitHub Releases
|
8. Binaries uploaded to GitHub Releases
|
||||||
|
9. Once the release is published, update the wiki (`MindWorkAI/AI-Studio.wiki`): carry the "Wiki" sections
|
||||||
|
of the pull requests merged since the last release into it, mark what needs the new version with
|
||||||
|
"Requires AI Studio vX.Y.Z or newer", and raise the version in `_Footer.md` and `de/_Footer.md`. The
|
||||||
|
pull requests are listed by the command of step 2 with `--json number,title,body` and without `--jq`.
|
||||||
|
The release command reminds you of this step when it is done.
|
||||||
|
|
||||||
## Crediting contributors
|
## Crediting contributors
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>Build</RootNamespace>
|
<RootNamespace>Build</RootNamespace>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
|
|
||||||
<!-- Pins Cocona's transitive Microsoft.Extensions.Hosting 6.0.0, which pulled in the vulnerable System.Text.Json 6.0.0 (GHSA-8g4q-xg66-9fp4) -->
|
<!-- Pins Cocona's transitive Microsoft.Extensions.Hosting 6.0.0, which pulled in the vulnerable System.Text.Json 6.0.0 (GHSA-8g4q-xg66-9fp4) -->
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.22" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
await this.PerformPrepare(action, true, version);
|
await this.PerformPrepare(action, true, version);
|
||||||
|
|
||||||
await this.BuildPreparedRelease(offline);
|
await this.BuildPreparedRelease(offline);
|
||||||
|
|
||||||
|
// The wiki is a repository of its own, and nothing in this build touches it.
|
||||||
|
// It describes what users can install, so it changes once the release is published:
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
Console.WriteLine("- Warning: Remember the wiki, https://github.com/MindWorkAI/AI-Studio/wiki");
|
||||||
|
Console.WriteLine(" Once the release is published, carry the \"Wiki\" sections of the pull requests merged since the");
|
||||||
|
Console.WriteLine(" last release into the wiki, and raise the version in _Footer.md and de/_Footer.md. See step 9 of");
|
||||||
|
Console.WriteLine(" \"Release Process\" in app/Build/AGENTS.md.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("rebuild-release", Description = "Prepare & build a new build of the current release")]
|
[Command("rebuild-release", Description = "Prepare & build a new build of the current release")]
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace Build.Commands;
|
|||||||
|
|
||||||
public sealed class UpdateWebAssetsCommand
|
public sealed class UpdateWebAssetsCommand
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The pre-compressed copies and the source maps of the web assets. The app serves its web assets
|
||||||
|
/// from the files embedded into it and uses neither, so they would only enlarge it.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashSet<string> SKIPPED_EXTENSIONS = new(StringComparer.OrdinalIgnoreCase) { ".br", ".gz", ".map" };
|
||||||
|
|
||||||
[Command("update-web", Description = "Update web assets")]
|
[Command("update-web", Description = "Update web assets")]
|
||||||
public void UpdateWebAssets()
|
public void UpdateWebAssets()
|
||||||
{
|
{
|
||||||
@@ -29,26 +35,31 @@ public sealed class UpdateWebAssetsCommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The destination also holds files of our own, such as the code editor. Hence, we replace
|
||||||
|
// only the folder of each package, so that files a package no longer ships disappear:
|
||||||
|
//
|
||||||
var destinationPath = Path.Join(cwd, "wwwroot", "system");
|
var destinationPath = Path.Join(cwd, "wwwroot", "system");
|
||||||
if(Directory.Exists(destinationPath))
|
|
||||||
Directory.Delete(destinationPath, true);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
|
||||||
|
|
||||||
var sourcePaths = Directory.EnumerateFiles(contentPath, "*", SearchOption.AllDirectories);
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
foreach(var sourcePath in sourcePaths)
|
foreach (var packagePath in Directory.EnumerateDirectories(contentPath))
|
||||||
{
|
{
|
||||||
counter++;
|
var packageDestinationPath = Path.Join(destinationPath, Path.GetFileName(packagePath));
|
||||||
var relativePath = sourcePath
|
if (Directory.Exists(packageDestinationPath))
|
||||||
.Replace(contentPath, "")
|
Directory.Delete(packageDestinationPath, true);
|
||||||
.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
||||||
var targetPath = Path.Join(cwd, "wwwroot", "system", relativePath);
|
foreach (var sourcePath in Directory.EnumerateFiles(packagePath, "*", SearchOption.AllDirectories))
|
||||||
var targetDirectory = Path.GetDirectoryName(targetPath);
|
{
|
||||||
if (targetDirectory != null)
|
if (SKIPPED_EXTENSIONS.Contains(Path.GetExtension(sourcePath)))
|
||||||
Directory.CreateDirectory(targetDirectory);
|
continue;
|
||||||
|
|
||||||
File.Copy(sourcePath, targetPath, true);
|
counter++;
|
||||||
|
var targetPath = Path.Join(packageDestinationPath, Path.GetRelativePath(packagePath, sourcePath));
|
||||||
|
var targetDirectory = Path.GetDirectoryName(targetPath);
|
||||||
|
if (targetDirectory != null)
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
|
||||||
|
File.Copy(sourcePath, targetPath, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
|
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Build.Tools;
|
|||||||
|
|
||||||
public static class Environment
|
public static class Environment
|
||||||
{
|
{
|
||||||
public const string DOTNET_VERSION = "net9.0";
|
public const string DOTNET_VERSION = "net10.0";
|
||||||
public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE";
|
public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE";
|
||||||
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
|
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build Script.csproj", "{447A5590-68E1-4EF8-9451-A41AF5FBE571}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratedMappings", "SourceGeneratedMappings\SourceGeneratedMappings.csproj", "{4D7141D5-9C22-4D85-B748-290D15FF484C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CD46329B-D135-4594-9A70-55D3480F8FEE}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimSmokeTest", "TrimSmokeTest\TrimSmokeTest.csproj", "{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Build/Build Script.csproj" />
|
||||||
|
<Project Path="MindWork AI Studio/MindWork AI Studio.csproj" />
|
||||||
|
<Project Path="SharedTools/SharedTools.csproj" />
|
||||||
|
<Project Path="SourceCodeRules/SourceCodeRules/SourceCodeRules.csproj" />
|
||||||
|
<Project Path="SourceGeneratedMappings/SourceGeneratedMappings.csproj" />
|
||||||
|
<Project Path="Tests/Tests.csproj" />
|
||||||
|
<Project Path="TrimSmokeTest/TrimSmokeTest.csproj" />
|
||||||
|
</Solution>
|
||||||
File renamed without changes.
@@ -13,11 +13,11 @@ public sealed class AssistantAuditFinding
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning restore MWAIS0005
|
#pragma warning restore MWAIS0005
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public AssistantAuditLevel Severity { get; private init; } = AssistantAuditLevel.UNKNOWN;
|
public AssistantAuditLevel Severity { get; private set; } = AssistantAuditLevel.UNKNOWN;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or initializes the JSON-facing severity label used by the audit model response.
|
/// Gets or sets the JSON-facing severity label used by the audit model response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("severity")]
|
[JsonPropertyName("severity")]
|
||||||
public string SeverityText
|
public string SeverityText
|
||||||
@@ -30,7 +30,7 @@ public sealed class AssistantAuditFinding
|
|||||||
_ => "unknown",
|
_ => "unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
init => this.Severity = value.Trim().ToLowerInvariant() switch
|
set => this.Severity = value.Trim().ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"critical" => AssistantAuditLevel.DANGEROUS,
|
"critical" => AssistantAuditLevel.DANGEROUS,
|
||||||
"medium" => AssistantAuditLevel.CAUTION,
|
"medium" => AssistantAuditLevel.CAUTION,
|
||||||
@@ -39,7 +39,7 @@ public sealed class AssistantAuditFinding
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Category { get; init; } = string.Empty;
|
public string Category { get; set; } = string.Empty;
|
||||||
public string Location { get; init; } = string.Empty;
|
public string Location { get; set; } = string.Empty;
|
||||||
public string Description { get; init; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -168,8 +168,8 @@ public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
|
|||||||
private string inputContent = string.Empty;
|
private string inputContent = string.Empty;
|
||||||
private string inputDuration = string.Empty;
|
private string inputDuration = string.Empty;
|
||||||
private string inputStartTime = string.Empty;
|
private string inputStartTime = string.Empty;
|
||||||
private IEnumerable<string> selectedFoci = new HashSet<string>();
|
private IReadOnlyCollection<string> selectedFoci = new HashSet<string>();
|
||||||
private IEnumerable<string> justBriefly = new HashSet<string>();
|
private IReadOnlyCollection<string> justBriefly = new HashSet<string>();
|
||||||
private string inputObjective = string.Empty;
|
private string inputObjective = string.Empty;
|
||||||
private string inputModerator = string.Empty;
|
private string inputModerator = string.Empty;
|
||||||
private CommonLanguages selectedTargetLanguage;
|
private CommonLanguages selectedTargetLanguage;
|
||||||
@@ -411,7 +411,7 @@ public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
|
|||||||
|
|
||||||
private async Task CreateAgenda()
|
private async Task CreateAgenda()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
|
|||||||
private string PromptSocialActivity()
|
private string PromptSocialActivity()
|
||||||
{
|
{
|
||||||
if (this.doingSocialActivity)
|
if (this.doingSocialActivity)
|
||||||
return "- The participants will engage in a social activity after the meeting or seminar. This can be a team-building exercise, a sightseeing tour, or a visit to a local attraction. Please make a recommendation for an activity at '{this.inputLocation}'.";
|
return $"- The participants will engage in a social activity after the meeting or seminar. This can be a team-building exercise, a sightseeing tour, or a visit to a local attraction. Please make a recommendation for an activity at '{this.inputLocation}'.";
|
||||||
|
|
||||||
return "- The participants will not engage in a social activity after the meeting or seminar.";
|
return "- The participants will not engage in a social activity after the meeting or seminar.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,8 +307,8 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
{
|
{
|
||||||
// Reset the validation when not editing and on the first render.
|
// Reset the validation when not editing and on the first render.
|
||||||
// We don't want to show validation errors when the user opens the dialog.
|
// We don't want to show validation errors when the user opens the dialog.
|
||||||
if(firstRender)
|
if(firstRender && this.Form is not null)
|
||||||
this.Form?.ResetValidation();
|
await this.Form.ResetValidationAsync();
|
||||||
|
|
||||||
if (this.pendingRenderedAssistantSessionSnapshot is { } snapshot)
|
if (this.pendingRenderedAssistantSessionSnapshot is { } snapshot)
|
||||||
{
|
{
|
||||||
@@ -434,7 +434,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
private async Task ReportProviderIssueAsync()
|
private async Task ReportProviderIssueAsync()
|
||||||
{
|
{
|
||||||
if (this.Form is not null)
|
if (this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
|
|
||||||
if (this.ValidatingProvider(this.ProviderSettings) is { } issue && !this.InputIssues.Contains(issue))
|
if (this.ValidatingProvider(this.ProviderSettings) is { } issue && !this.InputIssues.Contains(issue))
|
||||||
this.AddInputIssue(issue);
|
this.AddInputIssue(issue);
|
||||||
@@ -962,9 +962,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
this.InputIsValid = false;
|
this.InputIsValid = false;
|
||||||
this.InputIssues = [];
|
this.InputIssues = [];
|
||||||
|
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
|
|
||||||
await this.RefreshAssistantUIAsync();
|
await this.RefreshAssistantUIAsync();
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetResetColor() => this.SettingsManager.IsDarkMode switch
|
private string GetResetColor() => this.SettingsManager.IsDarkMode switch
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<FilePatternSelection>
|
<FilePatternSelection>
|
||||||
<AllowedContent>
|
<AllowedContent>
|
||||||
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Files to include")" AdornmentIcon="@Icons.Material.Filled.Restore" Adornment="Adornment.End" OnAdornmentClick="@this.RestoreDefaultFilePatterns" AdornmentAriaLabel="@T("Restore default patterns")" Lines="1" AutoGrow="@true" MaxLines="@FilePatternSelection.MAX_FIELD_LINES" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Files to include")" AdornmentIcon="@Icons.Material.Filled.Restore" Adornment="Adornment.End" OnAdornmentClick="@this.RestoreDefaultFilePatterns" AdornmentAriaLabel="@T("Restore default patterns")" Lines="1" Sizing="InputSizing.Auto" MaxLines="@FilePatternSelection.MAX_FIELD_LINES" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
</AllowedContent>
|
</AllowedContent>
|
||||||
</FilePatternSelection>
|
</FilePatternSelection>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
{
|
{
|
||||||
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" Disabled="@this.isProcessingBatch"/>
|
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" Disabled="@this.isProcessingBatch"/>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.freePrompt" Validation="@this.ValidateFreePrompt" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("What should the AI do with each document?")" HelperText="@T("These instructions are applied to every single document of the batch run.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" AutoGrow="@true" MaxLines="26" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.freePrompt" Validation="@this.ValidateFreePrompt" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("What should the AI do with each document?")" HelperText="@T("These instructions are applied to every single document of the batch run.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" Sizing="InputSizing.Auto" MaxLines="26" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.SelectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isProcessingBatch" Label="@T("Tools for this batch run")" Help="@T("The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here.")"/>
|
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.SelectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isProcessingBatch" Label="@T("Tools for this batch run")" Help="@T("The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here.")"/>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ public partial class AssistantBatchProcessing
|
|||||||
/// <returns>The output folder and the documents, or <c>null</c> when the run must not start.</returns>
|
/// <returns>The output folder and the documents, or <c>null</c> when the run must not start.</returns>
|
||||||
private async Task<(string ResolvedOutputDirectory, IReadOnlyList<string> Files)?> PrepareRunAsync()
|
private async Task<(string ResolvedOutputDirectory, IReadOnlyList<string> Files)?> PrepareRunAsync()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
|
|
||||||
//
|
//
|
||||||
// The exclusion list sits in the expert settings, which are folded away by default. Unfold
|
// The exclusion list sits in the expert settings, which are folded away by default. Unfold
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAss
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@if (this.step is BuilderStep.DESCRIBE)
|
@if (this.step is BuilderStep.DESCRIBE)
|
||||||
{
|
{
|
||||||
<ReadFileContent Text="@T("Load description from file")" @bind-FileContent="@this.assistantDescription" EnableDragDrop="true" CatchAllDocuments="true"/>
|
<ReadFileContent Text="@T("Load description from file")" @bind-FileContent="@this.assistantDescription" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.assistantDescription" Validation="@this.ValidateAssistantDescription" AdornmentIcon="@Icons.Material.Filled.AutoAwesome" Adornment="Adornment.Start" Label="@T("Describe your assistant")" HelperText="@T("Describe the task, inputs, and desired output in your own words. The model will infer all the plugin details.")" Placeholder="@T("I need an assistant that turns meeting notes into clear tasks with owners and deadlines.")" Variant="Variant.Outlined" Lines="8" AutoGrow="@true" MaxLines="18" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.assistantDescription" Validation="@this.ValidateAssistantDescription" AdornmentIcon="@Icons.Material.Filled.AutoAwesome" Adornment="Adornment.Start" Label="@T("Describe your assistant")" HelperText="@T("Describe the task, inputs, and desired output in your own words. The model will infer all the plugin details.")" Placeholder="@T("I need an assistant that turns meeting notes into clear tasks with owners and deadlines.")" Variant="Variant.Outlined" Lines="8" Sizing="InputSizing.Auto" MaxLines="18" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
@* This switch chooses between the two kinds of assistant the Builder can create, so it stays
|
@* This switch chooses between the two kinds of assistant the Builder can create, so it stays
|
||||||
outside the advanced options. Its fields are required, and a collapsed panel would hide
|
outside the advanced options. Its fields are required, and a collapsed panel would hide
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
<EnumSelection T="AssistantCategory" NameFunc="@(category => category.NameSelecting())" @bind-Value="@this.selectedCategory" ValidateSelection="@this.ValidatingCategory" Icon="@Icons.Material.Filled.Category" IconSize="Size.Small" Label="@T("Category (Optional)")" AllowOther="@true" OtherValue="AssistantCategory.OTHER" @bind-OtherInput="@this.customCategory" ValidateOther="@this.ValidateCustomCategory" LabelOther="@T("Custom assistant category")" />
|
<EnumSelection T="AssistantCategory" NameFunc="@(category => category.NameSelecting())" @bind-Value="@this.selectedCategory" ValidateSelection="@this.ValidatingCategory" Icon="@Icons.Material.Filled.Category" IconSize="Size.Small" Label="@T("Category (Optional)")" AllowOther="@true" OtherValue="AssistantCategory.OTHER" @bind-OtherInput="@this.customCategory" ValidateOther="@this.ValidateCustomCategory" LabelOther="@T("Custom assistant category")" />
|
||||||
@if (!this.createChatLauncher)
|
@if (!this.createChatLauncher)
|
||||||
{
|
{
|
||||||
<MudTextField T="string" @bind-Text="@this.typicalInput" AdornmentIcon="@Icons.Material.Filled.Login" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Typical input (Optional)")" Placeholder="@T("What users provide, e.g. text, notes, files, or a URL")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" AutoGrow="@true" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.typicalInput" AdornmentIcon="@Icons.Material.Filled.Login" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Typical input (Optional)")" Placeholder="@T("What users provide, e.g. text, notes, files, or a URL")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" Sizing="InputSizing.Auto" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.expectedOutput" AdornmentIcon="@Icons.Material.Filled.Logout" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Expected output (Optional)")" Placeholder="@T("What users should get, e.g. a summary or checklist")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" AutoGrow="@true" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.expectedOutput" AdornmentIcon="@Icons.Material.Filled.Logout" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Expected output (Optional)")" Placeholder="@T("What users should get, e.g. a summary or checklist")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" Sizing="InputSizing.Auto" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudSelect T="AssistantComponentType" Label="@T("Input and UI components (Optional)")" MultiSelection="@true" @bind-SelectedValues="@this.selectedAssistantComponents" MultiSelectionTextFunc="@this.GetSelectedAssistantComponentText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ViewDay" IconSize="Size.Small">
|
<MudSelect T="AssistantComponentType" Label="@T("Input and UI components (Optional)")" MultiSelection="@true" @bind-SelectedValues="@this.selectedAssistantComponents" MultiSelectionTextFunc="@this.GetSelectedAssistantComponentText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ViewDay" IconSize="Size.Small">
|
||||||
@foreach (var component in ASSISTANT_COMPONENT_OPTIONS)
|
@foreach (var component in ASSISTANT_COMPONENT_OPTIONS)
|
||||||
{
|
{
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
</MudSelect>
|
</MudSelect>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedOutputLanguage" Icon="@Icons.Material.Filled.Translate" IconSize="Size.Small" Label="@T("(Optional) Output language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customOutputLanguage" ValidateOther="@this.ValidateCustomOutputLanguage" LabelOther="@T("Custom output language")" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedOutputLanguage" Icon="@Icons.Material.Filled.Translate" IconSize="Size.Small" Label="@T("(Optional) Output language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customOutputLanguage" ValidateOther="@this.ValidateCustomOutputLanguage" LabelOther="@T("Custom output language")" />
|
||||||
<MudSwitch T="bool" @bind-Value="@this.allowGeneratedAssistantProfiles" Label="@T("Allow AI Studio profiles")" LabelPlacement="Placement.End" Color="Color.Primary" Class="mb-3"/>
|
<MudSwitch T="bool" @bind-Value="@this.allowGeneratedAssistantProfiles" Label="@T("Allow AI Studio profiles")" LabelPlacement="Placement.End" Color="Color.Primary" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.extraRules" AdornmentIcon="@Icons.Material.Filled.Rule" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Additional rules (Optional)")" Placeholder="@T("What to avoid or consider, e.g. do not invent missing facts")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" AutoGrow="@true" MaxLines="10" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.extraRules" AdornmentIcon="@Icons.Material.Filled.Rule" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Additional rules (Optional)")" Placeholder="@T("What to avoid or consider, e.g. do not invent missing facts")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" Sizing="InputSizing.Auto" MaxLines="10" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.exampleRequest" AdornmentIcon="@Icons.Material.Filled.Lightbulb" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Example prompt (Optional)")" Placeholder="@T("An expected user prompt, e.g. summarize this document")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" AutoGrow="@true" MaxLines="10" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.exampleRequest" AdornmentIcon="@Icons.Material.Filled.Lightbulb" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Example prompt (Optional)")" Placeholder="@T("An expected user prompt, e.g. summarize this document")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="3" Sizing="InputSizing.Auto" MaxLines="10" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
</CompactExpansionPanel>
|
</CompactExpansionPanel>
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ else
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.reviewNotes" AdornmentIcon="@Icons.Material.Filled.EditNote" Adornment="Adornment.Start" Label="@T("Additional changes (Optional)")" HelperText="@T("These notes are applied on top of the accepted draft and can still change the generated assistant plugin. Leave empty to use the draft as-is.")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="2" AutoGrow="@true" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.reviewNotes" AdornmentIcon="@Icons.Material.Filled.EditNote" Adornment="Adornment.Start" Label="@T("Additional changes (Optional)")" HelperText="@T("These notes are applied on top of the accepted draft and can still change the generated assistant plugin. Leave empty to use the draft as-is.")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="2" Sizing="InputSizing.Auto" MaxLines="8" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-6">
|
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-6">
|
||||||
<MudTooltip Text="@T("Return to the original assistant description. The current draft and the plugin preview will be discarded.")">
|
<MudTooltip Text="@T("Return to the original assistant description. The current draft and the plugin preview will be discarded.")">
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
|||||||
private string launcherProviderId = string.Empty;
|
private string launcherProviderId = string.Empty;
|
||||||
private string launcherProfileId = string.Empty;
|
private string launcherProfileId = string.Empty;
|
||||||
private string launcherChatTemplateId = string.Empty;
|
private string launcherChatTemplateId = string.Empty;
|
||||||
private IEnumerable<string> launcherDataSourceIds = [];
|
private IReadOnlyCollection<string> launcherDataSourceIds = [];
|
||||||
private HashSet<string> launcherToolIds = [];
|
private HashSet<string> launcherToolIds = [];
|
||||||
private IEnumerable<AssistantComponentType> selectedAssistantComponents = [];
|
private IReadOnlyCollection<AssistantComponentType> selectedAssistantComponents = [];
|
||||||
private CommonLanguages selectedOutputLanguage = CommonLanguages.AS_IS;
|
private CommonLanguages selectedOutputLanguage = CommonLanguages.AS_IS;
|
||||||
private string customOutputLanguage = string.Empty;
|
private string customOutputLanguage = string.Empty;
|
||||||
private bool allowGeneratedAssistantProfiles = true;
|
private bool allowGeneratedAssistantProfiles = true;
|
||||||
@@ -391,7 +391,7 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
|||||||
|
|
||||||
private async Task GenerateAssistantSpec()
|
private async Task GenerateAssistantSpec()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
|||||||
|
|
||||||
private async Task GenerateLuaAssistant()
|
private async Task GenerateLuaAssistant()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -530,7 +530,7 @@ public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistan
|
|||||||
_ => this.selectedOutputLanguage.Name(),
|
_ => this.selectedOutputLanguage.Name(),
|
||||||
};
|
};
|
||||||
|
|
||||||
private string GetSelectedAssistantComponentText(List<string?>? selectedValues)
|
private string GetSelectedAssistantComponentText(IReadOnlyList<string?>? selectedValues)
|
||||||
{
|
{
|
||||||
if (selectedValues is null || selectedValues.Count == 0)
|
if (selectedValues is null || selectedValues.Count == 0)
|
||||||
return T("Model decides");
|
return T("Model decides");
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
<MudTextSwitch Label="@T("Do you want to provide compiler messages?")" @bind-Value="@this.provideCompilerMessages" LabelOn="@T("Yes, provide compiler messages")" LabelOff="@T("No, there are no compiler messages")" />
|
<MudTextSwitch Label="@T("Do you want to provide compiler messages?")" @bind-Value="@this.provideCompilerMessages" LabelOn="@T("Yes, provide compiler messages")" LabelOff="@T("No, there are no compiler messages")" />
|
||||||
@if (this.provideCompilerMessages)
|
@if (this.provideCompilerMessages)
|
||||||
{
|
{
|
||||||
<MudTextField T="string" @bind-Text="@this.compilerMessages" Validation="@this.ValidatingCompilerMessages" AdornmentIcon="@Icons.Material.Filled.Error" Adornment="Adornment.Start" Label="@T("Compiler messages")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.compilerMessages" Validation="@this.ValidatingCompilerMessages" AdornmentIcon="@Icons.Material.Filled.Error" Adornment="Adornment.Start" Label="@T("Compiler messages")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.questions" Validation="@this.ValidateQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionMark" Adornment="Adornment.Start" Label="@T("Your question(s)")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.questions" Validation="@this.ValidateQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionMark" Adornment="Adornment.Start" Label="@T("Your question(s)")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
@@ -193,7 +193,7 @@ public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding>
|
|||||||
|
|
||||||
private async Task GetSupport()
|
private async Task GetSupport()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ else
|
|||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyName" Validation="@this.ValidatePolicyName" Immediate="@true" Label="@T("Policy name")" HelperText="@T("Please give your policy a name that provides information about the intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="@(() => this.PolicyNameWasChanged())"/>
|
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyName" Validation="@this.ValidatePolicyName" Immediate="@true" Label="@T("Policy name")" HelperText="@T("Please give your policy a name that provides information about the intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="@(() => this.PolicyNameWasChanged())"/>
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyDescription" Validation="@this.ValidatePolicyDescription" Immediate="@true" Label="@T("Policy description")" HelperText="@T("Please provide a brief description of your policy. Describe or explain what your policy does. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyDescription" Validation="@this.ValidatePolicyDescription" Immediate="@true" Label="@T("Policy description")" HelperText="@T("Please provide a brief description of your policy. Describe or explain what your policy does. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" Sizing="InputSizing.Auto" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|
||||||
<MudTextSwitch Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Hide the policy definition when distributed via configuration plugin?")" Value="@this.policyHidePolicyDefinition" ValueChanged="async state => await this.PolicyHidePolicyDefinitionWasChanged(state)" LabelOn="@T("Yes, hide the policy definition")" LabelOff="@T("No, show the policy definition")" />
|
<MudTextSwitch Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Hide the policy definition when distributed via configuration plugin?")" Value="@this.policyHidePolicyDefinition" ValueChanged="async state => await this.PolicyHidePolicyDefinitionWasChanged(state)" LabelOn="@T("Yes, hide the policy definition")" LabelOff="@T("No, show the policy definition")" />
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ else
|
|||||||
@T("The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules.")
|
@T("The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules.")
|
||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyAnalysisRules" Validation="@this.ValidateAnalysisRules" Immediate="@true" Label="@T("Analysis rules")" HelperText="@T("Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" AutoGrow="@true" MaxLines="26" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyAnalysisRules" Validation="@this.ValidateAnalysisRules" Immediate="@true" Label="@T("Analysis rules")" HelperText="@T("Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" Sizing="InputSizing.Auto" MaxLines="26" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|
||||||
@* No default target for the rule zones: while the policy definition is open, three
|
@* No default target for the rule zones: while the policy definition is open, three
|
||||||
zones are in play, so each drop has to be aimed at the one it belongs to. *@
|
zones are in play, so each drop has to be aimed at the one it belongs to. *@
|
||||||
@@ -138,7 +138,7 @@ else
|
|||||||
@T("After the AI has processed all documents, it needs your instructions on how the result should be formatted. Would you like a structured list with keywords or a continuous text? Should the output include emojis or be written in formal business language? You can specify all these preferences in the output rules. There, you can also predefine a desired structure—for example, by using Markdown formatting to define headings, paragraphs, or bullet points.")
|
@T("After the AI has processed all documents, it needs your instructions on how the result should be formatted. Would you like a structured list with keywords or a continuous text? Should the output include emojis or be written in formal business language? You can specify all these preferences in the output rules. There, you can also predefine a desired structure—for example, by using Markdown formatting to define headings, paragraphs, or bullet points.")
|
||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyOutputRules" Validation="@this.ValidateOutputRules" Immediate="@true" Label="@T("Output rules")" HelperText="@T("Please provide a description of your output rules. This rules will be used to instruct the AI on how to format the output of the analysis.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" AutoGrow="@true" MaxLines="26" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.IsNoPolicySelectedOrProtected" @bind-Text="@this.policyOutputRules" Validation="@this.ValidateOutputRules" Immediate="@true" Label="@T("Output rules")" HelperText="@T("Please provide a description of your output rules. This rules will be used to instruct the AI on how to format the output of the analysis.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" Sizing="InputSizing.Auto" MaxLines="26" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|
||||||
<ReadFileContent Text="@T("Load output rules from document")" @bind-FileContent="@this.policyOutputRules" EnableDragDrop="true" Disabled="@this.IsNoPolicySelectedOrProtected"/>
|
<ReadFileContent Text="@T("Load output rules from document")" @bind-FileContent="@this.policyOutputRules" EnableDragDrop="true" Disabled="@this.IsNoPolicySelectedOrProtected"/>
|
||||||
|
|
||||||
|
|||||||
+16
-11
@@ -445,7 +445,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
|
|
||||||
private bool ArePolicyControlsDisabled => this.IsProcessing || this.IsMediaImportBusy;
|
private bool ArePolicyControlsDisabled => this.IsProcessing || this.IsMediaImportBusy;
|
||||||
|
|
||||||
private void SelectedPolicyChanged(DataDocumentAnalysisPolicy? policy)
|
private async Task SelectedPolicyChanged(DataDocumentAnalysisPolicy? policy)
|
||||||
{
|
{
|
||||||
if (this.ArePolicyControlsDisabled)
|
if (this.ArePolicyControlsDisabled)
|
||||||
return;
|
return;
|
||||||
@@ -455,8 +455,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
this.policyDefinitionExpanded = !this.selectedPolicy?.IsProtected ?? true;
|
this.policyDefinitionExpanded = !this.selectedPolicy?.IsProtected ?? true;
|
||||||
this.documentSelectionExpanded = !this.policyDefinitionExpanded;
|
this.documentSelectionExpanded = !this.policyDefinitionExpanded;
|
||||||
this.ApplyPolicyPreselection(preferPolicyPreselection: true);
|
this.ApplyPolicyPreselection(preferPolicyPreselection: true);
|
||||||
|
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
|
|
||||||
this.ClearInputIssues();
|
this.ClearInputIssues();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +504,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
|
|
||||||
this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.Add(addedPolicy);
|
this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.Add(addedPolicy);
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
this.SelectedPolicyChanged(addedPolicy);
|
await this.SelectedPolicyChanged(addedPolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddPolicy()
|
private async Task AddPolicy()
|
||||||
@@ -556,7 +558,8 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
this.ResetForm();
|
this.ResetForm();
|
||||||
|
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -728,7 +731,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
|
|
||||||
#region Overrides of MSGComponentBase
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
{
|
{
|
||||||
switch (triggeredEvent)
|
switch (triggeredEvent)
|
||||||
{
|
{
|
||||||
@@ -738,17 +741,17 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case Event.PLUGINS_RELOADED:
|
case Event.PLUGINS_RELOADED:
|
||||||
this.HandlePluginsReloaded();
|
await this.HandlePluginsReloaded();
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void HandlePluginsReloaded()
|
private async Task HandlePluginsReloaded()
|
||||||
{
|
{
|
||||||
// Check if the currently selected policy still exists after plugin reload:
|
// Check if the currently selected policy still exists after plugin reload:
|
||||||
if (this.selectedPolicy is not null)
|
if (this.selectedPolicy is not null)
|
||||||
@@ -788,7 +791,9 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
this.ApplyPolicyPreselection(preferPolicyPreselection: true);
|
this.ApplyPolicyPreselection(preferPolicyPreselection: true);
|
||||||
|
|
||||||
// Reset validation state:
|
// Reset validation state:
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
|
|
||||||
this.ClearInputIssues();
|
this.ClearInputIssues();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -959,7 +964,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
private async Task Analyze()
|
private async Task Analyze()
|
||||||
{
|
{
|
||||||
await this.AutoSave();
|
await this.AutoSave();
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ else
|
|||||||
if (component is AssistantTextArea textArea)
|
if (component is AssistantTextArea textArea)
|
||||||
{
|
{
|
||||||
var lines = textArea.IsSingleLine ? 1 : 6;
|
var lines = textArea.IsSingleLine ? 1 : 6;
|
||||||
var autoGrow = !textArea.IsSingleLine;
|
var sizing = textArea.IsSingleLine ? InputSizing.Fixed : InputSizing.Auto;
|
||||||
|
|
||||||
<MudTextField T="string"
|
<MudTextField T="string"
|
||||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||||
Text="@this.assistantState.Text[textArea.Name]"
|
Text="@this.assistantState.Text[textArea.Name]"
|
||||||
TextChanged="@(value => this.assistantState.Text[textArea.Name] = value)"
|
TextChanged="@(value => this.assistantState.Text[textArea.Name] = value ?? string.Empty)"
|
||||||
Label="@textArea.Label"
|
Label="@textArea.Label"
|
||||||
HelperText="@textArea.HelperText"
|
HelperText="@textArea.HelperText"
|
||||||
HelperTextOnFocus="@textArea.HelperTextOnFocus"
|
HelperTextOnFocus="@textArea.HelperTextOnFocus"
|
||||||
@@ -109,7 +109,7 @@ else
|
|||||||
AdornmentColor="@textArea.GetAdornmentColor()"
|
AdornmentColor="@textArea.GetAdornmentColor()"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Lines="@lines"
|
Lines="@lines"
|
||||||
AutoGrow="@autoGrow"
|
Sizing="@sizing"
|
||||||
MaxLines="12"
|
MaxLines="12"
|
||||||
Class='@MergeClass(textArea.Class, "mb-3")'
|
Class='@MergeClass(textArea.Class, "mb-3")'
|
||||||
Style="@GetOptionalStyle(textArea.Style)" />
|
Style="@GetOptionalStyle(textArea.Style)" />
|
||||||
@@ -479,7 +479,7 @@ else
|
|||||||
|
|
||||||
<MudItem Class="d-flex">
|
<MudItem Class="d-flex">
|
||||||
<MudColorPicker Text="@this.assistantState.Colors[colorPicker.Name]"
|
<MudColorPicker Text="@this.assistantState.Colors[colorPicker.Name]"
|
||||||
TextChanged="@(value => this.assistantState.Colors[colorPicker.Name] = value)"
|
TextChanged="@(value => this.assistantState.Colors[colorPicker.Name] = value ?? string.Empty)"
|
||||||
Label="@colorPicker.Label"
|
Label="@colorPicker.Label"
|
||||||
Placeholder="@colorPicker.Placeholder"
|
Placeholder="@colorPicker.Placeholder"
|
||||||
ShowAlpha="@colorPicker.ShowAlpha"
|
ShowAlpha="@colorPicker.ShowAlpha"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@if (this.provideHistory)
|
@if (this.provideHistory)
|
||||||
{
|
{
|
||||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||||
<MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="@T("Previous conversation")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Provide the previous conversation, e.g., the last e-mail, the last chat, etc.")" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
|
<MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="@T("Previous conversation")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Provide the previous conversation, e.g., the last e-mail, the last chat, etc.")" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
|
|||||||
private string inputGreeting = string.Empty;
|
private string inputGreeting = string.Empty;
|
||||||
private string inputBulletPoints = string.Empty;
|
private string inputBulletPoints = string.Empty;
|
||||||
private readonly List<string> bulletPointsLines = [];
|
private readonly List<string> bulletPointsLines = [];
|
||||||
private IEnumerable<string> selectedFoci = new HashSet<string>();
|
private IReadOnlyCollection<string> selectedFoci = new HashSet<string>();
|
||||||
private string inputName = string.Empty;
|
private string inputName = string.Empty;
|
||||||
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
|
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
|
||||||
private string customTargetLanguage = string.Empty;
|
private string customTargetLanguage = string.Empty;
|
||||||
@@ -259,7 +259,7 @@ public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMa
|
|||||||
|
|
||||||
private async Task CreateMail()
|
private async Task CreateMail()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ else
|
|||||||
@T("Common ERI server settings")
|
@T("Common ERI server settings")
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="@T("ERI server name")" HelperText="@T("Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
|
<MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="@T("ERI server name")" HelperText="@T("Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio.")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
|
||||||
<MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="@T("ERI server description")" HelperText="@T("Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.IsERIInputDisabled" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="@T("ERI server description")" HelperText="@T("Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio.")" Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" Sizing="InputSizing.Auto" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|
||||||
<MudStack Row="@true" Class="mb-3">
|
<MudStack Row="@true" Class="mb-3">
|
||||||
<MudSelect Disabled="@this.IsERIInputDisabled" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="@T("Programming language")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
|
<MudSelect Disabled="@this.IsERIInputDisabled" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="@T("Programming language")" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
|
||||||
@@ -116,8 +116,8 @@ else
|
|||||||
</MudSelectItem>
|
</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationDownloadDisabled" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
|
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationSavingDisabled" OnClick="@this.SaveSpecificationAsync">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> @T("Download specification")
|
<MudIcon Icon="@Icons.Material.Filled.Save" Class="mr-2"/> @T("Save specification")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ else
|
|||||||
</MudSelect>
|
</MudSelect>
|
||||||
@if (this.selectedDataSource is DataSources.CUSTOM)
|
@if (this.selectedDataSource is DataSources.CUSTOM)
|
||||||
{
|
{
|
||||||
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="@T("Describe your data source")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="@T("Describe your data source")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" Sizing="InputSizing.Auto" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ else
|
|||||||
</MudSelectItemExtended>
|
</MudSelectItemExtended>
|
||||||
}
|
}
|
||||||
</MudSelectExtended>
|
</MudSelectExtended>
|
||||||
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" Sizing="InputSizing.Auto" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
|
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
|
||||||
@@ -316,7 +316,7 @@ else
|
|||||||
@T("You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.")
|
@T("You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.")
|
||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.additionalLibraries" Label="@T("(Optional) Additional libraries")" HelperText="@T("Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField Disabled="@this.IsERIInputDisabled" T="string" @bind-Text="@this.additionalLibraries" Label="@T("(Optional) Additional libraries")" HelperText="@T("Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" Sizing="InputSizing.Auto" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
|
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
|
||||||
@T("Provider selection for generation")
|
@T("Provider selection for generation")
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Dialogs;
|
using AIStudio.Dialogs;
|
||||||
using AIStudio.Dialogs.Settings;
|
using AIStudio.Dialogs.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.AssistantSessions;
|
using AIStudio.Tools.AssistantSessions;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
@@ -16,7 +14,7 @@ namespace AIStudio.Assistants.ERI;
|
|||||||
public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
private HttpClient HttpClient { get; set; } = null!;
|
private WebRootFiles WebRootFiles { get; set; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IDialogService DialogService { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
@@ -27,270 +25,25 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
|
|
||||||
protected override string Description => T("The ERI is the External Retrieval Interface for AI Studio and other tools. The ERI acts as a contract between decentralized data sources and, e.g., AI Studio. The ERI is implemented by the data sources, allowing them to be integrated into AI Studio later. This means that the data sources assume the server role and AI Studio (or any other LLM tool) assumes the client role of the API. This approach serves to realize a Retrieval-Augmented Generation (RAG) process with external data.");
|
protected override string Description => T("The ERI is the External Retrieval Interface for AI Studio and other tools. The ERI acts as a contract between decentralized data sources and, e.g., AI Studio. The ERI is implemented by the data sources, allowing them to be integrated into AI Studio later. This means that the data sources assume the server role and AI Studio (or any other LLM tool) assumes the client role of the API. This approach serves to realize a Retrieval-Augmented Generation (RAG) process with external data.");
|
||||||
|
|
||||||
protected override string SystemPrompt
|
protected override string SystemPrompt => new ERIServerPrompt
|
||||||
{
|
{
|
||||||
get
|
ProgrammingLanguage = this.selectedProgrammingLanguage,
|
||||||
{
|
OtherProgrammingLanguage = this.otherProgrammingLanguage,
|
||||||
var sb = new StringBuilder();
|
ERISpecification = this.eriSpecification,
|
||||||
|
ServerName = this.serverName,
|
||||||
//
|
ServerDescription = this.serverDescription,
|
||||||
// ---------------------------------
|
DataSource = this.selectedDataSource,
|
||||||
// Introduction
|
OtherDataSource = this.otherDataSource,
|
||||||
// ---------------------------------
|
DataSourceProductName = this.dataSourceProductName,
|
||||||
//
|
DataSourceHostname = this.dataSourceHostname,
|
||||||
var programmingLanguage = this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER ? this.otherProgrammingLanguage : this.selectedProgrammingLanguage.ToPrompt();
|
DataSourcePort = this.dataSourcePort,
|
||||||
sb.Append($"""
|
AuthenticationMethods = this.selectedAuthenticationMethods,
|
||||||
# Introduction
|
AuthDescription = this.authDescription,
|
||||||
You are an experienced {programmingLanguage} developer. Your task is to implement an API server in
|
OperatingSystem = this.selectedOperatingSystem,
|
||||||
the {programmingLanguage} language according to the following OpenAPI Description (OAD):
|
AllowedLLMProviders = this.allowedLLMProviders,
|
||||||
|
RetrievalProcesses = this.retrievalProcesses,
|
||||||
```
|
AdditionalLibraries = this.additionalLibraries,
|
||||||
{this.eriSpecification}
|
}.ToSystemPrompt();
|
||||||
```
|
|
||||||
|
|
||||||
The server realizes the data retrieval component for a "Retrieval-Augmentation Generation" (RAG) process.
|
|
||||||
The server is called "{this.serverName}" and is described as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
{this.serverDescription}
|
|
||||||
```
|
|
||||||
""");
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Data Source
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
# Data Source
|
|
||||||
""");
|
|
||||||
|
|
||||||
switch (this.selectedDataSource)
|
|
||||||
{
|
|
||||||
case DataSources.CUSTOM:
|
|
||||||
sb.Append($"""
|
|
||||||
The data source for the retrieval process is described as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
{this.otherDataSource}
|
|
||||||
```
|
|
||||||
""");
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.dataSourceHostname))
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
The data source is accessible via the hostname `{this.dataSourceHostname}`.
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.dataSourcePort is not null)
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
The data source is accessible via port `{this.dataSourcePort}`.
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DataSources.FILE_SYSTEM:
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
The data source for the retrieval process is the local file system. Use a placeholder for the data source path.
|
|
||||||
""");
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
case DataSources.OBJECT_STORAGE:
|
|
||||||
case DataSources.KEY_VALUE_STORE:
|
|
||||||
case DataSources.DOCUMENT_STORE:
|
|
||||||
case DataSources.RELATIONAL_DATABASE:
|
|
||||||
case DataSources.GRAPH_DATABASE:
|
|
||||||
sb.Append($"""
|
|
||||||
The data source for the retrieval process is an "{this.dataSourceProductName}" database running on the
|
|
||||||
host `{this.dataSourceHostname}` and is accessible via port `{this.dataSourcePort}`.
|
|
||||||
""");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Authentication and Authorization
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
# Authentication and Authorization
|
|
||||||
The process for authentication and authorization is two-step. Step 1: Users must authenticate
|
|
||||||
with the API server using a `POST` call on `/auth` with the chosen method. If this step is
|
|
||||||
successful, the API server returns a token. Step 2: This token is then required for all
|
|
||||||
other API calls.
|
|
||||||
|
|
||||||
Important notes:
|
|
||||||
- Calls to `/auth` and `/auth/methods` are accessible without authentication. All other API
|
|
||||||
endpoints require a valid step 2 token.
|
|
||||||
|
|
||||||
- It is possible that a token (step 1 token) is desired as `authMethod`. These step 1 tokens
|
|
||||||
must never be accepted as valid tokens for step 2.
|
|
||||||
|
|
||||||
- The OpenAPI Description (OAD) for `/auth` is not complete. This is because, at the time of
|
|
||||||
writing the OAD, it is not known what kind of authentication is desired. Therefore, you must
|
|
||||||
supplement the corresponding fields or data in the implementation. Example: If username/password
|
|
||||||
is desired, you must expect and read both. If both token and username/password are desired, you
|
|
||||||
must dynamically read the `authMethod` and expect and evaluate different fields accordingly.
|
|
||||||
|
|
||||||
The following authentications and authorizations should be implemented for the API server:
|
|
||||||
""");
|
|
||||||
|
|
||||||
foreach (var auth in this.selectedAuthenticationMethods)
|
|
||||||
sb.Append($"- {auth.ToPrompt()}");
|
|
||||||
|
|
||||||
if(this.IsUsingKerberos())
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
The server will run on {this.selectedOperatingSystem.ToPrompt()} operating systems. Keep
|
|
||||||
this in mind when implementing the SSO with Kerberos.
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Security
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
# Security
|
|
||||||
The following security requirement for `allowedProviderType` was chosen: `{this.allowedLLMProviders}`
|
|
||||||
""");
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Retrieval Processes
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
# Retrieval Processes
|
|
||||||
You are implementing the following data retrieval processes:
|
|
||||||
""");
|
|
||||||
|
|
||||||
var retrievalProcessCounter = 1;
|
|
||||||
foreach (var retrievalProcess in this.retrievalProcesses)
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
## {retrievalProcessCounter++}. Retrieval Process
|
|
||||||
- Name: {retrievalProcess.Name}
|
|
||||||
- Description:
|
|
||||||
|
|
||||||
```
|
|
||||||
{retrievalProcess.Description}
|
|
||||||
```
|
|
||||||
""");
|
|
||||||
|
|
||||||
if(retrievalProcess.ParametersDescription?.Count > 0)
|
|
||||||
{
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
This retrieval process recognizes the following parameters:
|
|
||||||
""");
|
|
||||||
|
|
||||||
var parameterCounter = 1;
|
|
||||||
foreach (var (parameter, description) in retrievalProcess.ParametersDescription)
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
- The {parameterCounter++} parameter is named "{parameter}":
|
|
||||||
|
|
||||||
```
|
|
||||||
{description}
|
|
||||||
```
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
Please use sensible default values for the parameters. They are optional
|
|
||||||
for the user.
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(retrievalProcess.Embeddings?.Count > 0)
|
|
||||||
{
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
The following embeddings are implemented for this retrieval process:
|
|
||||||
""");
|
|
||||||
|
|
||||||
var embeddingCounter = 1;
|
|
||||||
foreach (var embedding in retrievalProcess.Embeddings)
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
- {embeddingCounter++}. Embedding
|
|
||||||
- Name: {embedding.EmbeddingName}
|
|
||||||
- Type: {embedding.EmbeddingType}
|
|
||||||
|
|
||||||
- Description:
|
|
||||||
|
|
||||||
```
|
|
||||||
{embedding.Description}
|
|
||||||
```
|
|
||||||
|
|
||||||
- When used:
|
|
||||||
|
|
||||||
```
|
|
||||||
{embedding.UsedWhen}
|
|
||||||
```
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Additional Libraries
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(this.additionalLibraries))
|
|
||||||
{
|
|
||||||
sb.Append($"""
|
|
||||||
|
|
||||||
# Additional Libraries
|
|
||||||
You use the following libraries for your implementation:
|
|
||||||
|
|
||||||
{this.additionalLibraries}
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ---------------------------------
|
|
||||||
// Remarks
|
|
||||||
// ---------------------------------
|
|
||||||
//
|
|
||||||
|
|
||||||
sb.Append("""
|
|
||||||
|
|
||||||
# Remarks
|
|
||||||
- You do not ask follow-up questions.
|
|
||||||
- You consider the security of the implementation by applying the Security by Design principle.
|
|
||||||
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
|
|
||||||
create a separate code block with its file path and name as chapter title.
|
|
||||||
- Important: The JSON objects of the API messages use camel case for the data field names.
|
|
||||||
""");
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons =>
|
protected override IReadOnlyList<IButtonData> FooterButtons =>
|
||||||
[
|
[
|
||||||
@@ -611,7 +364,8 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
this.ResetForm();
|
this.ResetForm();
|
||||||
|
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
this.Form?.ResetValidation();
|
if (this.Form is not null)
|
||||||
|
await this.Form.ResetValidationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsNoneERIServerSelected => this.selectedERIServer is null;
|
private bool IsNoneERIServerSelected => this.selectedERIServer is null;
|
||||||
@@ -622,9 +376,43 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
|
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the selected ERI specification cannot be downloaded.
|
/// Gets whether the selected ERI specification cannot be saved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
|
private bool IsSpecificationSavingDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets the user save the selected ERI specification, a copy of the one the app carries.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The app server answers the app window only, so a link to the file would fail in the browser
|
||||||
|
/// of the system. The copy also matches the installed version of AI Studio.
|
||||||
|
/// </remarks>
|
||||||
|
private async Task SaveSpecificationAsync()
|
||||||
|
{
|
||||||
|
var specificationPath = this.selectedERIVersion.SpecificationURL();
|
||||||
|
var response = await this.RustService.SaveFile(T("Save the ERI specification"), [FileTypes.JSON], Path.GetFileName(specificationPath));
|
||||||
|
if (response.UserCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await using var feedback = new FileExportFeedback();
|
||||||
|
feedback.ShowRunning(response.SaveFilePath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await this.WebRootFiles.CopyToAsync(specificationPath, response.SaveFilePath))
|
||||||
|
{
|
||||||
|
await feedback.SucceededAsync(Icons.Material.Filled.CheckCircle, T("The ERI specification was saved."), response.SaveFilePath, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Logger.LogError("The app carries no ERI specification at '{SpecificationPath}'.", specificationPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
this.Logger.LogError(exception, "Failed to save the ERI specification {ERIVersion}.", this.selectedERIVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, T("The ERI specification could not be saved.")));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the generated-code target directory selection should be disabled.
|
/// Gets whether the generated-code target directory selection should be disabled.
|
||||||
@@ -1070,43 +858,13 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
await this.AutoSave();
|
await this.AutoSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("""
|
|
||||||
"([\\/._\w-]+)"
|
|
||||||
""", RegexOptions.NonBacktracking)]
|
|
||||||
private static partial Regex FileExtractRegex();
|
|
||||||
|
|
||||||
private IEnumerable<string> ExtractFiles(string fileListAnswer)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
// We asked the LLM for answering using a specific JSON scheme.
|
|
||||||
// However, the LLM might not follow this scheme. Therefore, we
|
|
||||||
// need to parse the answer and extract the files.
|
|
||||||
// The parsing strategy is to look for all strings.
|
|
||||||
//
|
|
||||||
var matches = FileExtractRegex().Matches(fileListAnswer);
|
|
||||||
foreach (Match match in matches)
|
|
||||||
if(match.Groups[1].Value is {} file && !string.IsNullOrWhiteSpace(file) && !file.Equals("files", StringComparison.OrdinalIgnoreCase))
|
|
||||||
yield return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex("""
|
|
||||||
\s*#+\s+[\w\\/.]+\s*```\w*\s+([\s\w\W]+)\s*```\s*
|
|
||||||
""", RegexOptions.Singleline)]
|
|
||||||
private static partial Regex CodeExtractRegex();
|
|
||||||
|
|
||||||
private string ExtractCode(string markdown)
|
|
||||||
{
|
|
||||||
var match = CodeExtractRegex().Match(markdown);
|
|
||||||
return match.Success ? match.Groups[1].Value : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GenerateServer()
|
private async Task GenerateServer()
|
||||||
{
|
{
|
||||||
if(this.IsNoneERIServerSelected)
|
if(this.IsNoneERIServerSelected)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await this.AutoSave();
|
await this.AutoSave();
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1120,7 +878,7 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
var baseDirectorySnapshot = this.baseDirectory;
|
var baseDirectorySnapshot = this.baseDirectory;
|
||||||
var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray();
|
var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray();
|
||||||
|
|
||||||
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient);
|
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.WebRootFiles);
|
||||||
if (string.IsNullOrWhiteSpace(this.eriSpecification))
|
if (string.IsNullOrWhiteSpace(this.eriSpecification))
|
||||||
{
|
{
|
||||||
this.AddInputIssue(T("The ERI specification could not be loaded. Please try again later."));
|
this.AddInputIssue(T("The ERI specification could not be loaded. Please try again later."));
|
||||||
@@ -1151,13 +909,19 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
Use the following JSON schema:
|
Use the following JSON schema:
|
||||||
|
|
||||||
{
|
{
|
||||||
[
|
"files": [
|
||||||
"path/to/file1",
|
"path/to/file1",
|
||||||
"path/to/file2"
|
"path/to/file2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""", true);
|
""", true);
|
||||||
var fileListAnswer = await this.AddAIResponseAsync(time, true);
|
var fileListAnswer = await this.AddAIResponseAsync(time, true);
|
||||||
|
if (!ERIServerFileList.TryParse(fileListAnswer, out var filesToGenerate))
|
||||||
|
{
|
||||||
|
this.Logger.LogWarning("The list of files could not be read from the answer of the model: '{Answer}'", fileListAnswer);
|
||||||
|
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, T("The list of files in the answer of the model could not be read, so no files were created. Please try again.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Is this an update of the ERI server? If so, we need to delete the previously generated files.
|
// Is this an update of the ERI server? If so, we need to delete the previously generated files.
|
||||||
@@ -1165,7 +929,7 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
// and it outlives a change of the base directory, as well as a restore on another computer.
|
// and it outlives a change of the base directory, as well as a restore on another computer.
|
||||||
// There, the same path may belong to files of somebody else:
|
// There, the same path may belong to files of somebody else:
|
||||||
//
|
//
|
||||||
if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0 && !string.IsNullOrWhiteSpace(fileListAnswer))
|
if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0)
|
||||||
{
|
{
|
||||||
foreach (var file in previouslyGeneratedFilesSnapshot)
|
foreach (var file in previouslyGeneratedFilesSnapshot)
|
||||||
{
|
{
|
||||||
@@ -1195,7 +959,6 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var generatedFiles = new List<string>();
|
var generatedFiles = new List<string>();
|
||||||
var filesToGenerate = this.ExtractFiles(fileListAnswer).ToArray();
|
|
||||||
foreach (var file in filesToGenerate)
|
foreach (var file in filesToGenerate)
|
||||||
{
|
{
|
||||||
this.Logger.LogInformation($"The LLM want to create the file: '{file}'");
|
this.Logger.LogInformation($"The LLM want to create the file: '{file}'");
|
||||||
@@ -1214,48 +977,18 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
|||||||
```language
|
```language
|
||||||
content of the file
|
content of the file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When the file itself contains code blocks, enclose its content in a longer
|
||||||
|
fence, such as four backticks.
|
||||||
""", true);
|
""", true);
|
||||||
var generatedCodeMarkdown = await this.AddAIResponseAsync(time);
|
var generatedCodeMarkdown = await this.AddAIResponseAsync(time);
|
||||||
if (writeToFilesystemSnapshot)
|
if (writeToFilesystemSnapshot)
|
||||||
{
|
{
|
||||||
var desiredFilePath = Path.Join(baseDirectorySnapshot, file);
|
var outcome = await ERIServerFiles.WriteAsync(baseDirectorySnapshot, file, generatedCodeMarkdown, this.Logger);
|
||||||
|
if (outcome is ERIServerFileOutcome.WRITTEN)
|
||||||
// Security check: ensure that the desired file path is inside the base directory.
|
generatedFiles.Add(ERIServerFiles.PathOf(baseDirectorySnapshot, file));
|
||||||
// We cannot trust the beginning of the file path because it would be possible
|
|
||||||
// to escape by using `..` in the file path.
|
|
||||||
if (!PathTools.IsInsideDirectory(baseDirectorySnapshot, desiredFilePath))
|
|
||||||
this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{baseDirectorySnapshot}'.");
|
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, outcome.ToUserMessage(file)));
|
||||||
var code = this.ExtractCode(generatedCodeMarkdown);
|
|
||||||
if (string.IsNullOrWhiteSpace(code))
|
|
||||||
this.Logger.LogWarning($"The file content for '{desiredFilePath}' is empty or was not found.");
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
// Ensure that the directory exists:
|
|
||||||
var fileDirectory = Path.GetDirectoryName(desiredFilePath);
|
|
||||||
if (fileDirectory is null)
|
|
||||||
this.Logger.LogWarning($"The file path '{desiredFilePath}' does not contain a directory.");
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
generatedFiles.Add(desiredFilePath);
|
|
||||||
var fileDirectoryInfo = new DirectoryInfo(fileDirectory);
|
|
||||||
if(!fileDirectoryInfo.Exists)
|
|
||||||
{
|
|
||||||
fileDirectoryInfo.Create();
|
|
||||||
this.Logger.LogInformation($"The directory '{fileDirectory}' was created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the file to the file system:
|
|
||||||
await File.WriteAllTextAsync(desiredFilePath, code, Encoding.UTF8);
|
|
||||||
this.Logger.LogInformation($"The file '{desiredFilePath}' was created.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The files which the model of the ERI Server assistant wants to create.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The model answers with a JSON object of the form {"files": ["path/to/file1", "path/to/file2"]}. Up to
|
||||||
|
/// v26.10.1, the assistant asked for invalid JSON, an object holding a bare list, and took every string of
|
||||||
|
/// the answer for a file, apart from the word "files". A path with a space was lost, while any other key
|
||||||
|
/// the model came up with became a file.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class ERIServerFileList
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
AllowTrailingCommas = false,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Disallow,
|
||||||
|
MaxDepth = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The paths of the files, relative to the base directory.
|
||||||
|
/// </summary>
|
||||||
|
public List<string>? Files { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the list of files out of the answer of the model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="answer">The answer of the model to the request for the list of files.</param>
|
||||||
|
/// <param name="files">The paths of the files, without empty ones, or an empty list when the answer
|
||||||
|
/// holds none.</param>
|
||||||
|
/// <returns>True, when the answer holds a list with at least one file.</returns>
|
||||||
|
public static bool TryParse(string answer, out IReadOnlyList<string> files)
|
||||||
|
{
|
||||||
|
files = [];
|
||||||
|
var json = ModelAnswerJson.ExtractObject(answer);
|
||||||
|
if (json.IsWhiteSpace())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ERIServerFileList? parsed;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parsed = JsonSerializer.Deserialize<ERIServerFileList>(json, JSON_OPTIONS);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths = parsed?.Files?.Where(file => !string.IsNullOrWhiteSpace(file)).ToList() ?? [];
|
||||||
|
if (paths.Count is 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
files = paths;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What became of a file which the model of the ERI Server assistant created.
|
||||||
|
/// </summary>
|
||||||
|
internal enum ERIServerFileOutcome
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The file was written below the base directory.
|
||||||
|
/// </summary>
|
||||||
|
WRITTEN,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of the file leads outside the base directory, so the file was not written.
|
||||||
|
/// </summary>
|
||||||
|
OUTSIDE_BASE_DIRECTORY,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The answer holds no complete code block, so there was nothing to write.
|
||||||
|
/// </summary>
|
||||||
|
NO_COMPLETE_CODE_BLOCK,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writing the file failed, e.g., for lack of permission.
|
||||||
|
/// </summary>
|
||||||
|
WRITE_FAILED,
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tells the user why the ERI Server assistant did not write a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Up to v26.10.1, such a file only showed up in the log, and the user found out by comparing the
|
||||||
|
/// base directory with the list of files in the answer.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class ERIServerFileOutcomeExtensions
|
||||||
|
{
|
||||||
|
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ERIServerFileOutcomeExtensions).Namespace, nameof(ERIServerFileOutcomeExtensions));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the localized message which explains why a file was not written.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outcome">What became of the file.</param>
|
||||||
|
/// <param name="file">The path of the file, as the model named it.</param>
|
||||||
|
/// <returns>The localized message.</returns>
|
||||||
|
internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file);
|
||||||
|
|
||||||
|
private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch
|
||||||
|
{
|
||||||
|
ERIServerFileOutcome.OUTSIDE_BASE_DIRECTORY => TB("The file '{0}' was not written, because its path leads outside the base directory."),
|
||||||
|
ERIServerFileOutcome.NO_COMPLETE_CODE_BLOCK => TB("The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation."),
|
||||||
|
ERIServerFileOutcome.WRITE_FAILED => TB("The file '{0}' could not be written. The log of AI Studio names the reason."),
|
||||||
|
|
||||||
|
_ => TB("The file '{0}' was not written."),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Markdig.Extensions.Mathematics;
|
||||||
|
using Markdig.Syntax;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the files of an ERI server out of the answers of the model and writes them to disk.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ERIServerFiles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the content of a file out of the answer which wrote it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The model is asked for a heading with the path of the file, followed by a code block with its
|
||||||
|
/// content. Only the code block counts: models write the heading as they please, e.g., with the
|
||||||
|
/// path in backticks, with a "File:" in front, or not at all. Up to v26.10.1, a regular expression
|
||||||
|
/// insisted on a heading without a hyphen, which left out files such as docker-compose.yml, and it
|
||||||
|
/// read up to the last fence of the answer, which merged a usage example into the file.
|
||||||
|
///
|
||||||
|
/// Markdig reads the answer the way the chat shows it. So we take the first complete code block,
|
||||||
|
/// and a file which holds code blocks itself stays whole when the model fences it with more
|
||||||
|
/// backticks. A block the model never closed is left out: the answer broke off, and half a file
|
||||||
|
/// is nothing to write to disk.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="answer">The answer of the model to the request for one file.</param>
|
||||||
|
/// <returns>The content of the file, ending with a line break, or an empty string when the
|
||||||
|
/// answer holds no complete code block.</returns>
|
||||||
|
public static string ReadFileContent(string answer)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(answer))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Markdig reads a formula between $$ as a MathBlock, which is a fenced code block as well:
|
||||||
|
//
|
||||||
|
var document = Markdig.Markdown.Parse(answer, Markdown.SAFE_MARKDOWN_PIPELINE);
|
||||||
|
var codeBlock = document.Descendants<FencedCodeBlock>().FirstOrDefault(block => block is not MathBlock && block.ClosingFencedCharCount > 0);
|
||||||
|
if (codeBlock is null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var content = codeBlock.Lines.ToString();
|
||||||
|
return content.Length is 0 ? string.Empty : $"{content}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path to which a file of the model is written.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseDirectory">The base directory of the ERI server.</param>
|
||||||
|
/// <param name="file">The path of the file, as the model named it.</param>
|
||||||
|
/// <returns>The path of the file below the base directory.</returns>
|
||||||
|
public static string PathOf(string baseDirectory, string file) => Path.Join(baseDirectory, file);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a file of the model below the base directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A file which cannot be written ends nothing but its own attempt: the caller tells the user,
|
||||||
|
/// and the server gets its other files. Up to v26.10.1, an error while writing ended the whole
|
||||||
|
/// run, and the files written so far were never recorded for their deletion on the next run.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="baseDirectory">The base directory of the ERI server.</param>
|
||||||
|
/// <param name="file">The path of the file, as the model named it.</param>
|
||||||
|
/// <param name="answer">The answer of the model to the request for this file.</param>
|
||||||
|
/// <param name="logger">The logger which records the details.</param>
|
||||||
|
/// <returns>What became of the file.</returns>
|
||||||
|
public static async Task<ERIServerFileOutcome> WriteAsync(string baseDirectory, string file, string answer, ILogger logger)
|
||||||
|
{
|
||||||
|
var desiredFilePath = PathOf(baseDirectory, file);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Security check: ensure that the desired file path is inside the base directory.
|
||||||
|
// We cannot trust the beginning of the file path because it would be possible
|
||||||
|
// to escape by using `..` in the file path.
|
||||||
|
//
|
||||||
|
if (!PathTools.IsInsideDirectory(baseDirectory, desiredFilePath))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The file path '{FilePath}' is not inside the base directory '{BaseDirectory}'.", desiredFilePath, baseDirectory);
|
||||||
|
return ERIServerFileOutcome.OUTSIDE_BASE_DIRECTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = ReadFileContent(answer);
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The content of the file '{FilePath}' is empty or was not found.", desiredFilePath);
|
||||||
|
return ERIServerFileOutcome.NO_COMPLETE_CODE_BLOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// A path inside the base directory always has a directory:
|
||||||
|
var fileDirectory = Path.GetDirectoryName(desiredFilePath)!;
|
||||||
|
if (!Directory.Exists(fileDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(fileDirectory);
|
||||||
|
logger.LogInformation("The directory '{Directory}' was created.", fileDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(desiredFilePath, content, Encoding.UTF8);
|
||||||
|
logger.LogInformation("The file '{FilePath}' was created.", desiredFilePath);
|
||||||
|
return ERIServerFileOutcome.WRITTEN;
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(e, "The file '{FilePath}' could not be written.", desiredFilePath);
|
||||||
|
return ERIServerFileOutcome.WRITE_FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What the ERI Server assistant tells the model about the server it has to implement.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Kept apart from the assistant, so that the system prompt can be checked without a running app:
|
||||||
|
/// every input of the form has to arrive in it, and in a shape the model can read.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class ERIServerPrompt
|
||||||
|
{
|
||||||
|
public ProgrammingLanguages ProgrammingLanguage { get; init; } = ProgrammingLanguages.NONE;
|
||||||
|
|
||||||
|
public string OtherProgrammingLanguage { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The OpenAPI description of the ERI, or null while it is not loaded yet.
|
||||||
|
/// </summary>
|
||||||
|
public string? ERISpecification { get; init; }
|
||||||
|
|
||||||
|
public string ServerName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ServerDescription { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public DataSources DataSource { get; init; } = DataSources.NONE;
|
||||||
|
|
||||||
|
public string OtherDataSource { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string DataSourceProductName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string DataSourceHostname { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public int? DataSourcePort { get; init; }
|
||||||
|
|
||||||
|
public IEnumerable<Auth> AuthenticationMethods { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How the authentication methods are meant to be used, e.g., which data is public.
|
||||||
|
/// </summary>
|
||||||
|
public string AuthDescription { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public OperatingSystem OperatingSystem { get; init; } = OperatingSystem.NONE;
|
||||||
|
|
||||||
|
public AllowedLLMProviders AllowedLLMProviders { get; init; } = AllowedLLMProviders.NONE;
|
||||||
|
|
||||||
|
public IReadOnlyList<RetrievalInfo> RetrievalProcesses { get; init; } = [];
|
||||||
|
|
||||||
|
public string AdditionalLibraries { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ToSystemPrompt()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Introduction
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
var programmingLanguage = this.ProgrammingLanguage is ProgrammingLanguages.OTHER ? this.OtherProgrammingLanguage : this.ProgrammingLanguage.ToPrompt();
|
||||||
|
sb.Append($"""
|
||||||
|
# Introduction
|
||||||
|
You are an experienced {programmingLanguage} developer. Your task is to implement an API server in
|
||||||
|
the {programmingLanguage} language according to the following OpenAPI Description (OAD):
|
||||||
|
|
||||||
|
```
|
||||||
|
{this.ERISpecification}
|
||||||
|
```
|
||||||
|
|
||||||
|
The server realizes the data retrieval component for a "Retrieval-Augmentation Generation" (RAG) process.
|
||||||
|
The server is called "{this.ServerName}" and is described as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{this.ServerDescription}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Data Source
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
# Data Source
|
||||||
|
""");
|
||||||
|
|
||||||
|
switch (this.DataSource)
|
||||||
|
{
|
||||||
|
case DataSources.CUSTOM:
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The data source for the retrieval process is described as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{this.OtherDataSource}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(this.DataSourceHostname))
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The data source is accessible via the hostname `{this.DataSourceHostname}`.
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.DataSourcePort is not null)
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The data source is accessible via port `{this.DataSourcePort}`.
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSources.FILE_SYSTEM:
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
The data source for the retrieval process is the local file system. Use a placeholder for the data source path.
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
case DataSources.OBJECT_STORAGE:
|
||||||
|
case DataSources.KEY_VALUE_STORE:
|
||||||
|
case DataSources.DOCUMENT_STORE:
|
||||||
|
case DataSources.RELATIONAL_DATABASE:
|
||||||
|
case DataSources.GRAPH_DATABASE:
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The data source for the retrieval process is an "{this.DataSourceProductName}" database running on the
|
||||||
|
host `{this.DataSourceHostname}` and is accessible via port `{this.DataSourcePort}`.
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Authentication and Authorization
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
# Authentication and Authorization
|
||||||
|
The process for authentication and authorization is two-step. Step 1: Users must authenticate
|
||||||
|
with the API server using a `POST` call on `/auth` with the chosen method. If this step is
|
||||||
|
successful, the API server returns a token. Step 2: This token is then required for all
|
||||||
|
other API calls.
|
||||||
|
|
||||||
|
Important notes:
|
||||||
|
- Calls to `/auth` and `/auth/methods` are accessible without authentication. All other API
|
||||||
|
endpoints require a valid step 2 token.
|
||||||
|
|
||||||
|
- It is possible that a token (step 1 token) is desired as `authMethod`. These step 1 tokens
|
||||||
|
must never be accepted as valid tokens for step 2.
|
||||||
|
|
||||||
|
- The OpenAPI Description (OAD) for `/auth` is not complete. This is because, at the time of
|
||||||
|
writing the OAD, it is not known what kind of authentication is desired. Therefore, you must
|
||||||
|
supplement the corresponding fields or data in the implementation. Example: If username/password
|
||||||
|
is desired, you must expect and read both. If both token and username/password are desired, you
|
||||||
|
must dynamically read the `authMethod` and expect and evaluate different fields accordingly.
|
||||||
|
|
||||||
|
The following authentications and authorizations should be implemented for the API server:
|
||||||
|
""");
|
||||||
|
|
||||||
|
foreach (var auth in this.AuthenticationMethods)
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
- {auth.ToPrompt()}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(this.AuthDescription))
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The authentication process is planned as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{this.AuthDescription}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.AuthenticationMethods.Contains(Auth.KERBEROS))
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
The server will run on {this.OperatingSystem.ToPrompt()} operating systems. Keep
|
||||||
|
this in mind when implementing the SSO with Kerberos.
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Security
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
# Security
|
||||||
|
The following security requirement for `allowedProviderType` was chosen: `{this.AllowedLLMProviders}`
|
||||||
|
""");
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Retrieval Processes
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
# Retrieval Processes
|
||||||
|
You are implementing the following data retrieval processes:
|
||||||
|
""");
|
||||||
|
|
||||||
|
var retrievalProcessCounter = 1;
|
||||||
|
foreach (var retrievalProcess in this.RetrievalProcesses)
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
## {retrievalProcessCounter++}. Retrieval Process
|
||||||
|
- Name: {retrievalProcess.Name}
|
||||||
|
- Description:
|
||||||
|
|
||||||
|
```
|
||||||
|
{retrievalProcess.Description}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
|
||||||
|
if(retrievalProcess.ParametersDescription?.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
This retrieval process recognizes the following parameters:
|
||||||
|
""");
|
||||||
|
|
||||||
|
var parameterCounter = 1;
|
||||||
|
foreach (var (parameter, description) in retrievalProcess.ParametersDescription)
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
- The {parameterCounter++} parameter is named "{parameter}":
|
||||||
|
|
||||||
|
```
|
||||||
|
{description}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
Please use sensible default values for the parameters. They are optional
|
||||||
|
for the user.
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(retrievalProcess.Embeddings?.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
The following embeddings are implemented for this retrieval process:
|
||||||
|
""");
|
||||||
|
|
||||||
|
var embeddingCounter = 1;
|
||||||
|
foreach (var embedding in retrievalProcess.Embeddings)
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
- {embeddingCounter++}. Embedding
|
||||||
|
- Name: {embedding.EmbeddingName}
|
||||||
|
- Type: {embedding.EmbeddingType}
|
||||||
|
|
||||||
|
- Description:
|
||||||
|
|
||||||
|
```
|
||||||
|
{embedding.Description}
|
||||||
|
```
|
||||||
|
|
||||||
|
- When used:
|
||||||
|
|
||||||
|
```
|
||||||
|
{embedding.UsedWhen}
|
||||||
|
```
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Additional Libraries
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(this.AdditionalLibraries))
|
||||||
|
{
|
||||||
|
sb.Append($"""
|
||||||
|
|
||||||
|
# Additional Libraries
|
||||||
|
You use the following libraries for your implementation:
|
||||||
|
|
||||||
|
{this.AdditionalLibraries}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------------------------------
|
||||||
|
// Remarks
|
||||||
|
// ---------------------------------
|
||||||
|
//
|
||||||
|
|
||||||
|
sb.Append("""
|
||||||
|
|
||||||
|
# Remarks
|
||||||
|
- You do not ask follow-up questions.
|
||||||
|
- You consider the security of the implementation by applying the Security by Design principle.
|
||||||
|
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
|
||||||
|
create a separate code block with its file path and name as chapter title.
|
||||||
|
- Important: The JSON objects of the API messages use camel case for the data field names.
|
||||||
|
""");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,26 +2,14 @@ namespace AIStudio.Assistants.ERI;
|
|||||||
|
|
||||||
public static class ERIVersionExtensions
|
public static class ERIVersionExtensions
|
||||||
{
|
{
|
||||||
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
|
public static Task<string> ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var url = version.SpecificationURL();
|
|
||||||
using var response = await httpClient.GetAsync(url);
|
|
||||||
return await response.Content.ReadAsStringAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SpecificationURL(this ERIVersion version)
|
public static string SpecificationURL(this ERIVersion version)
|
||||||
{
|
{
|
||||||
var nameLower = version.ToString().ToLowerInvariant();
|
var nameLower = version.ToString().ToLowerInvariant();
|
||||||
var filename = $"{nameLower}.json";
|
var filename = $"{nameLower}.json";
|
||||||
return $"specs/eri/{filename}";
|
return $"specs/eri/{filename}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
|
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
|
||||||
|
|
||||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to check")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to check")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
@@ -135,7 +135,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialog
|
|||||||
|
|
||||||
private async Task ProofreadText()
|
private async Task ProofreadText()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
|
|||||||
|
|
||||||
private async Task LocalizeTextContent()
|
private async Task LocalizeTextContent()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -1537,12 +1537,21 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2205711729"] = "Opera
|
|||||||
-- You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
-- You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2241311898"] = "You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2241311898"] = "You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected."
|
||||||
|
|
||||||
|
-- The ERI specification was saved.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2258857117"] = "The ERI specification was saved."
|
||||||
|
|
||||||
|
-- Save the ERI specification
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2260814268"] = "Save the ERI specification"
|
||||||
|
|
||||||
-- The ERI specification could not be loaded. Please try again later.
|
-- The ERI specification could not be loaded. Please try again later.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2265640084"] = "The ERI specification could not be loaded. Please try again later."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2265640084"] = "The ERI specification could not be loaded. Please try again later."
|
||||||
|
|
||||||
-- You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions.
|
-- You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2299793803"] = "You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2299793803"] = "You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions."
|
||||||
|
|
||||||
|
-- The ERI specification could not be saved.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2361742875"] = "The ERI specification could not be saved."
|
||||||
|
|
||||||
-- Are you sure you want to delete the ERI server preset '{0}'?
|
-- Are you sure you want to delete the ERI server preset '{0}'?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2417944396"] = "Are you sure you want to delete the ERI server preset '{0}'?"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2417944396"] = "Are you sure you want to delete the ERI server preset '{0}'?"
|
||||||
|
|
||||||
@@ -1624,6 +1633,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3315518334"] = "Data
|
|||||||
-- No, just show me the code
|
-- No, just show me the code
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3316699247"] = "No, just show me the code"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3316699247"] = "No, just show me the code"
|
||||||
|
|
||||||
|
-- The list of files in the answer of the model could not be read, so no files were created. Please try again.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3342789164"] = "The list of files in the answer of the model could not be read, so no files were created. Please try again."
|
||||||
|
|
||||||
-- Other language
|
-- Other language
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3374524465"] = "Other language"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3374524465"] = "Other language"
|
||||||
|
|
||||||
@@ -1669,9 +1681,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3709185824"] = "Yes,
|
|||||||
-- Common ERI server settings
|
-- Common ERI server settings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T371718034"] = "Common ERI server settings"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T371718034"] = "Common ERI server settings"
|
||||||
|
|
||||||
-- Download specification
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3770339156"] = "Download specification"
|
|
||||||
|
|
||||||
-- Are you sure you want to delete the embedding '{0}'?
|
-- Are you sure you want to delete the embedding '{0}'?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3792084967"] = "Are you sure you want to delete the embedding '{0}'?"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3792084967"] = "Are you sure you want to delete the embedding '{0}'?"
|
||||||
|
|
||||||
@@ -1738,6 +1747,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T746826190"] = "ERI se
|
|||||||
-- Are you sure you want to delete the retrieval process '{0}'?
|
-- Are you sure you want to delete the retrieval process '{0}'?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T748762927"] = "Are you sure you want to delete the retrieval process '{0}'?"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T748762927"] = "Are you sure you want to delete the retrieval process '{0}'?"
|
||||||
|
|
||||||
|
-- Save specification
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T756799053"] = "Save specification"
|
||||||
|
|
||||||
-- Data source: hostname
|
-- Data source: hostname
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T761552889"] = "Data source: hostname"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T761552889"] = "Data source: hostname"
|
||||||
|
|
||||||
@@ -1792,6 +1804,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::DATASOURCESEXTENSIONS::T3394837922"]
|
|||||||
-- Unknown data source
|
-- Unknown data source
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::DATASOURCESEXTENSIONS::T752426884"] = "Unknown data source"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::DATASOURCESEXTENSIONS::T752426884"] = "Unknown data source"
|
||||||
|
|
||||||
|
-- The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T1485612676"] = "The file '{0}' was not written, because the answer of the model contains no complete code block for it. You find the answer in the conversation."
|
||||||
|
|
||||||
|
-- The file '{0}' was not written.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T1622509075"] = "The file '{0}' was not written."
|
||||||
|
|
||||||
|
-- The file '{0}' could not be written. The log of AI Studio names the reason.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T2881284775"] = "The file '{0}' could not be written. The log of AI Studio names the reason."
|
||||||
|
|
||||||
|
-- The file '{0}' was not written, because its path leads outside the base directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ERISERVERFILEOUTCOMEEXTENSIONS::T3820652397"] = "The file '{0}' was not written, because its path leads outside the base directory."
|
||||||
|
|
||||||
-- Windows
|
-- Windows
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::OPERATINGSYSTEMEXTENSIONS::T104858250"] = "Windows"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::OPERATINGSYSTEMEXTENSIONS::T104858250"] = "Windows"
|
||||||
|
|
||||||
@@ -4009,9 +4033,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1028969501"] = "{0}, plus
|
|||||||
-- Move chat
|
-- Move chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat"
|
||||||
|
|
||||||
-- Are you sure you want to move this chat? All unsaved changes will be lost.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost."
|
|
||||||
|
|
||||||
-- Bold
|
-- Bold
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold"
|
||||||
|
|
||||||
@@ -4048,9 +4069,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0}
|
|||||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
||||||
|
|
||||||
-- Unsaved Changes
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Unsaved Changes"
|
|
||||||
|
|
||||||
-- Start New Chat
|
-- Start New Chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
||||||
|
|
||||||
@@ -4168,6 +4186,33 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T1984243691"] = "You c
|
|||||||
-- Choose File
|
-- Choose File
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T4285779702"] = "Choose File"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T4285779702"] = "Choose File"
|
||||||
|
|
||||||
|
-- Applies to this screen only ({0}). The app default shows AI Studio in the same size on every laptop. System scaling follows your operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T1103704506"] = "Applies to this screen only ({0}). The app default shows AI Studio in the same size on every laptop. System scaling follows your operating system."
|
||||||
|
|
||||||
|
-- You can choose from {0} % to {1} %. The app default on this screen is {2} %.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T1110635214"] = "You can choose from {0} % to {1} %. The app default on this screen is {2} %."
|
||||||
|
|
||||||
|
-- {0} × {1} pixels, {2:0.#} inches
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T2376030041"] = "{0} × {1} pixels, {2:0.#} inches"
|
||||||
|
|
||||||
|
-- You can choose from {0} % to {1} %. The system scaling on this screen is {2} %.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T3009448744"] = "You can choose from {0} % to {1} %. The system scaling on this screen is {2} %."
|
||||||
|
|
||||||
|
-- Custom interface size on this screen
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T3268281815"] = "Custom interface size on this screen"
|
||||||
|
|
||||||
|
-- AI Studio cannot measure this screen right now, so you cannot set the interface size here.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T3560281885"] = "AI Studio cannot measure this screen right now, so you cannot set the interface size here."
|
||||||
|
|
||||||
|
-- Interface size on this screen
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T3568633122"] = "Interface size on this screen"
|
||||||
|
|
||||||
|
-- Applies to this screen only ({0}). System scaling follows your operating system.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T3653703912"] = "Applies to this screen only ({0}). System scaling follows your operating system."
|
||||||
|
|
||||||
|
-- {0} × {1} pixels
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONINTERFACESIZE::T990356864"] = "{0} × {1} pixels"
|
||||||
|
|
||||||
-- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.
|
-- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."
|
||||||
|
|
||||||
@@ -11338,6 +11383,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1024253064"] = "Welcome to MindWork AI
|
|||||||
-- Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.
|
-- Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1146553980"] = "Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1146553980"] = "Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider."
|
||||||
|
|
||||||
|
-- The AI can answer from your own documents. Add folders and files from your computer, or your organization's data through ERI, as data sources, and the AI searches them whenever a question calls for it and names the passages it used. AI Studio keeps the index on your computer, and you choose the embedding provider, including one you host yourself. As a prototype, the AI can also search and read your mailboxes. Both are preview features, which you enable in the app settings first.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1406933829"] = "The AI can answer from your own documents. Add folders and files from your computer, or your organization's data through ERI, as data sources, and the AI searches them whenever a question calls for it and names the passages it used. AI Studio keeps the index on your computer, and you choose the embedding provider, including one you host yourself. As a prototype, the AI can also search and read your mailboxes. Both are preview features, which you enable in the app settings first."
|
||||||
|
|
||||||
-- The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
|
-- The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life."
|
||||||
|
|
||||||
@@ -11401,6 +11449,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3275341342"] = "We hope you enjoy using
|
|||||||
-- Cost-effective
|
-- Cost-effective
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective"
|
||||||
|
|
||||||
|
-- Your own data
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3665704402"] = "Your own data"
|
||||||
|
|
||||||
-- Flexibility
|
-- Flexibility
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility"
|
||||||
|
|
||||||
@@ -12250,9 +12301,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request
|
|||||||
-- The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region.
|
-- The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1133173666"] = "The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1133173666"] = "The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."
|
||||||
|
|
||||||
-- 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}'"
|
|
||||||
|
|
||||||
-- Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'
|
-- Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1856278860"] = "Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"
|
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1856278860"] = "Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"
|
||||||
|
|
||||||
@@ -12511,6 +12559,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480
|
|||||||
-- No key is sending the input
|
-- No key is sending the input
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "No key is sending the input"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "No key is sending the input"
|
||||||
|
|
||||||
|
-- App default
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1382522201"] = "App default"
|
||||||
|
|
||||||
-- Navigation never expands, no tooltips
|
-- Navigation never expands, no tooltips
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigation never expands, no tooltips"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigation never expands, no tooltips"
|
||||||
|
|
||||||
@@ -12556,6 +12607,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T238099712"
|
|||||||
-- Delete disappearing chats older than 1 year
|
-- Delete disappearing chats older than 1 year
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2405705561"] = "Delete disappearing chats older than 1 year"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2405705561"] = "Delete disappearing chats older than 1 year"
|
||||||
|
|
||||||
|
-- Custom
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2502277006"] = "Custom"
|
||||||
|
|
||||||
-- Store chats manually
|
-- Store chats manually
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2520325466"] = "Store chats manually"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2520325466"] = "Store chats manually"
|
||||||
|
|
||||||
@@ -12619,6 +12673,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669
|
|||||||
-- All preview features are hidden
|
-- All preview features are hidden
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "All preview features are hidden"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "All preview features are hidden"
|
||||||
|
|
||||||
|
-- System scaling
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T453866521"] = "System scaling"
|
||||||
|
|
||||||
-- Chat
|
-- Chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat"
|
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
|
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
|
||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogIconFinder>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogIconFinder>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="@T("Your context")" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="@T("Your context")" Variant="Variant.Outlined" Lines="3" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||||
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="@T("Your icon source")" Variant="Variant.Outlined" Margin="Margin.Dense">
|
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="@T("Your icon source")" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconF
|
|||||||
|
|
||||||
private async Task FindIcon()
|
private async Task FindIcon()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="@T("Provide the country, where the company is located")" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="@T("This is important to consider the legal framework of the country.")"/>
|
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="@T("Provide the country, where the company is located")" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="@T("This is important to consider the legal framework of the country.")"/>
|
||||||
@* Four zones, so no default target: every drop has to be aimed at the field it belongs to. *@
|
@* Four zones, so no default target: every drop has to be aimed at the field it belongs to. *@
|
||||||
<ReadFileContent Text="@T("Load the mandatory information from file")" @bind-FileContent="@this.inputMandatoryInformation" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load the mandatory information from file")" @bind-FileContent="@this.inputMandatoryInformation" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputMandatoryInformation" Label="@T("(Optional) Provide mandatory information")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.TextSnippet" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Mandatory information that your company requires for all job postings. This can include the company description, etc.")" />
|
<MudTextField T="string" @bind-Text="@this.inputMandatoryInformation" Label="@T("(Optional) Provide mandatory information")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.TextSnippet" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Mandatory information that your company requires for all job postings. This can include the company description, etc.")" />
|
||||||
<ReadFileContent Text="@T("Load the job description from file")" @bind-FileContent="@this.inputJobDescription" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load the job description from file")" @bind-FileContent="@this.inputJobDescription" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputJobDescription" Label="@T("Job description")" Validation="@this.ValidateJobDescription" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe what the person is supposed to do in the company. This might be just short bullet points.")" />
|
<MudTextField T="string" @bind-Text="@this.inputJobDescription" Label="@T("Job description")" Validation="@this.ValidateJobDescription" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe what the person is supposed to do in the company. This might be just short bullet points.")" />
|
||||||
<ReadFileContent Text="@T("Load the qualifications from file")" @bind-FileContent="@this.inputQualifications" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load the qualifications from file")" @bind-FileContent="@this.inputQualifications" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputQualifications" Label="@T("(Optional) Provide necessary job qualifications")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe what the person should bring to the table. This might be just short bullet points.")" />
|
<MudTextField T="string" @bind-Text="@this.inputQualifications" Label="@T("(Optional) Provide necessary job qualifications")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe what the person should bring to the table. This might be just short bullet points.")" />
|
||||||
<ReadFileContent Text="@T("Load the responsibilities from file")" @bind-FileContent="@this.inputResponsibilities" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load the responsibilities from file")" @bind-FileContent="@this.inputResponsibilities" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputResponsibilities" Label="@T("(Optional) Provide job responsibilities")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe the responsibilities the person should take on in the company.")" />
|
<MudTextField T="string" @bind-Text="@this.inputResponsibilities" Label="@T("(Optional) Provide job responsibilities")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Describe the responsibilities the person should take on in the company.")" />
|
||||||
<MudTextField T="string" @bind-Text="@this.inputWorkLocation" Label="@T("(Optional) Provide the work location")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.MyLocation" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputWorkLocation" Label="@T("(Optional) Provide the work location")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.MyLocation" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputEntryDate" Label="@T("(Optional) Provide the entry date")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputEntryDate" Label="@T("(Optional) Provide the entry date")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputValidUntil" Label="@T("(Optional) Provide the date until the job posting is valid")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputValidUntil" Label="@T("(Optional) Provide the date until the job posting is valid")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ public partial class AssistantJobPostings : AssistantBaseCore<SettingsDialogJobP
|
|||||||
|
|
||||||
private async Task CreateJobPosting()
|
private async Task CreateJobPosting()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
@* Two zones, so no default target: the user has to aim at the one they mean. *@
|
@* Two zones, so no default target: the user has to aim at the one they mean. *@
|
||||||
<ReadFileContent Text="@T("Load the legal document from file")" @bind-FileContent="@this.inputLegalDocument" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load the legal document from file")" @bind-FileContent="@this.inputLegalDocument" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Legal document")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Legal document")" Variant="Variant.Outlined" Lines="12" Sizing="InputSizing.Auto" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<ReadFileContent Text="@T("Load your questions from file")" @bind-FileContent="@this.inputQuestions" EnableDragDrop="true"/>
|
<ReadFileContent Text="@T("Load your questions from file")" @bind-FileContent="@this.inputQuestions" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="@T("Your questions")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="@T("Your questions")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
@@ -116,7 +116,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegal
|
|||||||
|
|
||||||
private async Task AskQuestions()
|
private async Task AskQuestions()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ public partial class AssistantLogViewer : MSGComponentBase
|
|||||||
return value == OTHER_OPTION_VALUE ? T("Other") : value;
|
return value == OTHER_OPTION_VALUE ? T("Other") : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMultiSelectionText(List<string?>? selectedValues)
|
private string GetMultiSelectionText(IReadOnlyList<string?>? selectedValues)
|
||||||
{
|
{
|
||||||
if (selectedValues is null || selectedValues.Count == 0)
|
if (selectedValues is null || selectedValues.Count == 0)
|
||||||
return T("All");
|
return T("All");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks>
|
||||||
|
|
||||||
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.CurrentProfile"/>
|
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.CurrentProfile"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Text or email")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Text or email")" Variant="Variant.Outlined" Lines="12" Sizing="InputSizing.Auto" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudText Typo="Typo.h6" Class="mb-1 mt-1">@T("Attach documents")</MudText>
|
<MudText Typo="Typo.h6" Class="mb-1 mt-1">@T("Attach documents")</MudText>
|
||||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
@T("You can enter text, attach one or more documents, or use both. At least one input is required.")
|
@T("You can enter text, attach one or more documents, or use both. At least one input is required.")
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
|
|||||||
private async Task OnDocumentsChanged(HashSet<FileAttachment> _)
|
private async Task OnDocumentsChanged(HashSet<FileAttachment> _)
|
||||||
{
|
{
|
||||||
if(this.Form is not null)
|
if(this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? ValidateProfile(Profile profile)
|
private string? ValidateProfile(Profile profile)
|
||||||
@@ -204,7 +204,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
|
|||||||
|
|
||||||
private async Task AnalyzeText()
|
private async Task AnalyzeText()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
Label="@T("Prompt or prompt description")"
|
Label="@T("Prompt or prompt description")"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Lines="8"
|
Lines="8"
|
||||||
AutoGrow="@true"
|
Sizing="InputSizing.Auto"
|
||||||
MaxLines="20"
|
MaxLines="20"
|
||||||
Class="mb-3"
|
Class="mb-3"
|
||||||
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
LabelOther="@T("Custom language")"/>
|
LabelOther="@T("Custom language")"/>
|
||||||
|
|
||||||
<MudTextField T="string"
|
<MudTextField T="string"
|
||||||
AutoGrow="true"
|
Sizing="InputSizing.Auto"
|
||||||
Lines="2"
|
Lines="2"
|
||||||
@bind-Text="@this.importantAspects"
|
@bind-Text="@this.importantAspects"
|
||||||
Class="mb-3"
|
Class="mb-3"
|
||||||
@@ -59,22 +59,22 @@
|
|||||||
|
|
||||||
<MudGrid Class="mb-3">
|
<MudGrid Class="mb-3">
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recClarityDirectness" Label="@T("Be clear and direct")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recClarityDirectness" Label="@T("Be clear and direct")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recExamplesContext" Label="@T("Add examples and context")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recExamplesContext" Label="@T("Add examples and context")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recSequentialSteps" Label="@T("Use sequential steps")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recSequentialSteps" Label="@T("Use sequential steps")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recStructureMarkers" Label="@T("Structure with markers")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recStructureMarkers" Label="@T("Structure with markers")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recRoleDefinition" Label="@T("Give the model a role")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recRoleDefinition" Label="@T("Give the model a role")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudTextField T="string" Value="@this.recLanguageChoice" Label="@T("Choose prompt language deliberately")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" AutoGrow="@true" />
|
<MudTextField T="string" Value="@this.recLanguageChoice" Label="@T("Choose prompt language deliberately")" ReadOnly="true" Variant="Variant.Outlined" Lines="2" Sizing="InputSizing.Auto" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
|||||||
|
|
||||||
private async Task OptimizePromptAsync()
|
private async Task OptimizePromptAsync()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite>
|
||||||
|
|
||||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to improve")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to improve")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
||||||
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Writing style")" AllowOther="@false" />
|
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Writing style")" AllowOther="@false" />
|
||||||
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="@T("Sentence structure")" />
|
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="@T("Sentence structure")" />
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogR
|
|||||||
|
|
||||||
private async Task RewriteText()
|
private async Task RewriteText()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public partial class AssistantSWOTAnalysis
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private async Task AnalyzeText()
|
private async Task AnalyzeText()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
<MudText Typo="Typo.h5" Class="mb-3">
|
<MudText Typo="Typo.h5" Class="mb-3">
|
||||||
@T("Input")
|
@T("Input")
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Subject of analysis")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Subject of analysis")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<ReadFileContent @bind-FileContent="@this.inputText" MediaImportTargetId="swot-analysis-subject" EnableDragDrop="true" CatchAllDocuments="true"/>
|
<ReadFileContent @bind-FileContent="@this.inputText" MediaImportTargetId="swot-analysis-subject" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||||
@T("Analysis Options")
|
@T("Analysis Options")
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.analysisGoal" Validation="@this.ValidatingAnalysisGoal" AdornmentIcon="@Icons.Material.Filled.TrackChanges" Adornment="Adornment.Start" Label="@T("Analysis goal")" Variant="Variant.Outlined" Lines="4" AutoGrow="@true" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.analysisGoal" Validation="@this.ValidatingAnalysisGoal" AdornmentIcon="@Icons.Material.Filled.TrackChanges" Adornment="Adornment.Start" Label="@T("Analysis goal")" Variant="Variant.Outlined" Lines="4" Sizing="InputSizing.Auto" Class="mb-3"/>
|
||||||
<ReadFileContent @bind-FileContent="@this.analysisGoal" MediaImportTargetId="swot-analysis-goal" EnableDragDrop="true"/>
|
<ReadFileContent @bind-FileContent="@this.analysisGoal" MediaImportTargetId="swot-analysis-goal" EnableDragDrop="true"/>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.importantAspects" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" Label="@T("(Optional) Analysis focus")" HelperText="@T("Specify topics the SWOT analysis should pay particular attention to.")" Variant="Variant.Outlined" Lines="4" AutoGrow="@true" Class="mb-3"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.importantAspects" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" Label="@T("(Optional) Analysis focus")" HelperText="@T("Specify topics the SWOT analysis should pay particular attention to.")" Variant="Variant.Outlined" Lines="4" Sizing="InputSizing.Auto" Class="mb-3"/>
|
||||||
<ReadFileContent @bind-FileContent="@this.importantAspects" MediaImportTargetId="swot-analysis-focus" EnableDragDrop="true"/>
|
<ReadFileContent @bind-FileContent="@this.importantAspects" MediaImportTargetId="swot-analysis-focus" EnableDragDrop="true"/>
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
|
||||||
|
|||||||
@@ -10,13 +10,22 @@ internal static class SwotAnalysisContentPresentation
|
|||||||
/// Fails the reading of stored data which holds a null where the result expects a value, so that
|
/// Fails the reading of stored data which holds a null where the result expects a value, so that
|
||||||
/// the result view never meets one.
|
/// the result view never meets one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
RespectNullableAnnotations = true,
|
RespectNullableAnnotations = true,
|
||||||
};
|
TypeInfoResolver = SwotAnalysisJsonContext.Default,
|
||||||
|
}.AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the analysis with the defaults of System.Text.Json, which let a null pass, unlike the reading.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly JsonSerializerOptions WRITE_OPTIONS = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
TypeInfoResolver = SwotAnalysisJsonContext.Default,
|
||||||
|
}.AsReadOnly();
|
||||||
|
|
||||||
public static ContentPresentation Create(SwotAnalysisResult result) =>
|
public static ContentPresentation Create(SwotAnalysisResult result) =>
|
||||||
ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, result);
|
ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, result, WRITE_OPTIONS);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the SWOT analysis stored as the presentation of a text, so it can be shown as a matrix.
|
/// Reads the SWOT analysis stored as the presentation of a text, so it can be shown as a matrix.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.SWOTAnalysis;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes a SWOT analysis which a chat stores, built at
|
||||||
|
/// compile time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
[JsonSerializable(typeof(SwotAnalysisResult))]
|
||||||
|
internal sealed partial class SwotAnalysisJsonContext : JsonSerializerContext;
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
@T("You can enter text, use one or more documents or images, or use both. At least one of these options is required.")
|
@T("You can enter text, use one or more documents or images, or use both. At least one of these options is required.")
|
||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputContent" Validation="@this.ValidatingContext" Adornment="Adornment.Start" Lines="6" MaxLines="12" AutoGrow="@false" Label="@T("Text content")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputContent" Validation="@this.ValidatingContext" Adornment="Adornment.Start" Lines="6" MaxLines="12" Sizing="InputSizing.Fixed" Label="@T("Text content")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
@* Side by side where the window is wide enough; each column ends with its zone, so both zones line up below explanations of different length: *@
|
@* Side by side where the window is wide enough; each column ends with its zone, so both zones line up below explanations of different length: *@
|
||||||
<MudGrid Class="mt-1">
|
<MudGrid Class="mt-1">
|
||||||
@@ -60,7 +60,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")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Sizing="InputSizing.Auto" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" 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">
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public partial class SlideAssistant
|
|||||||
this.pictureLibraryPath = path;
|
this.pictureLibraryPath = path;
|
||||||
await this.ReadPictureLibraryOverviewAsync();
|
await this.ReadPictureLibraryOverviewAsync();
|
||||||
if (this.Form is not null)
|
if (this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UseNoPictureLibrary() => await this.OnPictureLibraryChanged(string.Empty);
|
private async Task UseNoPictureLibrary() => await this.OnPictureLibraryChanged(string.Empty);
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ public partial class SlideAssistant
|
|||||||
this.LoadTemplate();
|
this.LoadTemplate();
|
||||||
this.ApplyPreselectedTemplateSlideUses();
|
this.ApplyPreselectedTemplateSlideUses();
|
||||||
if (this.Form is not null)
|
if (this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -299,9 +299,9 @@ public partial class SlideAssistant
|
|||||||
/// <returns>The name, or empty when the LLM decides, which it also does when the template has no layout of this name.</returns>
|
/// <returns>The name, or empty when the LLM decides, which it also does when the template has no layout of this name.</returns>
|
||||||
private string LayoutNameOf(LayoutPurpose purpose) => this.template is not null && this.layoutNames.TryGetValue(purpose, out var name) && this.template.FindFamilyByName(name) is not null ? name : string.Empty;
|
private string LayoutNameOf(LayoutPurpose purpose) => this.template is not null && this.layoutNames.TryGetValue(purpose, out var name) && this.template.FindFamilyByName(name) is not null ? name : string.Empty;
|
||||||
|
|
||||||
private void SetLayoutName(LayoutPurpose purpose, string name)
|
private void SetLayoutName(LayoutPurpose purpose, string? name)
|
||||||
{
|
{
|
||||||
if (name.Length == 0)
|
if (string.IsNullOrEmpty(name))
|
||||||
this.layoutNames.Remove(purpose);
|
this.layoutNames.Remove(purpose);
|
||||||
else
|
else
|
||||||
this.layoutNames[purpose] = name;
|
this.layoutNames[purpose] = name;
|
||||||
|
|||||||
@@ -427,14 +427,14 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
|
|||||||
{
|
{
|
||||||
await this.KeepSlidePicturesOutOfDocumentsAsync();
|
await this.KeepSlidePicturesOutOfDocumentsAsync();
|
||||||
if(this.Form is not null)
|
if(this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSlidePicturesChanged(HashSet<FileAttachment> _)
|
private async Task OnSlidePicturesChanged(HashSet<FileAttachment> _)
|
||||||
{
|
{
|
||||||
await this.KeepSlidePicturesOutOfDocumentsAsync();
|
await this.KeepSlidePicturesOutOfDocumentsAsync();
|
||||||
if(this.Form is not null)
|
if(this.Form is not null)
|
||||||
await this.Form.Validate();
|
await this.Form.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -622,7 +622,7 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
|
|||||||
|
|
||||||
private async Task CreateSlideBuilder()
|
private async Task CreateSlideBuilder()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogSynonyms>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogSynonyms>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="@T("Your word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="@T("Your word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="@T("(Optional) The context for the given word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" Sizing="InputSizing.Fixed" Label="@T("(Optional) The context for the given word or phrase")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
@@ -183,7 +183,7 @@ public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonym
|
|||||||
|
|
||||||
private async Task FindSynonyms()
|
private async Task FindSynonyms()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomLanguage" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomLanguage" />
|
||||||
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="@T("Target complexity")" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="@T("Your expertise")" ValidateOther="@this.ValidateExpertInField" />
|
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="@T("Target complexity")" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="@T("Your expertise")" ValidateOther="@this.ValidateExpertInField" />
|
||||||
<MudTextField T="string" AutoGrow="true" Lines="2" @bind-Text="@this.importantAspects" class="mb-3" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects for the LLM to focus on when generating a summary, such as summary length or specific topics to emphasize.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/>
|
<MudTextField T="string" Sizing="InputSizing.Auto" Lines="2" @bind-Text="@this.importantAspects" class="mb-3" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects for the LLM to focus on when generating a summary, such as summary length or specific topics to emphasize.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/>
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
@@ -160,7 +160,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogT
|
|||||||
|
|
||||||
private async Task SummarizeText()
|
private async Task SummarizeText()
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" Sizing="InputSizing.Auto" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTran
|
|||||||
|
|
||||||
private async Task TranslateText(bool force)
|
private async Task TranslateText(bool force)
|
||||||
{
|
{
|
||||||
await this.Form!.Validate();
|
await this.Form!.ValidateAsync();
|
||||||
if (!this.InputIsValid)
|
if (!this.InputIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<MudTextField T="string" @bind-Text="@this.editor.Author" Label="@T("Author (optional)")" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.editor.Author" Label="@T("Author (optional)")" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
<MudTextField T="string" @bind-Text="@this.editor.Instruction" Label="@T("Briefing scope, notes, or current change instruction (optional)")" Variant="Variant.Outlined" AutoGrow="true" Lines="3" Class="mt-3" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.editor.Instruction" Label="@T("Briefing scope, notes, or current change instruction (optional)")" Variant="Variant.Outlined" Sizing="InputSizing.Auto" Lines="3" Class="mt-3" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<EnumSelection T="VisualBriefingProtectionLevel"
|
<EnumSelection T="VisualBriefingProtectionLevel"
|
||||||
NameFunc="@this.ProtectionLevelName"
|
NameFunc="@this.ProtectionLevelName"
|
||||||
|
|||||||
+6
-5
@@ -37,7 +37,7 @@ public partial class VisualBriefingAssistant
|
|||||||
if (selected is not null)
|
if (selected is not null)
|
||||||
await this.ApplySelectedProjectAsync(selected);
|
await this.ApplySelectedProjectAsync(selected);
|
||||||
else
|
else
|
||||||
this.ClearSelectedProject();
|
await this.ClearSelectedProjectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -150,7 +150,7 @@ public partial class VisualBriefingAssistant
|
|||||||
this.BuildOrchestrator.ForgetBriefing(id);
|
this.BuildOrchestrator.ForgetBriefing(id);
|
||||||
this.BuildProgressService.Forget(id);
|
this.BuildProgressService.Forget(id);
|
||||||
|
|
||||||
this.ClearSelectedProject();
|
await this.ClearSelectedProjectAsync();
|
||||||
|
|
||||||
await this.ReloadListAsync();
|
await this.ReloadListAsync();
|
||||||
}
|
}
|
||||||
@@ -296,14 +296,14 @@ public partial class VisualBriefingAssistant
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.Store.RememberSelectionAsync(project.BriefingId);
|
await this.Store.RememberSelectionAsync(project.BriefingId);
|
||||||
this.ClearSelectedProject();
|
await this.ClearSelectedProjectAsync();
|
||||||
this.selectedProject = project;
|
this.selectedProject = project;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears editor-only state so an unavailable project cannot trigger saves or background work.
|
/// Clears editor-only state so an unavailable project cannot trigger saves or background work.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ClearSelectedProject()
|
private async Task ClearSelectedProjectAsync()
|
||||||
{
|
{
|
||||||
this.selectedProject = null;
|
this.selectedProject = null;
|
||||||
this.selectedBriefing = null;
|
this.selectedBriefing = null;
|
||||||
@@ -316,7 +316,8 @@ public partial class VisualBriefingAssistant
|
|||||||
this.lastPersistedState = string.Empty;
|
this.lastPersistedState = string.Empty;
|
||||||
this.formIssues = [];
|
this.formIssues = [];
|
||||||
this.formValidationPending = false;
|
this.formValidationPending = false;
|
||||||
this.visualBriefingForm?.ResetValidation();
|
if (this.visualBriefingForm is not null)
|
||||||
|
await this.visualBriefingForm.ResetValidationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ public partial class VisualBriefingAssistant : MSGComponentBase
|
|||||||
if (this.formValidationPending && this.visualBriefingForm is not null)
|
if (this.formValidationPending && this.visualBriefingForm is not null)
|
||||||
{
|
{
|
||||||
this.formValidationPending = false;
|
this.formValidationPending = false;
|
||||||
await this.visualBriefingForm.Validate();
|
await this.visualBriefingForm.ValidateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedBriefing is null || this.IsCurrentBusy)
|
if (this.selectedBriefing is null || this.IsCurrentBusy)
|
||||||
|
|||||||
@@ -26,11 +26,16 @@ public sealed class ChatDraft
|
|||||||
{
|
{
|
||||||
private readonly Lock stateLock = new();
|
private readonly Lock stateLock = new();
|
||||||
|
|
||||||
|
//
|
||||||
|
// The setters are internal only for reading the JSON, since its source generation reaches no private
|
||||||
|
// setter. Everything else changes the draft through the methods below, which hold the lock.
|
||||||
|
//
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The text of the message.
|
/// The text of the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public string Text { get; private set; } = string.Empty;
|
public string Text { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The files attached to the message, the transcripts of audio and video files included.
|
/// The files attached to the message, the transcripts of audio and video files included.
|
||||||
@@ -40,7 +45,7 @@ public sealed class ChatDraft
|
|||||||
/// transcript is still a transcript after the chat was loaded again.
|
/// transcript is still a transcript after the chat was loaded again.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public IReadOnlyList<FileAttachment> FileAttachments { get; private set; } = [];
|
public IReadOnlyList<FileAttachment> FileAttachments { get; internal set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The texts a preview read of the attached files, which sending takes along instead of reading the files again.
|
/// The texts a preview read of the attached files, which sending takes along instead of reading the files again.
|
||||||
@@ -53,7 +58,7 @@ public sealed class ChatDraft
|
|||||||
/// store holds the texts themselves.
|
/// store holds the texts themselves.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public IReadOnlyList<PreviewedText> PreviewedTexts { get; private set; } = [];
|
public IReadOnlyList<PreviewedText> PreviewedTexts { get; internal set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the draft holds neither text nor attachments.
|
/// Whether the draft holds neither text nor attachments.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using ERIContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
|
||||||
|
|
||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes chats, built at compile time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Two types with the same name need a name of their own for their contract, else the generator builds
|
||||||
|
/// only the first of them and warns with SYSLIB1031.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
[JsonSerializable(typeof(ChatThread))]
|
||||||
|
[JsonSerializable(typeof(ERIContentType), TypeInfoPropertyName = "ERIContentType")]
|
||||||
|
internal sealed partial class ChatJsonContext : JsonSerializerContext;
|
||||||
@@ -23,7 +23,7 @@ public sealed record ChatThread
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unique identifier of the chat thread.
|
/// The unique identifier of the chat thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid ChatId { get; init; }
|
public Guid ChatId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unique identifier of the workspace.
|
/// The unique identifier of the workspace.
|
||||||
@@ -42,16 +42,16 @@ public sealed record ChatThread
|
|||||||
/// Every copy of this thread made with a with expression shares the draft, because such a copy is
|
/// Every copy of this thread made with a with expression shares the draft, because such a copy is
|
||||||
/// still the same chat. A copy which becomes another chat needs a draft of its own.
|
/// still the same chat. A copy which becomes another chat needs a draft of its own.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ChatDraft Draft { get; init; } = new();
|
public ChatDraft Draft { get; set; } = new();
|
||||||
|
|
||||||
// Compatibility shim: until v26.9.1, a chat stored only the transcripts of its draft, in a list of their own. See documentation/compatibility-shims/2026-10-chat-pending-media-transcripts.md; remove after 2027-04-11.
|
// Compatibility shim: until v26.9.1, a chat stored only the transcripts of its draft, in a list of their own. See documentation/compatibility-shims/2026-10-chat-pending-media-transcripts.md; remove after 2027-04-11.
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
[JsonPropertyName("pending_media_transcripts")]
|
[JsonPropertyName("pending_media_transcripts")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
private List<ManagedTranscriptAttachment>? LegacyPendingMediaTranscripts
|
internal List<ManagedTranscriptAttachment>? LegacyPendingMediaTranscripts
|
||||||
{
|
{
|
||||||
get => null;
|
get => null;
|
||||||
init
|
set
|
||||||
{
|
{
|
||||||
if (value is not null)
|
if (value is not null)
|
||||||
this.Draft.AddAttachments(value);
|
this.Draft.AddAttachments(value);
|
||||||
@@ -127,7 +127,7 @@ public sealed record ChatThread
|
|||||||
/// continues it must meet the level.
|
/// continues it must meet the level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public ConfidenceLevel RequiredProviderConfidence { get; private set; } = ConfidenceLevel.NONE;
|
public ConfidenceLevel RequiredProviderConfidence { get; internal set; } = ConfidenceLevel.NONE;
|
||||||
|
|
||||||
public void RequireProviderConfidence(ConfidenceLevel minimumProviderConfidence)
|
public void RequireProviderConfidence(ConfidenceLevel minimumProviderConfidence)
|
||||||
{
|
{
|
||||||
@@ -164,7 +164,7 @@ public sealed record ChatThread
|
|||||||
/// internal content demands nothing.
|
/// internal content demands nothing.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public OutboundDataRequirement RequiredOutboundDataRestriction { get; private set; } = OutboundDataRequirement.NONE;
|
public OutboundDataRequirement RequiredOutboundDataRestriction { get; internal set; } = OutboundDataRequirement.NONE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tightens where this chat may still send data to what the data brought in demands, and
|
/// Tightens where this chat may still send data to what the data brought in demands, and
|
||||||
@@ -229,7 +229,7 @@ public sealed record ChatThread
|
|||||||
/// immutable, so that a copy of the thread never shares a set which grows later.
|
/// immutable, so that a copy of the thread never shares a set which grows later.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public ImmutableHashSet<string> WebAddressesWrittenByModels { get; private set; } = [];
|
public ImmutableHashSet<string> WebAddressesWrittenByModels { get; internal set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||||
@@ -244,7 +244,7 @@ public sealed record ChatThread
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The content blocks of the chat thread.
|
/// The content blocks of the chat thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ContentBlock> Blocks { get; init; } = [];
|
public List<ContentBlock> Blocks { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The component running this thread, which decides the tools it may use and what its requests name.
|
/// The component running this thread, which decides the tools it may use and what its requests name.
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ public class ContentBlock
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time when the content block was created.
|
/// Time when the content block was created.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset Time { get; init; }
|
public DateTimeOffset Time { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the content block, e.g., text, image, voice, etc.
|
/// Type of the content block, e.g., text, image, voice, etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContentType ContentType { get; init; } = ContentType.NONE;
|
public ContentType ContentType { get; set; } = ContentType.NONE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The content of the block.
|
/// The content of the block.
|
||||||
@@ -28,7 +28,7 @@ public class ContentBlock
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should the content block be hidden from the user?
|
/// Should the content block be hidden from the user?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HideFromUser { get; init; }
|
public bool HideFromUser { get; set; }
|
||||||
|
|
||||||
public ContentBlock DeepClone(bool changeHideState = false, bool hideFromUser = true) => new()
|
public ContentBlock DeepClone(bool changeHideState = false, bool hideFromUser = true) => new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -865,7 +865,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
if (this.RemoveBlockFunc is null || !this.BlockActionsEnabled())
|
if (this.RemoveBlockFunc is null || !this.BlockActionsEnabled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var remove = await this.DialogService.ShowMessageBox(
|
var remove = await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Remove Message"),
|
T("Remove Message"),
|
||||||
T("Do you really want to remove this message?"),
|
T("Do you really want to remove this message?"),
|
||||||
T("Yes, remove it"),
|
T("Yes, remove it"),
|
||||||
@@ -947,7 +947,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
if(this.Role is not ChatRole.AI)
|
if(this.Role is not ChatRole.AI)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var regenerate = await this.DialogService.ShowMessageBox(
|
var regenerate = await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Regenerate Message"),
|
T("Regenerate Message"),
|
||||||
T("Do you really want to regenerate this message?"),
|
T("Do you really want to regenerate this message?"),
|
||||||
T("Yes, regenerate it"),
|
T("Yes, regenerate it"),
|
||||||
@@ -962,7 +962,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.BlockActionsEnabled())
|
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.BlockActionsEnabled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var rollback = await this.DialogService.ShowMessageBox(
|
var rollback = await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Roll Back Chat"),
|
T("Roll Back Chat"),
|
||||||
T("Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."),
|
T("Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."),
|
||||||
T("Yes, roll back the chat"),
|
T("Yes, roll back the chat"),
|
||||||
@@ -991,7 +991,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
if(this.Role is not ChatRole.USER)
|
if(this.Role is not ChatRole.USER)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var edit = await this.DialogService.ShowMessageBox(
|
var edit = await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Edit Message"),
|
T("Edit Message"),
|
||||||
T("Do you really want to edit this message? In order to edit this message, the AI response will be deleted."),
|
T("Do you really want to edit this message? In order to edit this message, the AI response will be deleted."),
|
||||||
T("Yes, remove the AI response and edit it"),
|
T("Yes, remove the AI response and edit it"),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public sealed class ContentImage : IContent, IImageSource
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public required ContentImageSource SourceType { get; init; }
|
public required ContentImageSource SourceType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The image source.
|
/// The image source.
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public sealed class ContentPresentation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ContentPresentation Create<T>(ContentPresentationKind kind, T data) => new()
|
internal static ContentPresentation Create<T>(ContentPresentationKind kind, T data, JsonSerializerOptions options) => new()
|
||||||
{
|
{
|
||||||
Kind = kind,
|
Kind = kind,
|
||||||
Data = JsonSerializer.SerializeToElement(data),
|
Data = JsonSerializer.SerializeToElement(data, options),
|
||||||
};
|
};
|
||||||
|
|
||||||
internal ContentPresentation DeepClone() => new()
|
internal ContentPresentation DeepClone() => new()
|
||||||
|
|||||||
@@ -15,8 +15,22 @@ namespace AIStudio.Chat;
|
|||||||
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
|
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
|
||||||
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
|
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
|
||||||
[JsonDerivedType(typeof(ManagedTranscriptAttachment), typeDiscriminator: "managed_transcript")]
|
[JsonDerivedType(typeof(ManagedTranscriptAttachment), typeDiscriminator: "managed_transcript")]
|
||||||
public record FileAttachment(FileAttachmentType Type, string FileName, [property: JsonConverter(typeof(DataDirectoryRelativePathConverter))] string FilePath, long FileSizeBytes)
|
public record FileAttachment(FileAttachmentType Type, string FileName, string FilePath, long FileSizeBytes)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. A derived attachment gets the type from
|
||||||
|
// its own constructor, and the source generation of System.Text.Json sets every init-only property while it
|
||||||
|
// creates the object, also one missing in the JSON, which would lose that type. Keep them first and in the
|
||||||
|
// order of the parameters, which is the order they get stored in:
|
||||||
|
//
|
||||||
|
public FileAttachmentType Type { get; set; } = Type;
|
||||||
|
public string FileName { get; set; } = FileName;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(DataDirectoryRelativePathConverter))]
|
||||||
|
public string FilePath { get; set; } = FilePath;
|
||||||
|
|
||||||
|
public long FileSizeBytes { get; set; } = FileSizeBytes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the file type is forbidden and should not be attached.
|
/// Gets a value indicating whether the file type is forbidden and should not be attached.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -69,7 +83,7 @@ public record FileAttachment(FileAttachmentType Type, string FileName, [property
|
|||||||
/// of chat templates, while null.
|
/// of chat templates, while null.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public Guid? ProvenanceId { get; init; }
|
public Guid? ProvenanceId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The state of the file which the stored text was read from, see ProvenanceId, or null while
|
/// The state of the file which the stored text was read from, see ProvenanceId, or null while
|
||||||
@@ -80,7 +94,7 @@ public record FileAttachment(FileAttachmentType Type, string FileName, [property
|
|||||||
/// DataChat.ReadChangedAttachmentsAgain. Left out of the chat file while null.
|
/// DataChat.ReadChangedAttachmentsAgain. Left out of the chat file while null.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public FileVersion? Version { get; init; }
|
public FileVersion? Version { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the file changed since its text was stored.
|
/// Whether the file changed since its text was stored.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public record FileAttachmentImage(string FileName, string FilePath, long FileSiz
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ContentImageSource SourceType { get; init; } = ContentImageSource.LOCAL_PATH;
|
public ContentImageSource SourceType { get; set; } = ContentImageSource.LOCAL_PATH;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The image source.
|
/// The image source.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public interface IImageSource
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ContentImageSource SourceType { get; init; }
|
public ContentImageSource SourceType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The image source.
|
/// The image source.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed record ReportedTokenUsage
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What everything sent to the model cost.
|
/// What everything sent to the model cost.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PromptTokens { get; init; }
|
public int PromptTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Which model the number was charged for.
|
/// Which model the number was charged for.
|
||||||
@@ -31,7 +31,7 @@ public sealed record ReportedTokenUsage
|
|||||||
/// being an answer about the request which is about to be sent, and the estimate, wrong as it
|
/// being an answer about the request which is about to be sent, and the estimate, wrong as it
|
||||||
/// is, is at least wrong about the right model.
|
/// is, is at least wrong about the right model.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string ModelId { get; init; } = string.Empty;
|
public string ModelId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many blocks the conversation had when the request went out, this answer included.
|
/// How many blocks the conversation had when the request went out, this answer included.
|
||||||
@@ -45,7 +45,7 @@ public sealed record ReportedTokenUsage
|
|||||||
/// Taken when the request is sent rather than when the report arrives: whatever is deleted
|
/// Taken when the request is sent rather than when the report arrives: whatever is deleted
|
||||||
/// while the answer streams in was still part of what the provider counted.
|
/// while the answer streams in was still part of what the provider counted.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public int BlockCount { get; init; }
|
public int BlockCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// States the stored number as a usage again.
|
/// States the stored number as a usage again.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace AIStudio.Components;
|
|||||||
public partial class Changelog : MSGComponentBase
|
public partial class Changelog : MSGComponentBase
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
private HttpClient HttpClient { get; set; } = null!;
|
private WebRootFiles WebRootFiles { get; set; } = null!;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ public partial class Changelog : MSGComponentBase
|
|||||||
|
|
||||||
private async Task ReadLogAsync()
|
private async Task ReadLogAsync()
|
||||||
{
|
{
|
||||||
using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
this.LogContent = await this.WebRootFiles.ReadTextAsync($"changelog/{this.SelectedLog.Filename}");
|
||||||
this.LogContent = await response.Content.ReadAsStringAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
@ref="@this.inputField"
|
@ref="@this.inputField"
|
||||||
@bind-Text="@this.UserInput"
|
@bind-Text="@this.UserInput"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
AutoGrow="@true"
|
Sizing="InputSizing.Auto"
|
||||||
Lines="3"
|
Lines="3"
|
||||||
MaxLines="12"
|
MaxLines="12"
|
||||||
Label="@this.InputLabel"
|
Label="@this.InputLabel"
|
||||||
|
|||||||
@@ -674,6 +674,9 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var leftChatHadNoThread = !isFirstChat && this.loadedParameterChatId == Guid.Empty;
|
var leftChatHadNoThread = !isFirstChat && this.loadedParameterChatId == Guid.Empty;
|
||||||
|
if (!isFirstChat)
|
||||||
|
await this.StopAnswerOfLeftChatAsync(this.loadedParameterChatId);
|
||||||
|
|
||||||
this.loadedParameterChatId = chatId;
|
this.loadedParameterChatId = chatId;
|
||||||
this.loadedParameterWorkspaceId = workspaceId;
|
this.loadedParameterWorkspaceId = workspaceId;
|
||||||
|
|
||||||
@@ -1580,9 +1583,15 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and stores the chat the composer belongs to, when there is none yet, e.g., for what an
|
/// Creates the chat the composer belongs to, when there is none yet, e.g., for what an attachment
|
||||||
/// attachment brings along before the first message is sent.
|
/// brings along before the first message is sent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only while chats are stored automatically, the new chat reaches the disk right away. Otherwise, it
|
||||||
|
/// counts as unsaved, like a message typed into a new chat: a transcript made for it waits in the
|
||||||
|
/// staging area until the user saves the chat, see WorkspaceBehaviour.StoreChatAsync, and the next
|
||||||
|
/// start removes it when they never do.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="nameWithoutInput">The name of the chat while the user typed nothing yet.</param>
|
/// <param name="nameWithoutInput">The name of the chat while the user typed nothing yet.</param>
|
||||||
/// <returns>The chat of the composer.</returns>
|
/// <returns>The chat of the composer.</returns>
|
||||||
private async Task<ChatThread?> EnsureChatAsync(string nameWithoutInput)
|
private async Task<ChatThread?> EnsureChatAsync(string nameWithoutInput)
|
||||||
@@ -1615,7 +1624,11 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
// lists the new chat right away, together with the progress of the transcription, instead
|
// lists the new chat right away, together with the progress of the transcription, instead
|
||||||
// of only after the first message was sent:
|
// of only after the first message was sent:
|
||||||
//
|
//
|
||||||
await this.StoreAndListChatAsync(chat);
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
await this.StoreAndListChatAsync(chat);
|
||||||
|
else
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
|
||||||
await this.SyncForegroundChatAsync();
|
await this.SyncForegroundChatAsync();
|
||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
@@ -1766,7 +1779,7 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
||||||
|
|
||||||
// From here on, the AI job stores the chat, in every storage mode:
|
// From here on, the AI job stores the chat, while chats are stored automatically:
|
||||||
this.unstoredChat = null;
|
this.unstoredChat = null;
|
||||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||||
{
|
{
|
||||||
@@ -1786,6 +1799,28 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the answer which is still being written in a chat the user leaves, unless chats are stored automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only while chats are stored automatically, the AI job stores the answer, so that the user finds
|
||||||
|
/// it when coming back. Otherwise, the answer would end up nowhere, and writing it would only cost
|
||||||
|
/// the user. The user was asked before, since an answer being written counts as an unsaved change
|
||||||
|
/// then.<br/><br/>
|
||||||
|
///
|
||||||
|
/// Called only where the user leaves a chat: opening another one, starting a new one, or leaving the
|
||||||
|
/// chat page. Never when this component goes away, since the page creates it anew, e.g., whenever the
|
||||||
|
/// workspace sidebar is shown or hidden, while the chat stays.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="chatId">The chat being left.</param>
|
||||||
|
private async Task StopAnswerOfLeftChatAsync(Guid chatId)
|
||||||
|
{
|
||||||
|
if (chatId == Guid.Empty || this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.AIJobService.CancelChatGenerationAsync(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Takes over the tool selection of the chat that was just loaded or handed to this component.
|
/// Takes over the tool selection of the chat that was just loaded or handed to this component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1825,7 +1860,21 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
if (!this.CanThreadBeSaved)
|
if (!this.CanThreadBeSaved)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var stagedTranscripts = this.ChatThread.Draft.MediaTranscripts.Where(transcript => transcript.IsStaged).Select(transcript => transcript.FilePath).ToHashSet(StringComparer.Ordinal);
|
||||||
await this.StoreAndListChatAsync(this.ChatThread);
|
await this.StoreAndListChatAsync(this.ChatThread);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Storing moved the staged transcripts of the message being written into the chat directory,
|
||||||
|
// see WorkspaceBehaviour.StoreChatAsync. The composer still knows them by where they were, so
|
||||||
|
// sending the message would lose them:
|
||||||
|
//
|
||||||
|
if (stagedTranscripts.Count > 0)
|
||||||
|
{
|
||||||
|
this.ComposerState.FileAttachments.RemoveWhere(attachment => stagedTranscripts.Contains(attachment.FilePath));
|
||||||
|
foreach (var transcript in this.ChatThread.Draft.MediaTranscripts)
|
||||||
|
this.ComposerState.FileAttachments.Add(transcript);
|
||||||
|
}
|
||||||
|
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
|
||||||
// A new chat is on the disk now, like any other:
|
// A new chat is on the disk now, like any other:
|
||||||
@@ -1876,9 +1925,11 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
|
|
||||||
//
|
//
|
||||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||||
// about possible data loss:
|
// about possible data loss. An answer still being written is lost as well, whenever chats are
|
||||||
|
// not stored automatically, since leaving stops it, see StopAnswerOfLeftChatAsync:
|
||||||
//
|
//
|
||||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
var storageBehavior = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior;
|
||||||
|
if ((storageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) || (storageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY && this.IsCurrentChatStreaming))
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||||
{
|
{
|
||||||
@@ -1930,6 +1981,9 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Reset our state:
|
// Reset our state:
|
||||||
//
|
//
|
||||||
@@ -2034,19 +2088,6 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
if(this.ChatThread is null)
|
if(this.ChatThread is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
|
||||||
{
|
|
||||||
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
|
|
||||||
{
|
|
||||||
{ x => x.Message, T("Are you sure you want to move this chat? All unsaved changes will be lost.") },
|
|
||||||
};
|
|
||||||
|
|
||||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Unsaved Changes"), confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
|
||||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
|
||||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
||||||
{
|
{
|
||||||
{ x => x.Message, T("Please select the workspace where you want to move the chat to.") },
|
{ x => x.Message, T("Please select the workspace where you want to move the chat to.") },
|
||||||
@@ -2065,13 +2106,15 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
|
|
||||||
//
|
//
|
||||||
// Like saving and copying, the move goes through the workspace component whenever there is
|
// Like saving and copying, the move goes through the workspace component whenever there is
|
||||||
// one, so its tree shows the chat in the new place right away:
|
// one, so its tree shows the chat in the new place right away. Nothing the user did not save
|
||||||
|
// gets lost, so nothing is asked: it reaches the new place only while chats are stored
|
||||||
|
// automatically, and otherwise stays unsaved, see WorkspaceBehaviour.MoveChatAsync:
|
||||||
//
|
//
|
||||||
var previousWorkspaceId = this.ChatThread!.WorkspaceId;
|
var previousWorkspaceId = this.ChatThread!.WorkspaceId;
|
||||||
if (this.Workspaces is not null)
|
if (this.Workspaces is not null)
|
||||||
await this.Workspaces.MoveChatAsync(this.ChatThread!, workspaceId);
|
await this.Workspaces.MoveChatAsync(this.ChatThread!, workspaceId);
|
||||||
else
|
else
|
||||||
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId);
|
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId, this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY);
|
||||||
|
|
||||||
await this.FollowMovedChatAsync(previousWorkspaceId);
|
await this.FollowMovedChatAsync(previousWorkspaceId);
|
||||||
}
|
}
|
||||||
@@ -2204,6 +2247,9 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
{
|
{
|
||||||
await this.FlushDraftAsync();
|
await this.FlushDraftAsync();
|
||||||
await this.PreserveNewChatDraftAsync(this.ChatThread is null);
|
await this.PreserveNewChatDraftAsync(this.ChatThread is null);
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
|
||||||
|
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
this.ComposerState.Clear();
|
this.ComposerState.Clear();
|
||||||
this.ClearWorkspaceHeaderState();
|
this.ClearWorkspaceHeaderState();
|
||||||
@@ -2283,8 +2329,11 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
if (!this.blockActions.TryRemove(this.ChatThread, block))
|
if (!this.blockActions.TryRemove(this.ChatThread, block))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Like every other change, stored right away only while chats are stored automatically:
|
||||||
this.hasUnsavedChanges = true;
|
this.hasUnsavedChanges = true;
|
||||||
await this.SaveThread();
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
await this.SaveThread();
|
||||||
|
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2310,8 +2359,12 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
|
|
||||||
// The rollback reset the AI-selected data sources, which the data source selection still shows:
|
// The rollback reset the AI-selected data sources, which the data source selection still shows:
|
||||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||||
|
|
||||||
|
// Like every other change, stored right away only while chats are stored automatically:
|
||||||
this.hasUnsavedChanges = true;
|
this.hasUnsavedChanges = true;
|
||||||
await this.SaveThread();
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
await this.SaveThread();
|
||||||
|
|
||||||
this.tokenTracker?.Nudge();
|
this.tokenTracker?.Nudge();
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
await this.inputField.FocusAsync();
|
await this.inputField.FocusAsync();
|
||||||
@@ -2568,7 +2621,8 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
||||||
if (!snapshot.IsActive)
|
if (!snapshot.IsActive)
|
||||||
{
|
{
|
||||||
this.hasUnsavedChanges = false;
|
// The job stored the answer only while chats are stored automatically, see AIJobService.CheckpointChatAsync:
|
||||||
|
this.hasUnsavedChanges = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY;
|
||||||
this.previousInputForbidden = true;
|
this.previousInputForbidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2587,8 +2641,9 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
|
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
|
||||||
|
|
||||||
|
// Nothing stores an answer being written now, and leaving stops it, see StopAnswerOfLeftChatAsync:
|
||||||
if (this.IsCurrentChatStreaming)
|
if (this.IsCurrentChatStreaming)
|
||||||
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
|
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) true));
|
||||||
|
|
||||||
//
|
//
|
||||||
// A message the user started counts as unsaved on its own only while there is no chat
|
// A message the user started counts as unsaved on its own only while there is no chat
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
<ActivatorContent>
|
<ActivatorContent>
|
||||||
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
|
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
|
||||||
{
|
{
|
||||||
<MudButton IconSize="Size.Large" StartIcon="@this.ChatTemplateIcon(this.CurrentChatTemplate)" IconColor="Color.Default">
|
<MudButton IconSize="Size.Large" StartIcon="@this.ChatTemplateIcon(this.CurrentChatTemplate)" IconColor="Color.Default" OnClick="@context.ToggleAsync">
|
||||||
@this.CurrentChatTemplate.GetSafeName()
|
@this.CurrentChatTemplate.GetSafeName()
|
||||||
</MudButton>
|
</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.RateReview" />
|
<MudIconButton Icon="@Icons.Material.Filled.RateReview" OnClick="@context.ToggleAsync" />
|
||||||
}
|
}
|
||||||
</ActivatorContent>
|
</ActivatorContent>
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudTabs ActivePanelIndex="this.SelectedIndex" ActivePanelIndexChanged="async idx => await this.TabChanged(idx)" PanelClass="code-block mt-1" MinimumTabWidth="30px" Class="mt-2">
|
<MudTabs ActivePanelIndex="this.SelectedIndex" ActivePanelIndexChanged="async idx => await this.TabChanged(idx)" TabPanelsClass="code-block mt-1" MinimumTabWidth="30px" Class="mt-2">
|
||||||
@foreach (var block in this.blocks)
|
@foreach (var block in this.blocks)
|
||||||
{
|
{
|
||||||
<MudTabPanel Text="@block.Title">
|
<MudTabPanel Text="@block.Title">
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ public partial class ConfigurationDirectory : ConfigurationBaseCore
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void InternalUpdate(string text)
|
private void InternalUpdate(string? text)
|
||||||
{
|
{
|
||||||
this.timer.Stop();
|
this.timer.Stop();
|
||||||
this.internalText = text;
|
this.internalText = text ?? string.Empty;
|
||||||
this.timer.Start();
|
this.timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void InternalUpdate(string text)
|
private void InternalUpdate(string? text)
|
||||||
{
|
{
|
||||||
this.timer.Stop();
|
this.timer.Stop();
|
||||||
this.internalText = text;
|
this.internalText = text ?? string.Empty;
|
||||||
this.timer.Start();
|
this.timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using AIStudio.Settings
|
||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
|
@*
|
||||||
|
Another screen may offer other sizes, and a select that gets its new value before the item of
|
||||||
|
that value exists keeps showing the old one. The key builds the select anew for every screen:
|
||||||
|
*@
|
||||||
|
<ConfigurationSelect @key="@this.ScreenKey" OptionDescription="@T("Interface size on this screen")" SelectedValue="@(() => this.SizeOnScreen)" Data="@ConfigurationSelectDataFactory.GetInterfaceSizeData(this.OffersAppDefault)" SelectionUpdateAsync="@this.UpdateSize" Disabled="@(() => this.MeasuredScreen is null)" OptionHelp="@this.SizeHelp"/>
|
||||||
|
@if (this.MeasuredScreen is { } screen && this.SizeOnScreen is InterfaceSize.CUSTOM)
|
||||||
|
{
|
||||||
|
<ConfigurationSlider T="int" OptionDescription="@T("Custom interface size on this screen")" Min="@screen.MinPercent" Max="@screen.MaxPercent" Step="5" Unit="%" Value="@(() => this.ScalePercent(screen))" ValueUpdate="@this.UpdateScalePercent" Immediate="@false" OptionHelp="@this.ScaleHelp(screen)"/>
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The interface size of the screen AI Studio shows on: a selection of the size, and a slider for the
|
||||||
|
/// scale of the custom size.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Always shows and changes the screen the window is on right now. The runtime reports that screen
|
||||||
|
/// when the option opens, and again whenever the size on the screen changed, e.g., because the window
|
||||||
|
/// moved to another screen. The runtime applies the sizes itself; InterfaceSizeService hands them over
|
||||||
|
/// whenever the settings change.
|
||||||
|
/// </remarks>
|
||||||
|
public partial class ConfigurationInterfaceSize : MSGComponentBase
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The screen AI Studio shows on, as the runtime reported it last: when the option opened, and
|
||||||
|
/// again whenever the size on the screen changed, e.g., because the window moved to another
|
||||||
|
/// screen. Null when the runtime could not measure the screen.
|
||||||
|
/// </summary>
|
||||||
|
private InterfaceSizeResponse? interfaceSizeOnScreen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The screen whose size the option shows and changes, or null when it is unknown.
|
||||||
|
/// </summary>
|
||||||
|
private InterfaceSizeResponse? MeasuredScreen => this.interfaceSizeOnScreen is { ScreenKey.Length: > 0 } screen ? screen : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The key of the screen whose size the option shows, or an empty one when the screen is unknown.
|
||||||
|
/// </summary>
|
||||||
|
private string ScreenKey => this.MeasuredScreen?.ScreenKey ?? string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this screen offers the app default. It only differs from the system scaling on the
|
||||||
|
/// screen of a laptop, which is also the only screen it is the default of.
|
||||||
|
/// </summary>
|
||||||
|
private bool OffersAppDefault => this.MeasuredScreen?.DefaultSize is InterfaceSize.APP_DEFAULT;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size on this screen, as the selection shows it: the one the user chose, or else the
|
||||||
|
/// default size of the screen.
|
||||||
|
/// </summary>
|
||||||
|
private InterfaceSize SizeOnScreen
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.MeasuredScreen is not { } screen)
|
||||||
|
return InterfaceSize.SYSTEM_SCALING;
|
||||||
|
|
||||||
|
var size = this.SettingsManager.ConfigurationData.App.GetInterfaceScreenSize(screen.ScreenKey)?.Size ?? screen.DefaultSize;
|
||||||
|
|
||||||
|
// Where the selection does not offer the app default, the app default shows the system scaling:
|
||||||
|
return size is InterfaceSize.APP_DEFAULT && !this.OffersAppDefault ? InterfaceSize.SYSTEM_SCALING : size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SizeHelp => this.MeasuredScreen switch
|
||||||
|
{
|
||||||
|
null => T("AI Studio cannot measure this screen right now, so you cannot set the interface size here."),
|
||||||
|
{ } screen when this.OffersAppDefault => string.Format(I18N.I.Culture, T("Applies to this screen only ({0}). The app default shows AI Studio in the same size on every laptop. System scaling follows your operating system."), this.DescribeScreen(screen)),
|
||||||
|
{ } screen => string.Format(I18N.I.Culture, T("Applies to this screen only ({0}). System scaling follows your operating system."), this.DescribeScreen(screen)),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The custom scale of the given screen, as the slider shows it: the one the user set, or else the
|
||||||
|
/// one the screen shows with. Within the range of the screen, as the runtime applies it.
|
||||||
|
/// </summary>
|
||||||
|
private int ScalePercent(InterfaceSizeResponse screen)
|
||||||
|
{
|
||||||
|
var percent = this.SettingsManager.ConfigurationData.App.GetInterfaceScreenSize(screen.ScreenKey)?.CustomPercent ?? screen.CurrentPercent;
|
||||||
|
return Math.Clamp(percent, screen.MinPercent, screen.MaxPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The help of the slider: the range of the given screen, and what its default size comes to for
|
||||||
|
/// a comparison, since the slider starts there when the user comes from the default.
|
||||||
|
/// </summary>
|
||||||
|
private string ScaleHelp(InterfaceSizeResponse screen) => this.OffersAppDefault
|
||||||
|
? string.Format(I18N.I.Culture, T("You can choose from {0} % to {1} %. The app default on this screen is {2} %."), screen.MinPercent, screen.MaxPercent, screen.AppDefaultPercent)
|
||||||
|
: string.Format(I18N.I.Culture, T("You can choose from {0} % to {1} %. The system scaling on this screen is {2} %."), screen.MinPercent, screen.MaxPercent, screen.SystemPercent);
|
||||||
|
|
||||||
|
private string DescribeScreen(InterfaceSizeResponse screen) => screen.ScreenDiagonalInches is { } diagonal
|
||||||
|
? string.Format(I18N.I.Culture, T("{0} × {1} pixels, {2:0.#} inches"), screen.ScreenWidth, screen.ScreenHeight, diagonal)
|
||||||
|
: string.Format(I18N.I.Culture, T("{0} × {1} pixels"), screen.ScreenWidth, screen.ScreenHeight);
|
||||||
|
|
||||||
|
private void UpdateScalePercent(int percent)
|
||||||
|
{
|
||||||
|
if (this.MeasuredScreen is { } screen)
|
||||||
|
this.SettingsManager.ConfigurationData.App.RememberInterfaceScreenSize(new(screen.ScreenKey, InterfaceSize.CUSTOM, percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the size on the screen the window shows on.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The screen is asked from the runtime right now rather than taken from its last report, in
|
||||||
|
/// case the window has only just moved. A screen whose custom scale the user set before goes
|
||||||
|
/// back to it with the custom size. Any other starts from the scale AI Studio shows with right
|
||||||
|
/// now, so that nothing changes on the screen until the user moves the slider; only a scale
|
||||||
|
/// beyond the range of the screen moves to its nearest end.
|
||||||
|
/// </remarks>
|
||||||
|
private async Task UpdateSize(InterfaceSize size)
|
||||||
|
{
|
||||||
|
this.interfaceSizeOnScreen = await this.RustService.GetInterfaceSize() ?? this.interfaceSizeOnScreen;
|
||||||
|
if (this.MeasuredScreen is not { } screen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var app = this.SettingsManager.ConfigurationData.App;
|
||||||
|
var customPercent = app.GetInterfaceScreenSize(screen.ScreenKey)?.CustomPercent;
|
||||||
|
if (size is InterfaceSize.CUSTOM)
|
||||||
|
customPercent ??= Math.Clamp(screen.CurrentPercent, screen.MinPercent, screen.MaxPercent);
|
||||||
|
|
||||||
|
app.RememberInterfaceScreenSize(new(screen.ScreenKey, size, customPercent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED, Event.TAURI_EVENT_RECEIVED ]);
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
this.interfaceSizeOnScreen = await this.RustService.GetInterfaceSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
|
{
|
||||||
|
switch (triggeredEvent)
|
||||||
|
{
|
||||||
|
// Another size may call for the slider, or no longer:
|
||||||
|
case Event.CONFIGURATION_CHANGED:
|
||||||
|
this.StateHasChanged();
|
||||||
|
break;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The window moved to another screen, or the size on the screen changed otherwise. The
|
||||||
|
// option has to show and change the screen the window is on now:
|
||||||
|
//
|
||||||
|
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.INTERFACE_SIZE_CHANGED }:
|
||||||
|
this.interfaceSizeOnScreen = await this.RustService.GetInterfaceSize() ?? this.interfaceSizeOnScreen;
|
||||||
|
this.StateHasChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -53,8 +53,12 @@ public partial class ConfigurationSelect<TConfig> : ConfigurationBaseCore
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task OptionChanged(TConfig updatedValue)
|
private async Task OptionChanged(TConfig? updatedValue)
|
||||||
{
|
{
|
||||||
|
// The select hands out null only when it has no value at all, which leaves nothing to store:
|
||||||
|
if (updatedValue is null)
|
||||||
|
return;
|
||||||
|
|
||||||
this.SelectionUpdate(updatedValue);
|
this.SelectionUpdate(updatedValue);
|
||||||
await this.SelectionUpdateAsync(updatedValue);
|
await this.SelectionUpdateAsync(updatedValue);
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@typeparam T
|
@typeparam T
|
||||||
@inherits ConfigurationBaseCore
|
@inherits ConfigurationBaseCore
|
||||||
|
|
||||||
<MudSlider T="@T" Size="Size.Medium" Value="@this.Value()" ValueChanged="@this.OptionChanged" Min="@this.Min" Max="@this.Max" Step="@this.Step" Immediate="@true" Disabled="@this.IsDisabled" Class="mb-1">
|
@* The input event bubbles up from the range input inside, also while the value waits for the release: *@
|
||||||
@this.Value() @this.Unit
|
<MudSlider T="@T" Size="Size.Medium" Value="@this.Value()" ValueChanged="@this.OptionChanged" Min="@this.Min" Max="@this.Max" Step="@this.Step" Immediate="@this.Immediate" Disabled="@this.IsDisabled" Class="mb-1" @oninput="@this.ShowDraggedValue">
|
||||||
|
@(this.draggedValue ?? this.Value()) @this.Unit
|
||||||
</MudSlider>
|
</MudSlider>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -42,6 +43,22 @@ public partial class ConfigurationSlider<T> : ConfigurationBaseCore where T : st
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Action<T> ValueUpdate { get; set; } = _ => { };
|
public Action<T> ValueUpdate { get; set; } = _ => { };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the option changes while the user drags the slider, or only once they release it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Defaults to true. An option which changes the layout of the page, such as the interface size,
|
||||||
|
/// should wait for the release: a page which changes under the pointer moves the slider away from
|
||||||
|
/// it. The slider shows the value it is dragged to either way.
|
||||||
|
/// </remarks>
|
||||||
|
[Parameter]
|
||||||
|
public bool Immediate { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value the user drags the slider to, while the option waits for the release, see Immediate.
|
||||||
|
/// </summary>
|
||||||
|
private T? draggedValue;
|
||||||
|
|
||||||
#region Overrides of ConfigurationBase
|
#region Overrides of ConfigurationBase
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -72,10 +89,24 @@ public partial class ConfigurationSlider<T> : ConfigurationBaseCore where T : st
|
|||||||
|
|
||||||
private async Task OptionChanged(T updatedValue)
|
private async Task OptionChanged(T updatedValue)
|
||||||
{
|
{
|
||||||
|
this.draggedValue = null;
|
||||||
this.ValueUpdate(updatedValue);
|
this.ValueUpdate(updatedValue);
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
await this.InformAboutChange();
|
await this.InformAboutChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the value the user drags the slider to, while the option waits for the release.
|
||||||
|
/// </summary>
|
||||||
|
private void ShowDraggedValue(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (this.Immediate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The range input reports its value with a dot as decimal separator, whatever the language:
|
||||||
|
if (T.TryParse(args.Value?.ToString(), CultureInfo.InvariantCulture, out var value))
|
||||||
|
this.draggedValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task EnsureMinMax()
|
private async Task EnsureMinMax()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
AdornmentColor="@this.IconColor"
|
AdornmentColor="@this.IconColor"
|
||||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||||
Lines="@this.NumLines"
|
Lines="@this.NumLines"
|
||||||
AutoGrow="@this.AutoGrow"
|
Sizing="@this.Sizing"
|
||||||
MaxLines="@this.GetMaxLines"
|
MaxLines="@this.GetMaxLines"
|
||||||
Class="@this.InputClass"
|
Class="@this.InputClass"
|
||||||
Immediate="@true"
|
Immediate="@true"
|
||||||
@@ -36,7 +36,7 @@ else
|
|||||||
AdornmentColor="@this.IconColor"
|
AdornmentColor="@this.IconColor"
|
||||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||||
Lines="@this.NumLines"
|
Lines="@this.NumLines"
|
||||||
AutoGrow="@this.AutoGrow"
|
Sizing="@this.Sizing"
|
||||||
MaxLines="@this.GetMaxLines"
|
MaxLines="@this.GetMaxLines"
|
||||||
Class="@this.InputClass"
|
Class="@this.InputClass"
|
||||||
Immediate="@true"
|
Immediate="@true"
|
||||||
|
|||||||
@@ -128,7 +128,9 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private bool AutoGrow => this.ExpandToContent || this.NumLines > 1;
|
private bool AutoGrow => this.ExpandToContent || this.NumLines > 1;
|
||||||
|
|
||||||
|
private InputSizing Sizing => this.AutoGrow ? InputSizing.Auto : InputSizing.Fixed;
|
||||||
|
|
||||||
private int GetMaxLines => this.ExpandToContent ? FilePatternSelection.MAX_FIELD_LINES : this.AutoGrow ? this.MaxLines : 1;
|
private int GetMaxLines => this.ExpandToContent ? FilePatternSelection.MAX_FIELD_LINES : this.AutoGrow ? this.MaxLines : 1;
|
||||||
|
|
||||||
private string InputClass => this.ExpandToContent ? "pattern-text-field" : string.Empty;
|
private string InputClass => this.ExpandToContent ? "pattern-text-field" : string.Empty;
|
||||||
@@ -139,13 +141,13 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
|||||||
//
|
//
|
||||||
private Adornment IconAdornment => this.NumLines == 1 ? Adornment.Start : Adornment.None;
|
private Adornment IconAdornment => this.NumLines == 1 ? Adornment.Start : Adornment.None;
|
||||||
|
|
||||||
protected void InternalUpdate(string text)
|
protected void InternalUpdate(string? text)
|
||||||
{
|
{
|
||||||
if (this.IsDisabled)
|
if (this.IsDisabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.timer.Stop();
|
this.timer.Stop();
|
||||||
this.internalText = text;
|
this.internalText = text ?? string.Empty;
|
||||||
this.timer.Start();
|
this.timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@using AIStudio.Tools.ChatArchive
|
@using AIStudio.Tools.ChatArchive
|
||||||
@inherits MSGComponentBase
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
<MudTabs Elevation="0" Rounded="@true" ApplyEffectsToContainer="@true" Outlined="@true" PanelClass="pa-4">
|
<MudTabs Elevation="0" Rounded="@true" ApplyEffectsToContainer="@true" Outlined="@true" TabPanelsClass="pa-4">
|
||||||
<MudTabPanel Text="@T("Back up")" Icon="@Icons.Material.Filled.Archive" Disabled="@(this.isBusy || this.DataRestoreService.IsPreparing)">
|
<MudTabPanel Text="@T("Back up")" Icon="@Icons.Material.Filled.Archive" Disabled="@(this.isBusy || this.DataRestoreService.IsPreparing)">
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
|
|
||||||
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
|
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
|
||||||
{
|
{
|
||||||
await this.DialogService.ShowMessageBox(
|
await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Export ERI Data Source"),
|
T("Export ERI Data Source"),
|
||||||
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
|
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
|
||||||
T("Close"));
|
T("Close"));
|
||||||
@@ -226,7 +226,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
|
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
|
||||||
if (!secretResponse.Success)
|
if (!secretResponse.Success)
|
||||||
{
|
{
|
||||||
await this.DialogService.ShowMessageBox(
|
await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Export ERI Data Source"),
|
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),
|
string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue),
|
||||||
T("Close"));
|
T("Close"));
|
||||||
@@ -236,7 +236,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
var encryption = PluginFactory.EnterpriseEncryption;
|
var encryption = PluginFactory.EnterpriseEncryption;
|
||||||
if (encryption?.IsAvailable != true)
|
if (encryption?.IsAvailable != true)
|
||||||
{
|
{
|
||||||
await this.DialogService.ShowMessageBox(
|
await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Export ERI Data Source"),
|
T("Export ERI Data Source"),
|
||||||
T("Cannot export this ERI data source because no enterprise encryption secret is configured."),
|
T("Cannot export this ERI data source because no enterprise encryption secret is configured."),
|
||||||
T("Close"));
|
T("Close"));
|
||||||
@@ -274,7 +274,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
|
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||||
if (!encryption.TryEncrypt(decryptedSecret, this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, out var encryptedSecret))
|
if (!encryption.TryEncrypt(decryptedSecret, this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, out var encryptedSecret))
|
||||||
{
|
{
|
||||||
await this.DialogService.ShowMessageBox(
|
await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Export ERI Data Source"),
|
T("Export ERI Data Source"),
|
||||||
T("Cannot export this ERI data source because the authentication secret could not be encrypted."),
|
T("Cannot export this ERI data source because the authentication secret could not be encrypted."),
|
||||||
T("Close"));
|
T("Close"));
|
||||||
@@ -353,7 +353,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
if (!this.DataSourceEmbeddingService.IsIndexingPaused)
|
if (!this.DataSourceEmbeddingService.IsIndexingPaused)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
await this.DialogService.ShowMessageBox(title, T("AI Studio is backing up or restoring the indexes of your data sources right now. Please try again once that is done."), T("Close"));
|
await this.DialogService.ShowMessageBoxAsync(title, T("AI Studio is backing up or restoring the indexes of your data sources right now. Please try again once that is done."), T("Close"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
|||||||
var deleteSecretResponse = await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
|
var deleteSecretResponse = await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
|
||||||
if (!deleteSecretResponse.Success)
|
if (!deleteSecretResponse.Success)
|
||||||
{
|
{
|
||||||
await this.DialogService.ShowMessageBox(
|
await this.DialogService.ShowMessageBoxAsync(
|
||||||
T("Delete Mailbox"),
|
T("Delete Mailbox"),
|
||||||
string.Format(T("The password of this mailbox could not be deleted from the operating system, so the mailbox was kept. The issue was: {0}"), deleteSecretResponse.Issue),
|
string.Format(T("The password of this mailbox could not be deleted from the operating system, so the mailbox was kept. The issue was: {0}"), deleteSecretResponse.Issue),
|
||||||
T("Close"));
|
T("Close"));
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
@inherits MudComponentBase
|
@inherits MudComponentBase
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.Disabled" Text="@this.text" TextChanged="@this.OnTextChanged" Validation="@this.ValidationFunc" Label="@this.Label" Variant="Variant.Outlined" Lines="@this.Lines" MaxLines="@this.MaxLines" AutoGrow="@(this.MaxLines > this.Lines)" Margin="Margin.Dense" UserAttributes="@this.Attributes" HelperText="@this.HelpText" Placeholder="@this.Placeholder" Class="mb-3" Immediate="true" Adornment="Adornment.Start" AdornmentIcon="@this.Icon"/>
|
<MudTextField T="string" Disabled="@this.Disabled" Text="@this.text" TextChanged="@this.OnTextChanged" Validation="@this.ValidationFunc" Label="@this.Label" Variant="Variant.Outlined" Lines="@this.Lines" MaxLines="@this.MaxLines" Sizing="@(this.MaxLines > this.Lines ? InputSizing.Auto : InputSizing.Fixed)" Margin="Margin.Dense" UserAttributes="@this.Attributes" HelperText="@this.HelpText" Placeholder="@this.Placeholder" Class="mb-3" Immediate="true" Adornment="Adornment.Start" AdornmentIcon="@this.Icon"/>
|
||||||
Loaded 100 of 1082 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user