mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-28 09:19:49 +00:00
Compare commits
214 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e6a3add08f | ||
|
56600d3dd3 | ||
|
47b6a89685 | ||
|
2494ee2294 | ||
|
90bd450193 | ||
|
d272d619cb | ||
|
cdc155890f | ||
|
81030019c7 | ||
|
201fb2514d | ||
|
45974f9bcf | ||
|
151830ef15 | ||
|
3e13d50302 | ||
|
02c3e4c817 | ||
|
bafd62429d | ||
|
69fc4d764f | ||
|
9cc3c68dda | ||
|
fdca581c90 | ||
|
7bf5b5cf7a | ||
|
1dc1c6563d | ||
|
e594e5c0e8 | ||
|
7bd827e658 | ||
|
03cd299a86 | ||
|
2144cfe059 | ||
|
c10c084da4 | ||
|
f8c60d87ae | ||
|
c6e45c26d6 | ||
|
09020415db | ||
|
b153b9c4e9 | ||
|
c715b38f41 | ||
|
3fc15d9789 | ||
|
171ed37c27 | ||
|
80c58ea749 | ||
|
1ff27fe21f | ||
|
712ed2938f | ||
|
56e99ffaba | ||
|
693fe5e161 | ||
|
ac731f6f78 | ||
|
1993576903 | ||
|
364941701b | ||
|
f321d6982e | ||
|
a3ec2bbe7b | ||
|
be430766c5 | ||
|
6185db733a | ||
|
670b0653b5 | ||
|
f97612de0e | ||
|
c56232cba7 | ||
|
b456319434 | ||
|
ceefc0114b | ||
|
158252dc34 | ||
|
07c1182611 | ||
|
3ce6e168cb | ||
|
ff61df047a | ||
|
d0074a6fc7 | ||
|
b632854cd4 | ||
|
3f8f399cd9 | ||
|
e2b371f9a1 | ||
|
ee2a73ccd8 | ||
|
97454c59e9 | ||
|
d25a47789e | ||
|
820294c14d | ||
|
dd40ca3e5b | ||
|
c67f17cf41 | ||
|
b948909afa | ||
|
3e66d543b9 | ||
|
1ac125b7f5 | ||
|
aa0c2e7870 | ||
|
f5a49ff077 | ||
|
a5863a176c | ||
|
0c81f90b84 | ||
|
81417daa78 | ||
|
780e45911d | ||
|
dad709cccb | ||
|
75b3238e56 | ||
|
293b0ffdb0 | ||
|
08c18cd387 | ||
|
3ab8e7324e | ||
|
cf97fa888c | ||
|
dd23cfa1ff | ||
|
d6ead37102 | ||
|
686bb2ccb3 | ||
|
69d64f8b1f | ||
|
c3ab0ebb38 | ||
|
6af3cf4213 | ||
|
52c8052ce6 | ||
|
471536cc8f | ||
|
e5ce3263bc | ||
|
264b6dd716 | ||
|
d8ac2f3d49 | ||
|
64a7c9ab53 | ||
|
ee61425153 | ||
|
ce8d84a12a | ||
|
2e8444b5e3 | ||
|
5cc6e141c2 | ||
|
850d56cfcf | ||
|
d399b025a9 | ||
|
200964422d | ||
|
8a23b54e56 | ||
|
eccf65f0a6 | ||
|
2429788ccc | ||
|
0125f48f61 | ||
|
94028b2c06 | ||
|
c5acce7278 | ||
|
4eb51f947a | ||
|
e11211d215 | ||
|
032c7ee56e | ||
|
030990ee90 | ||
|
371731ac13 | ||
|
99622cec48 | ||
|
6a4a7dc0d6 | ||
|
df01ce188e | ||
|
3bfca45e11 | ||
|
ccaf2e894a | ||
|
1d22232005 | ||
|
a66d88c989 | ||
|
2e84f10a97 | ||
|
dbc2b4f6bc | ||
|
9d2b63bbaa | ||
|
277dc73468 | ||
|
ccac2b04ac | ||
|
0c4b94c527 | ||
|
37a9046be4 | ||
|
b984b4432e | ||
|
5e3b632eee | ||
|
99dac520cb | ||
|
e21e30ce8c | ||
|
5a077e1bdb | ||
|
8dd5b3bab8 | ||
|
03431b85b4 | ||
|
9699754ec7 | ||
|
64656d3605 | ||
|
9bd79bd3a0 | ||
|
bfc9f2ea1d | ||
|
96e6372fcd | ||
|
f01cf498e2 | ||
|
77d427610b | ||
|
7c59aa11fe | ||
|
98810ce884 | ||
|
657fda4961 | ||
|
f4780939fc | ||
|
954cf44939 | ||
|
e973b51f75 | ||
|
c9474c55b7 | ||
|
c0b7016c3a | ||
|
78315195cb | ||
|
6040e88dad | ||
|
ad3d8cbb3a | ||
|
c836bd7f33 | ||
|
9d71978feb | ||
|
940459ca46 | ||
|
09d67bc1c2 | ||
|
1681405fb9 | ||
|
be408a6aee | ||
|
cf975f2a6c | ||
|
1df1d3f328 | ||
|
ed14525390 | ||
|
14caf550ec | ||
|
f50e041640 | ||
|
ebe00812e2 | ||
|
ae72c50236 | ||
|
48874d617a | ||
|
63be312bb4 | ||
|
06f66fdab2 | ||
|
b9a65d9ca0 | ||
|
e4b1ea32fb | ||
|
e9975a5fbc | ||
|
2ef4d9d53f | ||
|
33a2728644 | ||
|
a54c0bdbbf | ||
|
d6521850e9 | ||
|
258bc7a338 | ||
|
187663bbf2 | ||
|
8060fc01dd | ||
|
b2ca49ab92 | ||
|
5e445f09fa | ||
|
6f5aecd92b | ||
|
317d3c9eec | ||
|
0acb3a20a2 | ||
|
3b93841982 | ||
|
38a45955fb | ||
|
caec3bfd2c | ||
|
12b3d6fc3d | ||
|
21927754b6 | ||
|
2131bf1817 | ||
|
8cbef49d89 | ||
|
cdf717ad5f | ||
|
02b6f526da | ||
|
a365539282 | ||
|
7250b270f5 | ||
|
d437d08640 | ||
|
61f7a3dd72 | ||
|
55d7895f58 | ||
|
e2859b3d76 | ||
|
0a951ead3e | ||
|
432ae30fdf | ||
|
799112eb9d | ||
|
f806ee28c5 | ||
|
32ff143b21 | ||
|
90a1f681c5 | ||
|
99fe9f3a4f | ||
|
cf8a9800f3 | ||
|
f57e971f80 | ||
|
0d104f5abc | ||
|
ba6b8d2b3c | ||
|
1c72c4d6ec | ||
|
4ca5aba58f | ||
|
c4d2574508 | ||
|
ceac9cefe5 | ||
|
ebe2ad62c6 | ||
|
9c0dea9927 | ||
|
d2a2d658e6 | ||
|
d82427ba56 | ||
|
386121d198 | ||
|
f9245d52c2 | ||
|
9fa2a1a135 |
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@ -2,13 +2,13 @@
|
|||||||
* @MindWorkAI/maintainer
|
* @MindWorkAI/maintainer
|
||||||
|
|
||||||
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
||||||
/.github/ @MindWorkAI/release
|
/.github/ @MindWorkAI/release @SommerEngineering
|
||||||
|
|
||||||
# The release team is responsible for the update directory:
|
|
||||||
/.updates/ @MindWorkAI/release
|
|
||||||
|
|
||||||
# Our Rust experts are responsible for the Rust codebase:
|
# Our Rust experts are responsible for the Rust codebase:
|
||||||
/runtime/ @MindWorkAI/rust-experts
|
/runtime/ @MindWorkAI/rust-experts
|
||||||
|
|
||||||
# Our .NET experts are responsible for the .NET codebase:
|
# Our .NET experts are responsible for the .NET codebase:
|
||||||
/app/ @MindWorkAI/net-experts
|
/app/ @MindWorkAI/net-experts
|
||||||
|
|
||||||
|
# The source code rules must be reviewed by the release team:
|
||||||
|
/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,2 +1 @@
|
|||||||
github: [MindWorkAI]
|
github: [MindWorkAI]
|
||||||
open_collective: mindwork-ai
|
|
272
.github/workflows/build-and-release.yml
vendored
272
.github/workflows/build-and-release.yml
vendored
@ -1,6 +1,8 @@
|
|||||||
name: Build and Release
|
name: Build and Release
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ jobs:
|
|||||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Check tag vs. metadata version
|
- name: Check tag vs. metadata version
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
run: |
|
run: |
|
||||||
# Ensure, that the tag matches the version in the metadata file:
|
# Ensure, that the tag matches the version in the metadata file:
|
||||||
if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then
|
if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then
|
||||||
@ -99,6 +102,12 @@ jobs:
|
|||||||
dotnet_runtime: 'linux-x64'
|
dotnet_runtime: 'linux-x64'
|
||||||
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
||||||
tauri_bundle: 'appimage deb updater'
|
tauri_bundle: 'appimage deb updater'
|
||||||
|
|
||||||
|
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
|
||||||
|
rust_target: 'aarch64-unknown-linux-gnu'
|
||||||
|
dotnet_runtime: 'linux-arm64'
|
||||||
|
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
|
||||||
|
tauri_bundle: 'appimage deb updater'
|
||||||
|
|
||||||
- platform: 'windows-latest' # for x86-based Windows
|
- platform: 'windows-latest' # for x86-based Windows
|
||||||
rust_target: 'x86_64-pc-windows-msvc'
|
rust_target: 'x86_64-pc-windows-msvc'
|
||||||
@ -151,6 +160,13 @@ jobs:
|
|||||||
# Format the app version:
|
# Format the app version:
|
||||||
formatted_app_version="v${app_version}"
|
formatted_app_version="v${app_version}"
|
||||||
|
|
||||||
|
# Set the architecture:
|
||||||
|
if sed --version 2>/dev/null | grep -q GNU; then
|
||||||
|
sed -i "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
|
||||||
|
else
|
||||||
|
sed -i '' "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
|
||||||
|
fi
|
||||||
|
|
||||||
# Write the metadata to the environment:
|
# Write the metadata to the environment:
|
||||||
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
||||||
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
|
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
|
||||||
@ -161,6 +177,7 @@ jobs:
|
|||||||
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
|
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
|
||||||
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
|
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
|
||||||
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
||||||
|
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
# Log the metadata:
|
# Log the metadata:
|
||||||
echo "App version: '${formatted_app_version}'"
|
echo "App version: '${formatted_app_version}'"
|
||||||
@ -171,6 +188,7 @@ jobs:
|
|||||||
echo "Rust version: '${rust_version}'"
|
echo "Rust version: '${rust_version}'"
|
||||||
echo "MudBlazor version: '${mud_blazor_version}'"
|
echo "MudBlazor version: '${mud_blazor_version}'"
|
||||||
echo "Tauri version: '${tauri_version}'"
|
echo "Tauri version: '${tauri_version}'"
|
||||||
|
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||||
|
|
||||||
- name: Read and format metadata (Windows)
|
- name: Read and format metadata (Windows)
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
@ -203,6 +221,12 @@ jobs:
|
|||||||
# Format the app version:
|
# Format the app version:
|
||||||
$formatted_app_version = "v${app_version}"
|
$formatted_app_version = "v${app_version}"
|
||||||
|
|
||||||
|
# Set the architecture:
|
||||||
|
$metadata[9] = "${{ matrix.dotnet_runtime }}"
|
||||||
|
|
||||||
|
# Write the changed metadata back to the file:
|
||||||
|
Set-Content -Path metadata.txt -Value $metadata
|
||||||
|
|
||||||
# Write the metadata to the environment:
|
# Write the metadata to the environment:
|
||||||
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
|
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
|
||||||
Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV
|
Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV
|
||||||
@ -212,6 +236,7 @@ jobs:
|
|||||||
Write-Output "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $env:GITHUB_ENV
|
Write-Output "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $env:GITHUB_ENV
|
||||||
Write-Output "RUST_VERSION=${rust_version}" >> $env:GITHUB_ENV
|
Write-Output "RUST_VERSION=${rust_version}" >> $env:GITHUB_ENV
|
||||||
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
|
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
|
||||||
|
Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
# Log the metadata:
|
# Log the metadata:
|
||||||
Write-Output "App version: '${formatted_app_version}'"
|
Write-Output "App version: '${formatted_app_version}'"
|
||||||
@ -222,6 +247,7 @@ jobs:
|
|||||||
Write-Output "Rust version: '${rust_version}'"
|
Write-Output "Rust version: '${rust_version}'"
|
||||||
Write-Output "MudBlazor version: '${mud_blazor_version}'"
|
Write-Output "MudBlazor version: '${mud_blazor_version}'"
|
||||||
Write-Output "Tauri version: '${tauri_version}'"
|
Write-Output "Tauri version: '${tauri_version}'"
|
||||||
|
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
@ -274,6 +300,12 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Setup dependencies (Ubuntu-specific, ARM)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Setup Tauri (Unix)
|
- name: Setup Tauri (Unix)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
@ -291,6 +323,35 @@ jobs:
|
|||||||
} else {
|
} else {
|
||||||
Write-Output "Tauri is already installed"
|
Write-Output "Tauri is already installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- name: Delete previous artifact, which may exist due to caching (macOS)
|
||||||
|
if: startsWith(matrix.platform, 'macos')
|
||||||
|
run: |
|
||||||
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
||||||
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
|
||||||
|
|
||||||
|
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
|
||||||
|
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
||||||
|
run: |
|
||||||
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio_*.msi" -ErrorAction SilentlyContinue
|
||||||
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio*msi.zip*" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
- name: Delete previous artifact, which may exist due to caching (Windows - NSIS)
|
||||||
|
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
|
||||||
|
run: |
|
||||||
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
|
||||||
|
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
|
||||||
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
||||||
|
run: |
|
||||||
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||||
|
|
||||||
|
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
|
||||||
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
||||||
|
run: |
|
||||||
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
|
||||||
|
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
||||||
|
|
||||||
- name: Build Tauri project (Unix)
|
- name: Build Tauri project (Unix)
|
||||||
if: matrix.platform != 'windows-latest'
|
if: matrix.platform != 'windows-latest'
|
||||||
@ -315,7 +376,7 @@ jobs:
|
|||||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
|
cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
|
||||||
|
|
||||||
- name: Upload artifact (macOS)
|
- name: Upload artifact (macOS)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
|
||||||
@ -326,7 +387,7 @@ jobs:
|
|||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||||
|
|
||||||
- name: Upload artifact (Windows - MSI)
|
- name: Upload artifact (Windows - MSI)
|
||||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') && startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
|
||||||
@ -337,7 +398,7 @@ jobs:
|
|||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||||
|
|
||||||
- name: Upload artifact (Windows - NSIS)
|
- name: Upload artifact (Windows - NSIS)
|
||||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
|
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis') && startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
|
||||||
@ -348,7 +409,7 @@ jobs:
|
|||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||||
|
|
||||||
- name: Upload artifact (Linux - Debian Package)
|
- name: Upload artifact (Linux - Debian Package)
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') && startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
|
||||||
@ -358,7 +419,7 @@ jobs:
|
|||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||||
|
|
||||||
- name: Upload artifact (Linux - AppImage)
|
- name: Upload artifact (Linux - AppImage)
|
||||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') && startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
|
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
|
||||||
@ -367,205 +428,12 @@ jobs:
|
|||||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
||||||
|
|
||||||
build_linux_arm64:
|
|
||||||
name: Build app (linux-arm64)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: read_metadata
|
|
||||||
env:
|
|
||||||
SKIP: false # allows disabling this long-running job temporarily
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: false
|
|
||||||
|
|
||||||
- name: Read and format metadata
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
id: metadata
|
|
||||||
run: |
|
|
||||||
# Read the lines of the metadata file:
|
|
||||||
app_version=$(sed -n '1p' metadata.txt)
|
|
||||||
build_time=$(sed -n '2p' metadata.txt)
|
|
||||||
build_number=$(sed -n '3p' metadata.txt)
|
|
||||||
|
|
||||||
# Next line is the .NET SDK version.
|
|
||||||
# The format is '8.0.205 (commit 3e1383b780)'.
|
|
||||||
# We extract only the version number, though:
|
|
||||||
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
|
||||||
|
|
||||||
# Next line is the .NET runtime version.
|
|
||||||
# The format is '8.0.5 (commit 087e15321b)'.
|
|
||||||
# We extract only the version number, though:
|
|
||||||
dotnet_runtime_version=$(sed -n '5p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
|
||||||
|
|
||||||
# Next line is the Rust version.
|
|
||||||
# The format is '1.78.0 (commit 9b00956e5)'.
|
|
||||||
# We extract only the version number, though:
|
|
||||||
rust_version=$(sed -n '6p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
|
||||||
|
|
||||||
# Next line is the MudBlazor version:
|
|
||||||
mud_blazor_version=$(sed -n '7p' metadata.txt)
|
|
||||||
|
|
||||||
# Next line is the Tauri version:
|
|
||||||
tauri_version=$(sed -n '8p' metadata.txt)
|
|
||||||
|
|
||||||
# Format the app version:
|
|
||||||
formatted_app_version="v${app_version}"
|
|
||||||
|
|
||||||
# Write the metadata to the environment:
|
|
||||||
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
|
||||||
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
|
|
||||||
echo "BUILD_TIME=${build_time}" >> $GITHUB_ENV
|
|
||||||
echo "BUILD_NUMBER=${build_number}" >> $GITHUB_ENV
|
|
||||||
echo "DOTNET_SDK_VERSION=${dotnet_sdk_version}" >> $GITHUB_ENV
|
|
||||||
echo "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $GITHUB_ENV
|
|
||||||
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
|
|
||||||
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
|
|
||||||
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Log the metadata:
|
|
||||||
echo "App version: '${formatted_app_version}'"
|
|
||||||
echo "Build time: '${build_time}'"
|
|
||||||
echo "Build number: '${build_number}'"
|
|
||||||
echo ".NET SDK version: '${dotnet_sdk_version}'"
|
|
||||||
echo ".NET runtime version: '${dotnet_runtime_version}'"
|
|
||||||
echo "Rust version: '${rust_version}'"
|
|
||||||
echo "MudBlazor version: '${mud_blazor_version}'"
|
|
||||||
echo "Tauri version: '${tauri_version}'"
|
|
||||||
|
|
||||||
- name: Setup .NET
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ env.DOTNET_SDK_VERSI }}
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: 'app/MindWork AI Studio/packages.lock.json'
|
|
||||||
|
|
||||||
- name: Build .NET project
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
run: |
|
|
||||||
cd "app/MindWork AI Studio"
|
|
||||||
dotnet publish --configuration release --runtime linux-arm64 --disable-build-servers --force --output ../../publish/dotnet
|
|
||||||
|
|
||||||
- name: Move & rename the .NET artifact
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
run: |
|
|
||||||
mkdir -p "app/MindWork AI Studio/bin/dist"
|
|
||||||
cd publish/dotnet
|
|
||||||
mv mindworkAIStudio "../../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer-aarch64-unknown-linux-gnu"
|
|
||||||
|
|
||||||
- name: Cache linux arm64 runner image
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: linux_arm_cache
|
|
||||||
with:
|
|
||||||
path: ${{ runner.temp }}/linux_arm_qemu_cache.img
|
|
||||||
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}
|
|
||||||
|
|
||||||
- name: Build linux arm runner image
|
|
||||||
uses: pguyot/arm-runner-action@v2
|
|
||||||
id: build-linux-arm-runner
|
|
||||||
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
|
|
||||||
env:
|
|
||||||
RUST_VERSION: ${{ env.RUST_VERSION }}
|
|
||||||
TAURI_VERSION: ${{ env.TAURI_VERSION }}
|
|
||||||
|
|
||||||
with:
|
|
||||||
base_image: dietpi:rpi_armv8_bullseye
|
|
||||||
cpu: cortex-a53
|
|
||||||
image_additional_mb: 6000 # ~ 6GB
|
|
||||||
optimize_image: false
|
|
||||||
shell: /bin/bash
|
|
||||||
commands: |
|
|
||||||
# Rust complains (rightly) that $HOME doesn't match eid home:
|
|
||||||
export HOME=/root
|
|
||||||
|
|
||||||
# Workaround to CI worker being stuck on Updating crates.io index:
|
|
||||||
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
|
||||||
|
|
||||||
# Update and upgrade the system:
|
|
||||||
apt-get update --yes --allow-releaseinfo-change
|
|
||||||
apt-get upgrade --yes
|
|
||||||
apt-get autoremove --yes
|
|
||||||
apt-get install curl wget --yes
|
|
||||||
|
|
||||||
# Install Rust:
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
rustup toolchain install $RUST_VERSION
|
|
||||||
|
|
||||||
# Install build tools and tauri-cli requirements:
|
|
||||||
apt-get install --yes libwebkit2gtk-4.0-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
|
|
||||||
|
|
||||||
# Setup Tauri:
|
|
||||||
cargo install --version 1.6.2 tauri-cli
|
|
||||||
|
|
||||||
- name: Add the built runner image to the cache
|
|
||||||
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
|
|
||||||
run: |
|
|
||||||
mv ${{ steps.build-linux-arm-runner.outputs.image }} ${{ runner.temp }}/linux_arm_qemu_cache.img
|
|
||||||
|
|
||||||
- name: Build Tauri project
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
uses: pguyot/arm-runner-action@v2
|
|
||||||
id: build-linux-arm
|
|
||||||
|
|
||||||
with:
|
|
||||||
base_image: file://${{ runner.temp }}/linux_arm_qemu_cache.img
|
|
||||||
cpu: cortex-a53
|
|
||||||
optimize_image: false
|
|
||||||
copy_artifact_path: runtime
|
|
||||||
copy_artifact_dest: result
|
|
||||||
bind_mount_repository: true
|
|
||||||
|
|
||||||
#
|
|
||||||
# We do not need to set the PRIVATE_PUBLISH_KEY and PRIVATE_PUBLISH_KEY_PASSWORD here,
|
|
||||||
# because we cannot produce the AppImage on arm64. Only the AppImage supports the automatic
|
|
||||||
# update feature. The Debian package does not support this feature.
|
|
||||||
#
|
|
||||||
#PRIVATE_PUBLISH_KEY: ${{ secrets.PRIVATE_PUBLISH_KEY }}
|
|
||||||
#PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
|
||||||
#
|
|
||||||
|
|
||||||
shell: /bin/bash
|
|
||||||
commands: |
|
|
||||||
export HOME=/root
|
|
||||||
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
cd runtime
|
|
||||||
|
|
||||||
# Try to restore the Rust cache from previous build:
|
|
||||||
mkdir -p /rust-cache/target
|
|
||||||
rm -fr target
|
|
||||||
cp -Rp /rust-cache/target target
|
|
||||||
|
|
||||||
cargo tauri build --target aarch64-unknown-linux-gnu --bundles deb
|
|
||||||
|
|
||||||
# Save the built libraries for the next job:
|
|
||||||
rm -fr /rust-cache/target
|
|
||||||
cp -Rp target /rust-cache
|
|
||||||
|
|
||||||
- name: Update the runner image to cache the Rust runtime build
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
run: |
|
|
||||||
mv ${{ steps.build-linux-arm.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img
|
|
||||||
|
|
||||||
- name: Upload artifact (Linux - Debian Package)
|
|
||||||
if: ${{ env.SKIP != 'true' }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: MindWork AI Studio (Linux - deb linux-arm64)
|
|
||||||
path: |
|
|
||||||
result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
|
|
||||||
if-no-files-found: warn
|
|
||||||
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
|
|
||||||
|
|
||||||
create_release:
|
create_release:
|
||||||
name: Prepare & create release
|
name: Prepare & create release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build_main, read_metadata, build_linux_arm64]
|
needs: [build_main, read_metadata]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- name: Create artifact directory
|
- name: Create artifact directory
|
||||||
run: mkdir -p $GITHUB_WORKSPACE/artifacts
|
run: mkdir -p $GITHUB_WORKSPACE/artifacts
|
||||||
@ -638,6 +506,7 @@ jobs:
|
|||||||
# - platform=darwin-aarch64 when path contains 'aarch64-apple-darwin'
|
# - platform=darwin-aarch64 when path contains 'aarch64-apple-darwin'
|
||||||
# - platform=darwin-x86_64 when path contains 'x86_64-apple-darwin'
|
# - platform=darwin-x86_64 when path contains 'x86_64-apple-darwin'
|
||||||
# - platform=linux-x86_64 when path contains 'x86_64-unknown-linux-'
|
# - platform=linux-x86_64 when path contains 'x86_64-unknown-linux-'
|
||||||
|
# - platform=linux-aarch64 when path contains 'aarch64-unknown-linux-'
|
||||||
# - platform=windows-x86_64 when path contains 'x86_64-pc-windows-'
|
# - platform=windows-x86_64 when path contains 'x86_64-pc-windows-'
|
||||||
# - platform=windows-aarch64 when path contains 'aarch64-pc-windows-'
|
# - platform=windows-aarch64 when path contains 'aarch64-pc-windows-'
|
||||||
#
|
#
|
||||||
@ -647,6 +516,8 @@ jobs:
|
|||||||
platform="darwin-x86_64"
|
platform="darwin-x86_64"
|
||||||
elif [[ "$sig_file" == *"amd64.AppImage"* ]]; then
|
elif [[ "$sig_file" == *"amd64.AppImage"* ]]; then
|
||||||
platform="linux-x86_64"
|
platform="linux-x86_64"
|
||||||
|
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
||||||
|
platform="linux-aarch64"
|
||||||
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
|
||||||
platform="windows-x86_64"
|
platform="windows-x86_64"
|
||||||
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
|
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
|
||||||
@ -723,6 +594,7 @@ jobs:
|
|||||||
name: Publish release
|
name: Publish release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [read_metadata, create_release]
|
needs: [read_metadata, create_release]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -147,3 +147,4 @@ orleans.codegen.cs
|
|||||||
**/.idea/**/dynamic.xml
|
**/.idea/**/dynamic.xml
|
||||||
**/.idea/**/uiDesigner.xml
|
**/.idea/**/uiDesigner.xml
|
||||||
**/.idea/**/dbnavigator.xml
|
**/.idea/**/dbnavigator.xml
|
||||||
|
**/.vs
|
||||||
|
22
CITATION.cff
Normal file
22
CITATION.cff
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# This CITATION.cff file was generated with cffinit.
|
||||||
|
# Visit https://bit.ly/cffinit to generate yours today!
|
||||||
|
|
||||||
|
cff-version: 1.2.0
|
||||||
|
title: AI Studio
|
||||||
|
message: >-
|
||||||
|
When you want to cite AI Studio in your scientific work,
|
||||||
|
please use these metadata.
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- given-names: Thorsten
|
||||||
|
family-names: Sommer
|
||||||
|
email: thorsten.sommer@dlr.de
|
||||||
|
affiliation: Deutsches Zentrum für Luft- und Raumfahrt (DLR)
|
||||||
|
orcid: 'https://orcid.org/0000-0002-3264-9934'
|
||||||
|
- name: Open Source Community
|
||||||
|
repository-code: 'https://github.com/MindWorkAI/AI-Studio'
|
||||||
|
url: 'https://mindworkai.org/'
|
||||||
|
keywords:
|
||||||
|
- LLM
|
||||||
|
- AI
|
||||||
|
- Orchestration
|
@ -6,7 +6,7 @@ FSL-1.1-MIT
|
|||||||
|
|
||||||
## Notice
|
## Notice
|
||||||
|
|
||||||
Copyright 2024 Thorsten Sommer
|
Copyright 2025 Thorsten Sommer
|
||||||
|
|
||||||
## Terms and Conditions
|
## Terms and Conditions
|
||||||
|
|
||||||
|
93
README.md
93
README.md
@ -1,4 +1,5 @@
|
|||||||
# MindWork AI Studio
|
# MindWork AI Studio
|
||||||
|
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
|
||||||
|
|
||||||
## News
|
## News
|
||||||
Things we are currently working on:
|
Things we are currently working on:
|
||||||
@ -6,51 +7,84 @@ Things we are currently working on:
|
|||||||
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
|
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
|
||||||
|
|
||||||
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
|
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
|
||||||
- [x] ~~Define the [External Data API (EDI)](https://github.com/MindWorkAI/EDI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/EDI/pull/1))~~
|
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
|
||||||
- [ ] App: Metadata for providers (which provider offers embeddings?)
|
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
|
||||||
- [ ] App: Management of data sources (local data)
|
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
|
||||||
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
|
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||||
- [ ] App: Implement embedding providers
|
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||||
- [ ] App: Implement the process to vectorize local data using embeddings
|
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||||
|
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||||
|
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||||
|
- [ ] App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393))
|
||||||
|
- [ ] App: Implement external embedding providers
|
||||||
|
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||||
- [ ] App: Define an interface for the integration of RAG processes in chats
|
- [ ] App: Implement the continuous process of vectorizing data
|
||||||
- [ ] App: Integrate data sources in chats
|
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
|
||||||
- [ ] App: Management of data sources (external data via [EDI](https://github.com/MindWorkAI/EDI))
|
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
|
||||||
|
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||||
|
|
||||||
|
|
||||||
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. Related PR: [#167](https://github.com/MindWorkAI/AI-Studio/pull/167).
|
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[PR #167](https://github.com/MindWorkAI/AI-Studio/pull/167), [PR #226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~, [PR #376](https://github.com/MindWorkAI/AI-Studio/pull/376).
|
||||||
|
|
||||||
|
- Since March 2025: We have started developing the plugin system. There will be language plugins to offer AI Studio in other languages, configuration plugins to centrally manage certain providers and rules within an organization, and assistant plugins that allow anyone to develop their own assistants. We are using Lua as the plugin language:
|
||||||
|
- [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~
|
||||||
|
- [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~
|
||||||
|
- [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~
|
||||||
|
- [ ] Add support for other languages (I18N) to AI Studio (~~[PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429))~~
|
||||||
|
- [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~
|
||||||
|
- [ ] Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430))
|
||||||
|
- [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations
|
||||||
|
- [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform.
|
||||||
|
- [ ] Add assistant plugins
|
||||||
|
|
||||||
Other News:
|
Other News:
|
||||||
|
|
||||||
- October 2024: We've found the first two financial supporters. Huge thanks to `richard-stanton` and `peerschuett` for backing the project. Thanks for having the courage to be the first to support us.
|
- April 2025: We have two active financial supporters: Peer `peerschuett` and Dominic `donework`. Thank you very much for your support. MindWork AI reinvests these donations by passing them on to our AI Studio dependencies ([see here](https://github.com/orgs/MindWorkAI/sponsoring)). In the event that we receive large donations, we will first sign the app ([#56](https://github.com/MindWorkAI/Planning/issues/56)). In case we receive more donations, we will look for and pay staff to develop features for AI Studio.
|
||||||
|
|
||||||
- October 2024: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of one project and will also contribute to its further development. This is great news.
|
- April 2025: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of three projects and will also contribute to its further development. This is great news.
|
||||||
|
|
||||||
|
|
||||||
Features we have recently released:
|
Features we have recently released:
|
||||||
|
|
||||||
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
|
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
|
||||||
- v0.9.16: Added workspace display options & improved the layout of the app window.
|
- v0.9.39: Added the plugin system as a preview feature.
|
||||||
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
|
- v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well.
|
||||||
- v0.9.13: You can use `ollama` providers secured with API keys.
|
- v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process)
|
||||||
- v0.9.12: Added a job posting assistant to the business category and improved grammar & spelling check and rewrite assistants.
|
- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature.
|
||||||
- v0.9.11: Added enforcement of minimal confidence levels & dark mode.
|
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
|
||||||
- v0.9.10: Added support for the OpenAI `o1` model.
|
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
|
||||||
|
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
|
||||||
|
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
|
||||||
|
|
||||||
## What is AI Studio?
|
## What is AI Studio?
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
MindWork AI Studio is a desktop application available for macOS, Windows, and Linux. It provides a unified chat interface for Large Language Models (LLMs). You bring your own API key for the respective LLM provider to use the models. The API keys are securely stored by the operating system.
|
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 o1, or even your own local AI models.
|
||||||
|
|
||||||
|
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
||||||
|
|
||||||
**Key advantages:**
|
**Key advantages:**
|
||||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), or [Fireworks](https://fireworks.ai/). Support for Google Gemini, and [Replicate](https://replicate.com/) is planned.
|
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support:
|
||||||
|
- [OpenAI](https://openai.com/) (GPT4o, GPT4.1, o1, o3, o4, etc.)
|
||||||
|
- [Mistral](https://mistral.ai/)
|
||||||
|
- [Anthropic](https://www.anthropic.com/) (Claude)
|
||||||
|
- [Google Gemini](https://gemini.google.com)
|
||||||
|
- [xAI](https://x.ai/) (Grok)
|
||||||
|
- [DeepSeek](https://www.deepseek.com/en)
|
||||||
|
- [Alibaba Cloud](https://www.alibabacloud.com) (Qwen)
|
||||||
|
- [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face
|
||||||
|
- Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/)
|
||||||
|
- [Groq](https://groq.com/)
|
||||||
|
- [Fireworks](https://fireworks.ai/)
|
||||||
|
- For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
|
||||||
|
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
||||||
- **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**: The data entered into the app is not used for training by the providers since we are using the provider's API.
|
- **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.
|
||||||
- **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.
|
||||||
|
|
||||||
@ -71,11 +105,16 @@ To view all available tiers, please visit our [GitHub Sponsors page](https://git
|
|||||||
Your support, whether big or small, keeps the wheels turning and is deeply appreciated ❤️.
|
Your support, whether big or small, keeps the wheels turning and is deeply appreciated ❤️.
|
||||||
|
|
||||||
## Planned Features
|
## Planned Features
|
||||||
Here's an exciting look at some of the features we're planning to add to MindWork 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:
|
||||||
- **More providers**: We plan to add support for additional LLM providers, such as Google Gemini, giving you more options to choose from.
|
- **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
|
||||||
- **System prompts**: Integration of a system prompt library will allow you to control the behavior of the LLM with predefined prompts, ensuring consistency and efficiency.
|
- **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.
|
||||||
- **Text replacement for better privacy**: Define keywords that will be replaced in your chats before sending content to the provider, enhancing your privacy.
|
- **Useful assistants:** We'll develop more assistants for everyday tasks.
|
||||||
- **Advanced interactions**: We're full of ideas for advanced interactions tailored for specific use cases, whether in a business context or for writers and other professionals.
|
- **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'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.
|
||||||
|
- **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.
|
||||||
|
- **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 🤩.
|
||||||
|
|
||||||
|
17
app/.run/Collect I18N content.run.xml
Normal file
17
app/.run/Collect I18N content.run.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Collect I18N content" type="ShConfigurationType">
|
||||||
|
<option name="SCRIPT_TEXT" value="dotnet run collect-i18n" />
|
||||||
|
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||||
|
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/Build" />
|
||||||
|
<option name="SCRIPT_OPTIONS" value="" />
|
||||||
|
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||||
|
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/Build" />
|
||||||
|
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||||
|
<option name="INTERPRETER_PATH" value="/opt/homebrew/bin/nu" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||||
|
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -12,6 +12,8 @@
|
|||||||
<option name="EXECUTE_IN_TERMINAL" value="false" />
|
<option name="EXECUTE_IN_TERMINAL" value="false" />
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||||
<envs />
|
<envs />
|
||||||
<method v="2" />
|
<method v="2">
|
||||||
|
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
|
||||||
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
21
app/Build/Build Script.csproj
Normal file
21
app/Build/Build Script.csproj
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RootNamespace>Build</RootNamespace>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>build</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
3
app/Build/Commands/AppVersion.cs
Normal file
3
app/Build/Commands/AppVersion.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
public record AppVersion(string VersionText, int Major, int Minor, int Patch);
|
21
app/Build/Commands/CheckRidsCommand.cs
Normal file
21
app/Build/Commands/CheckRidsCommand.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
// ReSharper disable UnusedType.Global
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
public sealed class CheckRidsCommand
|
||||||
|
{
|
||||||
|
[Command("check-rids", Description = "Check the RIDs for the current OS")]
|
||||||
|
public void GetRids()
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var rids = Environment.GetRidsForCurrentOS();
|
||||||
|
Console.WriteLine("The following RIDs are available for the current OS:");
|
||||||
|
foreach (var rid in rids)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- {rid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
272
app/Build/Commands/CollectI18NKeysCommand.cs
Normal file
272
app/Build/Commands/CollectI18NKeysCommand.cs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
// ReSharper disable UnusedType.Global
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
|
||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
public sealed partial class CollectI18NKeysCommand
|
||||||
|
{
|
||||||
|
[Command("collect-i18n", Description = "Collect I18N keys")]
|
||||||
|
public async Task CollectI18NKeys()
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine("=========================");
|
||||||
|
Console.Write("- Collecting I18N keys ...");
|
||||||
|
|
||||||
|
var cwd = Environment.GetAIStudioDirectory();
|
||||||
|
var binPath = Path.Join(cwd, "bin");
|
||||||
|
var objPath = Path.Join(cwd, "obj");
|
||||||
|
var wwwrootPath = Path.Join(cwd, "wwwroot");
|
||||||
|
var allFiles = Directory.EnumerateFiles(cwd, "*", SearchOption.AllDirectories);
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
var allI18NContent = new Dictionary<string, string>();
|
||||||
|
foreach (var filePath in allFiles)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(filePath.StartsWith(objPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(filePath.StartsWith(wwwrootPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
|
||||||
|
var matches = this.FindAllTextTags(content);
|
||||||
|
if (matches.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var ns = this.DetermineNamespace(filePath);
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty).Replace(".razor", string.Empty);
|
||||||
|
var langNamespace = $"{ns}.{name}".ToUpperInvariant();
|
||||||
|
foreach (var match in matches)
|
||||||
|
{
|
||||||
|
// The key in the format A.B.C.D.T{hash}:
|
||||||
|
var key = $"UI_TEXT_CONTENT.{langNamespace}.T{match.ToFNV32()}";
|
||||||
|
allI18NContent.TryAdd(key, match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" {counter:###,###} files processed, {allI18NContent.Count:###,###} keys found.");
|
||||||
|
|
||||||
|
Console.Write("- Creating Lua code ...");
|
||||||
|
var luaCode = this.ExportToLuaAssignments(allI18NContent);
|
||||||
|
|
||||||
|
// Build the path, where we want to store the Lua code:
|
||||||
|
var luaPath = Path.Join(cwd, "Assistants", "I18N", "allTexts.lua");
|
||||||
|
|
||||||
|
// Store the Lua code:
|
||||||
|
await File.WriteAllTextAsync(luaPath, luaCode, Encoding.UTF8);
|
||||||
|
|
||||||
|
Console.WriteLine(" done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExportToLuaAssignments(Dictionary<string, string> keyValuePairs)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
// Add the mandatory plugin metadata:
|
||||||
|
sb.AppendLine(
|
||||||
|
"""
|
||||||
|
-- The ID for this plugin:
|
||||||
|
ID = "77c2688a-a68f-45cc-820e-fa8f3038a146"
|
||||||
|
|
||||||
|
-- The icon for the plugin:
|
||||||
|
ICON_SVG = ""
|
||||||
|
|
||||||
|
-- The name of the plugin:
|
||||||
|
NAME = "Collected I18N keys"
|
||||||
|
|
||||||
|
-- The description of the plugin:
|
||||||
|
DESCRIPTION = "This plugin is not meant to be used directly. Its a collection of all I18N keys found in the project."
|
||||||
|
|
||||||
|
-- The version of the plugin:
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
|
-- The type of the plugin:
|
||||||
|
TYPE = "LANGUAGE"
|
||||||
|
|
||||||
|
-- The authors of the plugin:
|
||||||
|
AUTHORS = {"MindWork AI Community"}
|
||||||
|
|
||||||
|
-- The support contact for the plugin:
|
||||||
|
SUPPORT_CONTACT = "MindWork AI Community"
|
||||||
|
|
||||||
|
-- The source URL for the plugin:
|
||||||
|
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
|
||||||
|
|
||||||
|
-- The categories for the plugin:
|
||||||
|
CATEGORIES = { "CORE" }
|
||||||
|
|
||||||
|
-- The target groups for the plugin:
|
||||||
|
TARGET_GROUPS = { "EVERYONE" }
|
||||||
|
|
||||||
|
-- The flag for whether the plugin is maintained:
|
||||||
|
IS_MAINTAINED = true
|
||||||
|
|
||||||
|
-- When the plugin is deprecated, this message will be shown to users:
|
||||||
|
DEPRECATION_MESSAGE = ""
|
||||||
|
|
||||||
|
-- The IETF BCP 47 tag for the language. It's the ISO 639 language
|
||||||
|
-- code followed by the ISO 3166-1 country code:
|
||||||
|
IETF_TAG = "en-US"
|
||||||
|
|
||||||
|
-- The language name in the user's language:
|
||||||
|
LANG_NAME = "English (United States)"
|
||||||
|
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the UI_TEXT_CONTENT table:
|
||||||
|
LuaTable.Create(ref sb, "UI_TEXT_CONTENT", keyValuePairs);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> FindAllTextTags(ReadOnlySpan<char> fileContent)
|
||||||
|
{
|
||||||
|
const string START_TAG1 = """
|
||||||
|
T("
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string START_TAG2 = """
|
||||||
|
TB("
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string END_TAG = """
|
||||||
|
")
|
||||||
|
""";
|
||||||
|
|
||||||
|
(int Index, int Len) FindNextStart(ReadOnlySpan<char> content)
|
||||||
|
{
|
||||||
|
var startIdx1 = content.IndexOf(START_TAG1);
|
||||||
|
var startIdx2 = content.IndexOf(START_TAG2);
|
||||||
|
|
||||||
|
if (startIdx1 == -1 && startIdx2 == -1)
|
||||||
|
return (-1, 0);
|
||||||
|
|
||||||
|
if (startIdx1 == -1)
|
||||||
|
return (startIdx2, START_TAG2.Length);
|
||||||
|
|
||||||
|
if (startIdx2 == -1)
|
||||||
|
return (startIdx1, START_TAG1.Length);
|
||||||
|
|
||||||
|
if (startIdx1 < startIdx2)
|
||||||
|
return (startIdx1, START_TAG1.Length);
|
||||||
|
|
||||||
|
return (startIdx2, START_TAG2.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = new List<string>();
|
||||||
|
var startIdx = FindNextStart(fileContent);
|
||||||
|
var content = fileContent;
|
||||||
|
while (startIdx.Index > -1)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// In some cases, after the initial " there follow more " characters.
|
||||||
|
// We need to skip them:
|
||||||
|
//
|
||||||
|
content = content[(startIdx.Index + startIdx.Len)..];
|
||||||
|
while(content[0] == '"')
|
||||||
|
content = content[1..];
|
||||||
|
|
||||||
|
var endIdx = content.IndexOf(END_TAG);
|
||||||
|
if (endIdx == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var match = content[..endIdx];
|
||||||
|
while (match[^1] == '"')
|
||||||
|
match = match[..^1];
|
||||||
|
|
||||||
|
matches.Add(match.ToString());
|
||||||
|
startIdx = FindNextStart(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? DetermineNamespace(string filePath)
|
||||||
|
{
|
||||||
|
// Is it a C# file? Then we can read the namespace from it:
|
||||||
|
if (filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return this.ReadNamespaceFromCSharp(filePath);
|
||||||
|
|
||||||
|
// Is it a Razor file? Then, it depends:
|
||||||
|
if (filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Check if the file contains a namespace declaration:
|
||||||
|
var blazorNamespace = this.ReadNamespaceFromRazor(filePath);
|
||||||
|
if (blazorNamespace != null)
|
||||||
|
return blazorNamespace;
|
||||||
|
|
||||||
|
// Alright, no namespace declaration. Let's check the corresponding C# file:
|
||||||
|
var csFilePath = $"{filePath}.cs";
|
||||||
|
if (File.Exists(csFilePath))
|
||||||
|
{
|
||||||
|
var csNamespace = this.ReadNamespaceFromCSharp(csFilePath);
|
||||||
|
if (csNamespace != null)
|
||||||
|
return csNamespace;
|
||||||
|
|
||||||
|
Console.WriteLine($"- Error: Neither the blazor file '{filePath}' nor the corresponding C# file '{csFilePath}' contain a namespace declaration.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"- Error: The blazor file '{filePath}' does not contain a namespace declaration and the corresponding C# file '{csFilePath}' does not exist.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a C# or Razor file. We can't determine the namespace:
|
||||||
|
Console.WriteLine($"- Error: The file '{filePath}' is neither a C# nor a Razor file. We can't determine the namespace.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ReadNamespaceFromCSharp(string filePath)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var matches = CSharpNamespaceRegex().Matches(content);
|
||||||
|
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (matches.Count > 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = matches[0];
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ReadNamespaceFromRazor(string filePath)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var matches = BlazorNamespaceRegex().Matches(content);
|
||||||
|
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (matches.Count > 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = matches[0];
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||||
|
private static partial Regex BlazorNamespaceRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||||
|
private static partial Regex CSharpNamespaceRegex();
|
||||||
|
}
|
10
app/Build/Commands/PrepareAction.cs
Normal file
10
app/Build/Commands/PrepareAction.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
public enum PrepareAction
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
PATCH,
|
||||||
|
MINOR,
|
||||||
|
MAJOR,
|
||||||
|
}
|
651
app/Build/Commands/UpdateMetadataCommands.cs
Normal file
651
app/Build/Commands/UpdateMetadataCommands.cs
Normal file
@ -0,0 +1,651 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
// ReSharper disable MemberCanBePrivate.Global
|
||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
// ReSharper disable UnusedType.Global
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
|
||||||
|
public sealed partial class UpdateMetadataCommands
|
||||||
|
{
|
||||||
|
[Command("release", Description = "Prepare & build the next release")]
|
||||||
|
public async Task Release(PrepareAction action)
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Prepare the metadata for the next release:
|
||||||
|
await this.PerformPrepare(action, true);
|
||||||
|
|
||||||
|
// Build once to allow the Rust compiler to read the changed metadata
|
||||||
|
// and to update all .NET artifacts:
|
||||||
|
await this.Build();
|
||||||
|
|
||||||
|
// Now, we update the web assets (which may were updated by the first build):
|
||||||
|
new UpdateWebAssetsCommand().UpdateWebAssets();
|
||||||
|
|
||||||
|
// Collect the I18N keys from the source code. This step yields a I18N file
|
||||||
|
// that must be part of the final release:
|
||||||
|
await new CollectI18NKeysCommand().CollectI18NKeys();
|
||||||
|
|
||||||
|
// Build the final release, where Rust knows the updated metadata, the .NET
|
||||||
|
// artifacts are already in place, and .NET knows the updated web assets, etc.:
|
||||||
|
await this.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command("update-versions", Description = "The command will update the package versions in the metadata file")]
|
||||||
|
public async Task UpdateVersions()
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
Console.WriteLine("- Update the main package versions ...");
|
||||||
|
|
||||||
|
await this.UpdateDotnetVersion();
|
||||||
|
await this.UpdateRustVersion();
|
||||||
|
await this.UpdateMudBlazorVersion();
|
||||||
|
await this.UpdateTauriVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command("prepare", Description = "Prepare the metadata for the next release")]
|
||||||
|
public async Task Prepare(PrepareAction action)
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
Console.Write("- Are you trying to prepare a new release? (y/n) ");
|
||||||
|
var userAnswer = Console.ReadLine();
|
||||||
|
if (userAnswer?.ToLowerInvariant() == "y")
|
||||||
|
{
|
||||||
|
Console.WriteLine("- Please use the 'release' command instead");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.PerformPrepare(action, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PerformPrepare(PrepareAction action, bool internalCall)
|
||||||
|
{
|
||||||
|
if(internalCall)
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
|
||||||
|
Console.WriteLine("- Prepare the metadata for the next release ...");
|
||||||
|
|
||||||
|
var appVersion = await this.UpdateAppVersion(action);
|
||||||
|
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
|
||||||
|
{
|
||||||
|
var buildNumber = await this.IncreaseBuildNumber();
|
||||||
|
var buildTime = await this.UpdateBuildTime();
|
||||||
|
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
|
||||||
|
await this.CreateNextChangelog(buildNumber, appVersion);
|
||||||
|
await this.UpdateDotnetVersion();
|
||||||
|
await this.UpdateRustVersion();
|
||||||
|
await this.UpdateMudBlazorVersion();
|
||||||
|
await this.UpdateTauriVersion();
|
||||||
|
await this.UpdateProjectCommitHash();
|
||||||
|
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
|
||||||
|
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "About.razor.cs")));
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command("build", Description = "Build MindWork AI Studio")]
|
||||||
|
public async Task Build()
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Build the .NET project:
|
||||||
|
//
|
||||||
|
var pathApp = Environment.GetAIStudioDirectory();
|
||||||
|
var rids = Environment.GetRidsForCurrentOS();
|
||||||
|
foreach (var rid in rids)
|
||||||
|
{
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
await this.UpdateArchitecture(rid);
|
||||||
|
|
||||||
|
Console.Write($"- Start .NET build for '{rid.AsMicrosoftRid()}' ...");
|
||||||
|
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
|
||||||
|
var dotnetBuildOutput = await this.ReadCommandOutput(pathApp, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --disable-build-servers --force");
|
||||||
|
var dotnetBuildOutputLines = dotnetBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var foundIssue = false;
|
||||||
|
foreach (var buildOutputLine in dotnetBuildOutputLines)
|
||||||
|
{
|
||||||
|
if(buildOutputLine.Contains(" error ") || buildOutputLine.Contains("#warning"))
|
||||||
|
{
|
||||||
|
if(!foundIssue)
|
||||||
|
{
|
||||||
|
foundIssue = true;
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("- Build has issues:");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write(" - ");
|
||||||
|
Console.WriteLine(buildOutputLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(foundIssue)
|
||||||
|
Console.WriteLine();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(" completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Prepare the .NET artifact to be used by Tauri as sidecar:
|
||||||
|
//
|
||||||
|
var os = Environment.GetOS();
|
||||||
|
var tauriSidecarArtifactName = rid switch
|
||||||
|
{
|
||||||
|
RID.WIN_X64 => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
|
||||||
|
RID.WIN_ARM64 => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
|
||||||
|
|
||||||
|
RID.LINUX_X64 => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
|
||||||
|
RID.LINUX_ARM64 => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
|
||||||
|
|
||||||
|
RID.OSX_ARM64 => "mindworkAIStudioServer-aarch64-apple-darwin",
|
||||||
|
RID.OSX_X64 => "mindworkAIStudioServer-x86_64-apple-darwin",
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
|
||||||
|
if(!Directory.Exists(dotnetArtifactPath))
|
||||||
|
Directory.CreateDirectory(dotnetArtifactPath);
|
||||||
|
|
||||||
|
var dotnetArtifactFilename = os switch
|
||||||
|
{
|
||||||
|
"windows" => "mindworkAIStudio.exe",
|
||||||
|
_ => "mindworkAIStudio",
|
||||||
|
};
|
||||||
|
|
||||||
|
var dotnetPublishedPath = Path.Combine(pathApp, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", dotnetArtifactFilename);
|
||||||
|
var finalDestination = Path.Combine(dotnetArtifactPath, tauriSidecarArtifactName);
|
||||||
|
|
||||||
|
if(File.Exists(dotnetPublishedPath))
|
||||||
|
Console.WriteLine("- Published .NET artifact found.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(dotnetPublishedPath, finalDestination, true);
|
||||||
|
Console.WriteLine(" done.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" failed.");
|
||||||
|
Console.WriteLine($" - Error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Build the Rust project / runtime:
|
||||||
|
//
|
||||||
|
|
||||||
|
Console.WriteLine("==============================");
|
||||||
|
Console.WriteLine("- Start building the Rust runtime ...");
|
||||||
|
|
||||||
|
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||||
|
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
|
||||||
|
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var foundRustIssue = false;
|
||||||
|
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||||
|
{
|
||||||
|
if(buildOutputLine.Contains("error", StringComparison.OrdinalIgnoreCase) || buildOutputLine.Contains("warning"))
|
||||||
|
{
|
||||||
|
if(!foundRustIssue)
|
||||||
|
{
|
||||||
|
foundRustIssue = true;
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("- Build has issues:");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write(" - ");
|
||||||
|
Console.WriteLine(buildOutputLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(foundRustIssue)
|
||||||
|
Console.WriteLine();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("- Compilation completed successfully.");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateNextChangelog(int currentBuildNumber, AppVersion currentAppVersion)
|
||||||
|
{
|
||||||
|
Console.Write("- Create the next changelog ...");
|
||||||
|
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
|
||||||
|
var nextBuildNumber = currentBuildNumber + 1;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We assume that most of the time, there will be patch releases:
|
||||||
|
//
|
||||||
|
var nextMajor = currentAppVersion.Major;
|
||||||
|
var nextMinor = currentAppVersion.Minor;
|
||||||
|
var nextPatch = currentAppVersion.Patch + 1;
|
||||||
|
|
||||||
|
var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}";
|
||||||
|
var nextChangelogFilename = $"v{nextAppVersion}.md";
|
||||||
|
var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Regarding the next build time: We assume that the next release will take place in one week from now.
|
||||||
|
// Thus, we check how many days this month has left. In the end, we want to predict the year and month
|
||||||
|
// for the next build. Day, hour, minute and second are all set to x.
|
||||||
|
//
|
||||||
|
var nextBuildMonth = (DateTime.Today + TimeSpan.FromDays(7)).Month;
|
||||||
|
var nextBuildYear = (DateTime.Today + TimeSpan.FromDays(7)).Year;
|
||||||
|
var nextBuildTimeString = $"{nextBuildYear}-{nextBuildMonth:00}-xx xx:xx UTC";
|
||||||
|
|
||||||
|
var changelogHeader = $"""
|
||||||
|
# v{nextAppVersion}, build {nextBuildNumber} ({nextBuildTimeString})
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
if(!File.Exists(nextChangelogFilePath))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(nextChangelogFilePath, changelogHeader, Environment.UTF8_NO_BOM);
|
||||||
|
Console.WriteLine($" done. Changelog '{nextChangelogFilename}' created.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(" failed.");
|
||||||
|
Console.WriteLine("- Error: The changelog file already exists.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateChangelog(int buildNumber, string appVersion, string buildTime)
|
||||||
|
{
|
||||||
|
Console.Write("- Updating the in-app changelog list ...");
|
||||||
|
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
|
||||||
|
var expectedLogFilename = $"v{appVersion}.md";
|
||||||
|
var expectedLogFilePath = Path.Combine(pathChangelogs, expectedLogFilename);
|
||||||
|
|
||||||
|
if(!File.Exists(expectedLogFilePath))
|
||||||
|
{
|
||||||
|
Console.WriteLine(" failed.");
|
||||||
|
Console.WriteLine($"- Error: The changelog file '{expectedLogFilename}' does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right now, the build time is formatted as "yyyy-MM-dd HH:mm:ss UTC", but must remove the seconds:
|
||||||
|
buildTime = buildTime[..^7] + " UTC";
|
||||||
|
|
||||||
|
const string CODE_START =
|
||||||
|
"""
|
||||||
|
LOGS =
|
||||||
|
[
|
||||||
|
""";
|
||||||
|
|
||||||
|
var changelogCodePath = Path.Join(Environment.GetAIStudioDirectory(), "Components", "Changelog.Logs.cs");
|
||||||
|
var changelogCode = await File.ReadAllTextAsync(changelogCodePath, Encoding.UTF8);
|
||||||
|
var updatedCode =
|
||||||
|
$"""
|
||||||
|
{CODE_START}
|
||||||
|
new ({buildNumber}, "v{appVersion}, build {buildNumber} ({buildTime})", "{expectedLogFilename}"),
|
||||||
|
""";
|
||||||
|
|
||||||
|
changelogCode = changelogCode.Replace(CODE_START, updatedCode);
|
||||||
|
await File.WriteAllTextAsync(changelogCodePath, changelogCode, Environment.UTF8_NO_BOM);
|
||||||
|
Console.WriteLine(" done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateArchitecture(RID rid)
|
||||||
|
{
|
||||||
|
const int ARCHITECTURE_INDEX = 9;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
Console.Write("- Updating architecture ...");
|
||||||
|
lines[ARCHITECTURE_INDEX] = rid.AsMicrosoftRid();
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
Console.WriteLine(" done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateProjectCommitHash()
|
||||||
|
{
|
||||||
|
const int COMMIT_HASH_INDEX = 8;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentCommitHash = lines[COMMIT_HASH_INDEX].Trim();
|
||||||
|
var headCommitHash = await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD");
|
||||||
|
var first10Chars = headCommitHash[..11];
|
||||||
|
var updatedCommitHash = $"{first10Chars}, release";
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updating commit hash from '{currentCommitHash}' to '{updatedCommitHash}'.");
|
||||||
|
lines[COMMIT_HASH_INDEX] = updatedCommitHash;
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AppVersion> UpdateAppVersion(PrepareAction action)
|
||||||
|
{
|
||||||
|
const int APP_VERSION_INDEX = 0;
|
||||||
|
|
||||||
|
if (action == PrepareAction.NONE)
|
||||||
|
{
|
||||||
|
Console.WriteLine("- No action specified. Skipping app version update.");
|
||||||
|
return new(string.Empty, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim();
|
||||||
|
var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
|
||||||
|
var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
|
||||||
|
var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
|
||||||
|
var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value);
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case PrepareAction.PATCH:
|
||||||
|
currentPatch++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrepareAction.MINOR:
|
||||||
|
currentPatch = 0;
|
||||||
|
currentMinor++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrepareAction.MAJOR:
|
||||||
|
currentPatch = 0;
|
||||||
|
currentMinor = 0;
|
||||||
|
currentMajor++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}";
|
||||||
|
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
|
||||||
|
|
||||||
|
lines[APP_VERSION_INDEX] = updatedAppVersion;
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
|
||||||
|
return new(updatedAppVersion, currentMajor, currentMinor, currentPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateLicenceYear(string licenceFilePath)
|
||||||
|
{
|
||||||
|
var currentYear = DateTime.UtcNow.Year.ToString();
|
||||||
|
var lines = await File.ReadAllLinesAsync(licenceFilePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var found = false;
|
||||||
|
var copyrightYear = string.Empty;
|
||||||
|
var updatedLines = new List<string>(lines.Length);
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
var match = FindCopyrightRegex().Match(line);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
copyrightYear = match.Groups["year"].Value;
|
||||||
|
|
||||||
|
if(!found && copyrightYear != currentYear)
|
||||||
|
Console.WriteLine($"- Updating the licence's year in '{Path.GetFileName(licenceFilePath)}' from '{copyrightYear}' to '{currentYear}'.");
|
||||||
|
|
||||||
|
updatedLines.Add(ReplaceCopyrightYearRegex().Replace(line, currentYear));
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
updatedLines.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(licenceFilePath, updatedLines, Environment.UTF8_NO_BOM);
|
||||||
|
if (!found)
|
||||||
|
Console.WriteLine($"- Error: No copyright year found in '{Path.GetFileName(licenceFilePath)}'.");
|
||||||
|
else if (copyrightYear == currentYear)
|
||||||
|
Console.WriteLine($"- The copyright year in '{Path.GetFileName(licenceFilePath)}' is already up to date.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateTauriVersion()
|
||||||
|
{
|
||||||
|
const int TAURI_VERSION_INDEX = 7;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentTauriVersion = lines[TAURI_VERSION_INDEX].Trim();
|
||||||
|
|
||||||
|
var matches = await this.DetermineVersion("Tauri", Environment.GetRustRuntimeDirectory(), TauriVersionRegex(), "cargo", "tree --depth 1");
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedTauriVersion = matches[0].Groups["version"].Value;
|
||||||
|
if(currentTauriVersion == updatedTauriVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine("- The Tauri version is already up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updated Tauri version from {currentTauriVersion} to {updatedTauriVersion}.");
|
||||||
|
lines[TAURI_VERSION_INDEX] = updatedTauriVersion;
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateMudBlazorVersion()
|
||||||
|
{
|
||||||
|
const int MUD_BLAZOR_VERSION_INDEX = 6;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentMudBlazorVersion = lines[MUD_BLAZOR_VERSION_INDEX].Trim();
|
||||||
|
|
||||||
|
var matches = await this.DetermineVersion("MudBlazor", Environment.GetAIStudioDirectory(), MudBlazorVersionRegex(), "dotnet", "list package");
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedMudBlazorVersion = matches[0].Groups["version"].Value;
|
||||||
|
if(currentMudBlazorVersion == updatedMudBlazorVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine("- The MudBlazor version is already up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updated MudBlazor version from {currentMudBlazorVersion} to {updatedMudBlazorVersion}.");
|
||||||
|
lines[MUD_BLAZOR_VERSION_INDEX] = updatedMudBlazorVersion;
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateRustVersion()
|
||||||
|
{
|
||||||
|
const int RUST_VERSION_INDEX = 5;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentRustVersion = lines[RUST_VERSION_INDEX].Trim();
|
||||||
|
var matches = await this.DetermineVersion("Rust", Environment.GetRustRuntimeDirectory(), RustVersionRegex(), "rustc", "-Vv");
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedRustVersion = matches[0].Groups["version"].Value + " (commit " + matches[0].Groups["commit"].Value + ")";
|
||||||
|
if(currentRustVersion == updatedRustVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine("- Rust version is already up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updated Rust version from {currentRustVersion} to {updatedRustVersion}.");
|
||||||
|
lines[RUST_VERSION_INDEX] = updatedRustVersion;
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateDotnetVersion()
|
||||||
|
{
|
||||||
|
const int DOTNET_VERSION_INDEX = 4;
|
||||||
|
const int DOTNET_SDK_VERSION_INDEX = 3;
|
||||||
|
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var currentDotnetVersion = lines[DOTNET_VERSION_INDEX].Trim();
|
||||||
|
var currentDotnetSdkVersion = lines[DOTNET_SDK_VERSION_INDEX].Trim();
|
||||||
|
|
||||||
|
var matches = await this.DetermineVersion(".NET", Environment.GetAIStudioDirectory(), DotnetVersionRegex(), "dotnet", "--info");
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedDotnetVersion = matches[0].Groups["hostVersion"].Value + " (commit " + matches[0].Groups["hostCommit"].Value + ")";
|
||||||
|
var updatedDotnetSdkVersion = matches[0].Groups["sdkVersion"].Value + " (commit " + matches[0].Groups["sdkCommit"].Value + ")";
|
||||||
|
if(currentDotnetVersion == updatedDotnetVersion && currentDotnetSdkVersion == updatedDotnetSdkVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine("- .NET version is already up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updated .NET SDK version from {currentDotnetSdkVersion} to {updatedDotnetSdkVersion}.");
|
||||||
|
Console.WriteLine($"- Updated .NET version from {currentDotnetVersion} to {updatedDotnetVersion}.");
|
||||||
|
|
||||||
|
lines[DOTNET_VERSION_INDEX] = updatedDotnetVersion;
|
||||||
|
lines[DOTNET_SDK_VERSION_INDEX] = updatedDotnetSdkVersion;
|
||||||
|
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IList<Match>> DetermineVersion(string name, string workingDirectory, Regex regex, string program, string command)
|
||||||
|
{
|
||||||
|
var processInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
FileName = program,
|
||||||
|
Arguments = command,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = processInfo;
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
var output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
var matches = regex.Matches(output);
|
||||||
|
if (matches.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- Error: Was not able to determine the {name} version.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
|
||||||
|
{
|
||||||
|
var processInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
FileName = program,
|
||||||
|
Arguments = command,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = processInfo;
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
process.OutputDataReceived += (_, args) =>
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(args.Data))
|
||||||
|
{
|
||||||
|
if(showLiveOutput)
|
||||||
|
Console.WriteLine(args.Data);
|
||||||
|
sb.AppendLine(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (_, args) =>
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(args.Data))
|
||||||
|
{
|
||||||
|
if(showLiveOutput)
|
||||||
|
Console.WriteLine(args.Data);
|
||||||
|
sb.AppendLine(args.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> IncreaseBuildNumber()
|
||||||
|
{
|
||||||
|
const int BUILD_NUMBER_INDEX = 2;
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var buildNumber = int.Parse(lines[BUILD_NUMBER_INDEX]) + 1;
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updating build number from '{lines[BUILD_NUMBER_INDEX]}' to '{buildNumber}'.");
|
||||||
|
|
||||||
|
lines[BUILD_NUMBER_INDEX] = buildNumber.ToString();
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
return buildNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> UpdateBuildTime()
|
||||||
|
{
|
||||||
|
const int BUILD_TIME_INDEX = 1;
|
||||||
|
var pathMetadata = Environment.GetMetadataPath();
|
||||||
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
|
var buildTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC";
|
||||||
|
|
||||||
|
Console.WriteLine($"- Updating build time from '{lines[BUILD_TIME_INDEX]}' to '{buildTime}'.");
|
||||||
|
|
||||||
|
lines[BUILD_TIME_INDEX] = buildTime;
|
||||||
|
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||||
|
return buildTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?<sdkVersion>[0-9.]+).+Commit:\s+(?<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?<hostVersion>[0-9.]+).+Commit:\s+(?<hostCommit>[a-zA-Z0-9]+)""")]
|
||||||
|
private static partial Regex DotnetVersionRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)""")]
|
||||||
|
private static partial Regex RustVersionRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
|
||||||
|
private static partial Regex MudBlazorVersionRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
|
||||||
|
private static partial Regex TauriVersionRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""^\s*Copyright\s+(?<year>[0-9]{4})""")]
|
||||||
|
private static partial Regex FindCopyrightRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""([0-9]{4})""")]
|
||||||
|
private static partial Regex ReplaceCopyrightYearRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
|
||||||
|
private static partial Regex AppVersionRegex();
|
||||||
|
}
|
49
app/Build/Commands/UpdateWebAssetsCommand.cs
Normal file
49
app/Build/Commands/UpdateWebAssetsCommand.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
// ReSharper disable UnusedType.Global
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
namespace Build.Commands;
|
||||||
|
|
||||||
|
public sealed class UpdateWebAssetsCommand
|
||||||
|
{
|
||||||
|
[Command("update-web", Description = "Update web assets")]
|
||||||
|
public void UpdateWebAssets()
|
||||||
|
{
|
||||||
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine("=========================");
|
||||||
|
Console.Write("- Updating web assets ...");
|
||||||
|
|
||||||
|
var rid = Environment.GetRidsForCurrentOS().First();
|
||||||
|
var cwd = Environment.GetAIStudioDirectory();
|
||||||
|
var contentPath = Path.Join(cwd, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", "wwwroot", "_content");
|
||||||
|
var isMudBlazorDirectoryPresent = Directory.Exists(Path.Join(contentPath, "MudBlazor"));
|
||||||
|
if (!isMudBlazorDirectoryPresent)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"- Error: No web assets found for RID '{rid}'. Please publish the project first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.Join(cwd, "wwwroot", "system"));
|
||||||
|
var sourcePaths = Directory.EnumerateFiles(contentPath, "*", SearchOption.AllDirectories);
|
||||||
|
var counter = 0;
|
||||||
|
foreach(var sourcePath in sourcePaths)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
var relativePath = Path.GetRelativePath(cwd, sourcePath);
|
||||||
|
var targetPath = Path.Join(cwd, "wwwroot", relativePath);
|
||||||
|
var targetDirectory = Path.GetDirectoryName(targetPath);
|
||||||
|
if (targetDirectory != null)
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
|
||||||
|
File.Copy(sourcePath, targetPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
7
app/Build/GlobalUsings.cs
Normal file
7
app/Build/GlobalUsings.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Global using directives
|
||||||
|
|
||||||
|
global using System.Text;
|
||||||
|
|
||||||
|
global using Cocona;
|
||||||
|
|
||||||
|
global using Environment = Build.Tools.Environment;
|
9
app/Build/Program.cs
Normal file
9
app/Build/Program.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Build.Commands;
|
||||||
|
|
||||||
|
var builder = CoconaApp.CreateBuilder();
|
||||||
|
var app = builder.Build();
|
||||||
|
app.AddCommands<CheckRidsCommand>();
|
||||||
|
app.AddCommands<UpdateMetadataCommands>();
|
||||||
|
app.AddCommands<UpdateWebAssetsCommand>();
|
||||||
|
app.AddCommands<CollectI18NKeysCommand>();
|
||||||
|
app.Run();
|
79
app/Build/Tools/Environment.cs
Normal file
79
app/Build/Tools/Environment.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
namespace Build.Tools;
|
||||||
|
|
||||||
|
public static class Environment
|
||||||
|
{
|
||||||
|
public const string DOTNET_VERSION = "net9.0";
|
||||||
|
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
|
||||||
|
|
||||||
|
private static readonly Dictionary<RID, string> ALL_RIDS = Enum.GetValues<RID>().Select(rid => new KeyValuePair<RID, string>(rid, rid.AsMicrosoftRid())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||||
|
|
||||||
|
public static bool IsWorkingDirectoryValid()
|
||||||
|
{
|
||||||
|
var currentDirectory = Directory.GetCurrentDirectory();
|
||||||
|
var mainFile = Path.Combine(currentDirectory, "Program.cs");
|
||||||
|
var projectFile = Path.Combine(currentDirectory, "Build Script.csproj");
|
||||||
|
|
||||||
|
if (!currentDirectory.EndsWith("Build", StringComparison.Ordinal) || !File.Exists(mainFile) || !File.Exists(projectFile))
|
||||||
|
{
|
||||||
|
Console.WriteLine("The current directory is not a valid working directory for the build script. Go to the /app/Build directory within the git repository.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetAIStudioDirectory()
|
||||||
|
{
|
||||||
|
var currentDirectory = Directory.GetCurrentDirectory();
|
||||||
|
var directory = Path.Combine(currentDirectory, "..", "MindWork AI Studio");
|
||||||
|
return Path.GetFullPath(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetRustRuntimeDirectory()
|
||||||
|
{
|
||||||
|
var currentDirectory = Directory.GetCurrentDirectory();
|
||||||
|
var directory = Path.Combine(currentDirectory, "..", "..", "runtime");
|
||||||
|
return Path.GetFullPath(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetMetadataPath()
|
||||||
|
{
|
||||||
|
var currentDirectory = Directory.GetCurrentDirectory();
|
||||||
|
var directory = Path.Combine(currentDirectory, "..", "..", "metadata.txt");
|
||||||
|
return Path.GetFullPath(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetOS()
|
||||||
|
{
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return "windows";
|
||||||
|
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
return "linux";
|
||||||
|
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
return "darwin";
|
||||||
|
|
||||||
|
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<RID> GetRidsForCurrentOS()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return ALL_RIDS.Where(rid => rid.Value.StartsWith("win-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
return ALL_RIDS.Where(rid => rid.Value.StartsWith("osx-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
return ALL_RIDS.Where(rid => rid.Value.StartsWith("linux-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||||
|
|
||||||
|
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,12 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
||||||
EndProject
|
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
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -12,5 +18,19 @@ Global
|
|||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = 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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RID/@EntryIndexedValue">RID</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TB/@EntryIndexedValue">TB</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=I18N/@EntryIndexedValue">I18N</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=huggingface/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,19 +1,37 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
// ReSharper disable MemberCanBePrivate.Global
|
||||||
|
|
||||||
namespace AIStudio.Agents;
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : IAgent
|
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : IAgent
|
||||||
{
|
{
|
||||||
|
protected static readonly ContentBlock EMPTY_BLOCK = new()
|
||||||
|
{
|
||||||
|
Content = null,
|
||||||
|
ContentType = ContentType.NONE,
|
||||||
|
Role = ChatRole.AGENT,
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected DataSourceService DataSourceService { get; init; } = dataSourceService;
|
||||||
|
|
||||||
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||||
|
|
||||||
protected ThreadSafeRandom RNG { get; init; } = rng;
|
protected ThreadSafeRandom RNG { get; init; } = rng;
|
||||||
|
|
||||||
protected ILogger<AgentBase> Logger { get; init; } = logger;
|
protected ILogger<AgentBase> Logger { get; init; } = logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the type or category of this agent.
|
/// Represents the type or category of this agent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -60,24 +78,30 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
|||||||
Blocks = [],
|
Blocks = [],
|
||||||
};
|
};
|
||||||
|
|
||||||
protected DateTimeOffset AddUserRequest(ChatThread thread, string request)
|
protected UserRequest AddUserRequest(ChatThread thread, string request)
|
||||||
{
|
{
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
var lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = request,
|
||||||
|
};
|
||||||
|
|
||||||
thread.Blocks.Add(new ContentBlock
|
thread.Blocks.Add(new ContentBlock
|
||||||
{
|
{
|
||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = lastUserPrompt,
|
||||||
{
|
|
||||||
Text = request,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return time;
|
return new()
|
||||||
|
{
|
||||||
|
Time = time,
|
||||||
|
UserPrompt = lastUserPrompt,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task AddAIResponseAsync(ChatThread thread, DateTimeOffset time)
|
protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time)
|
||||||
{
|
{
|
||||||
if(this.ProviderSettings is null)
|
if(this.ProviderSettings is null)
|
||||||
return;
|
return;
|
||||||
@ -103,6 +127,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.SettingsManager, providerSettings.Model, thread);
|
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), providerSettings.Model, lastUserPrompt, thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
378
app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
Normal file
378
app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
|
||||||
|
{
|
||||||
|
private readonly List<ContentBlock> answers = new();
|
||||||
|
|
||||||
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Type Type => Type.SYSTEM;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "Data Source Selection";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string JobDescription =>
|
||||||
|
"""
|
||||||
|
You receive a system and a user prompt, as well as a list of possible data sources as input.
|
||||||
|
Your task is to select the appropriate data sources for the given task. You may choose none,
|
||||||
|
one, or multiple sources, depending on what best fits the system and user prompt. You need
|
||||||
|
to estimate and assess which source, based on its description, might be helpful in
|
||||||
|
processing the prompts.
|
||||||
|
|
||||||
|
Your response is a JSON list in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.87},
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.54}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
|
||||||
|
1.0 (you are absolutely certain that this source is needed).
|
||||||
|
|
||||||
|
The JSON schema is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"reason",
|
||||||
|
"confidence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When no data source is needed, you return an empty JSON list `[]`. You do not ask any
|
||||||
|
follow-up questions. You do not address the user. Your response consists solely of
|
||||||
|
the JSON list.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string SystemPrompt(string availableDataSources) => $"""
|
||||||
|
{this.JobDescription}
|
||||||
|
|
||||||
|
{availableDataSources}
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source selection agent does not work with context. Use
|
||||||
|
/// the process input method instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The chat thread without any changes.</returns>
|
||||||
|
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
|
||||||
|
{
|
||||||
|
if (input.Content is not ContentText text)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(text.InitialRemoteWait || text.IsStreaming)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(text.Text))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(!additionalData.TryGetValue("availableDataSources", out var availableDataSources) || string.IsNullOrWhiteSpace(availableDataSources))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
var thread = this.CreateChatThread(this.SystemPrompt(availableDataSources));
|
||||||
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
|
var answer = thread.Blocks[^1];
|
||||||
|
|
||||||
|
this.answers.Add(answer);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetAnswers() => this.answers;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
logger.LogInformation("The AI should select the appropriate data sources.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Which LLM provider should the agent use?
|
||||||
|
//
|
||||||
|
|
||||||
|
// We start with the provider currently selected by the user:
|
||||||
|
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true);
|
||||||
|
|
||||||
|
// Assign the provider settings to the agent:
|
||||||
|
logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
|
||||||
|
this.ProviderSettings = agentProvider;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Prepare the current system and user prompts as input for the agent:
|
||||||
|
//
|
||||||
|
var lastPromptContent = lastPrompt switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
|
||||||
|
// Image prompts may be empty, e.g., when the image is too large:
|
||||||
|
ContentImage image => await image.AsBase64(token),
|
||||||
|
|
||||||
|
// Other content types are not supported yet:
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lastPromptContent))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The last prompt is empty. The AI cannot select data sources.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Prepare the allowed data sources as input for the agent:
|
||||||
|
//
|
||||||
|
var additionalData = new Dictionary<string, string>();
|
||||||
|
logger.LogInformation("Preparing the list of allowed data sources for the agent to choose from.");
|
||||||
|
|
||||||
|
// Notice: We do not dispose the Rust service here. The Rust service is a singleton
|
||||||
|
// and will be disposed when the application shuts down:
|
||||||
|
var rustService = Program.SERVICE_PROVIDER.GetService<RustService>()!;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("The following data sources are available for selection:");
|
||||||
|
foreach (var ds in dataSources.AllowedDataSources)
|
||||||
|
{
|
||||||
|
switch (ds)
|
||||||
|
{
|
||||||
|
case DataSourceLocalDirectory localDirectory:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSourceLocalFile localFile:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IERIDataSource eriDataSource:
|
||||||
|
var eriServerDescription = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Call the ERI server to get the server description:
|
||||||
|
//
|
||||||
|
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
|
||||||
|
var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token);
|
||||||
|
if (authResponse.Successful)
|
||||||
|
{
|
||||||
|
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
|
||||||
|
if (serverDescriptionResponse.Successful)
|
||||||
|
{
|
||||||
|
eriServerDescription = serverDescriptionResponse.Data.Description;
|
||||||
|
|
||||||
|
// Remove all line breaks from the description:
|
||||||
|
eriServerDescription = eriServerDescription.Replace("\n", " ").Replace("\r", " ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to retrieve the server description from the ERI data source '{eriDataSource.Name}'. Message: {serverDescriptionResponse.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to authenticate with the ERI data source '{eriDataSource.Name}'. Message: {authResponse.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"The ERI data source '{eriDataSource.Name}' is not available. Thus, we cannot retrieve the server description. Error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Append the ERI data source to the list. Use the server description if available:
|
||||||
|
//
|
||||||
|
if (string.IsNullOrWhiteSpace(eriServerDescription))
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source");
|
||||||
|
else
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source, description='{eriServerDescription}'");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Prepared the list of allowed data sources for the agent.");
|
||||||
|
additionalData.Add("availableDataSources", sb.ToString());
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4. Let the agent select the data sources:
|
||||||
|
//
|
||||||
|
var prompt = $"""
|
||||||
|
The system prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{lastPromptContent}
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Call the agent:
|
||||||
|
var aiResponse = await this.ProcessInput(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = new ContentText
|
||||||
|
{
|
||||||
|
Text = prompt,
|
||||||
|
},
|
||||||
|
}, additionalData);
|
||||||
|
|
||||||
|
if(aiResponse.Content is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aiResponse)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// 5. Parse the agent response:
|
||||||
|
//
|
||||||
|
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// What we expect is a JSON list of SelectedDataSource objects:
|
||||||
|
//
|
||||||
|
var selectedDataSourcesJson = textContent.Text;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We know how bad LLM may be in generating JSON without surrounding text.
|
||||||
|
// Thus, we expect the worst and try to extract the JSON list from the text:
|
||||||
|
//
|
||||||
|
var json = ExtractJson(selectedDataSourcesJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var aiSelectedDataSources = JsonSerializer.Deserialize<List<SelectedDataSource>>(json, JSON_SERIALIZER_OPTIONS);
|
||||||
|
return aiSelectedDataSources ?? [];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case { ContentType: ContentType.TEXT }:
|
||||||
|
logger.LogWarning("The agent answered with an unexpected inner content type.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
case { ContentType: ContentType.NONE }:
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the JSON list from the given text. The text may contain additional
|
||||||
|
/// information around the JSON list. The method tries to extract the JSON list
|
||||||
|
/// from the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Algorithm: The method searches for the first line that contains only a '[' character.
|
||||||
|
/// Then, it searches for the first line that contains only a ']' character. The method
|
||||||
|
/// returns the text between these two lines (including the brackets). When the method
|
||||||
|
/// cannot find the JSON list, it returns an empty string.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="text">The text that may contain the JSON list.</param>
|
||||||
|
/// <returns>The extracted JSON list.</returns>
|
||||||
|
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> text)
|
||||||
|
{
|
||||||
|
var startIndex = -1;
|
||||||
|
var endIndex = -1;
|
||||||
|
var foundStart = false;
|
||||||
|
var foundEnd = false;
|
||||||
|
var lineStart = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i <= text.Length; i++)
|
||||||
|
{
|
||||||
|
// Handle the end of the line or the end of the text:
|
||||||
|
if (i == text.Length || text[i] == '\n')
|
||||||
|
{
|
||||||
|
if (IsCharacterAloneInLine(text, lineStart, i, '[') && !foundStart)
|
||||||
|
{
|
||||||
|
startIndex = lineStart;
|
||||||
|
foundStart = true;
|
||||||
|
}
|
||||||
|
else if (IsCharacterAloneInLine(text, lineStart, i, ']') && foundStart && !foundEnd)
|
||||||
|
{
|
||||||
|
endIndex = i;
|
||||||
|
foundEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart && foundEnd)
|
||||||
|
{
|
||||||
|
// Adjust endIndex for slicing, ensuring it's within bounds:
|
||||||
|
return text.Slice(startIndex, Math.Min(text.Length, endIndex + 1) - startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadOnlySpan<char>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCharacterAloneInLine(ReadOnlySpan<char> text, int lineStart, int lineEnd, char character)
|
||||||
|
{
|
||||||
|
for (var i = lineStart; i < lineEnd; i++)
|
||||||
|
if (!char.IsWhiteSpace(text[i]) && text[i] != character)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
385
app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs
Normal file
385
app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.RAG;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalContextValidation> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
|
||||||
|
{
|
||||||
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Type Type => Type.WORKER;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "Retrieval Context Validation";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string JobDescription =>
|
||||||
|
"""
|
||||||
|
You receive a system and user prompt as well as a retrieval context as input. Your task is to decide whether this
|
||||||
|
retrieval context is helpful in processing the prompts or not. You respond with the decision (true or false),
|
||||||
|
your reasoning, and your confidence in this decision.
|
||||||
|
|
||||||
|
Your response is only one JSON object in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"decision": true, "reason": "Why did you choose this source?", "confidence": 0.87}
|
||||||
|
```
|
||||||
|
|
||||||
|
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
|
||||||
|
1.0 (you are absolutely certain that this retrieval context is needed).
|
||||||
|
|
||||||
|
The JSON schema is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"decision": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"decision",
|
||||||
|
"reason",
|
||||||
|
"confidence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You do not ask any follow-up questions. You do not address the user. Your response consists solely of
|
||||||
|
that one JSON object.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string SystemPrompt(string retrievalContext) => $"""
|
||||||
|
{this.JobDescription}
|
||||||
|
|
||||||
|
{retrievalContext}
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The retrieval context validation agent does not work with context. Use
|
||||||
|
/// the process input method instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The chat thread without any changes.</returns>
|
||||||
|
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
|
||||||
|
{
|
||||||
|
if (input.Content is not ContentText text)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(text.InitialRemoteWait || text.IsStreaming)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(text.Text))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(!additionalData.TryGetValue("retrievalContext", out var retrievalContext) || string.IsNullOrWhiteSpace(retrievalContext))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
var thread = this.CreateChatThread(this.SystemPrompt(retrievalContext));
|
||||||
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
|
return thread.Blocks[^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We do not provide any context. This agent will process many retrieval contexts.
|
||||||
|
/// This would block a huge amount of memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An empty list.</returns>
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We do not provide any answers. This agent will process many retrieval contexts.
|
||||||
|
/// This would block a huge amount of memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An empty list.</returns>
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetAnswers() => [];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the LLM provider for the agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When you have to call the validation in parallel for many retrieval contexts,
|
||||||
|
/// you can set the provider once and then call the validation method in parallel.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="provider">The current LLM provider. When the user doesn't preselect an agent provider, the agent uses this provider.</param>
|
||||||
|
public void SetLLMProvider(IProvider provider)
|
||||||
|
{
|
||||||
|
// We start with the provider currently selected by the user:
|
||||||
|
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, provider.Id, true);
|
||||||
|
|
||||||
|
// Assign the provider settings to the agent:
|
||||||
|
logger.LogInformation($"The agent for the retrieval context validation uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
|
||||||
|
this.ProviderSettings = agentProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate all retrieval contexts against the last user and the system prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastPrompt">The last user prompt.</param>
|
||||||
|
/// <param name="chatThread">The chat thread.</param>
|
||||||
|
/// <param name="retrievalContexts">All retrieval contexts to validate.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The validation results.</returns>
|
||||||
|
public async Task<IReadOnlyList<RetrievalContextValidationResult>> ValidateRetrievalContextsAsync(IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
// Check if the retrieval context validation is enabled:
|
||||||
|
if (!this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
logger.LogInformation($"Validating {retrievalContexts.Count:###,###,###,###} retrieval contexts.");
|
||||||
|
|
||||||
|
// Prepare the list of validation tasks:
|
||||||
|
var validationTasks = new List<Task<RetrievalContextValidationResult>>(retrievalContexts.Count);
|
||||||
|
|
||||||
|
// Read the number of parallel validations:
|
||||||
|
var numParallelValidations = 3;
|
||||||
|
if(this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions)
|
||||||
|
numParallelValidations = this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.NumParallelValidations;
|
||||||
|
|
||||||
|
numParallelValidations = Math.Max(1, numParallelValidations);
|
||||||
|
|
||||||
|
// Use a semaphore to limit the number of parallel validations:
|
||||||
|
using var semaphore = new SemaphoreSlim(numParallelValidations);
|
||||||
|
foreach (var retrievalContext in retrievalContexts)
|
||||||
|
{
|
||||||
|
// Wait for an available slot in the semaphore:
|
||||||
|
await semaphore.WaitAsync(token);
|
||||||
|
|
||||||
|
// Start the next validation task:
|
||||||
|
validationTasks.Add(this.ValidateRetrievalContextAsync(lastPrompt, chatThread, retrievalContext, token, semaphore));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all validation tasks to complete:
|
||||||
|
return await Task.WhenAll(validationTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the retrieval context against the last user and the system prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Probably, you have a lot of retrieval contexts to validate. In this case, you
|
||||||
|
/// can call this method in parallel for each retrieval context. You might use
|
||||||
|
/// the ValidateRetrievalContextsAsync method to validate all retrieval contexts.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="lastPrompt">The last user prompt.</param>
|
||||||
|
/// <param name="chatThread">The chat thread.</param>
|
||||||
|
/// <param name="retrievalContext">The retrieval context to validate.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <param name="semaphore">The optional semaphore to limit the number of parallel validations.</param>
|
||||||
|
/// <returns>The validation result.</returns>
|
||||||
|
public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsync(IContent lastPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Check if the validation was canceled. This could happen when the user
|
||||||
|
// canceled the validation process or when the validation process took
|
||||||
|
// too long:
|
||||||
|
//
|
||||||
|
if(token.IsCancellationRequested)
|
||||||
|
return new(false, "The validation was canceled.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Prepare the current system and user prompts as input for the agent:
|
||||||
|
//
|
||||||
|
var lastPromptContent = lastPrompt switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
|
||||||
|
// Image prompts may be empty, e.g., when the image is too large:
|
||||||
|
ContentImage image => await image.AsBase64(token),
|
||||||
|
|
||||||
|
// Other content types are not supported yet:
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lastPromptContent))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The last prompt is empty. The AI cannot validate the retrieval context.");
|
||||||
|
return new(false, "The last prompt was empty.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Prepare the retrieval context for the agent:
|
||||||
|
//
|
||||||
|
var additionalData = new Dictionary<string, string>();
|
||||||
|
var markdownRetrievalContext = await retrievalContext.AsMarkdown(token: token);
|
||||||
|
additionalData.Add("retrievalContext", markdownRetrievalContext);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Let the agent validate the retrieval context:
|
||||||
|
//
|
||||||
|
var prompt = $"""
|
||||||
|
The system prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{lastPromptContent}
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Call the agent:
|
||||||
|
var aiResponse = await this.ProcessInput(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = new ContentText
|
||||||
|
{
|
||||||
|
Text = prompt,
|
||||||
|
},
|
||||||
|
}, additionalData);
|
||||||
|
|
||||||
|
if (aiResponse.Content is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aiResponse)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4. Parse the agent response:
|
||||||
|
//
|
||||||
|
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// What we expect is one JSON object:
|
||||||
|
//
|
||||||
|
var validationJson = textContent.Text;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We know how bad LLM may be in generating JSON without surrounding text.
|
||||||
|
// Thus, we expect the worst and try to extract the JSON list from the text:
|
||||||
|
//
|
||||||
|
var json = ExtractJson(validationJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<RetrievalContextValidationResult>(json, JSON_SERIALIZER_OPTIONS);
|
||||||
|
return result with { RetrievalContext = retrievalContext };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
|
||||||
|
return new(false, "The agent answered with an invalid or unexpected JSON format.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case { ContentType: ContentType.TEXT }:
|
||||||
|
logger.LogWarning("The agent answered with an unexpected inner content type.");
|
||||||
|
return new(false, "The agent answered with an unexpected inner content type.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
case { ContentType: ContentType.NONE }:
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
|
||||||
|
return new(false, $"The agent answered with an unexpected content type '{aiResponse.ContentType}'.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Release the semaphore slot:
|
||||||
|
semaphore?.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// 1. Expect the best case ;-)
|
||||||
|
//
|
||||||
|
if (CheckJsonObjectStart(input))
|
||||||
|
return ExtractJsonPart(input);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Okay, we have some garbage before the
|
||||||
|
// JSON object. We expected that...
|
||||||
|
//
|
||||||
|
for (var index = 0; index < input.Length; index++)
|
||||||
|
{
|
||||||
|
if (input[index] is '{' && CheckJsonObjectStart(input[index..]))
|
||||||
|
return ExtractJsonPart(input[index..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckJsonObjectStart(ReadOnlySpan<char> area)
|
||||||
|
{
|
||||||
|
char[] expectedSymbols = ['{', '"', 'd'];
|
||||||
|
var symbolIndex = 0;
|
||||||
|
|
||||||
|
foreach (var c in area)
|
||||||
|
{
|
||||||
|
if (symbolIndex >= expectedSymbols.Length)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (char.IsWhiteSpace(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (c == expectedSymbols[symbolIndex++])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> ExtractJsonPart(ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
var insideString = false;
|
||||||
|
for (var index = 0; index < input.Length; index++)
|
||||||
|
{
|
||||||
|
if (input[index] is '"')
|
||||||
|
{
|
||||||
|
insideString = !insideString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insideString)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (input[index] is '}')
|
||||||
|
return input[..++index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,17 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
namespace AIStudio.Agents;
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, rng)
|
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, dataSourceService, rng)
|
||||||
{
|
{
|
||||||
private static readonly ContentBlock EMPTY_BLOCK = new()
|
|
||||||
{
|
|
||||||
Content = null,
|
|
||||||
ContentType = ContentType.NONE,
|
|
||||||
Role = ChatRole.AGENT,
|
|
||||||
Time = DateTimeOffset.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<ContentBlock> context = new();
|
private readonly List<ContentBlock> context = new();
|
||||||
private readonly List<ContentBlock> answers = new();
|
private readonly List<ContentBlock> answers = new();
|
||||||
|
|
||||||
#region Overrides of AgentBase
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
public override Settings.Provider? ProviderSettings { get; set; }
|
public override AIStudio.Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
protected override Type Type => Type.SYSTEM;
|
protected override Type Type => Type.SYSTEM;
|
||||||
|
|
||||||
@ -72,8 +65,8 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
|
|||||||
return EMPTY_BLOCK;
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
|
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
|
||||||
var time = this.AddUserRequest(thread, text.Text);
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
await this.AddAIResponseAsync(thread, time);
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
var answer = thread.Blocks[^1];
|
var answer = thread.Blocks[^1];
|
||||||
this.answers.Add(answer);
|
this.answers.Add(answer);
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using AIStudio.Tools.RAG;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a retrieval context validation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Decision">Whether the retrieval context is useful or not.</param>
|
||||||
|
/// <param name="Reason">The reason for the decision.</param>
|
||||||
|
/// <param name="Confidence">The confidence of the decision.</param>
|
||||||
|
/// <param name="RetrievalContext">The retrieval context that was validated.</param>
|
||||||
|
public readonly record struct RetrievalContextValidationResult(bool Decision, string Reason, float Confidence, IRetrievalContext? RetrievalContext) : IConfidence;
|
9
app/MindWork AI Studio/Agents/SelectedDataSource.cs
Normal file
9
app/MindWork AI Studio/Agents/SelectedDataSource.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a selected data source, chosen by the agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The data source ID.</param>
|
||||||
|
/// <param name="Reason">The reason for selecting the data source.</param>
|
||||||
|
/// <param name="Confidence">The confidence of the agent in the selection.</param>
|
||||||
|
public readonly record struct SelectedDataSource(string Id, string Reason, float Confidence) : IConfidence;
|
19
app/MindWork AI Studio/Agents/UserRequest.cs
Normal file
19
app/MindWork AI Studio/Agents/UserRequest.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using AIStudio.Chat;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The created user request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time when the request was created.
|
||||||
|
/// </summary>
|
||||||
|
public required DateTimeOffset Time { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user prompt.
|
||||||
|
/// </summary>
|
||||||
|
public required IContent UserPrompt { get; init; }
|
||||||
|
}
|
@ -13,6 +13,7 @@
|
|||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
|
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
|
||||||
|
<link href="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
|
||||||
<link href="app.css" rel="stylesheet" />
|
<link href="app.css" rel="stylesheet" />
|
||||||
<HeadOutlet/>
|
<HeadOutlet/>
|
||||||
<script src="diff.js"></script>
|
<script src="diff.js"></script>
|
||||||
@ -24,6 +25,7 @@
|
|||||||
<script src="boot.js"></script>
|
<script src="boot.js"></script>
|
||||||
<script src="system/MudBlazor/MudBlazor.min.js"></script>
|
<script src="system/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
|
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
|
||||||
|
<script src="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_AGENDA)]
|
@attribute [Route(Routes.ASSISTANT_AGENDA)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAgenda>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.Agenda;
|
namespace AIStudio.Assistants.Agenda;
|
||||||
|
|
||||||
public partial class AssistantAgenda : AssistantBaseCore
|
public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ public partial class AssistantAgenda : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputContent = string.Empty;
|
this.inputContent = string.Empty;
|
||||||
this.contentLines.Clear();
|
this.contentLines.Clear();
|
||||||
|
@ -1,120 +1,151 @@
|
|||||||
@using AIStudio.Chat
|
@using AIStudio.Chat
|
||||||
|
@inherits AssistantLowerBase
|
||||||
|
@typeparam TSettings
|
||||||
|
|
||||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
<div class="inner-scrolling-context">
|
||||||
@(this.Title)
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
<InnerScrolling HeaderHeight="6em">
|
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||||
<ChildContent>
|
@(this.Title)
|
||||||
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" Class="pr-2">
|
</MudText>
|
||||||
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
|
|
||||||
@this.Description
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
@if (this.Body is not null)
|
<InnerScrolling>
|
||||||
{
|
<ChildContent>
|
||||||
<CascadingValue Value="@this">
|
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
|
||||||
@this.Body
|
<MudGrid Class="mb-2">
|
||||||
</CascadingValue>
|
<MudItem xs="10">
|
||||||
|
<MudText Typo="Typo.body1" Align="Align.Justify">
|
||||||
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
|
@this.Description
|
||||||
@this.SubmitText
|
</MudText>
|
||||||
</MudButton>
|
</MudItem>
|
||||||
}
|
<MudItem xs="2" Class="d-flex justify-end align-start">
|
||||||
</MudForm>
|
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="() => this.OpenSettingsDialog()"/>
|
||||||
<Issues IssuesData="@(this.inputIssues)"/>
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
@if (this.ShowDedicatedProgress && this.isProcessing)
|
@if (this.Body is not null)
|
||||||
{
|
|
||||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (this.ShowResult && this.resultingContentBlock is not null)
|
|
||||||
{
|
|
||||||
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
|
|
||||||
</div>
|
|
||||||
</ChildContent>
|
|
||||||
<FooterContent>
|
|
||||||
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
|
|
||||||
|
|
||||||
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
|
|
||||||
{
|
|
||||||
@if (this.ShowSendTo)
|
|
||||||
{
|
{
|
||||||
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
<CascadingValue Value="@this">
|
||||||
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
@this.Body
|
||||||
|
</CascadingValue>
|
||||||
|
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3">
|
||||||
|
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" OnClick="async () => await this.Start()" Style="@this.SubmitButtonStyle">
|
||||||
|
@this.SubmitText
|
||||||
|
</MudButton>
|
||||||
|
@if (this.isProcessing && this.cancellationTokenSource is not null)
|
||||||
{
|
{
|
||||||
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
|
<MudTooltip Text="@TB("Stop generation")">
|
||||||
@assistant.Name()
|
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
|
||||||
</MudMenuItem>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
</MudMenu>
|
</MudStack>
|
||||||
|
}
|
||||||
|
</MudForm>
|
||||||
|
<Issues IssuesData="@(this.inputIssues)"/>
|
||||||
|
|
||||||
|
@if (this.ShowDedicatedProgress && this.isProcessing)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null)
|
||||||
|
{
|
||||||
|
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null)
|
||||||
|
{
|
||||||
|
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
|
||||||
|
{
|
||||||
|
@if (!block.HideFromUser)
|
||||||
|
{
|
||||||
|
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var button in this.FooterButtons)
|
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
|
||||||
{
|
</div>
|
||||||
switch (button)
|
</ChildContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
|
||||||
|
|
||||||
|
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
|
||||||
{
|
{
|
||||||
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
|
@if (this.ShowSendTo)
|
||||||
<MudTooltip Text="@buttonData.Tooltip">
|
{
|
||||||
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
|
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@TB("Send to ...")" Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||||
@buttonData.Text
|
|
||||||
</MudButton>
|
|
||||||
</MudTooltip>
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ButtonData buttonData:
|
|
||||||
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
|
|
||||||
@buttonData.Text
|
|
||||||
</MudButton>
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SendToButton sendToButton:
|
|
||||||
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
|
||||||
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
||||||
{
|
{
|
||||||
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
|
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
|
||||||
@assistant.Name()
|
@assistant.Name()
|
||||||
</MudMenuItem>
|
</MudMenuItem>
|
||||||
}
|
}
|
||||||
</MudMenu>
|
</MudMenu>
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.ShowCopyResult)
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
|
|
||||||
Copy result
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.ShowReset)
|
@foreach (var button in this.FooterButtons)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
|
switch (button)
|
||||||
Reset
|
{
|
||||||
</MudButton>
|
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
|
||||||
}
|
<MudTooltip Text="@buttonData.Tooltip">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" Disabled="@buttonData.DisabledAction()" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
|
||||||
|
@buttonData.Text
|
||||||
|
</MudButton>
|
||||||
|
</MudTooltip>
|
||||||
|
break;
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
case ButtonData buttonData:
|
||||||
{
|
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" Disabled="@buttonData.DisabledAction()" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
|
||||||
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
@buttonData.Text
|
||||||
}
|
</MudButton>
|
||||||
|
break;
|
||||||
|
|
||||||
@if (this.AllowProfiles && this.ShowProfileSelection)
|
case SendToButton sendToButton:
|
||||||
{
|
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@TB("Send to ...")" Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
|
||||||
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
|
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
|
||||||
}
|
{
|
||||||
</MudStack>
|
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
|
||||||
</FooterContent>
|
@assistant.Name()
|
||||||
</InnerScrolling>
|
</MudMenuItem>
|
||||||
|
}
|
||||||
|
</MudMenu>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.ShowCopyResult)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
|
||||||
|
@TB("Copy result")
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.ShowReset)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
|
||||||
|
@TB("Reset")
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||||
|
{
|
||||||
|
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.AllowProfiles && this.ShowProfileSelection)
|
||||||
|
{
|
||||||
|
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</FooterContent>
|
||||||
|
</InnerScrolling>
|
||||||
|
</div>
|
@ -1,17 +1,22 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
using RustService = AIStudio.Tools.RustService;
|
using MudBlazor.Utilities;
|
||||||
|
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
namespace AIStudio.Assistants;
|
namespace AIStudio.Assistants;
|
||||||
|
|
||||||
public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver, IDisposable
|
public abstract partial class AssistantBase<TSettings> : AssistantLowerBase where TSettings : IComponent
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IJSRuntime JsRuntime { get; init; } = null!;
|
protected IJSRuntime JsRuntime { get; init; } = null!;
|
||||||
@ -29,18 +34,11 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
protected NavigationManager NavigationManager { get; init; } = null!;
|
protected NavigationManager NavigationManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected ILogger<AssistantBase> Logger { get; init; } = null!;
|
protected ILogger<AssistantBase<TSettings>> Logger { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private MudTheme ColorTheme { get; init; } = null!;
|
private MudTheme ColorTheme { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private MessageBus MessageBus { get; init; } = null!;
|
|
||||||
|
|
||||||
internal const string RESULT_DIV_ID = "assistantResult";
|
|
||||||
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
|
|
||||||
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
|
|
||||||
|
|
||||||
protected abstract string Title { get; }
|
protected abstract string Title { get; }
|
||||||
|
|
||||||
protected abstract string Description { get; }
|
protected abstract string Description { get; }
|
||||||
@ -55,19 +53,21 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected abstract void ResetFrom();
|
protected abstract void ResetForm();
|
||||||
|
|
||||||
protected abstract bool MightPreselectValues();
|
protected abstract bool MightPreselectValues();
|
||||||
|
|
||||||
protected abstract string SubmitText { get; }
|
protected abstract string SubmitText { get; }
|
||||||
|
|
||||||
protected abstract Func<Task> SubmitAction { get; }
|
protected abstract Func<Task> SubmitAction { get; }
|
||||||
|
|
||||||
protected virtual bool SubmitDisabled => false;
|
protected virtual bool SubmitDisabled => false;
|
||||||
|
|
||||||
private protected virtual RenderFragment? Body => null;
|
private protected virtual RenderFragment? Body => null;
|
||||||
|
|
||||||
protected virtual bool ShowResult => true;
|
protected virtual bool ShowResult => true;
|
||||||
|
|
||||||
|
protected virtual bool ShowEntireChatThread => false;
|
||||||
|
|
||||||
protected virtual bool AllowProfiles => true;
|
protected virtual bool AllowProfiles => true;
|
||||||
|
|
||||||
@ -84,15 +84,17 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
protected virtual ChatThread ConvertToChatThread => this.chatThread ?? new();
|
protected virtual ChatThread ConvertToChatThread => this.chatThread ?? new();
|
||||||
|
|
||||||
protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
|
protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
|
||||||
|
|
||||||
protected AIStudio.Settings.Provider providerSettings;
|
protected AIStudio.Settings.Provider providerSettings;
|
||||||
protected MudForm? form;
|
protected MudForm? form;
|
||||||
protected bool inputIsValid;
|
protected bool inputIsValid;
|
||||||
protected Profile currentProfile = Profile.NO_PROFILE;
|
protected Profile currentProfile = Profile.NO_PROFILE;
|
||||||
|
|
||||||
protected ChatThread? chatThread;
|
protected ChatThread? chatThread;
|
||||||
|
protected IContent? lastUserPrompt;
|
||||||
|
protected CancellationTokenSource? cancellationTokenSource;
|
||||||
|
|
||||||
|
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
||||||
|
|
||||||
private ContentBlock? resultingContentBlock;
|
private ContentBlock? resultingContentBlock;
|
||||||
private string[] inputIssues = [];
|
private string[] inputIssues = [];
|
||||||
private bool isProcessing;
|
private bool isProcessing;
|
||||||
@ -101,14 +103,18 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
|
||||||
|
this.formChangeTimer.AutoReset = false;
|
||||||
|
this.formChangeTimer.Elapsed += async (_, _) =>
|
||||||
|
{
|
||||||
|
this.formChangeTimer.Stop();
|
||||||
|
await this.OnFormChange();
|
||||||
|
};
|
||||||
|
|
||||||
this.MightPreselectValues();
|
this.MightPreselectValues();
|
||||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
|
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
|
||||||
|
|
||||||
this.MessageBus.RegisterComponent(this);
|
|
||||||
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
@ -130,27 +136,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Implementation of IMessageBusReceiver
|
|
||||||
|
|
||||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>));
|
||||||
{
|
|
||||||
switch (triggeredEvent)
|
|
||||||
{
|
|
||||||
case Event.COLOR_THEME_CHANGED:
|
|
||||||
this.StateHasChanged();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TResult?>(default);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
|
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
|
||||||
|
|
||||||
@ -161,21 +148,56 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task Start()
|
||||||
|
{
|
||||||
|
using (this.cancellationTokenSource = new())
|
||||||
|
{
|
||||||
|
await this.SubmitAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerFormChange(FormFieldChangedEventArgs _)
|
||||||
|
{
|
||||||
|
this.formChangeTimer.Stop();
|
||||||
|
this.formChangeTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method is called after any form field has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is called after a delay of 1.6 seconds. This is to prevent
|
||||||
|
/// the method from being called too often. This method is called after
|
||||||
|
/// the user has stopped typing or selecting options.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual Task OnFormChange() => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an issue to the UI.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="issue">The issue to add.</param>
|
||||||
|
protected void AddInputIssue(string issue)
|
||||||
|
{
|
||||||
|
Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1);
|
||||||
|
this.inputIssues[^1] = issue;
|
||||||
|
this.inputIsValid = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
protected void CreateChatThread()
|
protected void CreateChatThread()
|
||||||
{
|
{
|
||||||
this.chatThread = new()
|
this.chatThread = new()
|
||||||
{
|
{
|
||||||
|
SelectedProvider = this.providerSettings.Id,
|
||||||
|
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
|
||||||
|
SystemPrompt = this.SystemPrompt,
|
||||||
WorkspaceId = Guid.Empty,
|
WorkspaceId = Guid.Empty,
|
||||||
ChatId = Guid.NewGuid(),
|
ChatId = Guid.NewGuid(),
|
||||||
Name = string.Empty,
|
Name = string.Format(T("Assistant - {0}"), this.Title),
|
||||||
Seed = this.RNG.Next(),
|
Seed = this.RNG.Next(),
|
||||||
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
|
|
||||||
$"""
|
|
||||||
{this.SystemPrompt}
|
|
||||||
|
|
||||||
{this.currentProfile.ToSystemPrompt()}
|
|
||||||
""",
|
|
||||||
Blocks = [],
|
Blocks = [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -185,16 +207,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
var chatId = Guid.NewGuid();
|
var chatId = Guid.NewGuid();
|
||||||
this.chatThread = new()
|
this.chatThread = new()
|
||||||
{
|
{
|
||||||
|
SelectedProvider = this.providerSettings.Id,
|
||||||
|
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
|
||||||
|
SystemPrompt = this.SystemPrompt,
|
||||||
WorkspaceId = workspaceId,
|
WorkspaceId = workspaceId,
|
||||||
ChatId = chatId,
|
ChatId = chatId,
|
||||||
Name = name,
|
Name = name,
|
||||||
Seed = this.RNG.Next(),
|
Seed = this.RNG.Next(),
|
||||||
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
|
|
||||||
$"""
|
|
||||||
{this.SystemPrompt}
|
|
||||||
|
|
||||||
{this.currentProfile.ToSystemPrompt()}
|
|
||||||
""",
|
|
||||||
Blocks = [],
|
Blocks = [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,22 +223,24 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
|
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
|
||||||
{
|
{
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
this.lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = request,
|
||||||
|
};
|
||||||
|
|
||||||
this.chatThread!.Blocks.Add(new ContentBlock
|
this.chatThread!.Blocks.Add(new ContentBlock
|
||||||
{
|
{
|
||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
HideFromUser = hideContentFromUser,
|
HideFromUser = hideContentFromUser,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = this.lastUserPrompt,
|
||||||
{
|
|
||||||
Text = request,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
|
protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false)
|
||||||
{
|
{
|
||||||
var aiText = new ContentText
|
var aiText = new ContentText
|
||||||
{
|
{
|
||||||
@ -234,16 +255,22 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
Role = ChatRole.AI,
|
Role = ChatRole.AI,
|
||||||
Content = aiText,
|
Content = aiText,
|
||||||
|
HideFromUser = hideContentFromUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.chatThread?.Blocks.Add(this.resultingContentBlock);
|
if (this.chatThread is not null)
|
||||||
|
{
|
||||||
|
this.chatThread.Blocks.Add(this.resultingContentBlock);
|
||||||
|
this.chatThread.SelectedProvider = this.providerSettings.Id;
|
||||||
|
}
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
|
||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread, this.cancellationTokenSource!.Token);
|
||||||
|
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
@ -252,6 +279,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
return aiText.Text;
|
return aiText.Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CancelStreaming()
|
||||||
|
{
|
||||||
|
if (this.cancellationTokenSource is not null)
|
||||||
|
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||||
|
await this.cancellationTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task CopyToClipboard()
|
protected async Task CopyToClipboard()
|
||||||
{
|
{
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
|
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
|
||||||
@ -265,6 +299,12 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task OpenSettingsDialog()
|
||||||
|
{
|
||||||
|
var dialogParameters = new DialogParameters();
|
||||||
|
await this.DialogService.ShowAsync<TSettings>(null, dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
}
|
||||||
|
|
||||||
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
|
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
|
||||||
{
|
{
|
||||||
if (!destination.AllowSendTo())
|
if (!destination.AllowSendTo())
|
||||||
@ -284,7 +324,9 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
switch (destination)
|
switch (destination)
|
||||||
{
|
{
|
||||||
case Tools.Components.CHAT:
|
case Tools.Components.CHAT:
|
||||||
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
|
var convertedChatThread = this.ConvertToChatThread;
|
||||||
|
convertedChatThread = convertedChatThread with { SelectedProvider = this.providerSettings.Id };
|
||||||
|
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, convertedChatThread);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -304,7 +346,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
|
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
|
||||||
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
|
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
|
||||||
|
|
||||||
this.ResetFrom();
|
this.ResetForm();
|
||||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||||
|
|
||||||
this.inputIsValid = false;
|
this.inputIsValid = false;
|
||||||
@ -327,11 +369,11 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}",
|
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}",
|
||||||
};
|
};
|
||||||
|
|
||||||
#region Implementation of IDisposable
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
public void Dispose()
|
protected override void DisposeResources()
|
||||||
{
|
{
|
||||||
this.MessageBus.Unregister(this);
|
this.formChangeTimer.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -7,7 +7,7 @@ namespace AIStudio.Assistants;
|
|||||||
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
|
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
|
||||||
//
|
//
|
||||||
|
|
||||||
public abstract class AssistantBaseCore : AssistantBase
|
public abstract class AssistantBaseCore<TSettings> : AssistantBase<TSettings> where TSettings : IComponent
|
||||||
{
|
{
|
||||||
private protected sealed override RenderFragment Body => this.BuildRenderTree;
|
private protected sealed override RenderFragment Body => this.BuildRenderTree;
|
||||||
|
|
||||||
|
12
app/MindWork AI Studio/Assistants/AssistantLowerBase.cs
Normal file
12
app/MindWork AI Studio/Assistants/AssistantLowerBase.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants;
|
||||||
|
|
||||||
|
public abstract class AssistantLowerBase : MSGComponentBase
|
||||||
|
{
|
||||||
|
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
|
internal const string RESULT_DIV_ID = "assistantResult";
|
||||||
|
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
|
||||||
|
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_BIAS)]
|
@attribute [Route(Routes.ASSISTANT_BIAS)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBias>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1">
|
<MudText Typo="Typo.body1">
|
||||||
<b>Links:</b>
|
<b>Links:</b>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using AIStudio.Components;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.BiasDay;
|
namespace AIStudio.Assistants.BiasDay;
|
||||||
|
|
||||||
public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAssistantBias>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
|||||||
|
|
||||||
protected override bool ShowReset => false;
|
protected override bool ShowReset => false;
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
if (!this.MightPreselectValues())
|
if (!this.MightPreselectValues())
|
||||||
{
|
{
|
||||||
@ -124,11 +125,11 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
|||||||
{
|
{
|
||||||
var biasChat = new LoadChat
|
var biasChat = new LoadChat
|
||||||
{
|
{
|
||||||
WorkspaceId = Workspaces.WORKSPACE_ID_BIAS,
|
WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID,
|
||||||
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
|
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Workspaces.IsChatExisting(biasChat))
|
if (WorkspaceBehaviour.IsChatExisting(biasChat))
|
||||||
{
|
{
|
||||||
MessageBus.INSTANCE.DeferMessage(this, Event.LOAD_CHAT, biasChat);
|
MessageBus.INSTANCE.DeferMessage(this, Event.LOAD_CHAT, biasChat);
|
||||||
this.NavigationManager.NavigateTo(Routes.CHAT);
|
this.NavigationManager.NavigateTo(Routes.CHAT);
|
||||||
@ -147,7 +148,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
|||||||
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
|
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
|
||||||
BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias);
|
BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias);
|
||||||
|
|
||||||
var chatId = this.CreateChatThread(Workspaces.WORKSPACE_ID_BIAS, this.biasOfTheDay.Name);
|
var chatId = this.CreateChatThread(KnownWorkspaces.BIAS_WORKSPACE_ID, this.biasOfTheDay.Name);
|
||||||
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id;
|
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id;
|
||||||
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId;
|
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId;
|
||||||
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);
|
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_CODING)]
|
@attribute [Route(Routes.ASSISTANT_CODING)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogCoding>
|
||||||
|
|
||||||
<MudExpansionPanels Class="mb-3">
|
<MudExpansionPanels Class="mb-3">
|
||||||
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
|
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.Coding;
|
namespace AIStudio.Assistants.Coding;
|
||||||
|
|
||||||
public partial class AssistantCoding : AssistantBaseCore
|
public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
|
||||||
|
|
||||||
@ -31,8 +33,8 @@ public partial class AssistantCoding : AssistantBaseCore
|
|||||||
protected override string SubmitText => "Get Support";
|
protected override string SubmitText => "Get Support";
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.GetSupport;
|
protected override Func<Task> SubmitAction => this.GetSupport;
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.codingContexts.Clear();
|
this.codingContexts.Clear();
|
||||||
this.compilerMessages = string.Empty;
|
this.compilerMessages = string.Empty;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
<MudStack Row="@true" Class="mb-3">
|
||||||
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
|
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||||
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
|
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
|
||||||
{
|
{
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_EMAIL)]
|
@attribute [Route(Routes.ASSISTANT_EMAIL)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogWritingEMails>
|
||||||
|
|
||||||
<MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" />
|
<MudTextSwitch Label="@T("Is there a history, a previous conversation?")" @bind-Value="@this.provideHistory" LabelOn="@T("Yes, I provide the previous conversation")" LabelOff="@T("No, I don't provide a previous conversation")" />
|
||||||
@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="Previous conversation" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Provide the previous conversation, e.g., the last e-mail, the last chat, etc." Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
|
<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"/>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputGreeting" Label="(Optional) The greeting phrase to use" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="Dear Colleagues" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputGreeting" Label="@T("(Optional) The greeting phrase to use")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="@T("Dear Colleagues")" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="Your bullet points" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items." Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/>
|
<MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="@T("Your bullet points")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items.")" Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/>
|
||||||
<MudSelect T="string" Label="(Optional) Are any of your points particularly important?" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
|
<MudSelect T="string" Label="@T("(Optional) Are any of your points particularly important?")" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
|
||||||
@foreach (var contentLine in this.bulletPointsLines)
|
@foreach (var contentLine in this.bulletPointsLines)
|
||||||
{
|
{
|
||||||
<MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
|
<MudSelectItem T="string" Value="@contentLine">
|
||||||
|
@contentLine
|
||||||
|
</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputName" Label="(Optional) Your name for the closing salutation" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Your name for the closing salutation of your e-mail." Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputName" Label="@T("(Optional) Your name for the closing salutation")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="@T("Your name for the closing salutation of your e-mail.")" Class="mb-3"/>
|
||||||
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Select the writing style" ValidateSelection="@this.ValidateWritingStyle"/>
|
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Select the writing style")" ValidateSelection="@this.ValidateWritingStyle"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,19 +1,17 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.EMail;
|
namespace AIStudio.Assistants.EMail;
|
||||||
|
|
||||||
public partial class AssistantEMail : AssistantBaseCore
|
public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMails>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "E-Mail";
|
protected override string Title => T("E-Mail");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input.");
|
||||||
"""
|
|
||||||
Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
$"""
|
$"""
|
||||||
@ -24,16 +22,16 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Create email";
|
protected override string SubmitText => T("Create email");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.CreateMail;
|
protected override Func<Task> SubmitAction => this.CreateMail;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
{
|
{
|
||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputBulletPoints = string.Empty;
|
this.inputBulletPoints = string.Empty;
|
||||||
this.bulletPointsLines.Clear();
|
this.bulletPointsLines.Clear();
|
||||||
@ -99,12 +97,12 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
private string? ValidateBulletPoints(string content)
|
private string? ValidateBulletPoints(string content)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(content))
|
if(string.IsNullOrWhiteSpace(content))
|
||||||
return "Please provide some content for the e-mail.";
|
return T("Please provide some content for the e-mail.");
|
||||||
|
|
||||||
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
if(!line.TrimStart().StartsWith('-'))
|
if(!line.TrimStart().StartsWith('-'))
|
||||||
return "Please start each line of your content list with a dash (-) to create a bullet point list.";
|
return T("Please start each line of your content list with a dash (-) to create a bullet point list.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -112,7 +110,7 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
private string? ValidateTargetLanguage(CommonLanguages language)
|
private string? ValidateTargetLanguage(CommonLanguages language)
|
||||||
{
|
{
|
||||||
if(language is CommonLanguages.AS_IS)
|
if(language is CommonLanguages.AS_IS)
|
||||||
return "Please select a target language for the e-mail.";
|
return T("Please select a target language for the e-mail.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -120,7 +118,7 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -128,7 +126,7 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
private string? ValidateWritingStyle(WritingStyles style)
|
private string? ValidateWritingStyle(WritingStyles style)
|
||||||
{
|
{
|
||||||
if(style == WritingStyles.NONE)
|
if(style == WritingStyles.NONE)
|
||||||
return "Please select a writing style for the e-mail.";
|
return T("Please select a writing style for the e-mail.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -136,7 +134,7 @@ public partial class AssistantEMail : AssistantBaseCore
|
|||||||
private string? ValidateHistory(string history)
|
private string? ValidateHistory(string history)
|
||||||
{
|
{
|
||||||
if(this.provideHistory && string.IsNullOrWhiteSpace(history))
|
if(this.provideHistory && string.IsNullOrWhiteSpace(history))
|
||||||
return "Please provide some history for the e-mail.";
|
return T("Please provide some history for the e-mail.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum AllowedLLMProviders
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
ANY,
|
||||||
|
SELF_HOSTED,
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class AllowedLLMProvidersExtensions
|
||||||
|
{
|
||||||
|
public static string Description(this AllowedLLMProviders provider) => provider switch
|
||||||
|
{
|
||||||
|
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
|
||||||
|
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
|
||||||
|
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
|
||||||
|
|
||||||
|
_ => "Unknown option was selected"
|
||||||
|
};
|
||||||
|
}
|
353
app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor
Normal file
353
app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
@attribute [Route(Routes.ASSISTANT_ERI)]
|
||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
@using MudExtensions
|
||||||
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogERIServer>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
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.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.body1">
|
||||||
|
<b>Related links:</b>
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string" Class="mb-6">
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://github.com/MindWorkAI/ERI">ERI repository with example implementation in .NET and C#</MudListItem>
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://mindworkai.org/swagger-ui.html">Interactive documentation aka Swagger UI</MudListItem>
|
||||||
|
</MudList>
|
||||||
|
|
||||||
|
<PreviewPrototype/>
|
||||||
|
<div class="mb-6"></div>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3">
|
||||||
|
ERI server presets
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if
|
||||||
|
you are responsible for multiple ERI servers.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
|
You have not yet added any ERI server presets.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudList Disabled="@this.AreServerPresetsBlocked" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged">
|
||||||
|
@foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers)
|
||||||
|
{
|
||||||
|
<MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server">
|
||||||
|
@server.ServerName
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudStack Row="@true" Class="mt-1">
|
||||||
|
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
|
||||||
|
Add ERI server preset
|
||||||
|
</MudButton>
|
||||||
|
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
|
||||||
|
Delete this server preset
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if(this.AreServerPresetsBlocked)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3 mt-3">
|
||||||
|
Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
|
||||||
|
Auto save
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to
|
||||||
|
regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be
|
||||||
|
automatically saved. Would you like this?
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if(this.AreServerPresetsBlocked)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTextSwitch Label="Should we automatically save any input made?" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="Yes, please save my inputs" LabelOff="No, I will enter everything again or configure it manually in the settings" />
|
||||||
|
|
||||||
|
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
|
||||||
|
Common ERI server settings
|
||||||
|
</MudText>
|
||||||
|
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="ERI server name" HelperText="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.IsNoneERIServerSelected" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="ERI server description" HelperText="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"/>
|
||||||
|
|
||||||
|
<MudStack Row="@true" Class="mb-3">
|
||||||
|
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Programming language" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
|
||||||
|
@foreach (var language in Enum.GetValues<ProgrammingLanguages>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
@if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER)
|
||||||
|
{
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||||
|
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="ERI specification version" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion">
|
||||||
|
@foreach (var version in Enum.GetValues<ERIVersion>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@version">@version</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@(!this.selectedERIVersion.WasSpecificationSelected() || this.IsNoneERIServerSelected)" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> Download specification
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
|
||||||
|
Data source settings
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudStack Row="@false" Spacing="1" Class="mb-3">
|
||||||
|
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="Data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged">
|
||||||
|
@foreach (var dataSource in Enum.GetValues<DataSources>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@dataSource">@dataSource.Name()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
@if (this.selectedDataSource is DataSources.CUSTOM)
|
||||||
|
{
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="Describe your data source" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if(this.selectedDataSource > DataSources.FILE_SYSTEM)
|
||||||
|
{
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceProductName" Label="Data source: product name" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.NeedHostnamePort())
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<MudStack Row="@true">
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceHostname" Label="Data source: hostname" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
<MudNumericField Disabled="@this.IsNoneERIServerSelected" Label="Data source: port" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/>
|
||||||
|
</MudStack>
|
||||||
|
@if (this.dataSourcePort < 1024)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
<b>Warning:</b> Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
|
||||||
|
Authentication settings
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudStack Row="@false" Spacing="1" Class="mb-1">
|
||||||
|
<MudSelectExtended
|
||||||
|
T="Auth"
|
||||||
|
Disabled="@this.IsNoneERIServerSelected"
|
||||||
|
ShrinkLabel="@true"
|
||||||
|
MultiSelection="@true"
|
||||||
|
MultiSelectionTextFunc="@this.GetMultiSelectionAuthText"
|
||||||
|
SelectedValues="@this.selectedAuthenticationMethods"
|
||||||
|
Validation="@this.ValidateAuthenticationMethods"
|
||||||
|
SelectedValuesChanged="@this.AuthenticationMethodWasChanged"
|
||||||
|
Label="Authentication method(s)"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense">
|
||||||
|
@foreach (var authMethod in Enum.GetValues<Auth>())
|
||||||
|
{
|
||||||
|
<MudSelectItemExtended Value="@authMethod">@authMethod.Name()</MudSelectItemExtended>
|
||||||
|
}
|
||||||
|
</MudSelectExtended>
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" 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"/>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
|
||||||
|
{
|
||||||
|
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="Operating system on which your ERI will run" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1">
|
||||||
|
@foreach (var os in Enum.GetValues<OperatingSystem>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@os">@os.Name()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
|
||||||
|
Data protection settings
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="Allowed LLM providers for this data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1">
|
||||||
|
@foreach (var option in Enum.GetValues<AllowedLLMProviders>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option.Description()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
|
||||||
|
Embedding settings
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector
|
||||||
|
space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't
|
||||||
|
have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You
|
||||||
|
store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't
|
||||||
|
need embeddings.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries
|
||||||
|
or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if (!this.IsNoneERIServerSelected)
|
||||||
|
{
|
||||||
|
<MudTable Items="@this.embeddings" Hover="@true" Class="border-dashed border rounded-lg">
|
||||||
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col style="width: 34em;"/>
|
||||||
|
<col style="width: 34em;"/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Name</MudTh>
|
||||||
|
<MudTh>Type</MudTh>
|
||||||
|
<MudTh>Actions</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.EmbeddingName</MudTd>
|
||||||
|
<MudTd>@context.EmbeddingType</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)">
|
||||||
|
Edit
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)">
|
||||||
|
Delete
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
@if (this.embeddings.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6" Class="mt-3">No embedding methods configured yet.</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding">
|
||||||
|
Add Embedding Method
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
|
||||||
|
Data retrieval settings
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process.
|
||||||
|
You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows
|
||||||
|
you to test with beta users which process works better. Or you might generally want to give users the choice so they can select
|
||||||
|
the process that best suits their circumstances.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if (!this.IsNoneERIServerSelected)
|
||||||
|
{
|
||||||
|
<MudTable Items="@this.retrievalProcesses" Hover="@true" Class="border-dashed border rounded-lg">
|
||||||
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col style="width: 34em;"/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Name</MudTh>
|
||||||
|
<MudTh>Actions</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Name</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)">
|
||||||
|
Edit
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)">
|
||||||
|
Delete
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
@if (this.retrievalProcesses.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6" Class="mt-3">No retrieval process configured yet.</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess">
|
||||||
|
Add Retrieval Process
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-1">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="(Optional) Additional libraries" HelperText="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"/>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
|
||||||
|
Provider selection for generation
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task.
|
||||||
|
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
<b>Important:</b> The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number
|
||||||
|
of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.
|
||||||
|
<b>However, generating all the files takes a certain amount of time.</b> Local or self-hosted models may work without these limitations
|
||||||
|
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
|
||||||
|
Write code to file system
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created
|
||||||
|
outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the
|
||||||
|
AI has made in which files.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||||
|
When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All
|
||||||
|
other files you have created remain. Then, the AI generates the new files. <b>But beware:</b> It may happen that the AI generates a
|
||||||
|
file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore,
|
||||||
|
you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization,
|
||||||
|
you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
|
||||||
|
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />
|
1112
app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs
Normal file
1112
app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs
Normal file
File diff suppressed because it is too large
Load Diff
9
app/MindWork AI Studio/Assistants/ERI/Auth.cs
Normal file
9
app/MindWork AI Studio/Assistants/ERI/Auth.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum Auth
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
KERBEROS,
|
||||||
|
USERNAME_PASSWORD,
|
||||||
|
TOKEN,
|
||||||
|
}
|
26
app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs
Normal file
26
app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class AuthExtensions
|
||||||
|
{
|
||||||
|
public static string Name(this Auth auth) => auth switch
|
||||||
|
{
|
||||||
|
Auth.NONE => "No login necessary: useful for public data sources",
|
||||||
|
|
||||||
|
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users",
|
||||||
|
Auth.USERNAME_PASSWORD => "Login by username and password: simple to implement and to operate, useful for few users; easy to use for users",
|
||||||
|
Auth.TOKEN => "Login by token: simple to implement and to operate, useful for few users; unusual for many users",
|
||||||
|
|
||||||
|
_ => "Unknown login method"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToPrompt(this Auth auth) => auth switch
|
||||||
|
{
|
||||||
|
Auth.NONE => "No login is necessary, the data source is public.",
|
||||||
|
|
||||||
|
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.",
|
||||||
|
Auth.USERNAME_PASSWORD => "Login by username and password.",
|
||||||
|
Auth.TOKEN => "Login by static token per user.",
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
15
app/MindWork AI Studio/Assistants/ERI/DataSources.cs
Normal file
15
app/MindWork AI Studio/Assistants/ERI/DataSources.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum DataSources
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
CUSTOM,
|
||||||
|
|
||||||
|
FILE_SYSTEM,
|
||||||
|
|
||||||
|
OBJECT_STORAGE,
|
||||||
|
KEY_VALUE_STORE,
|
||||||
|
DOCUMENT_STORE,
|
||||||
|
RELATIONAL_DATABASE,
|
||||||
|
GRAPH_DATABASE,
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class DataSourcesExtensions
|
||||||
|
{
|
||||||
|
public static string Name(this DataSources dataSource) => dataSource switch
|
||||||
|
{
|
||||||
|
DataSources.NONE => "No data source selected",
|
||||||
|
DataSources.CUSTOM => "Custom description",
|
||||||
|
|
||||||
|
DataSources.FILE_SYSTEM => "File system (local or network share)",
|
||||||
|
DataSources.OBJECT_STORAGE => "Object storage, like Amazon S3, MinIO, etc.",
|
||||||
|
DataSources.KEY_VALUE_STORE => "Key-Value store, like Redis, etc.",
|
||||||
|
DataSources.DOCUMENT_STORE => "Document store, like MongoDB, etc.",
|
||||||
|
DataSources.RELATIONAL_DATABASE => "Relational database, like MySQL, PostgreSQL, etc.",
|
||||||
|
DataSources.GRAPH_DATABASE => "Graph database, like Neo4j, ArangoDB, etc.",
|
||||||
|
|
||||||
|
_ => "Unknown data source"
|
||||||
|
};
|
||||||
|
}
|
8
app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs
Normal file
8
app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum ERIVersion
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
V1,
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class ERIVersionExtensions
|
||||||
|
{
|
||||||
|
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var nameLower = version.ToString().ToLowerInvariant();
|
||||||
|
var filename = $"{nameLower}.json";
|
||||||
|
return $"specs/eri/{filename}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
|
||||||
|
}
|
19
app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs
Normal file
19
app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about the used embedding for a data source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="EmbeddingType">What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
|
||||||
|
/// Embedding," "Graph Embedding," etc.</param>
|
||||||
|
/// <param name="EmbeddingName">Name the embedding used. This can be a library, a framework, or the name of the used
|
||||||
|
/// algorithm.</param>
|
||||||
|
/// <param name="Description">A short description of the embedding. Describe what the embedding is doing.</param>
|
||||||
|
/// <param name="UsedWhen">Describe when the embedding is used. For example, when the user prompt contains certain
|
||||||
|
/// keywords, or anytime?</param>
|
||||||
|
/// <param name="Link">A link to the embedding's documentation or the source code. Might be null.</param>
|
||||||
|
public readonly record struct EmbeddingInfo(
|
||||||
|
string EmbeddingType,
|
||||||
|
string EmbeddingName,
|
||||||
|
string Description,
|
||||||
|
string UsedWhen,
|
||||||
|
string? Link);
|
9
app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs
Normal file
9
app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum OperatingSystem
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
WINDOWS,
|
||||||
|
LINUX,
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class OperatingSystemExtensions
|
||||||
|
{
|
||||||
|
public static string Name(this OperatingSystem os) => os switch
|
||||||
|
{
|
||||||
|
OperatingSystem.NONE => "No operating system specified",
|
||||||
|
|
||||||
|
OperatingSystem.WINDOWS => "Windows",
|
||||||
|
OperatingSystem.LINUX => "Linux",
|
||||||
|
|
||||||
|
_ => "Unknown operating system"
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public enum ProgrammingLanguages
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
C,
|
||||||
|
CPP,
|
||||||
|
CSHARP,
|
||||||
|
GO,
|
||||||
|
JAVA,
|
||||||
|
JAVASCRIPT,
|
||||||
|
JULIA,
|
||||||
|
MATLAB,
|
||||||
|
PHP,
|
||||||
|
PYTHON,
|
||||||
|
RUST,
|
||||||
|
|
||||||
|
OTHER,
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public static class ProgrammingLanguagesExtensions
|
||||||
|
{
|
||||||
|
public static string Name(this ProgrammingLanguages language) => language switch
|
||||||
|
{
|
||||||
|
ProgrammingLanguages.NONE => "No programming language selected",
|
||||||
|
|
||||||
|
ProgrammingLanguages.C => "C",
|
||||||
|
ProgrammingLanguages.CPP => "C++",
|
||||||
|
ProgrammingLanguages.CSHARP => "C#",
|
||||||
|
ProgrammingLanguages.GO => "Go",
|
||||||
|
ProgrammingLanguages.JAVA => "Java",
|
||||||
|
ProgrammingLanguages.JAVASCRIPT => "JavaScript",
|
||||||
|
ProgrammingLanguages.JULIA => "Julia",
|
||||||
|
ProgrammingLanguages.MATLAB => "MATLAB",
|
||||||
|
ProgrammingLanguages.PHP => "PHP",
|
||||||
|
ProgrammingLanguages.PYTHON => "Python",
|
||||||
|
ProgrammingLanguages.RUST => "Rust",
|
||||||
|
|
||||||
|
ProgrammingLanguages.OTHER => "Other",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
}
|
18
app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs
Normal file
18
app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about a retrieval process, which this data source implements.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Name">The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".</param>
|
||||||
|
/// <param name="Description">A short description of the retrieval process. What kind of retrieval process is it?</param>
|
||||||
|
/// <param name="Link">A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.</param>
|
||||||
|
/// <param name="ParametersDescription">A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
|
||||||
|
/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
|
||||||
|
/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.</param>
|
||||||
|
/// <param name="Embeddings">A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.</param>
|
||||||
|
public readonly record struct RetrievalInfo(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string? Link,
|
||||||
|
Dictionary<string, string>? ParametersDescription,
|
||||||
|
List<EmbeddingInfo>? Embeddings);
|
14
app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs
Normal file
14
app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Assistants.ERI;
|
||||||
|
|
||||||
|
public sealed class RetrievalParameter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the parameter.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The description of the parameter.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
|
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<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"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
|
<EnumSelection T="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"/>
|
@ -1,17 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.GrammarSpelling;
|
namespace AIStudio.Assistants.GrammarSpelling;
|
||||||
|
|
||||||
public partial class AssistantGrammarSpelling : AssistantBaseCore
|
public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialogGrammarSpelling>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Grammar & Spelling Checker";
|
protected override string Title => T("Grammar & Spelling Checker");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Check the grammar and spelling of a text.");
|
||||||
"""
|
|
||||||
Check the grammar and spelling of a text.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
$"""
|
$"""
|
||||||
@ -39,16 +37,16 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected override string SubmitText => "Proofread";
|
protected override string SubmitText => T("Proofread");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.ProofreadText;
|
protected override Func<Task> SubmitAction => this.ProofreadText;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
{
|
{
|
||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
this.correctedText = string.Empty;
|
this.correctedText = string.Empty;
|
||||||
@ -92,7 +90,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
|
|||||||
private string? ValidateText(string text)
|
private string? ValidateText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a text as input. You might copy the desired text from a document or a website.";
|
return T("Please provide a text as input. You might copy the desired text from a document or a website.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -100,7 +98,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
124
app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor
Normal file
124
app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
@attribute [Route(Routes.ASSISTANT_AI_STUDIO_I18N)]
|
||||||
|
@using AIStudio.Settings
|
||||||
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N>
|
||||||
|
|
||||||
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" SelectionUpdated="_ => this.OnChangedLanguage()" />
|
||||||
|
<ConfigurationSelect OptionDescription="Language plugin used for comparision" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="Select the language plugin used for comparision."/>
|
||||||
|
@if (this.isLoading)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-6">
|
||||||
|
The data is being loaded, please wait...
|
||||||
|
</MudText>
|
||||||
|
} else if (!this.isLoading && !string.IsNullOrWhiteSpace(this.loadingIssue))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-6">
|
||||||
|
While loading the I18N data, an issue occurred: @this.loadingIssue
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
Added Content (@this.addedContent.Count entries)
|
||||||
|
</MudText>
|
||||||
|
<MudTable Items="@this.addedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
|
||||||
|
</ToolBarContent>
|
||||||
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Key</MudTh>
|
||||||
|
<MudTh>Text</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<pre style="font-size: 0.8em;">
|
||||||
|
@context.Key
|
||||||
|
</pre>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@context.Value
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<MudTablePager />
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
Removed Content (@this.removedContent.Count entries)
|
||||||
|
</MudText>
|
||||||
|
<MudTable Items="@this.removedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
|
||||||
|
</ToolBarContent>
|
||||||
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Key</MudTh>
|
||||||
|
<MudTh>Text</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<pre style="font-size: 0.8em;">
|
||||||
|
@context.Key
|
||||||
|
</pre>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@context.Value
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<MudTablePager />
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
@if (this.selectedTargetLanguage is CommonLanguages.EN_US)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-6">
|
||||||
|
Please note: neither is a translation needed nor performed for English (USA). Anyway, you might want to generate the related Lua code.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.localizedContent.Count > 0)
|
||||||
|
{
|
||||||
|
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6 mb-6"/>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
Localized Content (@this.localizedContent.Count entries of @this.NumTotalItems)
|
||||||
|
</MudText>
|
||||||
|
<MudTable Items="@this.localizedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
|
||||||
|
</ToolBarContent>
|
||||||
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Key</MudTh>
|
||||||
|
<MudTh>Text</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<pre style="font-size: 0.8em;">
|
||||||
|
@context.Key
|
||||||
|
</pre>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@context.Value
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<MudTablePager />
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
|
}
|
||||||
|
}
|
374
app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs
Normal file
374
app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
using SharedTools;
|
||||||
|
|
||||||
|
#if RELEASE
|
||||||
|
using System.Reflection;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.I18N;
|
||||||
|
|
||||||
|
public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
|
||||||
|
{
|
||||||
|
public override Tools.Components Component => Tools.Components.I18N_ASSISTANT;
|
||||||
|
|
||||||
|
protected override string Title => "Localization";
|
||||||
|
|
||||||
|
protected override string Description =>
|
||||||
|
"""
|
||||||
|
Translate MindWork AI Studio text content into another language.
|
||||||
|
""";
|
||||||
|
|
||||||
|
protected override string SystemPrompt =>
|
||||||
|
$"""
|
||||||
|
# Assignment
|
||||||
|
You are an expert in professional translations from English (US) to {this.SystemPromptLanguage()}.
|
||||||
|
You translate the texts without adding any new information. When necessary, you correct
|
||||||
|
spelling and grammar.
|
||||||
|
|
||||||
|
# Context
|
||||||
|
The texts to be translated come from the open source app "MindWork AI Studio". The goal
|
||||||
|
is to localize the app so that it can be offered in other languages. You will always
|
||||||
|
receive one text at a time. A text may be, for example, for a button, a label, or an
|
||||||
|
explanation within the app. The app "AI Studio" is a desktop app for macOS, Linux,
|
||||||
|
and Windows. Users can use Large Language Models (LLMs) in practical ways in their
|
||||||
|
daily lives with it. The app offers the regular chat mode for which LLMs have become
|
||||||
|
known. However, AI Studio also offers so-called assistants, where users no longer
|
||||||
|
have to prompt.
|
||||||
|
|
||||||
|
# Target Audience
|
||||||
|
The app is intended for everyone, not just IT specialists or scientists. When translating,
|
||||||
|
make sure the texts are easy for everyone to understand.
|
||||||
|
""";
|
||||||
|
|
||||||
|
protected override bool AllowProfiles => false;
|
||||||
|
|
||||||
|
protected override bool ShowResult => false;
|
||||||
|
|
||||||
|
protected override bool ShowCopyResult => false;
|
||||||
|
|
||||||
|
protected override bool ShowSendTo => false;
|
||||||
|
|
||||||
|
protected override IReadOnlyList<IButtonData> FooterButtons =>
|
||||||
|
[
|
||||||
|
new ButtonData
|
||||||
|
{
|
||||||
|
Text = "Copy Lua code to clipboard",
|
||||||
|
Icon = Icons.Material.Filled.Extension,
|
||||||
|
Color = Color.Default,
|
||||||
|
AsyncAction = async () => await this.RustService.CopyText2Clipboard(this.Snackbar, this.finalLuaCode.ToString()),
|
||||||
|
DisabledActionParam = () => this.finalLuaCode.Length == 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected override string SubmitText => "Localize AI Studio & generate the Lua code";
|
||||||
|
|
||||||
|
protected override Func<Task> SubmitAction => this.LocalizeTextContent;
|
||||||
|
|
||||||
|
protected override bool SubmitDisabled => !this.localizationPossible;
|
||||||
|
|
||||||
|
protected override bool ShowDedicatedProgress => true;
|
||||||
|
|
||||||
|
protected override void ResetForm()
|
||||||
|
{
|
||||||
|
if (!this.MightPreselectValues())
|
||||||
|
{
|
||||||
|
this.selectedLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id;
|
||||||
|
this.selectedTargetLanguage = CommonLanguages.AS_IS;
|
||||||
|
this.customTargetLanguage = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = this.OnChangedLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool MightPreselectValues()
|
||||||
|
{
|
||||||
|
if (this.SettingsManager.ConfigurationData.I18N.PreselectOptions)
|
||||||
|
{
|
||||||
|
this.selectedLanguagePluginId = this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId;
|
||||||
|
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.I18N.PreselectedTargetLanguage;
|
||||||
|
this.customTargetLanguage = this.SettingsManager.ConfigurationData.I18N.PreselectOtherLanguage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommonLanguages selectedTargetLanguage;
|
||||||
|
private string customTargetLanguage = string.Empty;
|
||||||
|
private bool isLoading = true;
|
||||||
|
private string loadingIssue = string.Empty;
|
||||||
|
private bool localizationPossible;
|
||||||
|
private string searchString = string.Empty;
|
||||||
|
private Guid selectedLanguagePluginId;
|
||||||
|
private ILanguagePlugin? selectedLanguagePlugin;
|
||||||
|
private Dictionary<string, string> addedContent = [];
|
||||||
|
private Dictionary<string, string> removedContent = [];
|
||||||
|
private Dictionary<string, string> localizedContent = [];
|
||||||
|
private StringBuilder finalLuaCode = new();
|
||||||
|
|
||||||
|
#region Overrides of AssistantBase<SettingsDialogI18N>
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
await this.OnLanguagePluginChanged(this.selectedLanguagePluginId);
|
||||||
|
await this.LoadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private string SystemPromptLanguage() => this.selectedTargetLanguage switch
|
||||||
|
{
|
||||||
|
CommonLanguages.OTHER => this.customTargetLanguage,
|
||||||
|
_ => $"{this.selectedTargetLanguage.Name()}",
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task OnLanguagePluginChanged(Guid pluginId)
|
||||||
|
{
|
||||||
|
this.selectedLanguagePluginId = pluginId;
|
||||||
|
await this.OnChangedLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnChangedLanguage()
|
||||||
|
{
|
||||||
|
this.finalLuaCode.Clear();
|
||||||
|
this.localizedContent.Clear();
|
||||||
|
this.localizationPossible = false;
|
||||||
|
if (PluginFactory.RunningPlugins.FirstOrDefault(n => n is PluginLanguage && n.Id == this.selectedLanguagePluginId) is not PluginLanguage comparisonPlugin)
|
||||||
|
{
|
||||||
|
this.loadingIssue = $"Was not able to load the language plugin for comparison ({this.selectedLanguagePluginId}). Please select a valid, loaded & running language plugin.";
|
||||||
|
this.selectedLanguagePlugin = null;
|
||||||
|
}
|
||||||
|
else if (comparisonPlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
|
||||||
|
{
|
||||||
|
this.loadingIssue = $"The selected language plugin for comparison uses the IETF tag '{comparisonPlugin.IETFTag}' which does not match the selected target language '{this.selectedTargetLanguage.ToIETFTag()}'. Please select a valid, loaded & running language plugin which matches the target language.";
|
||||||
|
this.selectedLanguagePlugin = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.selectedLanguagePlugin = comparisonPlugin;
|
||||||
|
this.loadingIssue = string.Empty;
|
||||||
|
await this.LoadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadData()
|
||||||
|
{
|
||||||
|
if (this.selectedLanguagePlugin is null)
|
||||||
|
{
|
||||||
|
this.loadingIssue = "Please select a language plugin for comparison.";
|
||||||
|
this.localizationPossible = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read the file `Assistants\I18N\allTexts.lua`:
|
||||||
|
//
|
||||||
|
#if DEBUG
|
||||||
|
var filePath = Path.Join(Environment.CurrentDirectory, "Assistants", "I18N");
|
||||||
|
var resourceFileProvider = new PhysicalFileProvider(filePath);
|
||||||
|
#else
|
||||||
|
var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Assistants.I18N");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var file = resourceFileProvider.GetFileInfo("allTexts.lua");
|
||||||
|
await using var fileStream = file.CreateReadStream();
|
||||||
|
using var reader = new StreamReader(fileStream);
|
||||||
|
var newI18NDataLuaCode = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Next, we try to load the text as a language plugin -- without
|
||||||
|
// actually starting the plugin:
|
||||||
|
//
|
||||||
|
var newI18NPlugin = await PluginFactory.Load(null, newI18NDataLuaCode);
|
||||||
|
switch (newI18NPlugin)
|
||||||
|
{
|
||||||
|
case NoPlugin noPlugin when noPlugin.Issues.Any():
|
||||||
|
this.loadingIssue = noPlugin.Issues.First();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NoPlugin:
|
||||||
|
this.loadingIssue = "Was not able to load the I18N plugin. Please check the plugin code.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case { IsValid: false } plugin when plugin.Issues.Any():
|
||||||
|
this.loadingIssue = plugin.Issues.First();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PluginLanguage pluginLanguage:
|
||||||
|
this.loadingIssue = string.Empty;
|
||||||
|
var newI18NContent = pluginLanguage.Content;
|
||||||
|
|
||||||
|
var currentI18NContent = this.selectedLanguagePlugin.Content;
|
||||||
|
this.addedContent = newI18NContent.ExceptBy(currentI18NContent.Keys, n => n.Key).ToDictionary();
|
||||||
|
this.removedContent = currentI18NContent.ExceptBy(newI18NContent.Keys, n => n.Key).ToDictionary();
|
||||||
|
this.localizationPossible = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FilterFunc(KeyValuePair<string, string> element)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(this.searchString))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (element.Key.Contains(this.searchString, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (element.Value.Contains(this.searchString, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ValidatingTargetLanguage(CommonLanguages language)
|
||||||
|
{
|
||||||
|
if(language == CommonLanguages.AS_IS)
|
||||||
|
return "Please select a target language.";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ValidateCustomLanguage(string language)
|
||||||
|
{
|
||||||
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
|
return "Please provide a custom language.";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count;
|
||||||
|
|
||||||
|
private async Task LocalizeTextContent()
|
||||||
|
{
|
||||||
|
await this.form!.Validate();
|
||||||
|
if (!this.inputIsValid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this.selectedLanguagePlugin is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.localizedContent.Clear();
|
||||||
|
if (this.selectedTargetLanguage is not CommonLanguages.EN_US)
|
||||||
|
{
|
||||||
|
// Phase 1: Translate added content
|
||||||
|
await this.Phase1TranslateAddedContent();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Case: no translation needed
|
||||||
|
this.localizedContent = this.addedContent.ToDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Now, we have localized the added content. Next, we must merge
|
||||||
|
// the localized content with the existing content. However, we
|
||||||
|
// must skip the removed content. We use the localizedContent
|
||||||
|
// dictionary for the final result:
|
||||||
|
//
|
||||||
|
foreach (var keyValuePair in this.selectedLanguagePlugin.Content)
|
||||||
|
{
|
||||||
|
if (this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (this.localizedContent.ContainsKey(keyValuePair.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (this.removedContent.ContainsKey(keyValuePair.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Phase 2: Create the Lua code. We want to use the base language
|
||||||
|
// for the comments, though:
|
||||||
|
//
|
||||||
|
var commentContent = new Dictionary<string, string>(this.addedContent);
|
||||||
|
foreach (var keyValuePair in PluginFactory.BaseLanguage.Content)
|
||||||
|
{
|
||||||
|
if (this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (this.removedContent.ContainsKey(keyValuePair.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Phase2CreateLuaCode(commentContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Phase1TranslateAddedContent()
|
||||||
|
{
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
var minimumTime = TimeSpan.FromMilliseconds(500);
|
||||||
|
foreach (var keyValuePair in this.addedContent)
|
||||||
|
{
|
||||||
|
if(this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We measure the time for each translation.
|
||||||
|
// We do not want to make more than 120 requests
|
||||||
|
// per minute, i.e., 2 requests per second.
|
||||||
|
//
|
||||||
|
stopwatch.Reset();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Translate one text at a time:
|
||||||
|
//
|
||||||
|
this.CreateChatThread();
|
||||||
|
var time = this.AddUserRequest(keyValuePair.Value);
|
||||||
|
this.localizedContent.Add(keyValuePair.Key, await this.AddAIResponseAsync(time));
|
||||||
|
|
||||||
|
if (this.cancellationTokenSource!.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure that we do not exceed the rate limit of 2 requests per second:
|
||||||
|
//
|
||||||
|
stopwatch.Stop();
|
||||||
|
if (stopwatch.Elapsed < minimumTime)
|
||||||
|
await Task.Delay(minimumTime - stopwatch.Elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Phase2CreateLuaCode(IReadOnlyDictionary<string, string> commentContent)
|
||||||
|
{
|
||||||
|
this.finalLuaCode.Clear();
|
||||||
|
LuaTable.Create(ref this.finalLuaCode, "UI_TEXT_CONTENT", this.localizedContent, commentContent, this.cancellationTokenSource!.Token);
|
||||||
|
|
||||||
|
// Next, we must remove the `root::` prefix from the keys:
|
||||||
|
this.finalLuaCode.Replace("""UI_TEXT_CONTENT["root::""", """
|
||||||
|
UI_TEXT_CONTENT["
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
2519
app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Normal file
2519
app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,22 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
|
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
|
||||||
@inherits AssistantBaseCore
|
@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="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" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||||
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="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">
|
||||||
@foreach (var source in Enum.GetValues<IconSources>())
|
@foreach (var source in Enum.GetValues<IconSources>())
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@source">@source.Name()</MudSelectItem>
|
<MudSelectItem Value="@source">
|
||||||
|
@source.Name()
|
||||||
|
</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
@if (this.selectedIconSource is not IconSources.GENERIC)
|
@if (this.selectedIconSource is not IconSources.GENERIC)
|
||||||
{
|
{
|
||||||
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
|
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">
|
||||||
|
@T("Open website")
|
||||||
|
</MudButton>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,20 +1,14 @@
|
|||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.IconFinder;
|
namespace AIStudio.Assistants.IconFinder;
|
||||||
|
|
||||||
public partial class AssistantIconFinder : AssistantBaseCore
|
public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconFinder>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Icon Finder";
|
protected override string Title => T("Icon Finder");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("""Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge: You need to extract a concept from your context, such as from a text. Let's take an example where your text contains statements about multiple departments. The sought-after concept could be "departments." The next challenge is that we need to anticipate the bias of the icon designers: under the search term "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source, it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.""");
|
||||||
"""
|
|
||||||
Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
|
|
||||||
You need to extract a concept from your context, such as from a text. Let's take an example where
|
|
||||||
your text contains statements about multiple departments. The sought-after concept could be "departments."
|
|
||||||
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
|
|
||||||
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
|
|
||||||
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
"""
|
"""
|
||||||
@ -29,11 +23,11 @@ public partial class AssistantIconFinder : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Find Icon";
|
protected override string SubmitText => T("Find Icon");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.FindIcon;
|
protected override Func<Task> SubmitAction => this.FindIcon;
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputContext = string.Empty;
|
this.inputContext = string.Empty;
|
||||||
if (!this.MightPreselectValues())
|
if (!this.MightPreselectValues())
|
||||||
@ -72,7 +66,7 @@ public partial class AssistantIconFinder : AssistantBaseCore
|
|||||||
private string? ValidatingContext(string context)
|
private string? ValidatingContext(string context)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(context))
|
if(string.IsNullOrWhiteSpace(context))
|
||||||
return "Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon.";
|
return T("Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_JOB_POSTING)]
|
@attribute [Route(Routes.ASSISTANT_JOB_POSTING)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogJobPostings>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
<MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||||
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/>
|
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.JobPosting;
|
namespace AIStudio.Assistants.JobPosting;
|
||||||
|
|
||||||
public partial class AssistantJobPostings : AssistantBaseCore
|
public partial class AssistantJobPostings : AssistantBaseCore<SettingsDialogJobPostings>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT;
|
||||||
|
|
||||||
@ -59,7 +60,7 @@ public partial class AssistantJobPostings : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputEntryDate = string.Empty;
|
this.inputEntryDate = string.Empty;
|
||||||
this.inputValidUntil = string.Empty;
|
this.inputValidUntil = string.Empty;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
|
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogLegalCheck>
|
||||||
|
|
||||||
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
|
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
|
||||||
{
|
{
|
||||||
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/>
|
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="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" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="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" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,19 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.LegalCheck;
|
namespace AIStudio.Assistants.LegalCheck;
|
||||||
|
|
||||||
public partial class AssistantLegalCheck : AssistantBaseCore
|
public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegalCheck>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Legal Check";
|
protected override string Title => T("Legal Check");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Provide a legal document and ask a question about it. This assistant does not replace legal advice. Consult a lawyer to get professional advice. Remember that LLMs can invent answers and facts. Please do not rely on this answers.");
|
||||||
"""
|
|
||||||
Provide a legal document and ask a question about it. This assistant does not
|
|
||||||
replace legal advice. Consult a lawyer to get professional advice. Remember
|
|
||||||
that LLMs can invent answers and facts. Please do not rely on this answers.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
"""
|
"""
|
||||||
@ -26,10 +22,10 @@ public partial class AssistantLegalCheck : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Ask your questions";
|
protected override string SubmitText => T("Ask your questions");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.AksQuestions;
|
protected override Func<Task> SubmitAction => this.AksQuestions;
|
||||||
|
|
||||||
protected override bool SubmitDisabled => this.isAgentRunning;
|
protected override bool SubmitDisabled => this.isAgentRunning;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
@ -37,7 +33,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputLegalDocument = string.Empty;
|
this.inputLegalDocument = string.Empty;
|
||||||
this.inputQuestions = string.Empty;
|
this.inputQuestions = string.Empty;
|
||||||
@ -66,7 +62,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
|
|||||||
private string? ValidatingLegalDocument(string text)
|
private string? ValidatingLegalDocument(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a legal document as input. You might copy the desired text from a document or a website.";
|
return T("Please provide a legal document as input. You might copy the desired text from a document or a website.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -74,7 +70,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
|
|||||||
private string? ValidatingQuestions(string text)
|
private string? ValidatingQuestions(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide your questions as input.";
|
return T("Please provide your questions as input.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_MY_TASKS)]
|
@attribute [Route(Routes.ASSISTANT_MY_TASKS)]
|
||||||
@inherits AssistantBaseCore
|
@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="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" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
|
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,21 +1,16 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.MyTasks;
|
namespace AIStudio.Assistants.MyTasks;
|
||||||
|
|
||||||
public partial class AssistantMyTasks : AssistantBaseCore
|
public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "My Tasks";
|
protected override string Title => T("My Tasks");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.");
|
||||||
"""
|
|
||||||
You received a cryptic email that was sent to many recipients and you are now wondering
|
|
||||||
if you need to do something? Copy the email into the input field. You also need to select
|
|
||||||
a personal profile. In this profile, you should describe your role in the organization.
|
|
||||||
The AI will then try to give you hints on what your tasks might be.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
$"""
|
$"""
|
||||||
@ -30,7 +25,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Analyze text";
|
protected override string SubmitText => T("Analyze text");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.AnalyzeText;
|
protected override Func<Task> SubmitAction => this.AnalyzeText;
|
||||||
|
|
||||||
@ -41,7 +36,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
if (!this.MightPreselectValues())
|
if (!this.MightPreselectValues())
|
||||||
@ -83,7 +78,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
|||||||
private string? ValidatingText(string text)
|
private string? ValidatingText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide some text as input. For example, an email.";
|
return T("Please provide some text as input. For example, an email.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -91,7 +86,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
|||||||
private string? ValidateProfile(Profile profile)
|
private string? ValidateProfile(Profile profile)
|
||||||
{
|
{
|
||||||
if(profile == default || profile == Profile.NO_PROFILE)
|
if(profile == default || profile == Profile.NO_PROFILE)
|
||||||
return "Please select one of your profiles.";
|
return T("Please select one of your profiles.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -99,7 +94,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_REWRITE)]
|
@attribute [Route(Routes.ASSISTANT_REWRITE)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite>
|
||||||
|
|
||||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<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"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
|
<EnumSelection T="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="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="Sentence structure" />
|
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="@T("Sentence structure")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,17 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.RewriteImprove;
|
namespace AIStudio.Assistants.RewriteImprove;
|
||||||
|
|
||||||
public partial class AssistantRewriteImprove : AssistantBaseCore
|
public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogRewrite>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Rewrite & Improve Text";
|
protected override string Title => T("Rewrite & Improve Text");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Rewrite and improve your text. Please note, that the capabilities of the different LLM providers will vary.");
|
||||||
"""
|
|
||||||
Rewrite and improve your text. Please note, that the capabilities of the different LLM providers will vary.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
$"""
|
$"""
|
||||||
@ -40,16 +38,16 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected override string SubmitText => "Improve";
|
protected override string SubmitText => T("Improve your text");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.RewriteText;
|
protected override Func<Task> SubmitAction => this.RewriteText;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
{
|
{
|
||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
this.rewrittenText = string.Empty;
|
this.rewrittenText = string.Empty;
|
||||||
@ -99,7 +97,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
|
|||||||
private string? ValidateText(string text)
|
private string? ValidateText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a text as input. You might copy the desired text from a document or a website.";
|
return T("Please provide a text as input. You might copy the desired text from a document or a website.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -107,7 +105,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_SYNONYMS)]
|
@attribute [Route(Routes.ASSISTANT_SYNONYMS)]
|
||||||
@inherits AssistantBaseCore
|
@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="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="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="@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="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="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"/>
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.Synonym;
|
namespace AIStudio.Assistants.Synonym;
|
||||||
|
|
||||||
public partial class AssistantSynonyms : AssistantBaseCore
|
public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonyms>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Synonyms";
|
protected override string Title => T("Synonyms");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Find synonyms for words or phrases.");
|
||||||
"""
|
|
||||||
Find synonyms for words or phrases.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
$"""
|
$"""
|
||||||
@ -51,16 +49,16 @@ public partial class AssistantSynonyms : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Find synonyms";
|
protected override string SubmitText => T("Find synonyms");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.FindSynonyms;
|
protected override Func<Task> SubmitAction => this.FindSynonyms;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
{
|
{
|
||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
this.inputContext = string.Empty;
|
this.inputContext = string.Empty;
|
||||||
@ -104,7 +102,7 @@ public partial class AssistantSynonyms : AssistantBaseCore
|
|||||||
private string? ValidatingText(string text)
|
private string? ValidatingText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a word or phrase as input.";
|
return T("Please provide a word or phrase as input.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -112,7 +110,7 @@ public partial class AssistantSynonyms : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
|
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTextSummarizer>
|
||||||
|
|
||||||
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
|
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
|
||||||
{
|
{
|
||||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectContentCleanerAgent)"/>
|
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectContentCleanerAgent)"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="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" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="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="Target complexity" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="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" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,20 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.TextSummarizer;
|
namespace AIStudio.Assistants.TextSummarizer;
|
||||||
|
|
||||||
public partial class AssistantTextSummarizer : AssistantBaseCore
|
public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogTextSummarizer>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Text Summarizer";
|
protected override string Title => T("Text Summarizer");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Summarize long text into a shorter version while retaining the main points. You might want to change the language of the summary to make it more readable. It is also possible to change the complexity of the summary to make it easy to understand.");
|
||||||
"""
|
|
||||||
Summarize long text into a shorter version while retaining the main points.
|
|
||||||
You might want to change the language of the summary to make it more readable.
|
|
||||||
It is also possible to change the complexity of the summary to make it
|
|
||||||
easy to understand.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
"""
|
"""
|
||||||
@ -29,10 +24,10 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Summarize";
|
protected override string SubmitText => T("Summarize");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => this.SummarizeText;
|
protected override Func<Task> SubmitAction => this.SummarizeText;
|
||||||
|
|
||||||
protected override bool SubmitDisabled => this.isAgentRunning;
|
protected override bool SubmitDisabled => this.isAgentRunning;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
@ -40,7 +35,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
if(!this.MightPreselectValues())
|
if(!this.MightPreselectValues())
|
||||||
@ -89,7 +84,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
|||||||
private string? ValidatingText(string text)
|
private string? ValidatingText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a text as input. You might copy the desired text from a document or a website.";
|
return T("Please provide a text as input. You might copy the desired text from a document or a website.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -97,7 +92,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -105,7 +100,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
|||||||
private string? ValidateExpertInField(string field)
|
private string? ValidateExpertInField(string field)
|
||||||
{
|
{
|
||||||
if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field))
|
if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field))
|
||||||
return "Please provide your field of expertise.";
|
return T("Please provide your field of expertise.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
@attribute [Route(Routes.ASSISTANT_TRANSLATION)]
|
@attribute [Route(Routes.ASSISTANT_TRANSLATION)]
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTranslation>
|
||||||
|
|
||||||
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
|
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
|
||||||
{
|
{
|
||||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectContentCleanerAgent)"/>
|
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectContentCleanerAgent)"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudTextSwitch Label="Live translation" @bind-Value="@this.liveTranslation" LabelOn="Live translation" LabelOff="No live translation"/>
|
<MudTextSwitch Label="@T("Live translation")" @bind-Value="@this.liveTranslation" LabelOn="@T("Live translation")" LabelOff="@T("No live translation")"/>
|
||||||
@if (this.liveTranslation)
|
@if (this.liveTranslation)
|
||||||
{
|
{
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="@this.SettingsManager.ConfigurationData.Translation.DebounceIntervalMilliseconds" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="@this.SettingsManager.ConfigurationData.Translation.DebounceIntervalMilliseconds" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
<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"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
|
<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")" />
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
@ -1,17 +1,15 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs.Settings;
|
||||||
|
|
||||||
namespace AIStudio.Assistants.Translation;
|
namespace AIStudio.Assistants.Translation;
|
||||||
|
|
||||||
public partial class AssistantTranslation : AssistantBaseCore
|
public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTranslation>
|
||||||
{
|
{
|
||||||
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
|
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
|
||||||
|
|
||||||
protected override string Title => "Translation";
|
protected override string Title => T("Translation");
|
||||||
|
|
||||||
protected override string Description =>
|
protected override string Description => T("Translate text from one language to another.");
|
||||||
"""
|
|
||||||
Translate text from one language to another.
|
|
||||||
""";
|
|
||||||
|
|
||||||
protected override string SystemPrompt =>
|
protected override string SystemPrompt =>
|
||||||
"""
|
"""
|
||||||
@ -25,10 +23,10 @@ public partial class AssistantTranslation : AssistantBaseCore
|
|||||||
|
|
||||||
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
protected override IReadOnlyList<IButtonData> FooterButtons => [];
|
||||||
|
|
||||||
protected override string SubmitText => "Translate";
|
protected override string SubmitText => T("Translate");
|
||||||
|
|
||||||
protected override Func<Task> SubmitAction => () => this.TranslateText(true);
|
protected override Func<Task> SubmitAction => () => this.TranslateText(true);
|
||||||
|
|
||||||
protected override bool SubmitDisabled => this.isAgentRunning;
|
protected override bool SubmitDisabled => this.isAgentRunning;
|
||||||
|
|
||||||
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
|
||||||
@ -36,7 +34,7 @@ public partial class AssistantTranslation : AssistantBaseCore
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void ResetFrom()
|
protected override void ResetForm()
|
||||||
{
|
{
|
||||||
this.inputText = string.Empty;
|
this.inputText = string.Empty;
|
||||||
this.inputTextLastTranslation = string.Empty;
|
this.inputTextLastTranslation = string.Empty;
|
||||||
@ -84,7 +82,7 @@ public partial class AssistantTranslation : AssistantBaseCore
|
|||||||
private string? ValidatingText(string text)
|
private string? ValidatingText(string text)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(text))
|
if(string.IsNullOrWhiteSpace(text))
|
||||||
return "Please provide a text as input. You might copy the desired text from a document or a website.";
|
return T("Please provide a text as input. You might copy the desired text from a document or a website.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -92,7 +90,7 @@ public partial class AssistantTranslation : AssistantBaseCore
|
|||||||
private string? ValidatingTargetLanguage(CommonLanguages language)
|
private string? ValidatingTargetLanguage(CommonLanguages language)
|
||||||
{
|
{
|
||||||
if(language == CommonLanguages.AS_IS)
|
if(language == CommonLanguages.AS_IS)
|
||||||
return "Please select a target language.";
|
return T("Please select a target language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -100,7 +98,7 @@ public partial class AssistantTranslation : AssistantBaseCore
|
|||||||
private string? ValidateCustomLanguage(string language)
|
private string? ValidateCustomLanguage(string language)
|
||||||
{
|
{
|
||||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||||
return "Please provide a custom language.";
|
return T("Please provide a custom language.");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -15,6 +20,36 @@ public sealed record ChatThread
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the provider selected for the chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public string SelectedProvider { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the profile selected for the chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public string SelectedProfile { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source options for this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public DataSourceOptions DataSourceOptions { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The AI-selected data sources for this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The augmented data for this chat thread. Will be inserted into the system prompt.
|
||||||
|
/// </summary>
|
||||||
|
public string AugmentedData { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data security to use, derived from the data sources used so far.
|
||||||
|
/// </summary>
|
||||||
|
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||||
|
|
||||||
/// <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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,4 +69,155 @@ public sealed record ChatThread
|
|||||||
/// 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; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the system prompt for the chat thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The actual system prompt depends on the selected profile. If no profile is selected,
|
||||||
|
/// the system prompt is returned as is. When a profile is selected, the system prompt
|
||||||
|
/// is extended with the profile chosen.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||||
|
/// <param name="chatThread">The chat thread to prepare the system prompt for.</param>
|
||||||
|
/// <param name="logger">The logger instance to use.</param>
|
||||||
|
/// <returns>The prepared system prompt.</returns>
|
||||||
|
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
|
||||||
|
{
|
||||||
|
var isAugmentedDataAvailable = !string.IsNullOrWhiteSpace(chatThread.AugmentedData);
|
||||||
|
var systemPromptWithAugmentedData = isAugmentedDataAvailable switch
|
||||||
|
{
|
||||||
|
true => $"""
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
|
||||||
|
{chatThread.AugmentedData}
|
||||||
|
""",
|
||||||
|
|
||||||
|
false => chatThread.SystemPrompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(isAugmentedDataAvailable)
|
||||||
|
logger.LogInformation("Augmented data is available for the chat thread.");
|
||||||
|
else
|
||||||
|
logger.LogInformation("No augmented data is available for the chat thread.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Prepare the system prompt:
|
||||||
|
//
|
||||||
|
string systemPromptText;
|
||||||
|
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
|
||||||
|
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
|
||||||
|
systemPromptText = systemPromptWithAugmentedData;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
|
||||||
|
systemPromptText = systemPromptWithAugmentedData;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
|
||||||
|
systemPromptText = systemPromptWithAugmentedData;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
|
||||||
|
if(profile == default)
|
||||||
|
systemPromptText = systemPromptWithAugmentedData;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
|
||||||
|
systemPromptText = $"""
|
||||||
|
{systemPromptWithAugmentedData}
|
||||||
|
|
||||||
|
{profile.ToSystemPrompt()}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(logMessage);
|
||||||
|
return systemPromptText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a content block from this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content block to remove.</param>
|
||||||
|
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
|
||||||
|
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
|
||||||
|
/// which will not remove the previous user block if it is hidden from the user.</param>
|
||||||
|
public void Remove(IContent content, bool removeForRegenerate = false)
|
||||||
|
{
|
||||||
|
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
|
||||||
|
if(block is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remove the previous user block if it is hidden from the user. Otherwise,
|
||||||
|
// the experience might be confusing for the user.
|
||||||
|
//
|
||||||
|
// Explanation, using the ERI assistant as an example:
|
||||||
|
// - The ERI assistant generates for every file a hidden user prompt.
|
||||||
|
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
|
||||||
|
// - Now, the user removes one AI response
|
||||||
|
// - The hidden user prompt is still there, but the user can't see it.
|
||||||
|
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
|
||||||
|
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
|
||||||
|
//
|
||||||
|
if (block.Role is ChatRole.AI && !removeForRegenerate)
|
||||||
|
{
|
||||||
|
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
|
||||||
|
var index = sortedBlocks.IndexOf(block);
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
var previousBlock = sortedBlocks[index - 1];
|
||||||
|
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
|
||||||
|
this.Blocks.Remove(previousBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the block from the chat thread:
|
||||||
|
this.Blocks.Remove(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms this chat thread to an ERI chat thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The ERI chat thread.</returns>
|
||||||
|
public async Task<Tools.ERIClient.DataModel.ChatThread> ToERIChatThread(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Transform the content blocks:
|
||||||
|
//
|
||||||
|
var contentBlocks = new List<Tools.ERIClient.DataModel.ContentBlock>(this.Blocks.Count);
|
||||||
|
foreach (var block in this.Blocks)
|
||||||
|
{
|
||||||
|
var (contentData, contentType) = block.Content switch
|
||||||
|
{
|
||||||
|
ContentImage image => (await image.AsBase64(token), Tools.ERIClient.DataModel.ContentType.IMAGE),
|
||||||
|
ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT),
|
||||||
|
|
||||||
|
_ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN),
|
||||||
|
};
|
||||||
|
|
||||||
|
contentBlocks.Add(new Tools.ERIClient.DataModel.ContentBlock
|
||||||
|
{
|
||||||
|
Role = block.Role switch
|
||||||
|
{
|
||||||
|
ChatRole.AI => Role.AI,
|
||||||
|
ChatRole.USER => Role.USER,
|
||||||
|
ChatRole.AGENT => Role.AGENT,
|
||||||
|
ChatRole.SYSTEM => Role.SYSTEM,
|
||||||
|
ChatRole.NONE => Role.NONE,
|
||||||
|
|
||||||
|
_ => Role.UNKNOWN,
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = contentData,
|
||||||
|
Type = contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
|
||||||
|
}
|
||||||
}
|
}
|
58
app/MindWork AI Studio/Chat/ChatThreadExtensions.cs
Normal file
58
app/MindWork AI Studio/Chat/ChatThreadExtensions.cs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
using AIStudio.Provider.SelfHosted;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public static class ChatThreadExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the specified provider is allowed for the chat thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// We don't check if the provider is allowed to use the data sources of the chat thread.
|
||||||
|
/// That kind of check is done in the RAG process itself.<br/><br/>
|
||||||
|
///
|
||||||
|
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
|
||||||
|
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
|
||||||
|
/// from the data sources is stored in the chat thread. This means we must check if the
|
||||||
|
/// selected provider is allowed to use this thread's data.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="chatThread">The chat thread to check.</param>
|
||||||
|
/// <param name="provider">The provider to check.</param>
|
||||||
|
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
|
||||||
|
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
|
||||||
|
{
|
||||||
|
// No chat thread available means we have a new chat. That's fine:
|
||||||
|
if (chatThread is null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// The chat thread is available, but the data security is not specified.
|
||||||
|
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
|
||||||
|
// That's fine as well:
|
||||||
|
if (chatThread.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Is the provider self-hosted?
|
||||||
|
//
|
||||||
|
var isSelfHostedProvider = provider switch
|
||||||
|
{
|
||||||
|
ProviderSelfHosted => true,
|
||||||
|
AIStudio.Settings.Provider p => p.IsSelfHosted,
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check the chat data security against the selected provider:
|
||||||
|
//
|
||||||
|
return isSelfHostedProvider switch
|
||||||
|
{
|
||||||
|
// The provider is self-hosted -- we can use any data source:
|
||||||
|
true => true,
|
||||||
|
|
||||||
|
// The provider is not self-hosted -- it depends on the data security of the chat thread:
|
||||||
|
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@using AIStudio.Tools
|
@using AIStudio.Tools
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
|
@inherits AIStudio.Components.MSGComponentBase
|
||||||
<MudCard Class="@this.CardClasses" Outlined="@true">
|
<MudCard Class="@this.CardClasses" Outlined="@true">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderAvatar>
|
<CardHeaderAvatar>
|
||||||
@ -9,10 +9,38 @@
|
|||||||
</MudAvatar>
|
</MudAvatar>
|
||||||
</CardHeaderAvatar>
|
</CardHeaderAvatar>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
|
<MudText Typo="Typo.body1">
|
||||||
|
@this.Role.ToName() (@this.Time)
|
||||||
|
</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
<CardHeaderActions>
|
<CardHeaderActions>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard" />
|
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Regenerate")" Placement="Placement.Bottom">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
@if (this.RemoveBlockFunc is not null)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
<MudTooltip Text="@T("Copies the content to the clipboard")" Placement="Placement.Bottom">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/>
|
||||||
|
</MudTooltip>
|
||||||
</CardHeaderActions>
|
</CardHeaderActions>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
@ -44,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
|
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,16 +80,16 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ContentType.IMAGE:
|
case ContentType.IMAGE:
|
||||||
if (this.Content is ContentImage imageContent)
|
if (this.Content is ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent)
|
||||||
{
|
{
|
||||||
<MudImage Src="@imageContent.URL"/>
|
<MudImage Src="@imageContent.Source"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
<MudText Typo="Typo.body2">
|
<MudText Typo="Typo.body2">
|
||||||
Cannot render content of type @this.Type yet.
|
@string.Format(T("Cannot render content of type {0} yet."), this.Type)
|
||||||
</MudText>
|
</MudText>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using AIStudio.Components;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using RustService = AIStudio.Tools.RustService;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The UI component for a chat content block, i.e., for any IContent.
|
/// The UI component for a chat content block, i.e., for any IContent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ContentBlockComponent : ComponentBase
|
public partial class ContentBlockComponent : MSGComponentBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The role of the chat content block.
|
/// The role of the chat content block.
|
||||||
@ -39,11 +40,35 @@ public partial class ContentBlockComponent : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string Class { get; set; } = string.Empty;
|
public string Class { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsLastContentBlock { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsSecondToLastBlock { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<IContent, Task>? RegenerateFunc { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<IContent, Task>? EditLastUserBlockFunc { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<bool> RegenerateEnabled { get; set; } = () => false;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private RustService RustService { get; init; } = null!;
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private ISnackbar Snackbar { get; init; } = null!;
|
private ISnackbar Snackbar { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
private bool HideContent { get; set; }
|
private bool HideContent { get; set; }
|
||||||
|
|
||||||
@ -104,7 +129,7 @@ public partial class ContentBlockComponent : ComponentBase
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.Snackbar.Add("Cannot copy this content type to clipboard!", Severity.Error, config =>
|
this.Snackbar.Add(T("Cannot copy this content type to clipboard!"), Severity.Error, config =>
|
||||||
{
|
{
|
||||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||||
config.IconSize = Size.Large;
|
config.IconSize = Size.Large;
|
||||||
@ -115,4 +140,68 @@ public partial class ContentBlockComponent : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
||||||
|
|
||||||
|
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
|
||||||
|
|
||||||
|
private async Task RemoveBlock()
|
||||||
|
{
|
||||||
|
if (this.RemoveBlockFunc is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remove = await this.DialogService.ShowMessageBox(
|
||||||
|
T("Remove Message"),
|
||||||
|
T("Do you really want to remove this message?"),
|
||||||
|
T("Yes, remove it"),
|
||||||
|
T("No, keep it"));
|
||||||
|
|
||||||
|
if (remove.HasValue && remove.Value)
|
||||||
|
await this.RemoveBlockFunc(this.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RegenerateBlock()
|
||||||
|
{
|
||||||
|
if (this.RegenerateFunc is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this.Role is not ChatRole.AI)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var regenerate = await this.DialogService.ShowMessageBox(
|
||||||
|
T("Regenerate Message"),
|
||||||
|
T("Do you really want to regenerate this message?"),
|
||||||
|
T("Yes, regenerate it"),
|
||||||
|
T("No, keep it"));
|
||||||
|
|
||||||
|
if (regenerate.HasValue && regenerate.Value)
|
||||||
|
await this.RegenerateFunc(this.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditLastBlock()
|
||||||
|
{
|
||||||
|
if (this.EditLastBlockFunc is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this.Role is not ChatRole.USER)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.EditLastBlockFunc(this.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditLastUserBlock()
|
||||||
|
{
|
||||||
|
if (this.EditLastUserBlockFunc is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this.Role is not ChatRole.USER)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var edit = await this.DialogService.ShowMessageBox(
|
||||||
|
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("Yes, remove the AI response and edit it"),
|
||||||
|
T("No, keep it"));
|
||||||
|
|
||||||
|
if (edit.HasValue && edit.Value)
|
||||||
|
await this.EditLastUserBlockFunc(this.Content);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,14 +1,13 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an image inside the chat.
|
/// Represents an image inside the chat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ContentImage : IContent
|
public sealed class ContentImage : IContent, IImageSource
|
||||||
{
|
{
|
||||||
#region Implementation of IContent
|
#region Implementation of IContent
|
||||||
|
|
||||||
@ -29,7 +28,7 @@ public sealed class ContentImage : IContent
|
|||||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
|
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@ -37,12 +36,15 @@ public sealed class ContentImage : IContent
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The URL of the image.
|
/// The type of the image source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string URL { get; set; } = string.Empty;
|
/// <remarks>
|
||||||
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
|
/// </remarks>
|
||||||
|
public required ContentImageSource SourceType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The local path of the image.
|
/// The image source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LocalPath { get; set; } = string.Empty;
|
public required string Source { get; set; }
|
||||||
}
|
}
|
8
app/MindWork AI Studio/Chat/ContentImageSource.cs
Normal file
8
app/MindWork AI Studio/Chat/ContentImageSource.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public enum ContentImageSource
|
||||||
|
{
|
||||||
|
URL,
|
||||||
|
LOCAL_PATH,
|
||||||
|
BASE64,
|
||||||
|
}
|
@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.RAG.RAGProcesses;
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
@ -35,17 +36,42 @@ public sealed class ContentText : IContent
|
|||||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
|
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if(chatThread is null)
|
if(chatThread is null)
|
||||||
return;
|
return new();
|
||||||
|
|
||||||
|
if(!chatThread.IsLLMProviderAllowed(provider))
|
||||||
|
{
|
||||||
|
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||||
|
logger.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
|
||||||
|
return chatThread;
|
||||||
|
}
|
||||||
|
|
||||||
// Store the last time we got a response. We use this ater
|
// Call the RAG process. Right now, we only have one RAG process:
|
||||||
|
if (lastPrompt is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rag = new AISrcSelWithRetCtxVal();
|
||||||
|
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||||
|
logger.LogError(e, "Skipping the RAG process due to an error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the last time we got a response. We use this later
|
||||||
// to determine whether we should notify the UI about the
|
// to determine whether we should notify the UI about the
|
||||||
// new content or not. Depends on the energy saving mode
|
// new content or not. Depends on the energy saving mode
|
||||||
// the user chose.
|
// the user chose.
|
||||||
var last = DateTimeOffset.Now;
|
var last = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// Get the settings manager:
|
||||||
|
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
|
||||||
|
|
||||||
// Start another thread by using a task to uncouple
|
// Start another thread by using a task to uncouple
|
||||||
// the UI thread from the AI processing:
|
// the UI thread from the AI processing:
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
@ -54,7 +80,7 @@ public sealed class ContentText : IContent
|
|||||||
this.InitialRemoteWait = true;
|
this.InitialRemoteWait = true;
|
||||||
|
|
||||||
// Iterate over the responses from the AI:
|
// Iterate over the responses from the AI:
|
||||||
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
|
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||||
{
|
{
|
||||||
// When the user cancels the request, we stop the loop:
|
// When the user cancels the request, we stop the loop:
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
@ -96,6 +122,7 @@ public sealed class ContentText : IContent
|
|||||||
|
|
||||||
// Inform the UI that the streaming is done:
|
// Inform the UI that the streaming is done:
|
||||||
await this.StreamingDone();
|
await this.StreamingDone();
|
||||||
|
return chatThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
@ -42,5 +41,16 @@ public interface IContent
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uses the provider to create the content.
|
/// Uses the provider to create the content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
|
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the corresponding ERI content type.
|
||||||
|
/// </summary>
|
||||||
|
public Tools.ERIClient.DataModel.ContentType ToERIContentType => this switch
|
||||||
|
{
|
||||||
|
ContentText => Tools.ERIClient.DataModel.ContentType.TEXT,
|
||||||
|
ContentImage => Tools.ERIClient.DataModel.ContentType.IMAGE,
|
||||||
|
|
||||||
|
_ => Tools.ERIClient.DataModel.ContentType.UNKNOWN,
|
||||||
|
};
|
||||||
}
|
}
|
17
app/MindWork AI Studio/Chat/IImageSource.cs
Normal file
17
app/MindWork AI Studio/Chat/IImageSource.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public interface IImageSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the image source.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
|
/// </remarks>
|
||||||
|
public ContentImageSource SourceType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The image source.
|
||||||
|
/// </summary>
|
||||||
|
public string Source { get; set; }
|
||||||
|
}
|
63
app/MindWork AI Studio/Chat/IImageSourceExtensions.cs
Normal file
63
app/MindWork AI Studio/Chat/IImageSourceExtensions.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public static class IImageSourceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read the image content as a base64 string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The images are directly converted to base64 strings. The maximum
|
||||||
|
/// size of the image is around 10 MB. If the image is larger, the method
|
||||||
|
/// returns an empty string.
|
||||||
|
///
|
||||||
|
/// As of now, this method does no sort of image processing. LLMs usually
|
||||||
|
/// do not work with arbitrary image sizes. In the future, we might have
|
||||||
|
/// to resize the images before sending them to the model.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="image">The image source.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The image content as a base64 string; might be empty.</returns>
|
||||||
|
public static async Task<string> AsBase64(this IImageSource image, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
switch (image.SourceType)
|
||||||
|
{
|
||||||
|
case ContentImageSource.BASE64:
|
||||||
|
return image.Source;
|
||||||
|
|
||||||
|
case ContentImageSource.URL:
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
if(response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// Read the length of the content:
|
||||||
|
var lengthBytes = response.Content.Headers.ContentLength;
|
||||||
|
if(lengthBytes > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync(token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ContentImageSource.LOCAL_PATH:
|
||||||
|
if(File.Exists(image.Source))
|
||||||
|
{
|
||||||
|
// Read the content length:
|
||||||
|
var length = new FileInfo(image.Source).Length;
|
||||||
|
if(length > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await File.ReadAllBytesAsync(image.Source, token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
app/MindWork AI Studio/Chat/KnownWorkspaces.cs
Normal file
7
app/MindWork AI Studio/Chat/KnownWorkspaces.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public static class KnownWorkspaces
|
||||||
|
{
|
||||||
|
public static readonly Guid BIAS_WORKSPACE_ID = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
|
||||||
|
public static readonly Guid ERI_SERVER_WORKSPACE_ID = Guid.Parse("8ec09cd3-9da7-4736-b245-2d8b67fc342f");
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
namespace AIStudio.Chat;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Data about a workspace.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name of the workspace.</param>
|
|
||||||
public sealed class Workspace(string name)
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = name;
|
|
||||||
|
|
||||||
public List<ChatThread> Threads { get; set; } = new();
|
|
||||||
}
|
|
@ -1,3 +1,6 @@
|
|||||||
|
@inherits MSGComponentBase
|
||||||
|
@typeparam TSettings
|
||||||
|
|
||||||
<MudCard Outlined="@true" Style="@this.BlockStyle">
|
<MudCard Outlined="@true" Style="@this.BlockStyle">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
@ -17,8 +20,11 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
<MudCardActions>
|
<MudCardActions>
|
||||||
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
|
<MudButtonGroup Variant="Variant.Outlined">
|
||||||
@this.ButtonText
|
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
|
||||||
</MudButton>
|
@this.ButtonText
|
||||||
|
</MudButton>
|
||||||
|
<MudIconButton Variant="Variant.Text" Icon="@Icons.Material.Filled.Settings" Color="Color.Default" OnClick="@this.OpenSettingsDialog"/>
|
||||||
|
</MudButtonGroup>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
@ -1,10 +1,10 @@
|
|||||||
using AIStudio.Settings;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
namespace AIStudio.Components;
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDisposable
|
public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent
|
||||||
{
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
@ -25,44 +25,15 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
|
|||||||
private MudTheme ColorTheme { get; init; } = null!;
|
private MudTheme ColorTheme { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
private async Task OpenSettingsDialog()
|
||||||
private MessageBus MessageBus { get; init; } = null!;
|
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
this.MessageBus.RegisterComponent(this);
|
var dialogParameters = new DialogParameters();
|
||||||
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await this.DialogService.ShowAsync<TSettings>(T("Open Settings"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Implementation of IMessageBusReceiver
|
|
||||||
|
|
||||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
|
||||||
{
|
|
||||||
switch (triggeredEvent)
|
|
||||||
{
|
|
||||||
case Event.COLOR_THEME_CHANGED:
|
|
||||||
this.StateHasChanged();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TResult?>(default);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private string BorderColor => this.SettingsManager.IsDarkMode switch
|
private string BorderColor => this.SettingsManager.IsDarkMode switch
|
||||||
{
|
{
|
||||||
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight,
|
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight,
|
||||||
@ -70,13 +41,4 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
|
|||||||
};
|
};
|
||||||
|
|
||||||
private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
|
private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
|
||||||
|
|
||||||
#region Implementation of IDisposable
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this.MessageBus.Unregister(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
@ -13,6 +13,29 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (216, "v0.9.41, build 216 (2025-04-27 14:18 UTC)", "v0.9.41.md"),
|
||||||
|
new (215, "v0.9.40, build 215 (2025-04-20 13:30 UTC)", "v0.9.40.md"),
|
||||||
|
new (214, "v0.9.39, build 214 (2025-04-07 17:39 UTC)", "v0.9.39.md"),
|
||||||
|
new (213, "v0.9.38, build 213 (2025-03-17 18:18 UTC)", "v0.9.38.md"),
|
||||||
|
new (212, "v0.9.37, build 212 (2025-03-16 20:32 UTC)", "v0.9.37.md"),
|
||||||
|
new (211, "v0.9.36, build 211 (2025-03-15 10:42 UTC)", "v0.9.36.md"),
|
||||||
|
new (210, "v0.9.35, build 210 (2025-03-13 08:44 UTC)", "v0.9.35.md"),
|
||||||
|
new (209, "v0.9.34, build 209 (2025-03-11 13:02 UTC)", "v0.9.34.md"),
|
||||||
|
new (208, "v0.9.33, build 208 (2025-03-11 08:14 UTC)", "v0.9.33.md"),
|
||||||
|
new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"),
|
||||||
|
new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"),
|
||||||
|
new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"),
|
||||||
|
new (204, "v0.9.29, build 204 (2025-02-24 13:48 UTC)", "v0.9.29.md"),
|
||||||
|
new (203, "v0.9.28, build 203 (2025-02-09 16:33 UTC)", "v0.9.28.md"),
|
||||||
|
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
|
||||||
|
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
|
||||||
|
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
|
||||||
|
new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"),
|
||||||
|
new (198, "v0.9.23, build 198 (2025-01-02 19:39 UTC)", "v0.9.23.md"),
|
||||||
|
new (197, "v0.9.22, build 197 (2024-12-04 10:58 UTC)", "v0.9.22.md"),
|
||||||
|
new (196, "v0.9.21, build 196 (2024-11-23 12:22 UTC)", "v0.9.21.md"),
|
||||||
|
new (195, "v0.9.20, build 195 (2024-11-16 20:44 UTC)", "v0.9.20.md"),
|
||||||
|
new (194, "v0.9.19, build 194 (2024-11-14 05:58 UTC)", "v0.9.19.md"),
|
||||||
new (193, "v0.9.18, build 193 (2024-11-09 21:10 UTC)", "v0.9.18.md"),
|
new (193, "v0.9.18, build 193 (2024-11-09 21:10 UTC)", "v0.9.18.md"),
|
||||||
new (192, "v0.9.17, build 192 (2024-11-03 11:11 UTC)", "v0.9.17.md"),
|
new (192, "v0.9.17, build 192 (2024-11-03 11:11 UTC)", "v0.9.17.md"),
|
||||||
new (191, "v0.9.16, build 191 (2024-11-02 22:04 UTC)", "v0.9.16.md"),
|
new (191, "v0.9.16, build 191 (2024-11-02 22:04 UTC)", "v0.9.16.md"),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<MudSelect T="Log" @bind-Value="@this.SelectedLog" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Margin="Margin.Dense" Label="Changelog" Class="mb-2 rounded-lg" Variant="Variant.Outlined" SelectedValuesChanged="() => this.ReadLogAsync()" OnKeyUp="() => this.ReadLogAsync()">
|
@inherits MSGComponentBase
|
||||||
|
<MudSelect T="Log" @bind-Value="@this.SelectedLog" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Margin="Margin.Dense" Label="@T("Changelog")" Class="mb-2 rounded-lg" Variant="Variant.Outlined" SelectedValuesChanged="() => this.ReadLogAsync()" OnKeyUp="() => this.ReadLogAsync()">
|
||||||
@foreach (var log in LOGS)
|
@foreach (var log in LOGS)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@log"/>
|
<MudSelectItem Value="@log"/>
|
||||||
|
@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components;
|
|||||||
|
|
||||||
namespace AIStudio.Components;
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
public partial class Changelog : ComponentBase
|
public partial class Changelog : MSGComponentBase
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
private HttpClient HttpClient { get; set; } = null!;
|
private HttpClient HttpClient { get; set; } = null!;
|
||||||
@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase
|
|||||||
|
|
||||||
private async Task ReadLogAsync()
|
private async Task ReadLogAsync()
|
||||||
{
|
{
|
||||||
var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
||||||
this.LogContent = await response.Content.ReadAsStringAsync();
|
this.LogContent = await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
126
app/MindWork AI Studio/Components/ChatComponent.razor
Normal file
126
app/MindWork AI Studio/Components/ChatComponent.razor
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
@using AIStudio.Chat
|
||||||
|
|
||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
|
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" MinWidth="36em" Style="height: 100%">
|
||||||
|
<ChildContent>
|
||||||
|
@if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
|
||||||
|
for (var i = 0; i < blocks.Count; i++)
|
||||||
|
{
|
||||||
|
var block = blocks[i];
|
||||||
|
var isLastBlock = i == blocks.Count - 1;
|
||||||
|
var isSecondLastBlock = i == blocks.Count - 2;
|
||||||
|
@if (!block.HideFromUser)
|
||||||
|
{
|
||||||
|
<ContentBlockComponent
|
||||||
|
Role="@block.Role"
|
||||||
|
Type="@block.ContentType"
|
||||||
|
Time="@block.Time"
|
||||||
|
Content="@block.Content"
|
||||||
|
RemoveBlockFunc="@this.RemoveBlock"
|
||||||
|
IsLastContentBlock="@isLastBlock"
|
||||||
|
IsSecondToLastBlock="@isSecondLastBlock"
|
||||||
|
RegenerateFunc="@this.RegenerateBlock"
|
||||||
|
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
|
||||||
|
EditLastBlockFunc="@this.EditLastBlock"
|
||||||
|
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ChildContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudElement Style="flex: 0 0 auto;">
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
@ref="@this.inputField"
|
||||||
|
@bind-Text="@this.userInput"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
AutoGrow="@true"
|
||||||
|
Lines="3"
|
||||||
|
MaxLines="12"
|
||||||
|
Label="@this.InputLabel"
|
||||||
|
Placeholder="@this.ProviderPlaceholder"
|
||||||
|
Adornment="Adornment.End"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||||
|
OnAdornmentClick="() => this.SendMessage()"
|
||||||
|
Disabled="@this.IsInputForbidden()"
|
||||||
|
Immediate="@true"
|
||||||
|
OnKeyUp="this.InputKeyEvent"
|
||||||
|
UserAttributes="@USER_INPUT_ATTRIBUTES"
|
||||||
|
Class="@this.UserInputClass"
|
||||||
|
Style="@this.UserInputStyle"/>
|
||||||
|
</MudElement>
|
||||||
|
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
|
||||||
|
@if (
|
||||||
|
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||||
|
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Show your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTooltip Text="@T("Start temporary chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||||
|
{
|
||||||
|
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
|
||||||
|
|
||||||
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@T("The selected provider is not allowed in this chat due to data security reasons.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
<MudIconButton />
|
||||||
|
</MudToolBar>
|
||||||
|
</FooterContent>
|
||||||
|
</InnerScrolling>
|
906
app/MindWork AI Studio/Components/ChatComponent.razor.cs
Normal file
906
app/MindWork AI Studio/Components/ChatComponent.razor.cs
Normal file
@ -0,0 +1,906 @@
|
|||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Dialogs;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
|
||||||
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public ChatThread? ChatThread { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<ChatThread?> ChatThreadChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public AIStudio.Settings.Provider Provider { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Action<string> WorkspaceName { get; set; } = _ => { };
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Workspaces? Workspaces { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
|
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||||
|
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
|
private DataSourceSelection? dataSourceSelectionComponent;
|
||||||
|
private DataSourceOptions earlyDataSourceOptions = new();
|
||||||
|
private Profile currentProfile = Profile.NO_PROFILE;
|
||||||
|
private bool hasUnsavedChanges;
|
||||||
|
private bool mustScrollToBottomAfterRender;
|
||||||
|
private InnerScrolling scrollingArea = null!;
|
||||||
|
private byte scrollRenderCountdown;
|
||||||
|
private bool isStreaming;
|
||||||
|
private string userInput = string.Empty;
|
||||||
|
private bool mustStoreChat;
|
||||||
|
private bool mustLoadChat;
|
||||||
|
private LoadChat loadChat;
|
||||||
|
private bool autoSaveEnabled;
|
||||||
|
private string currentWorkspaceName = string.Empty;
|
||||||
|
private Guid currentWorkspaceId = Guid.Empty;
|
||||||
|
private CancellationTokenSource? cancellationTokenSource;
|
||||||
|
|
||||||
|
// Unfortunately, we need the input field reference to blur the focus away. Without
|
||||||
|
// this, we cannot clear the input field.
|
||||||
|
private MudTextField<string> inputField = null!;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Apply the filters for the message bus:
|
||||||
|
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
||||||
|
|
||||||
|
// Configure the spellchecking for the user input:
|
||||||
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
|
|
||||||
|
// Get the preselected profile:
|
||||||
|
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check for deferred messages of the kind 'SEND_TO_CHAT',
|
||||||
|
// aka the user sends an assistant result to the chat:
|
||||||
|
//
|
||||||
|
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
||||||
|
if (deferredContent is not null)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Yes, the user sent an assistant result to the chat.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Use chat thread sent by the user:
|
||||||
|
this.ChatThread = deferredContent;
|
||||||
|
this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
|
||||||
|
// We know already that the chat thread is not null,
|
||||||
|
// but we have to check it again for the nullability
|
||||||
|
// for the compiler:
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Check if the chat thread has a name. If not, we
|
||||||
|
// generate the name now:
|
||||||
|
//
|
||||||
|
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||||
|
{
|
||||||
|
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
|
||||||
|
if (firstUserBlock is not null)
|
||||||
|
{
|
||||||
|
this.ChatThread.Name = firstUserBlock.Content switch
|
||||||
|
{
|
||||||
|
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
|
||||||
|
_ => "Thread"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to apply the standard chat data source options:
|
||||||
|
//
|
||||||
|
if (this.SettingsManager.ConfigurationData.Chat.SendToChatDataSourceBehavior is SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS)
|
||||||
|
this.ChatThread.DataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to store the chat automatically:
|
||||||
|
//
|
||||||
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
this.autoSaveEnabled = true;
|
||||||
|
this.mustStoreChat = true;
|
||||||
|
|
||||||
|
//
|
||||||
|
// When a standard workspace is used, we have to ensure
|
||||||
|
// that the workspace is available:
|
||||||
|
//
|
||||||
|
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
|
||||||
|
await WorkspaceBehaviour.EnsureERIServerWorkspace();
|
||||||
|
|
||||||
|
else if (this.ChatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID)
|
||||||
|
await WorkspaceBehaviour.EnsureBiasWorkspace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// No, the user did not send an assistant result to the chat.
|
||||||
|
//
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to show the latest message after loading:
|
||||||
|
//
|
||||||
|
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// We cannot scroll to the bottom right now because the
|
||||||
|
// chat component is not rendered yet. We have to wait for
|
||||||
|
// the rendering process to finish. Thus, we set a flag
|
||||||
|
// to scroll to the bottom after the rendering process.:
|
||||||
|
//
|
||||||
|
this.mustScrollToBottomAfterRender = true;
|
||||||
|
this.scrollRenderCountdown = 4;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if another component deferred the loading of a chat.
|
||||||
|
//
|
||||||
|
// This is used, e.g., for the bias-of-the-day component:
|
||||||
|
// when the bias for this day was already produced, the bias
|
||||||
|
// component sends a message to the chat component to load
|
||||||
|
// the chat with the bias:
|
||||||
|
//
|
||||||
|
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
||||||
|
if (deferredLoading != default)
|
||||||
|
{
|
||||||
|
this.loadChat = deferredLoading;
|
||||||
|
this.mustLoadChat = true;
|
||||||
|
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// When for whatever reason we have a chat thread, we have to
|
||||||
|
// ensure that the corresponding workspace id is set and the
|
||||||
|
// workspace name is loaded:
|
||||||
|
//
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the correct provider:
|
||||||
|
await this.SelectProviderWhenLoadingChat();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && this.ChatThread is not null && this.mustStoreChat)
|
||||||
|
{
|
||||||
|
this.mustStoreChat = false;
|
||||||
|
|
||||||
|
if(this.Workspaces is not null)
|
||||||
|
await this.Workspaces.StoreChat(this.ChatThread);
|
||||||
|
else
|
||||||
|
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||||
|
|
||||||
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstRender && this.mustLoadChat)
|
||||||
|
{
|
||||||
|
this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now.");
|
||||||
|
this.mustLoadChat = false;
|
||||||
|
this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
|
||||||
|
|
||||||
|
if(this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||||
|
|
||||||
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
await this.SelectProviderWhenLoadingChat();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.Logger.LogWarning($"The chat '{this.loadChat.ChatId}' could not be loaded.");
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.mustScrollToBottomAfterRender)
|
||||||
|
{
|
||||||
|
if (--this.scrollRenderCountdown == 0)
|
||||||
|
{
|
||||||
|
await this.scrollingArea.ScrollToBottom();
|
||||||
|
this.mustScrollToBottomAfterRender = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||||
|
|
||||||
|
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
|
||||||
|
|
||||||
|
private string InputLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.IsProviderSelected)
|
||||||
|
return string.Format(T("Your Prompt (use selected instance '{0}', provider '{1}')"), this.Provider.InstanceName, this.Provider.UsedLLMProvider.ToName());
|
||||||
|
|
||||||
|
return this.T("Select a provider first");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0;
|
||||||
|
|
||||||
|
private string TooltipAddChatToWorkspace => string.Format(T(@"Start new chat in workspace ""{0}"""), this.currentWorkspaceName);
|
||||||
|
|
||||||
|
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||||
|
|
||||||
|
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||||
|
|
||||||
|
private void ApplyStandardDataSourceOptions()
|
||||||
|
{
|
||||||
|
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||||
|
this.earlyDataSourceOptions = chatDefaultOptions;
|
||||||
|
if(this.ChatThread is not null)
|
||||||
|
this.ChatThread.DataSourceOptions = chatDefaultOptions;
|
||||||
|
|
||||||
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractThreadName(string firstUserInput)
|
||||||
|
{
|
||||||
|
// We select the first 10 words of the user input:
|
||||||
|
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var threadName = string.Join(' ', words.Take(10));
|
||||||
|
|
||||||
|
// If the thread name is empty, we use a default name:
|
||||||
|
if (string.IsNullOrWhiteSpace(threadName))
|
||||||
|
threadName = "Thread";
|
||||||
|
|
||||||
|
return threadName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProfileWasChanged(Profile profile)
|
||||||
|
{
|
||||||
|
this.currentProfile = profile;
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.ChatThread = this.ChatThread with
|
||||||
|
{
|
||||||
|
SelectedProfile = this.currentProfile.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
|
||||||
|
{
|
||||||
|
if (this.ChatThread is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return this.ChatThread.AISelectedDataSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataSourceOptions GetCurrentDataSourceOptions()
|
||||||
|
{
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
return this.ChatThread.DataSourceOptions;
|
||||||
|
|
||||||
|
return this.earlyDataSourceOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetCurrentDataSourceOptions(DataSourceOptions updatedOptions)
|
||||||
|
{
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
this.ChatThread.DataSourceOptions = updatedOptions;
|
||||||
|
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
await this.SaveThread();
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.earlyDataSourceOptions = updatedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsInputForbidden()
|
||||||
|
{
|
||||||
|
if (!this.IsProviderSelected)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if(this.isStreaming)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||||
|
{
|
||||||
|
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||||
|
this.dataSourceSelectionComponent.Hide();
|
||||||
|
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
var key = keyEvent.Code.ToLowerInvariant();
|
||||||
|
|
||||||
|
// Was the enter key (either enter or numpad enter) pressed?
|
||||||
|
var isEnter = key is "enter" or "numpadenter";
|
||||||
|
|
||||||
|
// Was a modifier key pressed as well?
|
||||||
|
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
|
||||||
|
|
||||||
|
// Depending on the user's settings, might react to shortcuts:
|
||||||
|
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
|
||||||
|
{
|
||||||
|
case SendBehavior.ENTER_IS_SENDING:
|
||||||
|
if (!isModifier && isEnter)
|
||||||
|
await this.SendMessage();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SendBehavior.MODIFER_ENTER_IS_SENDING:
|
||||||
|
if (isEnter && isModifier)
|
||||||
|
await this.SendMessage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage(bool reuseLastUserPrompt = false)
|
||||||
|
{
|
||||||
|
if (!this.IsProviderSelected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We need to blur the focus away from the input field
|
||||||
|
// to be able to clear the field:
|
||||||
|
await this.inputField.BlurAsync();
|
||||||
|
|
||||||
|
// Create a new chat thread if necessary:
|
||||||
|
if (this.ChatThread is null)
|
||||||
|
{
|
||||||
|
this.ChatThread = new()
|
||||||
|
{
|
||||||
|
SelectedProvider = this.Provider.Id,
|
||||||
|
SelectedProfile = this.currentProfile.Id,
|
||||||
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
|
WorkspaceId = this.currentWorkspaceId,
|
||||||
|
ChatId = Guid.NewGuid(),
|
||||||
|
DataSourceOptions = this.earlyDataSourceOptions,
|
||||||
|
Name = this.ExtractThreadName(this.userInput),
|
||||||
|
Seed = this.RNG.Next(),
|
||||||
|
Blocks = [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set the thread name if it is empty:
|
||||||
|
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||||
|
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
|
||||||
|
|
||||||
|
// Update provider and profile:
|
||||||
|
this.ChatThread.SelectedProvider = this.Provider.Id;
|
||||||
|
this.ChatThread.SelectedProfile = this.currentProfile.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var time = DateTimeOffset.Now;
|
||||||
|
IContent? lastUserPrompt;
|
||||||
|
if (!reuseLastUserPrompt)
|
||||||
|
{
|
||||||
|
lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = this.userInput,
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add the user message to the thread:
|
||||||
|
//
|
||||||
|
this.ChatThread?.Blocks.Add(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = time,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = lastUserPrompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the chat:
|
||||||
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
await this.SaveThread();
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lastUserPrompt = this.ChatThread.Blocks.Last(x => x.Role is ChatRole.USER).Content;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add the AI response to the thread:
|
||||||
|
//
|
||||||
|
var aiText = new ContentText
|
||||||
|
{
|
||||||
|
// We have to wait for the remote
|
||||||
|
// for the content stream:
|
||||||
|
InitialRemoteWait = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ChatThread?.Blocks.Add(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = time,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.AI,
|
||||||
|
Content = aiText,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the input field:
|
||||||
|
this.userInput = string.Empty;
|
||||||
|
|
||||||
|
// Enable the stream state for the chat component:
|
||||||
|
this.isStreaming = true;
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
|
||||||
|
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||||
|
{
|
||||||
|
this.mustScrollToBottomAfterRender = true;
|
||||||
|
this.scrollRenderCountdown = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||||
|
|
||||||
|
using (this.cancellationTokenSource = new())
|
||||||
|
{
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
// Use the selected provider to get the AI response.
|
||||||
|
// By awaiting this line, we wait for the entire
|
||||||
|
// content to be streamed.
|
||||||
|
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cancellationTokenSource = null;
|
||||||
|
|
||||||
|
// Save the chat:
|
||||||
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
await this.SaveThread();
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the stream state:
|
||||||
|
this.isStreaming = false;
|
||||||
|
|
||||||
|
// Update the UI:
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CancelStreaming()
|
||||||
|
{
|
||||||
|
if (this.cancellationTokenSource is not null)
|
||||||
|
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||||
|
await this.cancellationTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveThread()
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.CanThreadBeSaved)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// When the workspace component is visible, we store the chat
|
||||||
|
// through the workspace component. The advantage of this is that
|
||||||
|
// the workspace gets updated automatically when the chat is saved.
|
||||||
|
//
|
||||||
|
if (this.Workspaces is not null)
|
||||||
|
await this.Workspaces.StoreChat(this.ChatThread);
|
||||||
|
else
|
||||||
|
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||||
|
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||||
|
// about possible data loss:
|
||||||
|
//
|
||||||
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||||
|
{
|
||||||
|
var dialogParameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
var dialogResult = await dialogReference.Result;
|
||||||
|
if (dialogResult is null || dialogResult.Canceled)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete the previous chat when desired and necessary:
|
||||||
|
//
|
||||||
|
if (this.ChatThread is not null && deletePreviousChat)
|
||||||
|
{
|
||||||
|
string chatPath;
|
||||||
|
if (this.ChatThread.WorkspaceId == Guid.Empty)
|
||||||
|
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString());
|
||||||
|
else
|
||||||
|
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
|
||||||
|
|
||||||
|
if(this.Workspaces is null)
|
||||||
|
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||||
|
else
|
||||||
|
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset our state:
|
||||||
|
//
|
||||||
|
this.isStreaming = false;
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
this.userInput = string.Empty;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset the LLM provider considering the user's settings:
|
||||||
|
//
|
||||||
|
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
|
||||||
|
{
|
||||||
|
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
|
||||||
|
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||||
|
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
|
||||||
|
if(this.Provider == default)
|
||||||
|
{
|
||||||
|
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||||
|
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset the chat thread or create a new one:
|
||||||
|
//
|
||||||
|
if (!useSameWorkspace)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// When the user wants to start a new chat outside the current workspace,
|
||||||
|
// we have to reset the workspace id and the workspace name. Also, we have
|
||||||
|
// to reset the chat thread:
|
||||||
|
//
|
||||||
|
this.ChatThread = null;
|
||||||
|
this.currentWorkspaceId = Guid.Empty;
|
||||||
|
this.currentWorkspaceName = string.Empty;
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// When the user wants to start a new chat in the same workspace, we have to
|
||||||
|
// reset the chat thread only. The workspace id and the workspace name remain
|
||||||
|
// the same:
|
||||||
|
//
|
||||||
|
this.ChatThread = new()
|
||||||
|
{
|
||||||
|
SelectedProvider = this.Provider.Id,
|
||||||
|
SelectedProfile = this.currentProfile.Id,
|
||||||
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
|
WorkspaceId = this.currentWorkspaceId,
|
||||||
|
ChatId = Guid.NewGuid(),
|
||||||
|
Name = string.Empty,
|
||||||
|
Seed = this.RNG.Next(),
|
||||||
|
Blocks = [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we have to reset the data source options as well:
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
|
||||||
|
// Notify the parent component about the change:
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MoveChatToWorkspace()
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||||
|
{
|
||||||
|
var confirmationDialogParameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ "Message", T("Are you sure you want to move this chat? All unsaved changes will be lost.") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||||
|
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialogParameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ "Message", T("Please select the workspace where you want to move the chat to.") },
|
||||||
|
{ "SelectedWorkspace", this.ChatThread?.WorkspaceId },
|
||||||
|
{ "ConfirmText", T("Move chat") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
var dialogResult = await dialogReference.Result;
|
||||||
|
if (dialogResult is null || dialogResult.Canceled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var workspaceId = dialogResult.Data is Guid id ? id : Guid.Empty;
|
||||||
|
if (workspaceId == Guid.Empty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Delete the chat from the current workspace or the temporary storage:
|
||||||
|
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||||
|
|
||||||
|
this.ChatThread!.WorkspaceId = workspaceId;
|
||||||
|
await this.SaveThread();
|
||||||
|
|
||||||
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadedChatChanged()
|
||||||
|
{
|
||||||
|
this.isStreaming = false;
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
this.userInput = string.Empty;
|
||||||
|
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.currentWorkspaceId = Guid.Empty;
|
||||||
|
this.currentWorkspaceName = string.Empty;
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.SelectProviderWhenLoadingChat();
|
||||||
|
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||||
|
{
|
||||||
|
this.mustScrollToBottomAfterRender = true;
|
||||||
|
this.scrollRenderCountdown = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetState()
|
||||||
|
{
|
||||||
|
this.isStreaming = false;
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
this.userInput = string.Empty;
|
||||||
|
this.currentWorkspaceId = Guid.Empty;
|
||||||
|
|
||||||
|
this.currentWorkspaceName = string.Empty;
|
||||||
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
|
||||||
|
this.ChatThread = null;
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectProviderWhenLoadingChat()
|
||||||
|
{
|
||||||
|
var chatProvider = this.ChatThread?.SelectedProvider;
|
||||||
|
var chatProfile = this.ChatThread?.SelectedProfile;
|
||||||
|
|
||||||
|
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
|
||||||
|
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
|
||||||
|
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
|
||||||
|
if(this.Provider == default)
|
||||||
|
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||||
|
|
||||||
|
// Try to select the profile:
|
||||||
|
if (!string.IsNullOrWhiteSpace(chatProfile))
|
||||||
|
{
|
||||||
|
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile);
|
||||||
|
if(this.currentProfile == default)
|
||||||
|
this.currentProfile = Profile.NO_PROFILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleWorkspaceOverlay()
|
||||||
|
{
|
||||||
|
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveBlock(IContent block)
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.ChatThread.Remove(block);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
await this.SaveThread();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RegenerateBlock(IContent aiBlock)
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
await this.SendMessage(reuseLastUserPrompt: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task EditLastUserBlock(IContent block)
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (block is not ContentText textBlock)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
var lastBlock = this.ChatThread.Blocks.Last();
|
||||||
|
var lastBlockContent = lastBlock.Content;
|
||||||
|
if(lastBlockContent is null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
this.userInput = textBlock.Text;
|
||||||
|
this.ChatThread.Remove(block);
|
||||||
|
this.ChatThread.Remove(lastBlockContent);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task EditLastBlock(IContent block)
|
||||||
|
{
|
||||||
|
if(this.ChatThread is null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (block is not ContentText textBlock)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
this.userInput = textBlock.Text;
|
||||||
|
this.ChatThread.Remove(block);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
|
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
|
{
|
||||||
|
switch (triggeredEvent)
|
||||||
|
{
|
||||||
|
case Event.RESET_CHAT_STATE:
|
||||||
|
await this.ResetState();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.CHAT_STREAMING_DONE:
|
||||||
|
if(this.autoSaveEnabled)
|
||||||
|
await this.SaveThread();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
|
||||||
|
await this.LoadedChatChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||||
|
{
|
||||||
|
switch (triggeredEvent)
|
||||||
|
{
|
||||||
|
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||||
|
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
return Task.FromResult((TResult?) (object) false);
|
||||||
|
|
||||||
|
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(default(TResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Implementation of IAsyncDisposable
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
await this.SaveThread();
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cancellationTokenSource is not null)
|
||||||
|
{
|
||||||
|
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||||
|
await this.cancellationTokenSource.CancelAsync();
|
||||||
|
|
||||||
|
this.cancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
@ -1,14 +1,16 @@
|
|||||||
@using AIStudio.Provider
|
@using AIStudio.Provider
|
||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider.">
|
<MudTooltip Text="@T("Shows and hides the confidence card with information about the selected LLM provider.")" Placement="Placement.Top">
|
||||||
@if (this.Mode is ConfidenceInfoMode.ICON)
|
@if (this.Mode is PopoverTriggerMode.ICON)
|
||||||
{
|
{
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())">
|
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())">
|
||||||
Confidence
|
@T("Confidence")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
}
|
}
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
@ -17,16 +19,22 @@
|
|||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
<MudText Typo="Typo.h5">Confidence Card</MudText>
|
<MudText Typo="Typo.h5">
|
||||||
|
@T("Confidence Card")
|
||||||
|
</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent>
|
<MudCardContent Style="max-height: 50vh; max-width: 35vw; overflow: auto;">
|
||||||
<MudText Typo="Typo.h6">Description</MudText>
|
<MudText Typo="Typo.h6">
|
||||||
|
@T("Description")
|
||||||
|
</MudText>
|
||||||
<MudMarkdown Value="@this.currentConfidence.Description"/>
|
<MudMarkdown Value="@this.currentConfidence.Description"/>
|
||||||
|
|
||||||
@if (this.currentConfidence.Sources.Count > 0)
|
@if (this.currentConfidence.Sources.Count > 0)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.h6">Sources</MudText>
|
<MudText Typo="Typo.h6">
|
||||||
|
@T("Sources")
|
||||||
|
</MudText>
|
||||||
<MudList T="@string">
|
<MudList T="@string">
|
||||||
@foreach (var sourceTuple in this.GetConfidenceSources())
|
@foreach (var sourceTuple in this.GetConfidenceSources())
|
||||||
{
|
{
|
||||||
@ -37,13 +45,17 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
|
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.h6">Region</MudText>
|
<MudText Typo="Typo.h6">
|
||||||
|
@T("Region")
|
||||||
|
</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
<b>@this.currentConfidence.Region</b>
|
<b>@this.currentConfidence.Region</b>
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudText Typo="Typo.h6">Confidence Level</MudText>
|
<MudText Typo="Typo.h6">
|
||||||
|
@T("Confidence Level")
|
||||||
|
</MudText>
|
||||||
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
|
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
|
||||||
<b>@this.currentConfidence.Level.GetName()</b>
|
<b>@this.currentConfidence.Level.GetName()</b>
|
||||||
</MudText>
|
</MudText>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user