Compare commits

...

63 Commits

Author SHA1 Message Date
Dominic Neuburg
fc53278c60
Focus message composer when it becomes available (#822)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-23 09:02:40 +02:00
Thorsten Sommer
2acb6f2a57
Updated README.md with v26.6.2 release details (#820)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-21 20:44:44 +02:00
Thorsten Sommer
5af616f565
Enhanced settings manager with versioned backups and migrations (#819)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 18:46:21 +02:00
Thorsten Sommer
6d48252db3
Prepared release v26.6.2 (#818)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 16:19:17 +02:00
Thorsten Sommer
64e91ff4ff
Added support for organization-managed chat defaults (#817)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 15:59:23 +02:00
Thorsten Sommer
dddb40096d
Added support for organization-trusted providers (#816)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 15:16:37 +02:00
Thorsten Sommer
e65110a142
Added support for organization-managed provider confidence settings (#815)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 11:52:02 +02:00
Thorsten Sommer
5045da3a91
Added support for organization-managed introduction texts (#814)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 20:28:22 +02:00
Thorsten Sommer
e04879fd7f
Added a read-only view for managed profiles and chat templates (#813)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 17:06:43 +02:00
Thorsten Sommer
fc7197ec93
Added compatibility shim for the Qdrant Edge migration (#812)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 16:11:24 +02:00
Thorsten Sommer
c3bf2563cd
Fixed self-hosted provider API key handling (#811)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 15:55:09 +02:00
Thorsten Sommer
24952e796e
Updated README.md (#810)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-11 21:06:48 +02:00
Thorsten Sommer
4c328c8e72
Prepared release v26.6.1 (#809)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-11 15:57:45 +02:00
Thorsten Sommer
c0e6a9a644
Enhanced llama.cpp support for loading available models (#808)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-11 15:46:17 +02:00
Thorsten Sommer
71ae52753a
Fixed the Flatpak issue that prevented Pandoc from being found (#807)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-11 12:33:19 +02:00
Thorsten Sommer
e4fa1cd72a
Added offline build mode for local build script (#806) 2026-06-11 12:22:09 +02:00
Thorsten Sommer
0ea63a16c0
Allow external HTTP root certificates to be configured by a policy file (#805)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-11 11:37:40 +02:00
Thorsten Sommer
5272895441
Improved PDFium library path selection (#804)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-11 09:44:56 +02:00
Thorsten Sommer
f017b87abd
Fixed an issue where AI Studio could be started multiple times (#803)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-10 21:31:02 +02:00
Thorsten Sommer
c07a5227dc
Fixed issues with the DI system and singletons (#802)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-10 21:01:27 +02:00
Thorsten Sommer
1c2d243c1f
Improved voice recording shortcut labels (#800)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-09 20:11:32 +02:00
Thorsten Sommer
e9da7d31df
Fixed Windows terminal flash during Pandoc document processing (#799)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-08 13:43:41 +02:00
Thorsten Sommer
b9813fcbe7
Improved workspaces by highlighting the current chat (#798)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-08 12:52:48 +02:00
Thorsten Sommer
0a4208d91d
Use a patched version of permutation_iterator for Qdrant edge (#797)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-07 21:16:16 +02:00
Thorsten Sommer
102b344557
Improved the dialog for moving chats into workspaces (#796)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-06 10:06:41 +02:00
Thorsten Sommer
0b41f5eb96
Added the option to search for chats in all workspaces (#795)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
2026-06-04 19:34:52 +02:00
Thorsten Sommer
9fc7eaff99
Added support for hiding the quick start guide (#794)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-04 16:24:40 +02:00
Thorsten Sommer
25595a39a5
Fixed minor warnings for Qdrant edge client implementation (#791)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-04 15:53:30 +02:00
Thorsten Sommer
f47dd5fdc2
Fixed the Qdrant edge dependency (#790) 2026-06-04 15:28:33 +02:00
Paul Koudelka
5b5b6e0b28
Replace Qdrant with Qdrant Edge (#783)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-02 17:22:59 +02:00
Paul Koudelka
1000d7fbc4
Fixed plugin startup issue (#789)
Some checks are pending
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
2026-06-02 16:32:09 +02:00
Thorsten Sommer
bd9597c706
Added shortcut to start new chat in a workspace (#788)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-05-31 22:22:33 +02:00
Thorsten Sommer
b4c3abd6b0
Upgraded dependencies (#787)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-31 19:36:31 +02:00
Thorsten Sommer
86700847e9
Added support for reading policy files from Flatpak extension (#786)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-31 19:24:11 +02:00
Thorsten Sommer
b37f70d7ff
Added startup path & Linux package type to the info page (#785)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-31 19:10:19 +02:00
Thorsten Sommer
e27cd27dba
Added support for managed custom root certificate bundles (#784)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-31 18:46:54 +02:00
Thorsten Sommer
def685d2c2
Added support for up to 100,000 enterprise configuration slots (#782)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-31 12:11:09 +02:00
Thorsten Sommer
a15c47b56d
Updated README.md (#781)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-05-25 22:16:53 +02:00
Thorsten Sommer
9f18a50f17
Prepared release v26.5.5 (#780)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-25 20:59:44 +02:00
Thorsten Sommer
d05ff26e62
Fixed an issue with switching between chat threads while multiple chats are running (#779)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-25 20:48:26 +02:00
Thorsten Sommer
3e6e3bdcbd
Fixed error messages for provider requests (#778)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-25 17:32:54 +02:00
Thorsten Sommer
8417fa3984
Prepared release v26.5.5 (#777)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-24 15:30:42 +02:00
Thorsten Sommer
fa9cdb87ed
Improved Linux AppImages bundle (#776)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-24 14:54:48 +02:00
Thorsten Sommer
e9927ca769
Upgraded Qdrant to v1.18.1 (#775)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-24 14:01:29 +02:00
Thorsten Sommer
a0753488b3
Added support for parallel AI job processing (#774)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-24 13:50:42 +02:00
Thorsten Sommer
8853ea0cfe
Improved transcription error handling (#773)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-05-23 11:25:18 +02:00
Sabrina-devops
2317add71f
Fixed missing file attachments when editing a prompt (#748)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-22 16:18:36 +02:00
Sabrina-devops
7998fbcc48
Fixed handling of .doc files (#747)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-22 16:03:34 +02:00
Thorsten Sommer
c08f9e2ea1
Added support for exporting chat templates & profiles (#772)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-22 15:46:03 +02:00
Sabrina-devops
277309cd19
Added an option to configure the timeout setting for all requests (#746)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-05-21 16:48:34 +02:00
Thorsten Sommer
d28184af1a
Fixed PDFium initialization logic (#771)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-21 13:50:57 +02:00
Thorsten Sommer
cef1c99765
Improved Qdrant server startup & client initialization (#770)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-05-19 08:24:22 +02:00
Thorsten Sommer
97e6003686
Fixed missing translations for file type names (#769)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-18 19:24:07 +02:00
Thorsten Sommer
cad7a98e7b
Fixed the missed spellchecking settings for the slide builder assistant (#768)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-18 19:05:29 +02:00
Thorsten Sommer
7a09241888
Configure ERI servers in config plugins (#767)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-18 16:26:51 +02:00
Thorsten Sommer
378aaaa368
Released the transcription feature (#766) (#766)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-05-16 19:13:27 +02:00
Thorsten Sommer
9419c4ed44
Improved Rust syntax by Clippy suggestions (#765)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-16 18:53:53 +02:00
Thorsten Sommer
91cfe8dcd0
Fixed & improved pandoc handling (#762)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-16 18:27:16 +02:00
Thorsten Sommer
8f0effd25b
Added dedicated Tauri tool cache (#764)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-16 17:38:38 +02:00
Thorsten Sommer
fc3c000de6
Improved pipeline (#763)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-15 18:13:30 +02:00
Thorsten Sommer
d46688f364
Upgraded dependencies (#761)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-14 17:16:28 +02:00
Thorsten Sommer
6fc69751b9
Updated documentation & readme (#760)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-05-13 22:18:14 +02:00
Thorsten Sommer
3360c2fa29
Prepared test release v26.5.4 (#759) 2026-05-13 14:03:36 +02:00
275 changed files with 15080 additions and 3613 deletions

View File

@ -12,6 +12,10 @@ on:
- synchronize - synchronize
- reopened - reopened
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') }}
env: env:
RETENTION_INTERMEDIATE_ASSETS: 1 RETENTION_INTERMEDIATE_ASSETS: 1
RETENTION_RELEASE_ASSETS: 30 RETENTION_RELEASE_ASSETS: 30
@ -37,6 +41,8 @@ jobs:
id: determine id: determine
env: env:
EVENT_NAME: ${{ github.event_name }} EVENT_NAME: ${{ github.event_name }}
PR_ACTION: ${{ github.event.action }}
ACTION_LABEL_NAME: ${{ github.event.label.name }}
REF: ${{ github.ref }} REF: ${{ github.ref }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }} PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
@ -55,6 +61,11 @@ jobs:
is_internal_pr=true is_internal_pr=true
fi fi
has_run_pipeline_label=false
if [[ " $PR_LABELS " == *" run-pipeline "* ]]; then
has_run_pipeline_label=true
fi
if [[ "$REF" == refs/tags/v* ]]; then if [[ "$REF" == refs/tags/v* ]]; then
is_release=true is_release=true
build_enabled=true build_enabled=true
@ -65,13 +76,21 @@ jobs:
build_enabled=true build_enabled=true
artifact_retention_days=7 artifact_retention_days=7
skip_reason="" skip_reason=""
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " == *" run-pipeline "* ]]; then elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" && "$ACTION_LABEL_NAME" == "run-pipeline" ]]; then
is_labeled_pr=true is_labeled_pr=true
is_pr_build=true is_pr_build=true
build_enabled=true build_enabled=true
artifact_retention_days=3 artifact_retention_days=3
skip_reason="" skip_reason=""
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " != *" run-pipeline "* ]]; then elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" != "labeled" && "$has_run_pipeline_label" == "true" ]]; then
is_labeled_pr=true
is_pr_build=true
build_enabled=true
artifact_retention_days=3
skip_reason=""
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" ]]; then
skip_reason="Build disabled: label '${ACTION_LABEL_NAME}' is not 'run-pipeline'."
elif [[ "$EVENT_NAME" == "pull_request" && "$has_run_pipeline_label" != "true" ]]; then
skip_reason="Build disabled: PR does not have the required 'run-pipeline' label." skip_reason="Build disabled: PR does not have the required 'run-pipeline' label."
fi fi
@ -310,8 +329,8 @@ jobs:
pdfium_version=$(sed -n '11p' metadata.txt) pdfium_version=$(sed -n '11p' metadata.txt)
pdfium_version=$(echo $pdfium_version | cut -d'.' -f3) pdfium_version=$(echo $pdfium_version | cut -d'.' -f3)
# Next line is the Qdrant version: # Next line is the vector store version:
qdrant_version="v$(sed -n '12p' metadata.txt)" vector_store_version="$(sed -n '12p' metadata.txt)"
# 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
@ -325,7 +344,7 @@ jobs:
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV
echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV echo "VECTOR_STORE_VERSION=${vector_store_version}" >> $GITHUB_ENV
# Log the metadata: # Log the metadata:
echo "App version: '${formatted_app_version}'" echo "App version: '${formatted_app_version}'"
@ -338,7 +357,7 @@ jobs:
echo "Tauri version: '${tauri_version}'" echo "Tauri version: '${tauri_version}'"
echo "Architecture: '${{ matrix.dotnet_runtime }}'" echo "Architecture: '${{ matrix.dotnet_runtime }}'"
echo "PDFium version: '${pdfium_version}'" echo "PDFium version: '${pdfium_version}'"
echo "Qdrant version: '${qdrant_version}'" echo "Vector store version: '${vector_store_version}'"
- name: Read and format metadata (Windows) - name: Read and format metadata (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
@ -383,8 +402,8 @@ jobs:
$pdfium_version = $metadata[10] $pdfium_version = $metadata[10]
$pdfium_version = $pdfium_version.Split('.')[2] $pdfium_version = $pdfium_version.Split('.')[2]
# Next line is the necessary Qdrant version: # Next line is the vector store version:
$qdrant_version = "v$($metadata[11])" $vector_store_version = $metadata[11]
# 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
@ -397,7 +416,7 @@ jobs:
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 Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV
Write-Output "QDRANT_VERSION=${qdrant_version}" >> $env:GITHUB_ENV Write-Output "VECTOR_STORE_VERSION=${vector_store_version}" >> $env:GITHUB_ENV
# Log the metadata: # Log the metadata:
Write-Output "App version: '${formatted_app_version}'" Write-Output "App version: '${formatted_app_version}'"
@ -410,7 +429,7 @@ jobs:
Write-Output "Tauri version: '${tauri_version}'" Write-Output "Tauri version: '${tauri_version}'"
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'" Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
Write-Output "PDFium version: '${pdfium_version}'" Write-Output "PDFium version: '${pdfium_version}'"
Write-Output "Qdrant version: '${qdrant_version}'" Write-Output "Vector store version: '${vector_store_version}'"
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
@ -539,129 +558,6 @@ jobs:
} catch { } catch {
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)" Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
} }
- name: Deploy Qdrant (Unix)
if: matrix.platform != 'windows-latest'
env:
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
RUST_TARGET: ${{ matrix.rust_target }}
run: |
set -e
# Target directory:
TDB_DIR="runtime/target/databases/qdrant"
mkdir -p "$TDB_DIR"
case "${DOTNET_RUNTIME}" in
linux-x64)
QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
linux-arm64)
QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
osx-x64)
QDRANT_FILE="x86_64-apple-darwin.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
osx-arm64)
QDRANT_FILE="aarch64-apple-darwin.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
*)
echo "Unknown platform: ${DOTNET_RUNTIME}"
exit 1
;;
esac
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-${QDRANT_FILE}"
echo "Download Qdrant $QDRANT_URL ..."
TMP=$(mktemp -d)
ARCHIVE="${TMP}/qdrant.tgz"
curl -fsSL -o "$ARCHIVE" "$QDRANT_URL"
echo "Extracting Qdrant ..."
tar xzf "$ARCHIVE" -C "$TMP"
SRC="${TMP}/${DB_SOURCE}"
if [ ! -f "$SRC" ]; then
echo "Was not able to find Qdrant source: $SRC"
exit 1
fi
echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/"
cp -f "$SRC" "$TDB_DIR/$DB_TARGET"
echo "Cleaning up ..."
rm -fr "$TMP"
- name: Deploy Qdrant (Windows)
if: matrix.platform == 'windows-latest'
env:
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
RUST_TARGET: ${{ matrix.rust_target }}
run: |
$TDB_DIR = "runtime\target\databases\qdrant"
New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null
switch ($env:DOTNET_RUNTIME) {
"win-x64" {
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
$DB_SOURCE = "qdrant.exe"
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
}
"win-arm64" {
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
$DB_SOURCE = "qdrant.exe"
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
}
default {
Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)"
exit 1
}
}
$QDRANT_URL = "https://github.com/qdrant/qdrant/releases/download/$($env:QDRANT_VERSION)/qdrant-$QDRANT_FILE"
Write-Host "Download $QDRANT_URL ..."
# Create a unique temporary directory (not just a file)
$TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $TMP -Force | Out-Null
$ARCHIVE = Join-Path $TMP "qdrant.tgz"
Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE
Write-Host "Extracting Qdrant ..."
tar -xzf $ARCHIVE -C $TMP
$SRC = Join-Path $TMP $DB_SOURCE
if (!(Test-Path $SRC)) {
Write-Error "Cannot find Qdrant source: $SRC"
exit 1
}
$DEST = Join-Path $TDB_DIR $DB_TARGET
Copy-Item -Path $SRC -Destination $DEST -Force
Write-Host "Cleaning up ..."
Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue
# Try to remove the temporary directory, but ignore errors if files are still in use
try {
Remove-Item $TMP -Recurse -Force -ErrorAction Stop
Write-Host "Successfully cleaned up temporary directory: $TMP"
} catch {
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
}
- name: Build .NET project - name: Build .NET project
run: | run: |
cd "app/MindWork AI Studio" cd "app/MindWork AI Studio"
@ -685,11 +581,9 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/.cargo/bin
~/.cargo/git/db/ ~/.cargo/git/db/
~/.cargo/registry/index/ ~/.cargo/registry/index/
~/.cargo/registry/cache/ ~/.cargo/registry/cache/
~/.rustup/toolchains
runtime/target runtime/target
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }} key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
@ -699,24 +593,33 @@ jobs:
with: with:
toolchain: ${{ env.RUST_VERSION }} toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.rust_target }} targets: ${{ matrix.rust_target }}
- name: Cache Tauri CLI
uses: actions/cache@v4
with:
path: ~/.cargo-tauri-cli
key: tauri-cli-v2-${{ runner.os }}-${{ runner.arch }}
- name: Setup dependencies (Ubuntu-specific, x86) - name: Setup dependencies (Ubuntu-specific, x86)
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64') if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
- name: Setup dependencies (Ubuntu-specific, ARM) - name: Setup dependencies (Ubuntu-specific, ARM)
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64') if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
- name: Setup Tauri (Unix) - name: Setup Tauri (Unix)
if: matrix.platform != 'windows-latest' if: matrix.platform != 'windows-latest'
run: | run: |
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.cargo-tauri-cli/bin:$PATH"
if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then
cargo install tauri-cli --version "^2.11.0" --locked --force cargo install tauri-cli --version "^2.11.0" --locked --force --root "$HOME/.cargo-tauri-cli"
else else
echo "Tauri CLI v2 is already installed" echo "Tauri CLI v2 is already installed"
fi fi
@ -724,9 +627,12 @@ jobs:
- name: Setup Tauri (Windows) - name: Setup Tauri (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
run: | run: |
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
$env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH"
$tauriVersion = cargo tauri --version 2>$null $tauriVersion = cargo tauri --version 2>$null
if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') { if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') {
cargo install tauri-cli --version "^2.11.0" --locked --force cargo install tauri-cli --version "^2.11.0" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli"
} else { } else {
Write-Output "Tauri CLI v2 is already installed" Write-Output "Tauri CLI v2 is already installed"
} }
@ -771,17 +677,29 @@ jobs:
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }} PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
run: | run: |
bundles="${{ matrix.tauri_bundle }}" bundles="${{ matrix.tauri_bundle }}"
tauri_config_args=()
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
echo "Running PR test build without updater bundle signing" echo "Running PR test build without updater bundle signing"
bundles="${{ matrix.tauri_bundle_pr }}" bundles="${{ matrix.tauri_bundle_pr }}"
tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}')
else else
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
fi fi
cd runtime cd runtime
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" "${tauri_config_args[@]}"
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
updater_artifact_count=$(find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) | wc -l)
if [ "$updater_artifact_count" -ne 0 ]; then
echo "PR builds must not generate updater artifacts."
find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \)
exit 1
fi
fi
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l) app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l)
@ -800,17 +718,29 @@ jobs:
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }} PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
run: | run: |
$bundles = "${{ matrix.tauri_bundle }}" $bundles = "${{ matrix.tauri_bundle }}"
$tauriConfigArgs = @()
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") { if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
Write-Output "Running PR test build without updater bundle signing" Write-Output "Running PR test build without updater bundle signing"
$bundles = "${{ matrix.tauri_bundle_pr }}" $bundles = "${{ matrix.tauri_bundle_pr }}"
$tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}')
} else { } else {
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" $env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
} }
cd runtime cd runtime
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles @tauriConfigArgs
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
$updaterArtifacts = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle" -Recurse -File -Include "*.app.tar.gz*", "*.AppImage.tar.gz*", "*nsis.zip*" -ErrorAction SilentlyContinue
if ($updaterArtifacts.Count -ne 0) {
Write-Error "PR builds must not generate updater artifacts."
$updaterArtifacts | ForEach-Object { Write-Error $_.FullName }
exit 1
}
}
- name: Upload artifact (macOS) - name: Upload artifact (macOS)
if: startsWith(matrix.platform, 'macos') if: startsWith(matrix.platform, 'macos')
@ -1125,7 +1055,7 @@ jobs:
with: with:
prerelease: true prerelease: true
draft: false draft: false
make_latest: true make_latest: false
body: ${{ env.CHANGELOG }} body: ${{ env.CHANGELOG }}
name: "Release ${{ env.FORMATTED_VERSION }}" name: "Release ${{ env.FORMATTED_VERSION }}"
fail_on_unmatched_files: true fail_on_unmatched_files: true

View File

@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
MindWork AI Studio is a cross-platform desktop application for interacting with Large Language Models (LLMs). The app uses a hybrid architecture combining a Rust Tauri runtime (for the native desktop shell) with a .NET Blazor Server web application (for the UI and business logic). MindWork AI Studio is a cross-platform desktop application for interacting with Large Language Models (LLMs). The app uses a hybrid architecture combining a Rust Tauri runtime (for the native desktop shell) with a .NET Blazor Server web application (for the UI and business logic).
**Key Architecture Points:** **Key Architecture Points:**
- **Runtime:** Rust-based Tauri v1.8 application providing the native window, system integration, and IPC layer - **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer
- **App:** .NET 9 Blazor Server application providing the UI and core functionality - **App:** .NET 9 Blazor Server application providing the UI and core functionality
- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup - **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others - **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
@ -18,7 +18,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
### Prerequisites ### Prerequisites
- .NET 9 SDK - .NET 9 SDK
- Rust toolchain (stable) - Rust toolchain (stable)
- Tauri v1.6.2 CLI: `cargo install --version 1.6.2 tauri-cli` - Tauri v2 CLI
- Tauri prerequisites (platform-specific dependencies) - Tauri prerequisites (platform-specific dependencies)
- **Note:** Development on Linux is discouraged due to complex Tauri dependencies that vary by distribution - **Note:** Development on Linux is discouraged due to complex Tauri dependencies that vary by distribution
@ -49,7 +49,7 @@ Currently, no automated test suite exists in the repository.
Key modules: Key modules:
- `app_window.rs` - Tauri window management, updater integration - `app_window.rs` - Tauri window management, updater integration
- `dotnet.rs` - Launches and manages the .NET sidecar process - `dotnet.rs` - Launches and manages the .NET sidecar process
- `runtime_api.rs` - Rocket-based HTTPS API for .NET ↔ Rust communication - `runtime_api.rs` - Axum-based HTTPS API for .NET ↔ Rust communication
- `certificate.rs` - Generates self-signed TLS certificates for secure IPC - `certificate.rs` - Generates self-signed TLS certificates for secure IPC
- `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager) - `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager)
- `clipboard.rs` - Cross-platform clipboard operations - `clipboard.rs` - Cross-platform clipboard operations
@ -112,12 +112,16 @@ Plugins can configure:
- Chat templates - Chat templates
- etc. - etc.
When adding configuration options, update: Configuration plugins provide three kinds of values:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs`: In method `TryProcessConfiguration` register new options. - **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults.
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs`: In method `LoadAll` check for leftover configuration. - **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
- The corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, when adding config options (in contrast to complex config. objects) - **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option. When adding configuration plugin capabilities:
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, process the setting in `PluginConfiguration.TryProcessConfiguration`, and check for leftover managed configuration in `PluginFactory.Loading.LoadAll`.
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
## RAG (Retrieval-Augmented Generation) ## RAG (Retrieval-Augmented Generation)
@ -151,8 +155,8 @@ Multi-level confidence scheme allows users to control which providers see which
## Dependencies and Frameworks ## Dependencies and Frameworks
**Rust:** **Rust:**
- Tauri 1.8 - Desktop application framework - Tauri 2 - Desktop application framework
- Rocket - HTTPS API server - Axum - HTTPS API server
- tokio - Async runtime - tokio - Async runtime
- keyring - OS keyring integration - keyring - OS keyring integration
- pdfium-render - PDF text extraction - pdfium-render - PDF text extraction
@ -187,6 +191,7 @@ Multi-level confidence scheme allows users to control which providers see which
- **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >` - **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >`
- **End of file formatting** - Do not append an extra empty line at the end of files. - **End of file formatting** - Do not append an extra empty line at the end of files.
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit. - **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
- **Spaces in paths** - Always quote paths with spaces in bash commands - **Spaces in paths** - Always quote paths with spaces in bash commands
- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing. - **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing.
- **Debug environment** - Reads `startup.env` file with IPC credentials - **Debug environment** - Reads `startup.env` file with IPC credentials
@ -195,6 +200,7 @@ Multi-level confidence scheme allows users to control which providers see which
- **Encryption** - Initialized before Rust service is marked ready - **Encryption** - Initialized before Rust service is marked ready
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. - **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
- **Empty lines** - Avoid adding extra empty lines at the end of files. - **Empty lines** - Avoid adding extra empty lines at the end of files.
## Changelogs ## Changelogs

View File

@ -28,12 +28,11 @@ Since November 2024: Work on RAG (integration of your data and files) has begun.
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~ - [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~ - [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~ - [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
- [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~ - [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~
- [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~ - [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~
- [ ] App: Implement the process to vectorize one local file using embeddings - [ ] App: Implement the process to vectorize one local file using embeddings (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
- [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~ - [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~
- [ ] App: Implement the continuous process of vectorizing data - [ ] App: Implement the continuous process of vectorizing data (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~ - [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~ - [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~ - [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
@ -79,6 +78,9 @@ Since March 2025: We have started developing the plugin system. There will be la
</h3> </h3>
</summary> </summary>
- v26.6.2: Expanded enterprise configuration options with chat defaults, custom introduction panels, trust settings for data security, and managed confidence levels; added auto-backups for app settings & the possibility to view managed profiles and chat templates.
- v26.6.1: Increased enterprise configuration capacity for large organizations, broader Flatpak deployment support, startup and Linux package diagnostics, chat search across all workspaces, improved workspace workflows, better model discovery for self-hosted llama.cpp providers, and fixes for profile and chat template updates, workspace naming, and startup behavior.
- v26.5.5: Released voice recording and transcription for all users; added support for multiple chats running at the same time, export options for profiles, chat templates, and ERI data sources, organization-managed ERI servers, and configurable request timeouts; upgraded the native runtime to Tauri v2.
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation. - v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization. - v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
- v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection. - v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection.
@ -88,9 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation. - v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
- v0.9.45: Added chat templates to AI Studio, allowing you to create and use a library of system prompts for your chats. - v0.9.45: Added chat templates to AI Studio, allowing you to create and use a library of system prompts for your chats.
- v0.9.44: Added PDF import to the text summarizer, translation, and legal check assistants, allowing you to import PDF files and use them as input for the assistants. - v0.9.44: Added PDF import to the text summarizer, translation, and legal check assistants, allowing you to import PDF files and use them as input for the assistants.
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
- v0.9.39: Added the plugin system as a preview feature.
- v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well.
</details> </details>

View File

@ -53,6 +53,9 @@ public sealed partial class CollectI18NKeysCommand
foreach (var filePath in allFiles) foreach (var filePath in allFiles)
{ {
counter++; counter++;
if(!this.IsSupportedSourceFile(filePath))
continue;
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase)) if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
continue; continue;
@ -68,6 +71,9 @@ public sealed partial class CollectI18NKeysCommand
continue; continue;
var ns = this.DetermineNamespace(filePath); var ns = this.DetermineNamespace(filePath);
if(ns is null)
throw new InvalidOperationException($"Could not determine the namespace for I18N source file '{filePath}'.");
var fileInfo = new FileInfo(filePath); var fileInfo = new FileInfo(filePath);
var name = this.DetermineTypeName(filePath) var name = this.DetermineTypeName(filePath)
@ -204,6 +210,10 @@ public sealed partial class CollectI18NKeysCommand
return matches; return matches;
} }
private bool IsSupportedSourceFile(string filePath) =>
filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase);
private string? DetermineNamespace(string filePath) private string? DetermineNamespace(string filePath)
{ {
@ -302,10 +312,10 @@ public sealed partial class CollectI18NKeysCommand
return match.Groups[1].Value; return match.Groups[1].Value;
} }
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")] [GeneratedRegex("""(?m)^\s*@namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex BlazorNamespaceRegex(); private static partial Regex BlazorNamespaceRegex();
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")] [GeneratedRegex("""(?m)^\s*namespace\s+([a-zA-Z0-9_.]+)\s*[;{]""")]
private static partial Regex CSharpNamespaceRegex(); private static partial Regex CSharpNamespaceRegex();
[GeneratedRegex("""\bpartial\s+(?:class|struct|interface|record(?:\s+(?:class|struct))?)\s+([A-Za-z_][A-Za-z0-9_]*)""")] [GeneratedRegex("""\bpartial\s+(?:class|struct|interface|record(?:\s+(?:class|struct))?)\s+([A-Za-z_][A-Za-z0-9_]*)""")]

View File

@ -7,74 +7,95 @@ namespace Build.Commands;
public static class Pdfium public static class Pdfium
{ {
public static async Task InstallAsync(RID rid, string version) private static readonly HttpClient CLIENT = new()
{
Timeout = TimeSpan.FromMinutes(5)
};
public static async Task InstallAsync(RID rid, string version, bool offline)
{ {
Console.Write($"- Installing Pdfium {version} for {rid.ToUserFriendlyName()} ..."); Console.Write($"- Installing Pdfium {version} for {rid.ToUserFriendlyName()} ...");
var cwd = Environment.GetRustRuntimeDirectory(); var cwd = Environment.GetRustRuntimeDirectory();
var pdfiumTmpDownloadPath = Path.GetTempFileName();
var pdfiumTmpExtractPath = Directory.CreateTempSubdirectory();
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version); var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
var library = GetLibraryPath(rid);
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
// if (offline)
// Download the file:
//
Console.Write(" downloading ...");
using (var client = new HttpClient())
{ {
var response = await client.GetAsync(pdfiumUrl); if (File.Exists(pdfiumLibTargetPath))
if (!response.IsSuccessStatusCode)
{ {
Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}"); Console.WriteLine(" offline mode enabled and library already exists, skipping download");
return; return;
} }
await using var fileStream = File.Create(pdfiumTmpDownloadPath); Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist");
await response.Content.CopyToAsync(fileStream); return;
} }
//
// Extract the downloaded file:
//
Console.Write(" extracting ...");
await using(var tgzStream = File.Open(pdfiumTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
await TarFile.ExtractToDirectoryAsync(uncompressedStream, pdfiumTmpExtractPath.FullName, true);
}
//
// Copy the library to the target directory:
//
Console.Write(" deploying ...");
var library = GetLibraryPath(rid);
if (string.IsNullOrWhiteSpace(library.Path)) if (string.IsNullOrWhiteSpace(library.Path))
{ {
Console.WriteLine($" failed to find the library path for {rid.ToUserFriendlyName()}"); Console.WriteLine($" failed to find the library path for {rid.ToUserFriendlyName()}");
return; return;
} }
var pdfiumLibSourcePath = Path.Join(pdfiumTmpExtractPath.FullName, library.Path); var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries");
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename); var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp");
if (!File.Exists(pdfiumLibSourcePath)) var pdfiumLibArchivePath = library.Path.Replace('\\', '/');
//
// Download the file:
//
Console.Write(" downloading ...");
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{ {
Console.WriteLine($" failed to find the library file '{pdfiumLibSourcePath}'"); Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}");
return; return;
} }
Directory.CreateDirectory(Path.Join(cwd, "resources", "libraries"));
if (File.Exists(pdfiumLibTargetPath))
File.Delete(pdfiumLibTargetPath);
File.Copy(pdfiumLibSourcePath, pdfiumLibTargetPath);
// //
// Cleanup: // Extract the library from the downloaded file:
// //
Console.Write(" cleaning up ..."); Console.Write(" extracting ...");
File.Delete(pdfiumTmpDownloadPath); Directory.CreateDirectory(pdfiumLibTargetDirectory);
Directory.Delete(pdfiumTmpExtractPath.FullName, true);
var foundLibrary = false;
try
{
await using var downloadStream = await response.Content.ReadAsStreamAsync();
await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress);
await using var tarReader = new TarReader(uncompressedStream);
while (await tarReader.GetNextEntryAsync() is { } entry)
{
if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal))
continue;
if (entry.DataStream == null)
break;
await using var fileStream = File.Create(pdfiumLibTmpTargetPath);
await entry.DataStream.CopyToAsync(fileStream);
foundLibrary = true;
break;
}
if (!foundLibrary)
{
Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive");
return;
}
Console.Write(" deploying ...");
File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true);
}
finally
{
if (File.Exists(pdfiumLibTmpTargetPath))
File.Delete(pdfiumLibTmpTargetPath);
}
Console.WriteLine(" done."); Console.WriteLine(" done.");
} }

View File

@ -1,120 +0,0 @@
using System.Formats.Tar;
using System.IO.Compression;
using SharedTools;
namespace Build.Commands;
public static class Qdrant
{
public static async Task InstallAsync(RID rid, string version)
{
Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ...");
var cwd = Environment.GetRustRuntimeDirectory();
var qdrantTmpDownloadPath = Path.GetTempFileName();
var qdrantTmpExtractPath = Directory.CreateTempSubdirectory();
var qdrantUrl = GetQdrantDownloadUrl(rid, version);
//
// Download the file:
//
Console.Write(" downloading ...");
using (var client = new HttpClient())
{
var response = await client.GetAsync(qdrantUrl);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}");
return;
}
await using var fileStream = File.Create(qdrantTmpDownloadPath);
await response.Content.CopyToAsync(fileStream);
}
//
// Extract the downloaded file:
//
Console.Write(" extracting ...");
await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
if (rid == RID.WIN_X64)
{
using var archive = new ZipArchive(zStream, ZipArchiveMode.Read);
archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true);
}
else
{
await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress);
await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true);
}
}
//
// Copy the database to the target directory:
//
Console.Write(" deploying ...");
var database = GetDatabasePath(rid);
if (string.IsNullOrWhiteSpace(database.Path))
{
Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}");
return;
}
var qdrantDbSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path);
var qdrantDbTargetPath = Path.Join(cwd, "target", "databases", "qdrant",database.Filename);
if (!File.Exists(qdrantDbSourcePath))
{
Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'");
return;
}
Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant"));
if (File.Exists(qdrantDbTargetPath))
File.Delete(qdrantDbTargetPath);
File.Copy(qdrantDbSourcePath, qdrantDbTargetPath);
//
// Cleanup:
//
Console.Write(" cleaning up ...");
File.Delete(qdrantTmpDownloadPath);
Directory.Delete(qdrantTmpExtractPath.FullName, true);
Console.WriteLine(" done.");
}
private static Database GetDatabasePath(RID rid) => rid switch
{
RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"),
RID.WIN_ARM64 => new("qdrant.exe", "qdrant-aarch64-pc-windows-msvc.exe"),
_ => new(string.Empty, string.Empty),
};
private static string GetQdrantDownloadUrl(RID rid, string version)
{
var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-";
return rid switch
{
RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz",
RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz",
RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz",
RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz",
RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
_ => string.Empty,
};
}
}

View File

@ -15,7 +15,8 @@ public sealed partial class UpdateMetadataCommands
[Command("release", Description = "Prepare & build the next release")] [Command("release", Description = "Prepare & build the next release")]
public async Task Release( public async Task Release(
[Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE, [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE,
[Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null) [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null,
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
{ {
if(!Environment.IsWorkingDirectoryValid()) if(!Environment.IsWorkingDirectoryValid())
return; return;
@ -42,7 +43,7 @@ public sealed partial class UpdateMetadataCommands
// Build once to allow the Rust compiler to read the changed metadata // Build once to allow the Rust compiler to read the changed metadata
// and to update all .NET artifacts: // and to update all .NET artifacts:
await this.Build(); await this.Build(offline);
// Now, we update the web assets (which may were updated by the first build): // Now, we update the web assets (which may were updated by the first build):
new UpdateWebAssetsCommand().UpdateWebAssets(); new UpdateWebAssetsCommand().UpdateWebAssets();
@ -53,7 +54,7 @@ public sealed partial class UpdateMetadataCommands
// Build the final release, where Rust knows the updated metadata, the .NET // 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.: // artifacts are already in place, and .NET knows the updated web assets, etc.:
await this.Build(); await this.Build(offline);
} }
[Command("update-versions", Description = "The command will update the package versions in the metadata file")] [Command("update-versions", Description = "The command will update the package versions in the metadata file")]
@ -69,6 +70,7 @@ public sealed partial class UpdateMetadataCommands
await this.UpdateRustVersion(); await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion(); await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion(); await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
} }
[Command("prepare", Description = "Prepare the metadata for the next release")] [Command("prepare", Description = "Prepare the metadata for the next release")]
@ -126,6 +128,7 @@ public sealed partial class UpdateMetadataCommands
await this.UpdateRustVersion(); await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion(); await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion(); await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
await this.UpdateProjectCommitHash(); await this.UpdateProjectCommitHash();
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md"))); await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs"))); await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs")));
@ -134,7 +137,8 @@ public sealed partial class UpdateMetadataCommands
} }
[Command("build", Description = "Build MindWork AI Studio")] [Command("build", Description = "Build MindWork AI Studio")]
public async Task Build() public async Task Build(
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
{ {
if(!Environment.IsWorkingDirectoryValid()) if(!Environment.IsWorkingDirectoryValid())
return; return;
@ -147,12 +151,11 @@ public sealed partial class UpdateMetadataCommands
Console.WriteLine("=============================="); Console.WriteLine("==============================");
await this.UpdateArchitecture(rid); await this.UpdateArchitecture(rid);
await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
var pdfiumVersion = await this.ReadPdfiumVersion(); var pdfiumVersion = await this.ReadPdfiumVersion();
await Pdfium.InstallAsync(rid, pdfiumVersion); await Pdfium.InstallAsync(rid, pdfiumVersion, Environment.IsOfflineBuildRequested(offline));
var qdrantVersion = await this.ReadQdrantVersion();
await Qdrant.InstallAsync(rid, qdrantVersion);
Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ..."); Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ...");
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}"); await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
@ -367,16 +370,6 @@ public sealed partial class UpdateMetadataCommands
return shortVersion; return shortVersion;
} }
private async Task<string> ReadQdrantVersion()
{
const int QDRANT_VERSION_INDEX = 11;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim();
return currentQdrantVersion;
}
private async Task UpdateArchitecture(RID rid) private async Task UpdateArchitecture(RID rid)
{ {
const int ARCHITECTURE_INDEX = 9; const int ARCHITECTURE_INDEX = 9;
@ -529,7 +522,32 @@ public sealed partial class UpdateMetadataCommands
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM); await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
} }
private async Task UpdateVectorStoreVersion()
{
const int VECTOR_STORE_VERSION_INDEX = 11;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentVectorStoreVersion = lines[VECTOR_STORE_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Qdrant Edge", Environment.GetRustRuntimeDirectory(), QdrantEdgeVersionRegex(), "cargo", "tree --depth 1");
if (matches.Count == 0)
return;
var updatedVectorStoreVersion = matches[0].Groups["version"].Value;
if(currentVectorStoreVersion == updatedVectorStoreVersion)
{
Console.WriteLine("- The vector store version is already up to date.");
return;
}
Console.WriteLine($"- Updated vector store version from {currentVectorStoreVersion} to {updatedVectorStoreVersion}.");
lines[VECTOR_STORE_VERSION_INDEX] = updatedVectorStoreVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateMudBlazorVersion() private async Task UpdateMudBlazorVersion()
{ {
const int MUD_BLAZOR_VERSION_INDEX = 6; const int MUD_BLAZOR_VERSION_INDEX = 6;
@ -720,6 +738,9 @@ public sealed partial class UpdateMetadataCommands
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")] [GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
private static partial Regex MudBlazorVersionRegex(); private static partial Regex MudBlazorVersionRegex();
[GeneratedRegex("""qdrant-edge\s+v(?<version>[0-9.]+)""")]
private static partial Regex QdrantEdgeVersionRegex();
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")] [GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
private static partial Regex TauriVersionRegex(); private static partial Regex TauriVersionRegex();
@ -731,4 +752,4 @@ public sealed partial class UpdateMetadataCommands
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")] [GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
private static partial Regex AppVersionRegex(); private static partial Regex AppVersionRegex();
} }

View File

@ -7,6 +7,7 @@ namespace Build.Tools;
public static class Environment public static class Environment
{ {
public const string DOTNET_VERSION = "net9.0"; public const string DOTNET_VERSION = "net9.0";
public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE";
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false); 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); private static readonly Dictionary<RID, string> ALL_RIDS = Enum.GetValues<RID>().Select(rid => new KeyValuePair<RID, string>(rid, rid.AsMicrosoftRid())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
@ -47,6 +48,19 @@ public static class Environment
return Path.GetFullPath(directory); return Path.GetFullPath(directory);
} }
public static bool IsOfflineBuildRequested(bool offlineOption)
{
if (offlineOption)
return true;
var environmentValue = global::System.Environment.GetEnvironmentVariable(BUILD_OFFLINE_ENVIRONMENT_VARIABLE);
return environmentValue?.Trim().ToLowerInvariant() switch
{
"1" or "true" or "yes" or "on" => true,
_ => false,
};
}
public static string? GetOS() public static string? GetOS()
{ {
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

View File

@ -2,6 +2,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERIV/@EntryIndexedValue">ERIV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>

View File

@ -153,7 +153,7 @@
</MudButton> </MudButton>
} }
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{ {
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.ProviderSettings.UsedLLMProvider"/> <ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.ProviderSettings.UsedLLMProvider"/>
} }

View File

@ -174,7 +174,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>)); private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>));
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty; private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
private IReadOnlyList<Tools.Components> VisibleSendToAssistants => Enum.GetValues<AIStudio.Tools.Components>() private IReadOnlyList<Tools.Components> VisibleSendToAssistants => Enum.GetValues<AIStudio.Tools.Components>()
.Where(this.CanSendToAssistant) .Where(this.CanSendToAssistant)
@ -328,22 +328,40 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.isProcessing = true; this.isProcessing = true;
this.StateHasChanged(); this.StateHasChanged();
// Use the selected provider to get the AI response. try
// By awaiting this line, we wait for the entire
// content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
this.isProcessing = false;
this.StateHasChanged();
if(manageCancellationLocally)
{ {
this.CancellationTokenSource.Dispose(); // Use the selected provider to get the AI response.
this.CancellationTokenSource = null; // By awaiting this line, we wait for the entire
// content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
// Return the AI response:
return aiText.Text;
} }
catch (ProviderRequestException e)
{
this.Logger.LogError(e, "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", this.Title, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
if (this.resultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text))
{
this.ChatThread?.Blocks.Remove(this.resultingContentBlock);
this.resultingContentBlock = null;
}
return string.Empty;
}
finally
{
this.isProcessing = false;
this.StateHasChanged();
// Return the AI response: if(manageCancellationLocally)
return aiText.Text; {
this.CancellationTokenSource?.Dispose();
this.CancellationTokenSource = null;
}
}
} }
private async Task CancelStreaming() private async Task CancelStreaming()

View File

@ -10,6 +10,8 @@ using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using SharedTools;
using DialogOptions = AIStudio.Dialogs.DialogOptions; using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants.DocumentAnalysis; namespace AIStudio.Assistants.DocumentAnalysis;
@ -437,10 +439,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
private ConfidenceLevel GetPolicyMinimumConfidenceLevel() private ConfidenceLevel GetPolicyMinimumConfidenceLevel()
{ {
var minimumLevel = ConfidenceLevel.NONE; var minimumLevel = ConfidenceLevel.NONE;
var llmSettings = this.SettingsManager.ConfigurationData.LLMProviders; var confidenceSettings = this.SettingsManager.ConfigurationData.Confidence;
var enforceGlobalMinimumConfidence = llmSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN }; var enforceGlobalMinimumConfidence = confidenceSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
if (enforceGlobalMinimumConfidence) if (enforceGlobalMinimumConfidence)
minimumLevel = llmSettings.GlobalMinimumConfidence; minimumLevel = confidenceSettings.GlobalMinimumConfidence;
if (this.selectedPolicy is not null && this.selectedPolicy.MinimumProviderConfidence > minimumLevel) if (this.selectedPolicy is not null && this.selectedPolicy.MinimumProviderConfidence > minimumLevel)
minimumLevel = this.selectedPolicy.MinimumProviderConfidence; minimumLevel = this.selectedPolicy.MinimumProviderConfidence;
@ -747,16 +749,12 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return $$""" return $$"""
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = { CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
["Id"] = "{{id}}", ["Id"] = "{{id}}",
["PolicyName"] = "{{this.selectedPolicy.PolicyName.Trim()}}", ["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyName.Trim())}},
["PolicyDescription"] = "{{this.selectedPolicy.PolicyDescription.Trim()}}", ["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyDescription.Trim())}},
["AnalysisRules"] = [===[ ["AnalysisRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.AnalysisRules.Trim(), forceLongString: true)}},
{{this.selectedPolicy.AnalysisRules.Trim()}}
]===],
["OutputRules"] = [===[ ["OutputRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.OutputRules.Trim(), forceLongString: true)}},
{{this.selectedPolicy.OutputRules.Trim()}}
]===],
-- Optional: minimum provider confidence required for this policy. -- Optional: minimum provider confidence required for this policy.
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH -- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
@T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.") @T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")
</MudJustifiedText> </MudJustifiedText>
<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/> <MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText> <MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">

View File

@ -94,6 +94,8 @@ public sealed record ChatThread
/// <returns>The prepared system prompt.</returns> /// <returns>The prepared system prompt.</returns>
public string PrepareSystemPrompt(SettingsManager settingsManager) public string PrepareSystemPrompt(SettingsManager settingsManager)
{ {
this.allowProfile = true;
// //
// Use the information from the chat template, if provided. Otherwise, use the default system prompt // Use the information from the chat template, if provided. Otherwise, use the default system prompt
// //
@ -111,8 +113,8 @@ public sealed record ChatThread
systemPromptTextWithChatTemplate = this.SystemPrompt; systemPromptTextWithChatTemplate = this.SystemPrompt;
else else
{ {
var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.SelectedChatTemplate); var chatTemplate = settingsManager.GetChatTemplateById(this.SelectedChatTemplate);
if(chatTemplate == null) if(chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE)
systemPromptTextWithChatTemplate = this.SystemPrompt; systemPromptTextWithChatTemplate = this.SystemPrompt;
else else
{ {
@ -168,8 +170,8 @@ public sealed record ChatThread
systemPromptText = systemPromptWithAugmentedData; systemPromptText = systemPromptWithAugmentedData;
else else
{ {
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.SelectedProfile); var profile = settingsManager.GetProfileById(this.SelectedProfile);
if(profile is null) if(profile == Profile.NO_PROFILE)
systemPromptText = systemPromptWithAugmentedData; systemPromptText = systemPromptWithAugmentedData;
else else
{ {

View File

@ -1,4 +1,5 @@
using AIStudio.Provider.SelfHosted; using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@ -33,12 +34,13 @@ public static class ChatThreadExtensions
return true; return true;
// //
// Is the provider self-hosted? // Is the provider trusted for data-source security checks?
// //
var isSelfHostedProvider = provider switch var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
var isTrustedProvider = provider switch
{ {
ProviderSelfHosted => true, IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
AIStudio.Settings.Provider p => p.IsSelfHosted, AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
_ => false, _ => false,
}; };
@ -46,12 +48,12 @@ public static class ChatThreadExtensions
// //
// Check the chat data security against the selected provider: // Check the chat data security against the selected provider:
// //
return isSelfHostedProvider switch return isTrustedProvider switch
{ {
// The provider is self-hosted -- we can use any data source: // The provider is trusted -- we can use any data source:
true => true, true => true,
// The provider is not self-hosted -- it depends on the data security of the chat thread: // The provider is not trusted -- it depends on the data security of the chat thread:
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED, false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
}; };
} }

View File

@ -93,59 +93,70 @@ public sealed class ContentText : IContent
// 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 () => try
{ {
// We show the waiting animation until we get the first response: await Task.Run(async () =>
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
{ {
// When the user cancels the request, we stop the loop: try
if (token.IsCancellationRequested)
break;
// Stop the waiting animation:
this.InitialRemoteWait = false;
this.IsStreaming = true;
// Add the response to the text:
this.Text += contentStreamChunk;
// Merge the sources:
this.Sources.MergeSources(contentStreamChunk.Sources);
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
switch (settings.ConfigurationData.App.IsSavingEnergy)
{ {
// Energy saving mode is off. We notify the UI // We show the waiting animation until we get the first response:
// as fast as possible -- no matter the odds: this.InitialRemoteWait = true;
case false:
await this.StreamingEvent(); // Iterate over the responses from the AI:
break; await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
{
// Energy saving mode is on. We notify the UI // When the user cancels the request, we stop the loop:
// only when the time between two events is if (token.IsCancellationRequested)
// greater than the minimum time: break;
case true when now - last > MIN_TIME:
last = now; // Stop the waiting animation:
await this.StreamingEvent(); this.InitialRemoteWait = false;
break; this.IsStreaming = true;
// Add the response to the text:
this.Text += contentStreamChunk;
// Merge the sources:
this.Sources.MergeSources(contentStreamChunk.Sources);
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
switch (settings.ConfigurationData.App.IsSavingEnergy)
{
// Energy saving mode is off. We notify the UI
// as fast as possible -- no matter the odds:
case false:
await this.StreamingEvent();
break;
// Energy saving mode is on. We notify the UI
// only when the time between two events is
// greater than the minimum time:
case true when now - last > MIN_TIME:
last = now;
await this.StreamingEvent();
break;
}
}
} }
} finally
{
// Stop the waiting animation (in case the loop // Stop the waiting animation (in case the loop
// was stopped, or no content was received): // was stopped, or no content was received):
this.InitialRemoteWait = false; this.InitialRemoteWait = false;
this.IsStreaming = false; this.IsStreaming = false;
}, token); }
}, token);
this.Text = this.Text.RemoveThinkTags().Trim(); }
finally
{
this.Text = this.Text.RemoveThinkTags().Trim();
// Inform the UI that the streaming is done: // Inform the UI that the streaming is done:
await this.StreamingDone(); await this.StreamingDone();
}
return chatThread; return chatThread;
} }

View File

@ -89,8 +89,10 @@ public static class IImageSourceExtensions
case ContentImageSource.URL: case ContentImageSource.URL:
{ {
using var httpClient = new HttpClient(); using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token); using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
var timeoutToken = timeoutTokenSource.Token;
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);
if(response.IsSuccessStatusCode) if(response.IsSuccessStatusCode)
{ {
// Read the length of the content: // Read the length of the content:
@ -101,7 +103,7 @@ public static class IImageSourceExtensions
return (success: false, string.Empty); return (success: false, string.Empty);
} }
var bytes = await response.Content.ReadAsByteArrayAsync(token); var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken);
return (success: true, Convert.ToBase64String(bytes)); return (success: true, Convert.ToBase64String(bytes));
} }

View File

@ -52,29 +52,42 @@
} }
else else
{ {
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap"> @if (!this.Disabled)
<MudText Typo="Typo.body1" Inline="true"> {
@T("Drag and drop files into the marked area or click here to attach documents: ") <MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
</MudText> <MudText Typo="Typo.body1" Inline="true">
<MudButton @T("Drag and drop files into the marked area or click here to attach documents: ")
Variant="Variant.Filled" </MudText>
StartIcon="@Icons.Material.Filled.Add" <MudButton
Color="Color.Primary" Variant="Variant.Filled"
OnClick="@(() => this.AddFilesManually())" StartIcon="@Icons.Material.Filled.Add"
Style="vertical-align: top; margin-top: -2px;" Color="Color.Primary"
Size="Size.Small"> OnClick="@(() => this.AddFilesManually())"
@T("Add file") Style="vertical-align: top; margin-top: -2px;"
</MudButton> Size="Size.Small">
</MudStack> @T("Add file")
</MudButton>
</MudStack>
}
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave"> <div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
<MudPaper Height="20em" Outlined="true" Class="@this.dragClass" Style="overflow-y: auto;"> <MudPaper Height="20em" Outlined="true" Class="@this.dragClass" Style="overflow-y: auto;">
@foreach (var fileAttachment in this.DocumentPaths) @foreach (var fileAttachment in this.DocumentPaths)
{ {
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/> @if (this.Disabled)
{
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))"/>
}
else
{
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/>
}
} }
</MudPaper> </MudPaper>
</div> </div>
<MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete"> @if (!this.Disabled)
@T("Clear file list") {
</MudButton> <MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete">
@T("Clear file list")
</MudButton>
}
} }

View File

@ -14,16 +14,16 @@ using DialogOptions = Dialogs.DialogOptions;
public partial class AttachDocuments : MSGComponentBase public partial class AttachDocuments : MSGComponentBase
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AttachDocuments).Namespace, nameof(AttachDocuments)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AttachDocuments).Namespace, nameof(AttachDocuments));
[Parameter] [Parameter]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary> /// <summary>
/// On which layer to register the drop area. Higher layers have priority over lower layers. /// On which layer to register the drop area. Higher layers have priority over lower layers.
/// </summary> /// </summary>
[Parameter] [Parameter]
public int Layer { get; set; } public int Layer { get; set; }
/// <summary> /// <summary>
/// When true, pause catching dropped files. Default is false. /// When true, pause catching dropped files. Default is false.
/// </summary> /// </summary>
@ -38,16 +38,19 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter] [Parameter]
public Func<HashSet<FileAttachment>, Task> OnChange { get; set; } = _ => Task.CompletedTask; public Func<HashSet<FileAttachment>, Task> OnChange { get; set; } = _ => Task.CompletedTask;
/// <summary> /// <summary>
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone. /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool CatchAllDocuments { get; set; } public bool CatchAllDocuments { get; set; }
[Parameter] [Parameter]
public bool UseSmallForm { get; set; } public bool UseSmallForm { get; set; }
[Parameter]
public bool Disabled { get; set; }
/// <summary> /// <summary>
/// When true, validate media file types before attaching. Default is true. That means that /// When true, validate media file types before attaching. Default is true. That means that
/// the user cannot attach unsupported media file types when the provider or model does not /// the user cannot attach unsupported media file types when the provider or model does not
@ -56,16 +59,16 @@ public partial class AttachDocuments : MSGComponentBase
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool ValidateMediaFileTypes { get; set; } = true; public bool ValidateMediaFileTypes { get; set; } = true;
[Parameter] [Parameter]
public AIStudio.Settings.Provider? Provider { get; set; } public AIStudio.Settings.Provider? Provider { get; set; }
[Inject] [Inject]
private ILogger<AttachDocuments> Logger { get; set; } = null!; private ILogger<AttachDocuments> Logger { get; set; } = null!;
[Inject] [Inject]
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@ -74,17 +77,17 @@ public partial class AttachDocuments : MSGComponentBase
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them."); private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them.");
private uint numDropAreasAboveThis; private uint numDropAreasAboveThis;
private bool isComponentHovered; private bool isComponentHovered;
private bool isDraggingOver; private bool isDraggingOver;
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]); this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
// Register this drop area: // Register this drop area:
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer); await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
await base.OnInitializedAsync(); await base.OnInitializedAsync();
@ -92,6 +95,9 @@ public partial class AttachDocuments : MSGComponentBase
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
if (this.Disabled && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
return;
switch (triggeredEvent) switch (triggeredEvent)
{ {
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this: case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
@ -111,7 +117,7 @@ public partial class AttachDocuments : MSGComponentBase
{ {
if(this.numDropAreasAboveThis > 0) if(this.numDropAreasAboveThis > 0)
this.numDropAreasAboveThis--; this.numDropAreasAboveThis--;
if(this.numDropAreasAboveThis is 0) if(this.numDropAreasAboveThis is 0)
this.PauseCatchingDrops = false; this.PauseCatchingDrops = false;
} }
@ -122,40 +128,40 @@ public partial class AttachDocuments : MSGComponentBase
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }: case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
if(this.PauseCatchingDrops) if(this.PauseCatchingDrops)
return; return;
if(!this.isComponentHovered && !this.CatchAllDocuments) if(!this.isComponentHovered && !this.CatchAllDocuments)
{ {
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name); this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name);
return; return;
} }
this.isDraggingOver = true; this.isDraggingOver = true;
this.SetDragClass(); this.SetDragClass();
this.StateHasChanged(); this.StateHasChanged();
break; break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }: case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
if(this.PauseCatchingDrops) if(this.PauseCatchingDrops)
return; return;
this.isDraggingOver = false; this.isDraggingOver = false;
this.StateHasChanged(); this.StateHasChanged();
break; break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }: case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
if(this.PauseCatchingDrops) if(this.PauseCatchingDrops)
return; return;
this.isDraggingOver = false; this.isDraggingOver = false;
this.isComponentHovered = false; this.isComponentHovered = false;
this.ClearDragClass(); this.ClearDragClass();
this.StateHasChanged(); this.StateHasChanged();
break; break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }: case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
if(this.PauseCatchingDrops) if(this.PauseCatchingDrops)
return; return;
if(!this.isComponentHovered && !this.CatchAllDocuments) if(!this.isComponentHovered && !this.CatchAllDocuments)
{ {
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name); this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name);
@ -197,11 +203,14 @@ public partial class AttachDocuments : MSGComponentBase
#endregion #endregion
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full"; private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full";
private string dragClass = DEFAULT_DRAG_CLASS; private string dragClass = DEFAULT_DRAG_CLASS;
private async Task AddFilesManually() private async Task AddFilesManually()
{ {
if (this.Disabled)
return;
// Ensure that Pandoc is installed and ready: // Ensure that Pandoc is installed and ready:
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync( var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false, showSuccessMessage: false,
@ -228,43 +237,49 @@ public partial class AttachDocuments : MSGComponentBase
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath)); this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
} }
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths); await this.OnChange(this.DocumentPaths);
} }
private async Task OpenAttachmentsDialog() private async Task OpenAttachmentsDialog()
{ {
if (this.Disabled)
return;
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths); this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths);
} }
private async Task ClearAllFiles() private async Task ClearAllFiles()
{ {
if (this.Disabled)
return;
this.DocumentPaths.Clear(); this.DocumentPaths.Clear();
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths); await this.OnChange(this.DocumentPaths);
} }
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4"; private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4";
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS; private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
private void OnMouseEnter(EventArgs _) private void OnMouseEnter(EventArgs _)
{ {
if(this.PauseCatchingDrops) if(this.Disabled || this.PauseCatchingDrops)
return; return;
this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name); this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name);
this.isComponentHovered = true; this.isComponentHovered = true;
this.SetDragClass(); this.SetDragClass();
this.StateHasChanged(); this.StateHasChanged();
} }
private void OnMouseLeave(EventArgs _) private void OnMouseLeave(EventArgs _)
{ {
if(this.PauseCatchingDrops) if(this.Disabled || this.PauseCatchingDrops)
return; return;
this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name); this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name);
this.isComponentHovered = false; this.isComponentHovered = false;
this.ClearDragClass(); this.ClearDragClass();
@ -273,6 +288,9 @@ public partial class AttachDocuments : MSGComponentBase
private async Task RemoveDocument(FileAttachment fileAttachment) private async Task RemoveDocument(FileAttachment fileAttachment)
{ {
if (this.Disabled)
return;
this.DocumentPaths.Remove(fileAttachment); this.DocumentPaths.Remove(fileAttachment);
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);

View File

@ -13,6 +13,10 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (242, "v26.6.2, build 242 (2026-06-21 14:07 UTC)", "v26.6.2.md"),
new (241, "v26.6.1, build 241 (2026-06-11 13:49 UTC)", "v26.6.1.md"),
new (240, "v26.5.5, build 240 (2026-05-25 18:52 UTC)", "v26.5.5.md"),
new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"),
new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"), new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"),
new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"), new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"),
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"), new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),

View File

@ -37,7 +37,7 @@
<MudTextField <MudTextField
T="string" T="string"
@ref="@this.inputField" @ref="@this.inputField"
@bind-Text="@this.userInput" @bind-Text="@this.UserInput"
Variant="Variant.Outlined" Variant="Variant.Outlined"
AutoGrow="@true" AutoGrow="@true"
Lines="3" Lines="3"
@ -68,7 +68,7 @@
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{ {
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.isStreaming)"/> <MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
</MudTooltip> </MudTooltip>
} }
@ -89,35 +89,35 @@
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved)"/> <MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
</MudTooltip> </MudTooltip>
} }
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{ {
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="@(() => this.MoveChatToWorkspace())"/> <MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
</MudTooltip> </MudTooltip>
} }
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/> <AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/> <MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
<MudTooltip Text="@T("Bold")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Bold")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BOLD)" Disabled="@this.IsInputForbidden()"/> <MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BOLD))" Disabled="@this.IsInputForbidden()"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Italic")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Italic")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC)" Disabled="@this.IsInputForbidden()"/> <MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC))" Disabled="@this.IsInputForbidden()"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Heading")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Heading")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_HEADING)" Disabled="@this.IsInputForbidden()"/> <MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_HEADING))" Disabled="@this.IsInputForbidden()"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Bulleted List")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Bulleted List")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST)" Disabled="@this.IsInputForbidden()"/> <MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST))" Disabled="@this.IsInputForbidden()"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Code")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Code")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_CODE)" Disabled="@this.IsInputForbidden()"/> <MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_CODE))" Disabled="@this.IsInputForbidden()"/>
</MudTooltip> </MudTooltip>
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/> <MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
@ -129,15 +129,15 @@
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/> <DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
} }
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{ {
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/> <ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
} }
@if (this.isStreaming && this.cancellationTokenSource is not null) @if (this.IsCurrentChatStreaming)
{ {
<MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@(() => this.CancelStreaming())"/> <MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@this.CancelStreaming"/>
</MudTooltip> </MudTooltip>
} }

View File

@ -3,6 +3,7 @@ using AIStudio.Dialogs;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AIJobs;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
@ -37,6 +38,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Parameter] [Parameter]
public Workspaces? Workspaces { get; set; } public Workspaces? Workspaces { get; set; }
[Parameter]
public ChatComposerState ComposerState { get; set; } = new();
[Inject] [Inject]
private ILogger<ChatComponent> Logger { get; set; } = null!; private ILogger<ChatComponent> Logger { get; set; } = null!;
@ -47,6 +51,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject] [Inject]
private IJSRuntime JsRuntime { get; init; } = null!; private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private AIJobService AIJobService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
@ -58,29 +65,46 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private bool mustScrollToBottomAfterRender; private bool mustScrollToBottomAfterRender;
private InnerScrolling scrollingArea = null!; private InnerScrolling scrollingArea = null!;
private byte scrollRenderCountdown; private byte scrollRenderCountdown;
private bool isStreaming;
private string userInput = string.Empty;
private bool mustStoreChat; private bool mustStoreChat;
private bool mustLoadChat; private bool mustLoadChat;
private LoadChat loadChat; private LoadChat loadChat;
private bool autoSaveEnabled; private bool autoSaveEnabled;
private bool previousInputForbidden = true;
private Guid lastSeenChatId = Guid.Empty;
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
private string currentWorkspaceName = string.Empty; private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty; private Guid currentWorkspaceId = Guid.Empty;
private Guid currentChatThreadId = Guid.Empty; private Guid currentChatThreadId = Guid.Empty;
private Guid loadedParameterChatId = Guid.Empty;
private Guid loadedParameterWorkspaceId = Guid.Empty;
private Guid foregroundChatId = Guid.Empty;
private int workspaceHeaderSyncVersion; private int workspaceHeaderSyncVersion;
private CancellationTokenSource? cancellationTokenSource;
private HashSet<FileAttachment> chatDocumentPaths = [];
// Unfortunately, we need the input field reference to blur the focus away. Without // Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field. // this, we cannot clear the input field.
private MudTextField<string> inputField = null!; private MudTextField<string> inputField = null!;
/// <summary>
/// Represents the user's input in the chat interface.
/// </summary>
/// <remarks>
/// This property serves as a bridge between the chat component and the
/// underlying composer state, allowing user input to be dynamically updated
/// and managed. The setter also triggers state changes within the composer
/// to track whether the user has drafted any input.
/// </remarks>
private string UserInput
{
get => this.ComposerState.UserInput;
set => this.ComposerState.SetUserInput(value);
}
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Apply the filters for the message bus: // Apply the filters for the message bus:
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]); this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED, Event.CONFIGURATION_CHANGED ]);
// Configure the spellchecking for the user input: // Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
@ -91,15 +115,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Get the preselected chat template: // Get the preselected chat template:
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
this.userInput = this.currentChatTemplate.PredefinedUserPrompt; if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault(); var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(deferredInput)) if (!string.IsNullOrWhiteSpace(deferredInput))
this.userInput = deferredInput; this.ComposerState.SetUserInput(deferredInput);
// Apply template's file attachments, if any:
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment.Normalize());
// //
// Check for deferred messages of the kind 'SEND_TO_CHAT', // Check for deferred messages of the kind 'SEND_TO_CHAT',
@ -117,6 +138,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.ChatThread.IncludeDateTime = true; this.ChatThread.IncludeDateTime = true;
this.Logger.LogInformation($"The chat '{this.ChatThread.ChatId}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now."); this.Logger.LogInformation($"The chat '{this.ChatThread.ChatId}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
// We know already that the chat thread is not null, // We know already that the chat thread is not null,
@ -217,6 +239,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Select the correct provider: // Select the correct provider:
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
await this.SyncForegroundChatAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -242,6 +265,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.ChatThread is not null) if(this.ChatThread is not null)
{ {
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); 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.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
@ -266,18 +290,54 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.StateHasChanged(); this.StateHasChanged();
} }
} }
var inputForbidden = this.IsInputForbidden();
if (!inputForbidden && this.previousInputForbidden)
await this.inputField.FocusAsync();
this.previousInputForbidden = inputForbidden;
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
await this.SyncWorkspaceHeaderWithChatThreadAsync(); var incomingChatId = this.ChatThread?.ChatId ?? Guid.Empty;
if (incomingChatId != this.lastSeenChatId || this.Provider != this.lastSeenProvider)
{
this.lastSeenChatId = incomingChatId;
this.lastSeenProvider = this.Provider;
this.previousInputForbidden = true;
}
await this.ApplyLoadedChatParameterAsync();
await this.SyncForegroundChatAsync();
await base.OnParametersSetAsync(); await base.OnParametersSetAsync();
} }
#endregion #endregion
private async Task ApplyLoadedChatParameterAsync()
{
var chatId = this.ChatThread?.ChatId ?? Guid.Empty;
var workspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
if (this.loadedParameterChatId == chatId && this.loadedParameterWorkspaceId == workspaceId)
{
await this.SyncWorkspaceHeaderWithChatThreadAsync();
return;
}
this.loadedParameterChatId = chatId;
this.loadedParameterWorkspaceId = workspaceId;
await this.LoadedChatChanged(notifyParent: false);
}
private void MarkCurrentChatAsLoadedParameter()
{
this.loadedParameterChatId = this.ChatThread?.ChatId ?? Guid.Empty;
this.loadedParameterWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
}
private async Task SyncWorkspaceHeaderWithChatThreadAsync() private async Task SyncWorkspaceHeaderWithChatThreadAsync()
{ {
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion); var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
@ -333,7 +393,46 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.WorkspaceName(this.currentWorkspaceName); this.WorkspaceName(this.currentWorkspaceName);
} }
private async Task RefreshRenamedWorkspaceHeaderAsync(Guid workspaceId)
{
var currentChatThread = this.ChatThread;
if (currentChatThread is null || currentChatThread.WorkspaceId != workspaceId)
return;
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
var chatThreadId = currentChatThread.ChatId;
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
if (syncVersion != this.workspaceHeaderSyncVersion)
return;
if (this.ChatThread is null
|| this.ChatThread.ChatId != chatThreadId
|| this.ChatThread.WorkspaceId != workspaceId)
return;
this.currentChatThreadId = chatThreadId;
this.currentWorkspaceId = workspaceId;
this.PublishWorkspaceNameIfChanged(loadedWorkspaceName);
}
private async Task SyncForegroundChatAsync()
{
var nextForegroundChatId = this.ChatThread?.ChatId ?? Guid.Empty;
if (this.foregroundChatId == nextForegroundChatId)
return;
if (this.foregroundChatId != Guid.Empty)
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
this.foregroundChatId = nextForegroundChatId;
if (this.foregroundChatId != Guid.Empty)
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, true);
}
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE; private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
private bool IsCurrentChatStreaming => this.ChatThread is not null && this.AIJobService.IsChatGenerationActive(this.ChatThread.ChatId);
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first"); private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
@ -352,9 +451,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName); private string 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 UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty; private string UserInputClass => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? "confidence-border" : string.Empty;
private void ApplyStandardDataSourceOptions() private void ApplyStandardDataSourceOptions()
{ {
@ -387,7 +486,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task ProfileWasChanged(Profile profile) private async Task ProfileWasChanged(Profile profile)
{ {
this.currentProfile = profile; this.currentProfile = this.SettingsManager.GetProfileById(profile.Id);
if(this.ChatThread is null) if(this.ChatThread is null)
return; return;
@ -401,14 +500,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
{ {
this.currentChatTemplate = chatTemplate; this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt)) if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
this.userInput = this.currentChatTemplate.PredefinedUserPrompt; this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
// Apply template's file attachments (replaces existing): // Apply template's file attachments (replaces existing):
this.chatDocumentPaths.Clear(); this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment.Normalize());
if(this.ChatThread is null) if(this.ChatThread is null)
return; return;
@ -416,6 +513,42 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.StartNewChat(true); await this.StartNewChat(true);
} }
private void RefreshCurrentProfileAndChatTemplate()
{
this.currentProfile = this.SettingsManager.GetProfileById(this.currentProfile.Id);
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id);
}
private async Task RefreshChatSelectionsAfterConfigurationChange()
{
var previousProvider = this.Provider;
var previousChatTemplate = this.currentChatTemplate;
var chatProviderId = this.ChatThread?.SelectedProvider;
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProviderId);
if (this.Provider != previousProvider)
await this.ProviderChanged.InvokeAsync(this.Provider);
if (this.ChatThread is null)
{
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
}
else
{
this.currentProfile = string.IsNullOrWhiteSpace(this.ChatThread.SelectedProfile)
? this.SettingsManager.GetProfileById(this.currentProfile.Id)
: this.SettingsManager.GetProfileById(this.ChatThread.SelectedProfile);
this.currentChatTemplate = string.IsNullOrWhiteSpace(this.ChatThread.SelectedChatTemplate)
? this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id)
: this.SettingsManager.GetChatTemplateById(this.ChatThread.SelectedChatTemplate);
}
if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate)
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
}
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources() private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
{ {
if (this.ChatThread is null) if (this.ChatThread is null)
@ -453,7 +586,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (!this.IsProviderSelected) if (!this.IsProviderSelected)
return true; return true;
if(this.isStreaming) if(this.IsCurrentChatStreaming)
return true; return true;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider)) if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
@ -468,6 +601,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.dataSourceSelectionComponent.Hide(); this.dataSourceSelectionComponent.Hide();
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
this.ComposerState.MarkUserDraft();
var key = keyEvent.Code.ToLowerInvariant(); var key = keyEvent.Code.ToLowerInvariant();
// Was the enter key (either enter or numpad enter) pressed? // Was the enter key (either enter or numpad enter) pressed?
@ -499,7 +633,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.dataSourceSelectionComponent?.IsVisible ?? false) if(this.dataSourceSelectionComponent?.IsVisible ?? false)
this.dataSourceSelectionComponent.Hide(); this.dataSourceSelectionComponent.Hide();
this.userInput = await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType); this.ComposerState.SetUserInput(await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType));
this.hasUnsavedChanges = true;
}
private void ComposerAttachmentsChanged(HashSet<FileAttachment> attachments)
{
if (!ReferenceEquals(this.ComposerState.FileAttachments, attachments))
this.ComposerState.ReplaceFileAttachments(attachments);
this.ComposerState.MarkUserDraft();
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
} }
@ -510,7 +653,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider)) if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return; return;
this.RefreshCurrentProfileAndChatTemplate();
// Blur the focus away from the input field to be able to clear it: // Blur the focus away from the input field to be able to clear it:
await this.inputField.BlurAsync(); await this.inputField.BlurAsync();
@ -527,17 +672,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
WorkspaceId = this.currentWorkspaceId, WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(), ChatId = Guid.NewGuid(),
DataSourceOptions = this.earlyDataSourceOptions, DataSourceOptions = this.earlyDataSourceOptions,
Name = this.ExtractThreadName(this.userInput), Name = this.ExtractThreadName(this.ComposerState.UserInput),
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
}; };
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
else else
{ {
// Set the thread name if it is empty: // Set the thread name if it is empty:
if (string.IsNullOrWhiteSpace(this.ChatThread.Name)) if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
this.ChatThread.Name = this.ExtractThreadName(this.userInput); this.ChatThread.Name = this.ExtractThreadName(this.ComposerState.UserInput);
// Update provider, profile and chat template: // Update provider, profile and chat template:
this.ChatThread.SelectedProvider = this.Provider.Id; this.ChatThread.SelectedProvider = this.Provider.Id;
@ -554,14 +700,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
IContent? lastUserPrompt; IContent? lastUserPrompt;
if (!reuseLastUserPrompt) if (!reuseLastUserPrompt)
{ {
var normalizedAttachments = this.chatDocumentPaths var normalizedAttachments = this.ComposerState.FileAttachments
.Select(attachment => attachment.Normalize()) .Select(attachment => attachment.Normalize())
.Where(attachment => attachment.IsValid) .Where(attachment => attachment.IsValid)
.ToList(); .ToList();
lastUserPrompt = new ContentText lastUserPrompt = new ContentText
{ {
Text = this.userInput, Text = this.ComposerState.UserInput,
FileAttachments = normalizedAttachments, FileAttachments = normalizedAttachments,
}; };
@ -608,13 +754,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Clear the input field: // Clear the input field:
await this.inputField.FocusAsync(); await this.inputField.FocusAsync();
this.userInput = string.Empty; this.ComposerState.Clear();
this.chatDocumentPaths.Clear();
await this.inputField.BlurAsync(); await this.inputField.BlurAsync();
// Enable the stream state for the chat component: // Enable the stream state for the chat component:
this.isStreaming = true;
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
@ -624,38 +768,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'."); this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
using (this.cancellationTokenSource = new())
{ {
this.StateHasChanged(); ChatThread = this.ChatThread!,
AIText = aiText,
// Use the selected provider to get the AI response. LastUserPrompt = lastUserPrompt,
// By awaiting this line, we wait for the entire ProviderSettings = this.Provider,
// content to be streamed. IsForeground = true,
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token); });
}
this.cancellationTokenSource = null;
// Save the chat: await this.SyncForegroundChatAsync();
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(); this.StateHasChanged();
} }
private async Task CancelStreaming() private async Task CancelStreaming()
{ {
if (this.cancellationTokenSource is not null) if (this.ChatThread is not null)
if(!this.cancellationTokenSource.IsCancellationRequested) await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
await this.cancellationTokenSource.CancelAsync();
} }
private async Task SaveThread() private async Task SaveThread()
@ -685,7 +814,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Want the user to manage the chat storage manually? In that case, we have to ask the user // Want the user to manage the chat storage manually? In that case, we have to ask the user
// about possible data loss: // about possible data loss:
// //
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
{ {
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
{ {
@ -718,9 +847,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// //
// Reset our state: // Reset our state:
// //
this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.ComposerState.Clear();
this.RefreshCurrentProfileAndChatTemplate();
// //
// Reset the LLM provider considering the user's settings: // Reset the LLM provider considering the user's settings:
@ -777,17 +906,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
}; };
} }
this.userInput = this.currentChatTemplate.PredefinedUserPrompt; this.ComposerState.ApplyTemplate(this.currentChatTemplate);
// Apply template's file attachments:
this.chatDocumentPaths.Clear();
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment.Normalize());
// Now, we have to reset the data source options as well: // Now, we have to reset the data source options as well:
this.ApplyStandardDataSourceOptions(); this.ApplyStandardDataSourceOptions();
// Notify the parent component about the change: // Notify the parent component about the change:
await this.SyncForegroundChatAsync();
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
@ -796,7 +922,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.ChatThread is null) if(this.ChatThread is null)
return; return;
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
{ {
var confirmationDialogParameters = new DialogParameters<ConfirmDialog> var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
{ {
@ -816,7 +942,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{ x => x.ConfirmText, T("Move chat") }, { x => x.ConfirmText, T("Move chat") },
}; };
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
@ -829,25 +955,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
this.ChatThread!.WorkspaceId = workspaceId; this.ChatThread!.WorkspaceId = workspaceId;
this.MarkCurrentChatAsLoadedParameter();
await this.SaveThread(); await this.SaveThread();
await this.SyncWorkspaceHeaderWithChatThreadAsync(); await this.SyncWorkspaceHeaderWithChatThreadAsync();
} }
private async Task LoadedChatChanged() private async Task LoadedChatChanged(bool notifyParent = true)
{ {
this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.ComposerState.Clear();
if (this.ChatThread is not null) if (this.ChatThread is not null)
{ {
this.ChatThread = this.AIJobService.TryGetLiveChatThread(this.ChatThread.ChatId) ?? this.ChatThread;
this.loadedParameterChatId = this.ChatThread.ChatId;
this.loadedParameterWorkspaceId = this.ChatThread.WorkspaceId;
if (notifyParent)
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
await this.SyncWorkspaceHeaderWithChatThreadAsync(); await this.SyncWorkspaceHeaderWithChatThreadAsync();
await this.SyncForegroundChatAsync();
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources); this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
} }
else else
{ {
this.loadedParameterChatId = Guid.Empty;
this.loadedParameterWorkspaceId = Guid.Empty;
this.ClearWorkspaceHeaderState(); this.ClearWorkspaceHeaderState();
await this.SyncForegroundChatAsync();
this.ApplyStandardDataSourceOptions(); this.ApplyStandardDataSourceOptions();
} }
@ -863,12 +999,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task ResetState() private async Task ResetState()
{ {
this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.ComposerState.Clear();
this.ClearWorkspaceHeaderState(); this.ClearWorkspaceHeaderState();
this.ChatThread = null; this.ChatThread = null;
this.MarkCurrentChatAsLoadedParameter();
await this.SyncForegroundChatAsync();
this.ApplyStandardDataSourceOptions(); this.ApplyStandardDataSourceOptions();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
@ -885,14 +1022,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Try to select the profile: // Try to select the profile:
if (!string.IsNullOrWhiteSpace(chatProfile)) if (!string.IsNullOrWhiteSpace(chatProfile))
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile) ?? Profile.NO_PROFILE; this.currentProfile = this.SettingsManager.GetProfileById(chatProfile);
// Try to select the chat template: // Try to select the chat template:
if (!string.IsNullOrWhiteSpace(chatChatTemplate)) if (!string.IsNullOrWhiteSpace(chatChatTemplate))
{ this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatChatTemplate);
var selectedTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate);
this.currentChatTemplate = selectedTemplate ?? ChatTemplate.NO_CHAT_TEMPLATE;
}
} }
private async Task ToggleWorkspaceOverlay() private async Task ToggleWorkspaceOverlay()
@ -939,7 +1073,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(lastBlockContent is null) if(lastBlockContent is null)
return Task.CompletedTask; return Task.CompletedTask;
this.userInput = textBlock.Text; this.RestoreComposerFromTextBlock(textBlock);
this.ChatThread.Remove(block); this.ChatThread.Remove(block);
this.ChatThread.Remove(lastBlockContent); this.ChatThread.Remove(lastBlockContent);
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
@ -956,13 +1090,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (block is not ContentText textBlock) if (block is not ContentText textBlock)
return Task.CompletedTask; return Task.CompletedTask;
this.userInput = textBlock.Text; this.RestoreComposerFromTextBlock(textBlock);
this.ChatThread.Remove(block); this.ChatThread.Remove(block);
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
this.StateHasChanged(); this.StateHasChanged();
return Task.CompletedTask; return Task.CompletedTask;
} }
private void RestoreComposerFromTextBlock(ContentText textBlock)
{
this.ComposerState.RestoreFromTextBlock(textBlock);
}
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
@ -982,9 +1121,32 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.autoSaveEnabled) if(this.autoSaveEnabled)
await this.SaveThread(); await this.SaveThread();
break; break;
case Event.WORKSPACE_RENAMED:
if (data is Guid workspaceId)
await this.RefreshRenamedWorkspaceHeaderAsync(workspaceId);
break;
case Event.CONFIGURATION_CHANGED:
case Event.PLUGINS_RELOADED:
await this.RefreshChatSelectionsAfterConfigurationChange();
this.StateHasChanged();
break;
case Event.WORKSPACE_LOADED_CHAT_CHANGED: case Event.AI_JOB_CHANGED:
await this.LoadedChatChanged(); case Event.AI_JOB_FINISHED:
case Event.CHAT_GENERATION_CHANGED:
if (data is AIJobSnapshot { Kind: AIJobKind.CHAT_GENERATION } snapshot && this.ChatThread?.ChatId == snapshot.SubjectId)
{
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
if (!snapshot.IsActive)
{
this.hasUnsavedChanges = false;
this.previousInputForbidden = true;
}
this.StateHasChanged();
}
break; break;
} }
} }
@ -996,8 +1158,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
case Event.HAS_CHAT_UNSAVED_CHANGES: case Event.HAS_CHAT_UNSAVED_CHANGES:
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
return Task.FromResult((TResult?) (object) false); return Task.FromResult((TResult?) (object) false);
if (this.IsCurrentChatStreaming)
return Task.FromResult((TResult?) (object) false);
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges); return Task.FromResult((TResult?)(object)(this.hasUnsavedChanges || this.ComposerState.HasVisibleUserDraft));
} }
return Task.FromResult(default(TResult)); return Task.FromResult(default(TResult));
@ -1015,21 +1180,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
} }
if (this.cancellationTokenSource is not null) await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
{ this.Dispose();
try
{
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
this.cancellationTokenSource.Dispose();
}
catch
{
// ignored
}
}
} }
#endregion #endregion
} }

View File

@ -0,0 +1,65 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Components;
public sealed class ChatComposerState
{
public string UserInput { get; private set; } = string.Empty;
public HashSet<FileAttachment> FileAttachments { get; } = [];
public bool HasUserDraft { get; private set; }
public bool HasComposerContent => !string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0;
public bool HasVisibleUserDraft => this.HasUserDraft && (!string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0);
public void ApplyTemplate(ChatTemplate chatTemplate)
{
this.UserInput = chatTemplate.PredefinedUserPrompt;
this.FileAttachments.Clear();
foreach (var attachment in chatTemplate.FileAttachments)
this.FileAttachments.Add(attachment.Normalize());
this.HasUserDraft = false;
}
public void SetUserInput(string? userInput)
{
this.UserInput = userInput ?? string.Empty;
this.HasUserDraft = !string.IsNullOrWhiteSpace(userInput);
}
public void SetSystemInput(string? userInput)
{
this.UserInput = userInput ?? string.Empty;
this.HasUserDraft = false;
}
public void MarkUserDraft()
{
this.HasUserDraft = true;
}
public void ReplaceFileAttachments(IEnumerable<FileAttachment> fileAttachments)
{
this.FileAttachments.Clear();
foreach (var attachment in fileAttachments)
this.FileAttachments.Add(attachment.Normalize());
}
public void Clear()
{
this.UserInput = string.Empty;
this.FileAttachments.Clear();
this.HasUserDraft = false;
}
public void RestoreFromTextBlock(ContentText textBlock)
{
this.UserInput = textBlock.Text;
this.ReplaceFileAttachments(textBlock.FileAttachments);
this.HasUserDraft = true;
}
}

View File

@ -6,7 +6,7 @@
<ActivatorContent> <ActivatorContent>
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE) @if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
{ {
<MudButton IconSize="Size.Large" StartIcon="@Icons.Material.Filled.RateReview" IconColor="Color.Default"> <MudButton IconSize="Size.Large" StartIcon="@this.ChatTemplateIcon(this.CurrentChatTemplate)" IconColor="Color.Default">
@this.CurrentChatTemplate.GetSafeName() @this.CurrentChatTemplate.GetSafeName()
</MudButton> </MudButton>
} }
@ -22,7 +22,7 @@
<MudDivider/> <MudDivider/>
@foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates()) @foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates())
{ {
<MudMenuItem Icon="@Icons.Material.Filled.RateReview" OnClick="@(async () => await this.SelectionChanged(chatTemplate))"> <MudMenuItem Icon="@this.ChatTemplateIcon(chatTemplate)" OnClick="@(async () => await this.SelectionChanged(chatTemplate))">
@chatTemplate.GetSafeName() @chatTemplate.GetSafeName()
</MudMenuItem> </MudMenuItem>
} }

View File

@ -11,13 +11,13 @@ public partial class ChatTemplateSelection : MSGComponentBase
{ {
[Parameter] [Parameter]
public ChatTemplate CurrentChatTemplate { get; set; } = ChatTemplate.NO_CHAT_TEMPLATE; public ChatTemplate CurrentChatTemplate { get; set; } = ChatTemplate.NO_CHAT_TEMPLATE;
[Parameter] [Parameter]
public bool CanChatThreadBeUsedForTemplate { get; set; } public bool CanChatThreadBeUsedForTemplate { get; set; }
[Parameter] [Parameter]
public ChatThread? CurrentChatThread { get; set; } public ChatThread? CurrentChatThread { get; set; }
[Parameter] [Parameter]
public EventCallback<ChatTemplate> CurrentChatTemplateChanged { get; set; } public EventCallback<ChatTemplate> CurrentChatTemplateChanged { get; set; }
@ -26,24 +26,42 @@ public partial class ChatTemplateSelection : MSGComponentBase
[Parameter] [Parameter]
public string MarginRight { get; set; } = string.Empty; public string MarginRight { get; set; } = string.Empty;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}"; private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private string ChatTemplateIcon(ChatTemplate chatTemplate)
{
if (chatTemplate.IsEnterpriseConfiguration)
return Icons.Material.Filled.Business;
return Icons.Material.Filled.RateReview;
}
private async Task SelectionChanged(ChatTemplate chatTemplate) private async Task SelectionChanged(ChatTemplate chatTemplate)
{ {
this.CurrentChatTemplate = chatTemplate; this.CurrentChatTemplate = chatTemplate;
await this.CurrentChatTemplateChanged.InvokeAsync(chatTemplate); await this.CurrentChatTemplateChanged.InvokeAsync(chatTemplate);
} }
private async Task OpenSettingsDialog() private async Task OpenSettingsDialog()
{ {
var dialogParameters = new DialogParameters(); var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN); await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
} }
private async Task CreateNewChatTemplateFromChat() private async Task CreateNewChatTemplateFromChat()
{ {
var dialogParameters = new DialogParameters<SettingsDialogChatTemplate> var dialogParameters = new DialogParameters<SettingsDialogChatTemplate>
@ -53,4 +71,16 @@ public partial class ChatTemplateSelection : MSGComponentBase
}; };
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN); await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
} }
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
} }

View File

@ -56,10 +56,12 @@ public abstract partial class ConfigurationBase : MSGComponentBase
protected bool IsDisabled => this.Disabled() || this.IsLocked(); protected bool IsDisabled => this.Disabled() || this.IsLocked();
private string Classes => $"{this.GetClassForBase} {MARGIN_CLASS}"; private string Classes => $"{this.GetClassForBase} {JUSTIFIED_HELP_CLASS} {MARGIN_CLASS}";
private protected virtual RenderFragment? Body => null; private protected virtual RenderFragment? Body => null;
private const string JUSTIFIED_HELP_CLASS = "configuration-help-justified";
private const string MARGIN_CLASS = "mb-6"; private const string MARGIN_CLASS = "mb-6";
protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();

View File

@ -0,0 +1,27 @@
@inherits ConfigurationBaseCore
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudTextField
T="string"
Text="@this.Text()"
TextChanged="@this.InternalUpdate"
Disabled="@this.IsDisabled"
Adornment="Adornment.Start"
AdornmentIcon="@this.Icon"
AdornmentColor="@this.IconColor"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Immediate="@true"
Underline="false"
Class="flex-grow-1"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
Disabled="@this.IsDisabled"
Class="mb-1"
OnClick="@this.OpenFileDialog">
@T("Choose File")
</MudButton>
</MudStack>

View File

@ -0,0 +1,127 @@
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Timer = System.Timers.Timer;
namespace AIStudio.Components;
public partial class ConfigurationFile : ConfigurationBaseCore
{
/// <summary>
/// The text used for the textfield.
/// </summary>
[Parameter]
public Func<string> Text { get; set; } = () => string.Empty;
/// <summary>
/// An action which is called when the text was changed.
/// </summary>
[Parameter]
public Action<string> TextUpdate { get; set; } = _ => { };
/// <summary>
/// The icon to display next to the textfield.
/// </summary>
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.AttachFile;
/// <summary>
/// The color of the icon to use.
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Default;
/// <summary>
/// The title of the file selection dialog.
/// </summary>
[Parameter]
public string FileDialogTitle { get; set; } = "Select File";
/// <summary>
/// The optional file type filter for the file selection dialog.
/// </summary>
[Parameter]
public FileTypeFilter[]? Filter { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
private string internalText = string.Empty;
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
{
AutoReset = false
};
#region Overrides of ConfigurationBase
/// <inheritdoc />
protected override bool Stretch => true;
protected override Variant Variant => Variant.Outlined;
protected override string Label => this.OptionDescription;
#endregion
#region Overrides of ConfigurationBase
protected override async Task OnInitializedAsync()
{
this.timer.Elapsed += async (_, _) => await this.InvokeAsync(async () => await this.OptionChanged(this.internalText));
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
this.internalText = this.Text();
await base.OnParametersSetAsync();
}
#endregion
private void InternalUpdate(string text)
{
this.timer.Stop();
this.internalText = text;
this.timer.Start();
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
if (response.UserCancelled)
return;
this.timer.Stop();
this.internalText = response.SelectedFilePath;
await this.OptionChanged(response.SelectedFilePath);
}
private async Task OptionChanged(string updatedText)
{
this.TextUpdate(updatedText);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
this.timer.Stop();
this.timer.Dispose();
}
catch
{
// ignore
}
base.DisposeResources();
}
#endregion
}

View File

@ -41,9 +41,9 @@ public partial class ConfigurationMinConfidenceSelection : MSGComponentBase
if (this.SelectedValue() is ConfidenceLevel.NONE) if (this.SelectedValue() is ConfidenceLevel.NONE)
return ConfidenceLevel.NONE; return ConfidenceLevel.NONE;
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence) if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
{ {
var minimumLevel = this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence; var minimumLevel = this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence;
if(this.SelectedValue() < minimumLevel) if(this.SelectedValue() < minimumLevel)
return minimumLevel; return minimumLevel;
} }

View File

@ -3,7 +3,7 @@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@this.Icon" Color="@this.IconColor"/> <MudIcon Icon="@this.Icon" Color="@this.IconColor"/>
<MudText Typo="Typo.body1" Class="flex-grow-1"> <MudText Typo="Typo.body1" Class="flex-grow-1">
@if (string.IsNullOrWhiteSpace(this.Shortcut())) @if (string.IsNullOrWhiteSpace(this.Data.Value()))
{ {
@T("No shortcut configured") @T("No shortcut configured")
} }

View File

@ -1,5 +1,4 @@
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -19,22 +18,10 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
/// <summary> /// <summary>
/// The current shortcut value. /// The shortcut binding data.
/// </summary> /// </summary>
[Parameter] [Parameter]
public Func<string> Shortcut { get; set; } = () => string.Empty; public ConfigurationShortcutData Data { get; set; } = ConfigurationShortcutData.Empty;
/// <summary>
/// An action which is called when the shortcut was changed.
/// </summary>
[Parameter]
public Action<string> ShortcutUpdate { get; set; } = _ => { };
/// <summary>
/// The name/identifier of the shortcut (used for conflict detection and registration).
/// </summary>
[Parameter]
public Shortcut ShortcutId { get; init; }
/// <summary> /// <summary>
/// The icon to display. /// The icon to display.
@ -60,10 +47,18 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
private string GetDisplayShortcut() private string GetDisplayShortcut()
{ {
var shortcut = this.Shortcut(); var shortcut = this.Data.Value();
if (string.IsNullOrWhiteSpace(shortcut)) if (string.IsNullOrWhiteSpace(shortcut))
return string.Empty; return string.Empty;
var shortcutDisplayName = this.Data.DisplayName();
var shortcutDisplaySource = this.Data.DisplaySource();
if (!string.IsNullOrWhiteSpace(shortcutDisplayName)
&& string.Equals(shortcutDisplaySource, shortcut, StringComparison.Ordinal))
{
return shortcutDisplayName;
}
// Convert internal format to display format: // Convert internal format to display format:
return shortcut return shortcut
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl") .Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
@ -80,8 +75,8 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
{ {
var dialogParameters = new DialogParameters<ShortcutDialog> var dialogParameters = new DialogParameters<ShortcutDialog>
{ {
{ x => x.InitialShortcut, this.Shortcut() }, { x => x.InitialShortcut, this.Data.Value() },
{ x => x.ShortcutId, this.ShortcutId }, { x => x.ShortcutId, this.Data.Id },
}; };
var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>( var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>(
@ -93,9 +88,17 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
if (dialogResult.Data is string newShortcut) if (dialogResult.Data is ShortcutDialogResult shortcutResult)
{ {
this.ShortcutUpdate(newShortcut); this.Data.ValueUpdate(shortcutResult.Shortcut);
this.Data.DisplayUpdate(shortcutResult.DisplayName, shortcutResult.DisplaySource);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
else if (dialogResult.Data is string newShortcut)
{
this.Data.ValueUpdate(newShortcut);
this.Data.DisplayUpdate(string.Empty, string.Empty);
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.InformAboutChange(); await this.InformAboutChange();
} }

View File

@ -0,0 +1,44 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Components;
/// <summary>
/// UI binding data for a configurable keyboard shortcut.
/// </summary>
public sealed class ConfigurationShortcutData
{
/// <summary>
/// Empty shortcut binding.
/// </summary>
public static ConfigurationShortcutData Empty { get; } = new();
/// <summary>
/// The name/identifier of the shortcut, used for conflict detection and registration.
/// </summary>
public Shortcut Id { get; init; } = Shortcut.NONE;
/// <summary>
/// The current shortcut value.
/// </summary>
public Func<string> Value { get; init; } = () => string.Empty;
/// <summary>
/// An action that is called when the shortcut was changed.
/// </summary>
public Action<string> ValueUpdate { get; init; } = _ => { };
/// <summary>
/// The optional user-facing shortcut label.
/// </summary>
public Func<string> DisplayName { get; init; } = () => string.Empty;
/// <summary>
/// The canonical shortcut value the optional user-facing label belongs to.
/// </summary>
public Func<string> DisplaySource { get; init; } = () => string.Empty;
/// <summary>
/// An action that is called when the user-facing shortcut label was changed.
/// </summary>
public Action<string, string> DisplayUpdate { get; init; } = (_, _) => { };
}

View File

@ -37,6 +37,16 @@ public partial class ProfileSelection : MSGComponentBase
private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText; private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}"; private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private string ProfileIcon(Profile profile) private string ProfileIcon(Profile profile)
{ {
@ -57,4 +67,16 @@ public partial class ProfileSelection : MSGComponentBase
var dialogParameters = new DialogParameters(); var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<SettingsDialogProfiles>(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN); await this.DialogService.ShowAsync<SettingsDialogProfiles>(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN);
} }
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
} }

View File

@ -25,6 +25,16 @@ public partial class ProviderSelection : MSGComponentBase
[Inject] [Inject]
private ILogger<ProviderSelection> Logger { get; init; } = null!; private ILogger<ProviderSelection> Logger { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private async Task SelectionChanged(AIStudio.Settings.Provider provider) private async Task SelectionChanged(AIStudio.Settings.Provider provider)
{ {
@ -62,4 +72,16 @@ public partial class ProviderSelection : MSGComponentBase
break; break;
} }
} }
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
} }

View File

@ -3,9 +3,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Policy" HeaderText="@T("Agent: Security Audit for external Assistants")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Policy" HeaderText="@T("Agent: Security Audit for external Assistants")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("This Agent audits newly installed or updated external Plugin-Assistant for security risks before they are activated and stores the latest audit card until the plugin manifest changes.") @T("This Agent audits newly installed or updated external Plugin-Assistant for security risks before they are activated and stores the latest audit card until the plugin manifest changes.")
</MudText> </MudJustifiedText>
<MudField Label="@T("Require a security audit before activating external Assistants?")" Variant="Variant.Outlined" Underline="false" Class="mb-6" InnerPadding="false"> <MudField Label="@T("Require a security audit before activating external Assistants?")" Variant="Variant.Outlined" Underline="false" Class="mb-6" InnerPadding="false">
<MudSwitch T="bool" Value="@this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation" ValueChanged="@this.RequireAuditBeforeActivationChanged" Color="Color.Primary"> <MudSwitch T="bool" Value="@this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation" ValueChanged="@this.RequireAuditBeforeActivationChanged" Color="Color.Primary">
@(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit")) @(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit"))

View File

@ -2,9 +2,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="@T("Agent: Text Content Cleaner Options")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="@T("Agent: Text Content Cleaner Options")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things, and attempts to convert relative links into absolute links so that they can be used.") @T("Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things, and attempts to convert relative links into absolute links so that they can be used.")
</MudText> </MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Preselect text content cleaner options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/> <ConfigurationOption OptionDescription="@T("Preselect text content cleaner options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider = selectedValue)"/> <ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider = selectedValue)"/>
</MudPaper> </MudPaper>

View File

@ -2,9 +2,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="@T("Agent: Data Source Selection Options")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="@T("Agent: Data Source Selection Options")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to select the appropriate data sources for the current prompt.") @T("Use Case: this agent is used to select the appropriate data sources for the current prompt.")
</MudText> </MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Preselect data source selection options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/> <ConfigurationOption OptionDescription="@T("Preselect data source selection options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/> <ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/>
</MudPaper> </MudPaper>

View File

@ -1,9 +1,9 @@
@inherits SettingsPanelBase @inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Assessment" HeaderText="@T("Agent: Retrieval Context Validation Options")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Assessment" HeaderText="@T("Agent: Retrieval Context Validation Options")">
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.") @T("Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.")
</MudText> </MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Enable the retrieval context validation agent?")" LabelOn="@T("The validation agent is enabled")" LabelOff="@T("No validation is performed")" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation = updatedState)" OptionHelp="@T("When enabled, the retrieval context validation agent will check each retrieval context of any retrieval process, whether a context makes sense for the given prompt.")"/> <ConfigurationOption OptionDescription="@T("Enable the retrieval context validation agent?")" LabelOn="@T("The validation agent is enabled")" LabelOff="@T("No validation is performed")" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation = updatedState)" OptionHelp="@T("When enabled, the retrieval context validation agent will check each retrieval context of any retrieval process, whether a context makes sense for the given prompt.")"/>
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation) @if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
{ {

View File

@ -14,6 +14,7 @@
<ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/> <ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/>
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/> <ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/> <ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/> <ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
@ -36,7 +37,7 @@
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{ {
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
<ConfigurationShortcut ShortcutId="Shortcut.VOICE_RECORDING_TOGGLE" OptionDescription="@T("Voice recording shortcut")" Shortcut="@(() => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording)" ShortcutUpdate="@(shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut)" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/> <ConfigurationShortcut Data="@this.VoiceRecordingShortcut" OptionDescription="@T("Voice recording shortcut")" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
} }
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings) @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
@ -45,12 +46,12 @@
@T("Enterprise Administration") @T("Enterprise Administration")
</MudText> </MudText>
<MudText Typo="Typo.body2" Class="mb-3"> <MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("Generate a 256-bit encryption secret for encrypting API keys in configuration plugins. Deploy this secret to client machines via Group Policy (Windows Registry) or environment variables. Providers can then be exported with encrypted API keys using the export buttons in the provider settings.") @T("Generate a 256-bit encryption secret for encrypting API keys in configuration plugins. Deploy this secret to client machines via Group Policy (Windows Registry) or environment variables. Providers can then be exported with encrypted API keys using the export buttons in the provider settings.")
<MudLink Href="https://github.com/MindWorkAI/AI-Studio/blob/main/documentation/Enterprise%20IT.md" Target="_blank"> <MudLink Href="https://github.com/MindWorkAI/AI-Studio/blob/main/documentation/Enterprise%20IT.md" Target="_blank">
@T("Read the Enterprise IT documentation for details.") @T("Read the Enterprise IT documentation for details.")
</MudLink> </MudLink>
</MudText> </MudJustifiedText>
<MudButton StartIcon="@Icons.Material.Filled.Key" <MudButton StartIcon="@Icons.Material.Filled.Key"
Variant="Variant.Filled" Variant="Variant.Filled"
@ -58,5 +59,13 @@
OnClick="@this.GenerateEncryptionSecret"> OnClick="@this.GenerateEncryptionSecret">
@T("Generate an encryption secret and copy it to the clipboard") @T("Generate an encryption secret and copy it to the clipboard")
</MudButton> </MudButton>
<MudText Typo="Typo.h6" Class="mt-6 mb-3">
@T("External HTTPS certificates")
</MudText>
<ConfigurationOption OptionDescription="@T("Use additional root certificates for external HTTPS requests?")" LabelOn="@T("Additional root certificates are enabled")" LabelOff="@T("Additional root certificates are disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled = updatedState)" OptionHelp="@T("When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var meta) && meta.IsLocked"/>
<ConfigurationFile OptionDescription="@T("Root certificate bundle path")" Icon="@Icons.Material.Filled.Folder" Text="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath = updatedText)" FileDialogTitle="@T("Select a root certificate bundle")" Filter="@([FileTypes.CERTIFICATE_BUNDLE])" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var meta) && meta.IsLocked"/>
<ConfigurationText OptionDescription="@T("Allowed hosts for additional root certificates")" Icon="@Icons.Material.Filled.Dns" NumLines="3" Text="@this.GetExternalHttpCustomRootCertificateAllowedHostsText" TextUpdate="@this.UpdateExternalHttpCustomRootCertificateAllowedHosts" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var meta) && meta.IsLocked"/>
} }
</ExpansionPanel> </ExpansionPanel>

View File

@ -1,11 +1,22 @@
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Rust;
namespace AIStudio.Components.Settings; namespace AIStudio.Components.Settings;
public partial class SettingsPanelApp : SettingsPanelBase public partial class SettingsPanelApp : SettingsPanelBase
{ {
private ConfigurationShortcutData VoiceRecordingShortcut => new()
{
Id = Shortcut.VOICE_RECORDING_TOGGLE,
Value = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording,
ValueUpdate = shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut,
DisplayName = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName,
DisplaySource = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource,
DisplayUpdate = this.UpdateShortcutVoiceRecordingDisplay,
};
private async Task GenerateEncryptionSecret() private async Task GenerateEncryptionSecret()
{ {
var secret = EnterpriseEncryption.GenerateSecret(); var secret = EnterpriseEncryption.GenerateSecret();
@ -67,12 +78,38 @@ public partial class SettingsPanelApp : SettingsPanelBase
return enabled; return enabled;
} }
private string GetExternalHttpCustomRootCertificateAllowedHostsText()
{
return string.Join(Environment.NewLine, this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts.Order(StringComparer.OrdinalIgnoreCase));
}
private bool AreExternalHttpCustomRootCertificateDetailsDisabled()
{
return !this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled;
}
private void UpdateExternalHttpCustomRootCertificateAllowedHosts(string updatedText)
{
var patterns = updatedText
.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts = patterns;
}
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures) private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
{ {
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures()); selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures; this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
} }
private void UpdateShortcutVoiceRecordingDisplay(string displayName, string displaySource)
{
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName = displayName;
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource = displaySource;
}
private async Task UpdateLangBehaviour(LangBehavior behavior) private async Task UpdateLangBehaviour(LangBehavior behavior)
{ {
this.SettingsManager.ConfigurationData.App.LanguageBehavior = behavior; this.SettingsManager.ConfigurationData.App.LanguageBehavior = behavior;

View File

@ -0,0 +1,60 @@
@using AIStudio.Provider
@using AIStudio.Settings
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Security" HeaderText="@T("Provider Confidence")">
<MudText Typo="Typo.h4" Class="mb-3">
@T("Provider Confidence")
</MudText>
<MudJustifiedText Class="mb-3">
@T("Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.")
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Do you want to enforce an global minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
@if(this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
{
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.GlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
}
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ShowProviderConfidence, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ConfidenceScheme, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
{
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>@T("Provider")</MudTh>
<MudTh>@T("Description")</MudTh>
<MudTh>@T("Confidence Level")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd Style="vertical-align: top;">
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsCustomConfidenceSchemeLocked()">
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
{
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
continue;
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
@confidenceLevel.GetName()
</MudMenuItem>
}
</MudMenu>
</MudTd>
</RowTemplate>
</MudTable>
}
}
</ExpansionPanel>

View File

@ -0,0 +1,38 @@
using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelConfidence : SettingsPanelBase
{
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return level.GetName();
return T("Not yet configured");
}
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return $"background-color: {level.GetColor(this.SettingsManager)};";
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
}
private bool IsCustomConfidenceSchemeLocked()
{
return ManagedConfiguration.TryGet(x => x.Confidence, x => x.CustomConfidenceScheme, out var meta) && meta.IsLocked;
}
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
{
if (this.IsCustomConfidenceSchemeLocked())
return;
this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme[llmProvider] = level;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
}

View File

@ -1,4 +1,5 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits SettingsPanelProviderBase @inherits SettingsPanelProviderBase
@ -39,6 +40,12 @@
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration) @if (context.IsEnterpriseConfiguration)
{ {
<MudTooltip Text="@T("This embedding provider is managed by your organization.")"> <MudTooltip Text="@T("This embedding provider is managed by your organization.")">

View File

@ -31,6 +31,12 @@
<MudTd>@this.GetLLMProviderModelName(context)</MudTd> <MudTd>@this.GetLLMProviderModelName(context)</MudTd>
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This provider is trusted by your organization for data source security checks.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration) @if (context.IsEnterpriseConfiguration)
{ {
<MudTooltip Text="@T("This provider is managed by your organization.")"> <MudTooltip Text="@T("This provider is managed by your organization.")">
@ -68,59 +74,4 @@
} }
<LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="mt-3" /> <LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="mt-3" />
<MudText Typo="Typo.h4" Class="mb-3">
@T("LLM Provider Confidence")
</MudText>
<MudJustifiedText Class="mb-3">
@T("Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.")
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Do you want to enforce an app-wide minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.")"/>
@if(this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
{
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence = selectedValue)"/>
}
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")"/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")"/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
{
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>@T("LLM Provider")</MudTh>
<MudTh>@T("Description")</MudTh>
<MudTh>@T("Confidence Level")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd Style="vertical-align: top;">
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
{
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
continue;
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
@confidenceLevel.GetName()
</MudMenuItem>
}
</MudMenu>
</MudTd>
</RowTemplate>
</MudTable>
}
}
</ExpansionPanel> </ExpansionPanel>

View File

@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -166,25 +165,4 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders); await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders);
} }
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return level.GetName();
return T("Not yet configured");
}
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return $"background-color: {level.GetColor(this.SettingsManager)};";
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
}
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
{
this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[llmProvider] = level;
await this.SettingsManager.StoreSettings();
}
} }

View File

@ -1,11 +1,11 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits SettingsPanelProviderBase @inherits SettingsPanelProviderBase
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{ {
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
<PreviewBeta ApplyInnerScrollingFix="true"/>
<MudText Typo="Typo.h4" Class="mb-3"> <MudText Typo="Typo.h4" Class="mb-3">
@T("Configured Transcription Providers") @T("Configured Transcription Providers")
</MudText> </MudText>
@ -36,6 +36,12 @@
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This transcription provider is trusted by your organization for data source security checks.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration) @if (context.IsEnterpriseConfiguration)
{ {
<MudTooltip Text="@T("This transcription provider is managed by your organization.")"> <MudTooltip Text="@T("This transcription provider is managed by your organization.")">

View File

@ -12,10 +12,16 @@ public class TreeItemData : ITreeItem
public string Icon { get; init; } = string.Empty; public string Icon { get; init; } = string.Empty;
public string DefaultIcon { get; init; } = string.Empty;
public TreeItemType Type { get; init; } public TreeItemType Type { get; init; }
public string Path { get; init; } = string.Empty; public string Path { get; init; } = string.Empty;
public Guid ChatId { get; init; }
public Guid WorkspaceId { get; init; }
public bool Expandable { get; init; } = true; public bool Expandable { get; init; } = true;
public DateTimeOffset LastEditTime { get; init; } public DateTimeOffset LastEditTime { get; init; }

View File

@ -132,6 +132,7 @@ public partial class VoiceRecorder : MSGComponentBase
} }
var mimeTypes = GetPreferredMimeTypes( var mimeTypes = GetPreferredMimeTypes(
Builder.Create().UseAudio().UseSubtype(AudioSubtype.WEBM).Build(),
Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(), Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(), Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(), Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
@ -361,7 +362,18 @@ public partial class VoiceRecorder : MSGComponentBase
// Call the transcription API: // Call the transcription API:
this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.UsedLLMProvider, transcriptionProviderSettings.Model.ToString()); this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.UsedLLMProvider, transcriptionProviderSettings.Model.ToString());
var transcribedText = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager); var transcriptionResult = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
if (!transcriptionResult.Success)
{
this.Logger.LogWarning("The transcription request failed.");
var userMessage = string.IsNullOrWhiteSpace(transcriptionResult.ErrorMessage)
? this.T("Unfortunately, there was an error communicating with the AI system.")
: transcriptionResult.ErrorMessage;
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, userMessage));
return;
}
var transcribedText = transcriptionResult.Text;
if (string.IsNullOrWhiteSpace(transcribedText)) if (string.IsNullOrWhiteSpace(transcribedText))
{ {

View File

@ -11,6 +11,36 @@
} }
else else
{ {
@if (this.SearchVisible)
{
<MudStack Class="mx-3 mt-2 mb-1" Spacing="1" Style="position: sticky; top: 0; z-index: 2; background-color: var(--mud-palette-background);">
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
<MudTextField T="string"
Text="@this.searchText"
TextChanged="@this.OnSearchTextChanged"
Placeholder="@T("Search chats")"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Immediate="@true"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"/>
<MudTooltip Text="@T("Clear search")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Clear" Size="Size.Medium" Color="Color.Inherit" Disabled="@(string.IsNullOrWhiteSpace(this.searchText))" OnClick="@this.ClearSearchAsync"/>
</MudTooltip>
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
<MudSwitch T="bool" Value="@this.includeThreadContents" ValueChanged="@this.IncludeThreadContentsChanged" Color="Color.Primary">
@T("Search chat contents")
</MudSwitch>
@if (this.isSearchRunning)
{
<MudProgressCircular Size="Size.Small" Indeterminate="@true"/>
}
</MudStack>
</MudStack>
}
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3"> <MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
<ItemTemplate Context="item"> <ItemTemplate Context="item">
@switch (item.Value) @switch (item.Value)
@ -24,7 +54,7 @@ else
case TreeItemData treeItem: case TreeItemData treeItem:
@if (treeItem.Type is TreeItemType.LOADING) @if (treeItem.Type is TreeItemType.LOADING)
{ {
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)"> <MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
<BodyContent> <BodyContent>
<MudSkeleton Width="85%" Height="22px"/> <MudSkeleton Width="85%" Height="22px"/>
</BodyContent> </BodyContent>
@ -32,10 +62,10 @@ else
} }
else if (treeItem.Type is TreeItemType.CHAT) else if (treeItem.Type is TreeItemType.CHAT)
{ {
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))"> <MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
<BodyContent> <BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%"> <div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;"> <MudText Style="@this.GetChatTreeItemTextStyle(treeItem)">
@if (string.IsNullOrWhiteSpace(treeItem.Text)) @if (string.IsNullOrWhiteSpace(treeItem.Text))
{ {
@T("Empty chat") @T("Empty chat")
@ -48,15 +78,15 @@ else
<div style="justify-self: end;"> <div style="justify-self: end;">
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
</div> </div>
</div> </div>
@ -65,28 +95,35 @@ else
} }
else if (treeItem.Type is TreeItemType.WORKSPACE) else if (treeItem.Type is TreeItemType.WORKSPACE)
{ {
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))"> <MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
<BodyContent> <BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%"> <div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;"> <MudText Style="justify-self: start;">
@treeItem.Text @treeItem.Text
</MudText> </MudText>
<div style="justify-self: end;"> @if (!this.HasSearchQuery)
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> {
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/> <div style="justify-self: end;">
</MudTooltip> <MudTooltip Text="@this.GetAddChatToWorkspaceTooltip(treeItem.Text)" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.AddChatAsync(treeItem.Path))"/>
</MudTooltip>
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
</div>
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
</MudTooltip>
</div>
}
</div> </div>
</BodyContent> </BodyContent>
</MudTreeViewItem> </MudTreeViewItem>
} }
else else
{ {
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)"> <MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
<BodyContent> <BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%"> <div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;"> <MudText Style="justify-self: start;">

View File

@ -3,7 +3,7 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Tools.AIJobs;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -18,6 +18,9 @@ public partial class Workspaces : MSGComponentBase
[Inject] [Inject]
private ILogger<Workspaces> Logger { get; init; } = null!; private ILogger<Workspaces> Logger { get; init; } = null!;
[Inject]
private AIJobService AIJobService { get; init; } = null!;
[Parameter] [Parameter]
public ChatThread? CurrentChatThread { get; set; } public ChatThread? CurrentChatThread { get; set; }
@ -28,20 +31,32 @@ public partial class Workspaces : MSGComponentBase
[Parameter] [Parameter]
public bool ExpandRootNodes { get; set; } = true; public bool ExpandRootNodes { get; set; } = true;
[Parameter]
public bool SearchVisible { get; set; }
[Parameter]
public EventCallback<bool> SearchVisibleChanged { get; set; }
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1); private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1);
private readonly List<TreeItemData<ITreeItem>> treeItems = []; private readonly List<TreeItemData<ITreeItem>> treeItems = [];
private readonly HashSet<Guid> loadingWorkspaceChatLists = []; private readonly HashSet<Guid> loadingWorkspaceChatLists = [];
private CancellationTokenSource? prefetchCancellationTokenSource; private CancellationTokenSource? prefetchCancellationTokenSource;
private CancellationTokenSource? searchCancellationTokenSource;
private bool isInitialLoading = true; private bool isInitialLoading = true;
private bool isDisposed; private bool isDisposed;
private bool includeThreadContents;
private bool isSearchRunning;
private string searchText = string.Empty;
private long searchRevision;
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_CREATED ]);
_ = this.LoadTreeItemsAsync(startPrefetch: true); _ = this.LoadTreeItemsAsync(startPrefetch: true);
} }
@ -49,6 +64,7 @@ public partial class Workspaces : MSGComponentBase
private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false) private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false)
{ {
var shouldRunSearch = false;
await this.treeLoadingSemaphore.WaitAsync(); await this.treeLoadingSemaphore.WaitAsync();
try try
{ {
@ -59,7 +75,11 @@ public partial class Workspaces : MSGComponentBase
await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync(); await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync();
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
this.BuildTreeItems(snapshot); if (this.HasSearchQuery)
shouldRunSearch = true;
else
this.BuildTreeItems(snapshot);
this.isInitialLoading = false; this.isInitialLoading = false;
} }
finally finally
@ -67,12 +87,40 @@ public partial class Workspaces : MSGComponentBase
this.treeLoadingSemaphore.Release(); this.treeLoadingSemaphore.Release();
} }
await this.SafeStateHasChanged(); if (shouldRunSearch)
await this.SearchWorkspaceItemsAsync();
else
await this.SafeStateHasChanged();
if (startPrefetch) if (startPrefetch)
await this.StartPrefetchAsync(); await this.StartPrefetchAsync();
} }
private bool HasSearchQuery => this.SearchVisible && !string.IsNullOrWhiteSpace(this.searchText);
private string GetAddChatToWorkspaceTooltip(string workspaceName) => string.Format(T("Start a new chat in workspace '{0}'"), workspaceName);
private async Task<Func<string?, string?>> CreateWorkspaceNameValidationAsync(Guid excludedWorkspaceId = default, string? originalWorkspaceName = null)
{
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
return workspaceName =>
{
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return null;
if (!string.IsNullOrWhiteSpace(originalWorkspaceName) &&
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(originalWorkspaceName), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
return null;
var nameExists = snapshot.Workspaces.Any(workspace =>
workspace.WorkspaceId != excludedWorkspaceId &&
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
return nameExists ? T("There is already a workspace with this name. Please choose a different name.") : null;
};
}
private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot) private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot)
{ {
this.treeItems.Clear(); this.treeItems.Clear();
@ -111,7 +159,7 @@ public partial class Workspaces : MSGComponentBase
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>(); var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime)) foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime))
temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer)); temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
this.treeItems.Add(new TreeItemData<ITreeItem> this.treeItems.Add(new TreeItemData<ITreeItem>
{ {
@ -136,7 +184,7 @@ public partial class Workspaces : MSGComponentBase
if (workspace.ChatsLoaded) if (workspace.ChatsLoaded)
{ {
foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime)) foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime))
children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat)); children.Add(this.CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
} }
else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId)) else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId))
children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath)); children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath));
@ -192,7 +240,7 @@ public partial class Workspaces : MSGComponentBase
}; };
} }
private static TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon) private TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
{ {
return new TreeItemData<ITreeItem> return new TreeItemData<ITreeItem>
{ {
@ -204,13 +252,160 @@ public partial class Workspaces : MSGComponentBase
Branch = branch, Branch = branch,
Text = chat.Name, Text = chat.Name,
Icon = icon, Icon = icon,
DefaultIcon = icon,
Expandable = false, Expandable = false,
Path = chat.ChatPath, Path = chat.ChatPath,
ChatId = chat.ChatId,
WorkspaceId = chat.WorkspaceId,
LastEditTime = chat.LastEditTime, LastEditTime = chat.LastEditTime,
}, },
}; };
} }
private void BuildSearchTreeItems(WorkspaceSearchSnapshot snapshot)
{
this.treeItems.Clear();
if (snapshot.Workspaces.Count == 0 && snapshot.TemporaryChats.Count == 0)
{
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.NONE,
Text = T("No chats found"),
Icon = Icons.Material.Filled.Search,
Expandable = false,
Path = "search_empty",
},
});
return;
}
if (snapshot.Workspaces.Count > 0)
{
var workspaceChildren = new List<TreeItemData<ITreeItem>>();
foreach (var workspace in snapshot.Workspaces)
workspaceChildren.Add(this.CreateSearchWorkspaceTreeItem(workspace));
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.WORKSPACES,
Text = T("Workspaces"),
Icon = Icons.Material.Filled.Folder,
Expandable = true,
Path = "search_workspaces",
Children = workspaceChildren,
},
});
}
if (snapshot.Workspaces.Count > 0 && snapshot.TemporaryChats.Count > 0)
{
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeDivider(),
});
}
if (snapshot.TemporaryChats.Count > 0)
{
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
foreach (var temporaryChat in snapshot.TemporaryChats)
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat.Chat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = T("Disappearing Chats"),
Icon = Icons.Material.Filled.Timer,
Expandable = true,
Path = "search_temp",
Children = temporaryChatsChildren,
},
});
}
}
private TreeItemData<ITreeItem> CreateSearchWorkspaceTreeItem(WorkspaceSearchWorkspace workspace)
{
var children = new List<TreeItemData<ITreeItem>>();
foreach (var chat in workspace.Chats)
children.Add(this.CreateChatTreeItem(chat.Chat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
return new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Type = TreeItemType.WORKSPACE,
Depth = 1,
Branch = WorkspaceBranch.WORKSPACES,
Text = workspace.Name,
Icon = Icons.Material.Filled.Description,
Expandable = true,
Path = workspace.WorkspacePath,
Children = children,
},
};
}
private string GetTreeItemIcon(TreeItemData treeItem)
{
if (treeItem.Type is not TreeItemType.CHAT)
return treeItem.Icon;
var defaultIcon = string.IsNullOrWhiteSpace(treeItem.DefaultIcon) ? treeItem.Icon : treeItem.DefaultIcon;
return this.GetChatTreeIcon(treeItem.ChatId, defaultIcon);
}
private bool IsChatTreeItemBusy(TreeItemData treeItem)
{
return treeItem.Type is TreeItemType.CHAT && this.AIJobService.IsChatGenerationActive(treeItem.ChatId);
}
private string GetChatTreeItemTextStyle(TreeItemData treeItem)
{
return this.IsCurrentChatTreeItem(treeItem) ? "justify-self: start; font-weight: 700;" : "justify-self: start;";
}
private bool IsCurrentChatTreeItem(TreeItemData treeItem)
{
return treeItem.Type is TreeItemType.CHAT
&& this.CurrentChatThread is not null
&& treeItem.ChatId == this.CurrentChatThread.ChatId
&& treeItem.WorkspaceId == this.CurrentChatThread.WorkspaceId;
}
private string GetChatTreeIcon(Guid chatId, string defaultIcon)
{
var snapshot = this.AIJobService.TryGetChatSnapshot(chatId);
if (snapshot is null || !snapshot.IsActive)
return defaultIcon;
return snapshot.Status switch
{
AIJobStatus.WAITING_FOR_REMOTE => Icons.Material.Filled.HourglassTop,
AIJobStatus.RUNNING => Icons.Material.Filled.ChangeCircle,
_ => defaultIcon,
};
}
private async Task SafeStateHasChanged() private async Task SafeStateHasChanged()
{ {
if (this.isDisposed) if (this.isDisposed)
@ -253,6 +448,106 @@ public partial class Workspaces : MSGComponentBase
} }
} }
public async Task ToggleSearchAsync()
{
var searchVisible = !this.SearchVisible;
this.SearchVisible = searchVisible;
await this.SearchVisibleChanged.InvokeAsync(searchVisible);
if (this.SearchVisible)
{
await this.SafeStateHasChanged();
return;
}
await this.CancelSearchAsync();
this.searchText = string.Empty;
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
}
private async Task CancelSearchAsync()
{
this.searchRevision++;
if (this.searchCancellationTokenSource is not null)
{
await this.searchCancellationTokenSource.CancelAsync();
this.searchCancellationTokenSource.Dispose();
this.searchCancellationTokenSource = null;
}
}
private async Task OnSearchTextChanged(string value)
{
this.searchText = value;
if (string.IsNullOrWhiteSpace(this.searchText))
{
await this.CancelSearchAsync();
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
return;
}
await this.SearchWorkspaceItemsAsync();
}
private async Task IncludeThreadContentsChanged(bool value)
{
this.includeThreadContents = value;
if (this.HasSearchQuery)
await this.SearchWorkspaceItemsAsync();
}
private async Task ClearSearchAsync()
{
this.searchText = string.Empty;
await this.CancelSearchAsync();
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
}
private async Task SearchWorkspaceItemsAsync()
{
await this.CancelSearchAsync();
var text = this.searchText;
if (string.IsNullOrWhiteSpace(text))
return;
this.searchCancellationTokenSource = new CancellationTokenSource();
var token = this.searchCancellationTokenSource.Token;
var revision = ++this.searchRevision;
this.isSearchRunning = true;
await this.SafeStateHasChanged();
try
{
var snapshot = await WorkspaceBehaviour.SearchWorkspaceChatsAsync(text, this.includeThreadContents, token);
if (this.isDisposed || token.IsCancellationRequested || revision != this.searchRevision)
return;
this.BuildSearchTreeItems(snapshot);
}
catch (OperationCanceledException)
{
// Expected when the user keeps typing or hides the search row.
}
catch (Exception ex)
{
this.Logger.LogWarning(ex, "Failed while searching workspace chats.");
this.BuildSearchTreeItems(new([], []));
}
finally
{
if (revision == this.searchRevision)
{
this.isSearchRunning = false;
await this.SafeStateHasChanged();
}
}
}
private async Task OnWorkspaceClicked(TreeItemData treeItem) private async Task OnWorkspaceClicked(TreeItemData treeItem)
{ {
if (treeItem.Type is not TreeItemType.WORKSPACE) if (treeItem.Type is not TreeItemType.WORKSPACE)
@ -348,11 +643,13 @@ public partial class Workspaces : MSGComponentBase
{ {
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8); var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS); var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS);
if (chat is not null)
chat = this.AIJobService.TryGetLiveChatThread(chat.ChatId) ?? chat;
if (switchToChat) if (switchToChat)
{ {
this.CurrentChatThread = chat; this.CurrentChatThread = chat;
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
} }
return chat; return chat;
@ -371,6 +668,9 @@ public partial class Workspaces : MSGComponentBase
if (chat is null) if (chat is null)
return; return;
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
return;
if (askForConfirmation) if (askForConfirmation)
{ {
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId); var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
@ -398,7 +698,6 @@ public partial class Workspaces : MSGComponentBase
{ {
this.CurrentChatThread = null; this.CurrentChatThread = null;
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
} }
} }
@ -407,6 +706,9 @@ public partial class Workspaces : MSGComponentBase
var chat = await this.LoadChatAsync(chatPath, false); var chat = await this.LoadChatAsync(chatPath, false);
if (chat is null) if (chat is null)
return; return;
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
return;
var dialogParameters = new DialogParameters<SingleInputDialog> var dialogParameters = new DialogParameters<SingleInputDialog>
{ {
@ -429,7 +731,6 @@ public partial class Workspaces : MSGComponentBase
{ {
this.CurrentChatThread.Name = chat.Name; this.CurrentChatThread.Name = chat.Name;
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
} }
await WorkspaceBehaviour.StoreChatAsync(chat); await WorkspaceBehaviour.StoreChatAsync(chat);
@ -452,6 +753,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmColor, Color.Info }, { x => x.ConfirmColor, Color.Info },
{ x => x.AllowEmptyInput, false }, { x => x.AllowEmptyInput, false },
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") }, { x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync(workspaceId, workspaceName) },
}; };
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Rename Workspace"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Rename Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
@ -460,9 +762,10 @@ public partial class Workspaces : MSGComponentBase
return; return;
var alteredWorkspaceName = (dialogResult.Data as string)!; var alteredWorkspaceName = (dialogResult.Data as string)!;
var workspaceNamePath = Path.Join(workspacePath, "name"); if (!await WorkspaceBehaviour.RenameWorkspaceAsync(workspaceId, alteredWorkspaceName))
await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8); return;
await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName);
await this.SendMessage(Event.WORKSPACE_RENAMED, workspaceId);
await this.LoadTreeItemsAsync(startPrefetch: false); await this.LoadTreeItemsAsync(startPrefetch: false);
} }
@ -477,6 +780,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmColor, Color.Info }, { x => x.ConfirmColor, Color.Info },
{ x => x.AllowEmptyInput, false }, { x => x.AllowEmptyInput, false },
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") }, { x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync() },
}; };
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Add Workspace"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Add Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
@ -484,14 +788,10 @@ public partial class Workspaces : MSGComponentBase
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var workspaceId = Guid.NewGuid();
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
Directory.CreateDirectory(workspacePath);
var workspaceName = (dialogResult.Data as string)!; var workspaceName = (dialogResult.Data as string)!;
var workspaceNamePath = Path.Join(workspacePath, "name"); var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(workspaceName);
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); if (!result.Success)
await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); return;
await this.LoadTreeItemsAsync(startPrefetch: false); await this.LoadTreeItemsAsync(startPrefetch: false);
} }
@ -525,6 +825,9 @@ public partial class Workspaces : MSGComponentBase
var chat = await this.LoadChatAsync(chatPath, false); var chat = await this.LoadChatAsync(chatPath, false);
if (chat is null) if (chat is null)
return; return;
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
return;
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog> var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
{ {
@ -533,7 +836,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmText, T("Move chat") }, { x => x.ConfirmText, T("Move chat") },
}; };
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
@ -549,7 +852,6 @@ public partial class Workspaces : MSGComponentBase
{ {
this.CurrentChatThread = chat; this.CurrentChatThread = chat;
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
} }
await WorkspaceBehaviour.StoreChatAsync(chat); await WorkspaceBehaviour.StoreChatAsync(chat);
@ -597,6 +899,16 @@ public partial class Workspaces : MSGComponentBase
case Event.PLUGINS_RELOADED: case Event.PLUGINS_RELOADED:
await this.ForceRefreshFromDiskAsync(); await this.ForceRefreshFromDiskAsync();
break; break;
case Event.WORKSPACE_CREATED:
await this.LoadTreeItemsAsync(startPrefetch: false);
break;
case Event.AI_JOB_CHANGED:
case Event.AI_JOB_FINISHED:
case Event.CHAT_GENERATION_CHANGED:
await this.SafeStateHasChanged();
break;
} }
} }
@ -606,9 +918,12 @@ public partial class Workspaces : MSGComponentBase
this.prefetchCancellationTokenSource?.Cancel(); this.prefetchCancellationTokenSource?.Cancel();
this.prefetchCancellationTokenSource?.Dispose(); this.prefetchCancellationTokenSource?.Dispose();
this.prefetchCancellationTokenSource = null; this.prefetchCancellationTokenSource = null;
this.searchCancellationTokenSource?.Cancel();
this.searchCancellationTokenSource?.Dispose();
this.searchCancellationTokenSource = null;
base.DisposeResources(); base.DisposeResources();
} }
#endregion #endregion
} }

View File

@ -10,7 +10,7 @@
<MudJustifiedText Class="mb-3" Typo="Typo.body1"> <MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("The name of the chat template is mandatory. Each chat template must have a unique name.") @T("The name of the chat template is mandatory. Each chat template must have a unique name.")
</MudJustifiedText> </MudJustifiedText>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues"> <MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@* ReSharper disable once CSharpWarnings::CS8974 *@ @* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField <MudTextField
@ -26,9 +26,10 @@
AdornmentColor="Color.Info" AdornmentColor="Color.Info"
Validation="@this.ValidateName" Validation="@this.ValidateName"
Variant="Variant.Outlined" Variant="Variant.Outlined"
ReadOnly="@this.IsReadOnly"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
/> />
<MudText Typo="Typo.h6" Class="mb-3 mt-3"> <MudText Typo="Typo.h6" Class="mb-3 mt-3">
@T("System Prompt") @T("System Prompt")
</MudText> </MudText>
@ -47,16 +48,17 @@
Class="mb-3" Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your system prompt.")" HelperText="@T("Tell the AI your system prompt.")"
ReadOnly="@this.IsReadOnly"
/> />
<MudJustifiedText Class="mb-3" Typo="Typo.body1"> <MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.") @T("Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.")
</MudJustifiedText> </MudJustifiedText>
<MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled"> <MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled" Disabled="@this.IsReadOnly">
@T("Use the default system prompt") @T("Use the default system prompt")
</MudButton> </MudButton>
<ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt"/> <ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt" Disabled="@this.IsReadOnly"/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6"> <MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Predefined User Input") @T("Predefined User Input")
</MudText> </MudText>
@ -77,6 +79,7 @@
Class="mb-3" Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your predefined user input.")" HelperText="@T("Tell the AI your predefined user input.")"
ReadOnly="@this.IsReadOnly"
/> />
<MudText Typo="Typo.h6" Class="mb-3 mt-6"> <MudText Typo="Typo.h6" Class="mb-3 mt-6">
@ -92,6 +95,7 @@
UseSmallForm="false" UseSmallForm="false"
CatchAllDocuments="true" CatchAllDocuments="true"
ValidateMediaFileTypes="false" ValidateMediaFileTypes="false"
Disabled="@this.IsReadOnly"
/> />
<MudText Typo="Typo.h6" Class="mb-3 mt-6"> <MudText Typo="Typo.h6" Class="mb-3 mt-6">
@ -100,8 +104,8 @@
<MudJustifiedText Class="mb-3" Typo="Typo.body1"> <MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.") @T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.")
</MudJustifiedText> </MudJustifiedText>
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" /> <MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
<MudText Typo="Typo.h6" Class="mb-3 mt-6"> <MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Example Conversation") @T("Example Conversation")
</MudText> </MudText>
@ -129,18 +133,18 @@
case ContentText textContent: case ContentText textContent:
<MudTextField AutoGrow="true" Value="@textContent.Text" Placeholder="@T("Enter a message")" ReadOnly="true" Variant="Variant.Text" Validation="@this.ValidateExampleTextMessage"/> <MudTextField AutoGrow="true" Value="@textContent.Text" Placeholder="@T("Enter a message")" ReadOnly="true" Variant="Variant.Text" Validation="@this.ValidateExampleTextMessage"/>
break; break;
case ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent: case ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent:
<MudImage Src="@imageContent.Source" Alt="@T("Image content")" Fluid="true" /> <MudImage Src="@imageContent.Source" Alt="@T("Image content")" Fluid="true" />
break; break;
default: default:
@T("Unsupported content type") @T("Unsupported content type")
break; break;
} }
</MudTd> </MudTd>
<MudTd> <MudTd>
@if (!this.isInlineEditOnGoing) @if (!this.isInlineEditOnGoing && !this.IsReadOnly)
{ {
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("Add a new message below")"> <MudTooltip Text="@T("Add a new message below")">
@ -153,22 +157,29 @@
</RowTemplate> </RowTemplate>
<RowEditingTemplate> <RowEditingTemplate>
<MudTd> <MudTd>
<MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true"> @if (this.IsReadOnly)
@foreach (var role in ChatRoles.ChatTemplateRoles()) {
{ @context.Role.ToChatTemplateName()
<MudSelectItem Value="@role"> }
@role.ToChatTemplateName() else
</MudSelectItem> {
} <MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true">
</MudSelect> @foreach (var role in ChatRoles.ChatTemplateRoles())
{
<MudSelectItem Value="@role">
@role.ToChatTemplateName()
</MudSelectItem>
}
</MudSelect>
}
</MudTd> </MudTd>
<MudTd> <MudTd>
@switch(context.Content) @switch(context.Content)
{ {
case ContentText textContent: case ContentText textContent:
<MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")"/> <MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")" ReadOnly="@this.IsReadOnly"/>
break; break;
default: default:
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
@T("Only text content is supported in the editing mode yet.") @T("Only text content is supported in the editing mode yet.")
@ -182,8 +193,8 @@
</PagerContent> </PagerContent>
</MudTable> </MudTable>
</MudForm> </MudForm>
@if (!this.isInlineEditOnGoing) @if (!this.isInlineEditOnGoing && !this.IsReadOnly)
{ {
<MudButton Class="mb-6" Color="Color.Primary" OnClick="@this.AddMessageToEnd" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled"> <MudButton Class="mb-6" Color="Color.Primary" OnClick="@this.AddMessageToEnd" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled">
@T("Add a message") @T("Add a message")
@ -193,22 +204,31 @@
<Issues IssuesData="@this.dataIssues"/> <Issues IssuesData="@this.dataIssues"/>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled"> @if (this.IsReadOnly)
@T("Cancel")
</MudButton>
@if (!this.isInlineEditOnGoing)
{ {
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary"> <MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@if (this.IsEditing) @T("Close")
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton> </MudButton>
} }
else
{
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (!this.isInlineEditOnGoing)
{
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if (this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
}
}
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>

View File

@ -16,37 +16,40 @@ public partial class ChatTemplateDialog : MSGComponentBase
/// </summary> /// </summary>
[Parameter] [Parameter]
public uint DataNum { get; set; } public uint DataNum { get; set; }
/// <summary> /// <summary>
/// The chat template's ID. /// The chat template's ID.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString(); public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary> /// <summary>
/// The chat template name chosen by the user. /// The chat template name chosen by the user.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataName { get; set; } = string.Empty; public string DataName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// What is the system prompt? /// What is the system prompt?
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataSystemPrompt { get; set; } = string.Empty; public string DataSystemPrompt { get; set; } = string.Empty;
/// <summary> /// <summary>
/// What is the predefined user prompt? /// What is the predefined user prompt?
/// </summary> /// </summary>
[Parameter] [Parameter]
public string PredefinedUserPrompt { get; set; } = string.Empty; public string PredefinedUserPrompt { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Should the dialog be in editing mode? /// Should the dialog be in editing mode?
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool IsEditing { get; init; } public bool IsEditing { get; init; }
[Parameter]
public bool IsReadOnly { get; init; }
[Parameter] [Parameter]
public IReadOnlyCollection<ContentBlock> ExampleConversation { get; init; } = []; public IReadOnlyCollection<ContentBlock> ExampleConversation { get; init; } = [];
@ -55,23 +58,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
[Parameter] [Parameter]
public bool AllowProfileUsage { get; set; } = true; public bool AllowProfileUsage { get; set; } = true;
[Parameter] [Parameter]
public bool CreateFromExistingChatThread { get; set; } public bool CreateFromExistingChatThread { get; set; }
[Parameter] [Parameter]
public ChatThread? ExistingChatThread { get; set; } public ChatThread? ExistingChatThread { get; set; }
[Inject] [Inject]
private ILogger<ChatTemplateDialog> Logger { get; init; } = null!; private ILogger<ChatTemplateDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary> /// <summary>
/// The list of used chat template names. We need this to check for uniqueness. /// The list of used chat template names. We need this to check for uniqueness.
/// </summary> /// </summary>
private List<string> UsedNames { get; set; } = []; private List<string> UsedNames { get; set; } = [];
private bool dataIsValid; private bool dataIsValid;
private List<ContentBlock> dataExampleConversation = []; private List<ContentBlock> dataExampleConversation = [];
private HashSet<FileAttachment> fileAttachments = []; private HashSet<FileAttachment> fileAttachments = [];
@ -80,20 +83,20 @@ public partial class ChatTemplateDialog : MSGComponentBase
private bool isInlineEditOnGoing; private bool isInlineEditOnGoing;
private ContentBlock? messageEntryBeforeEdit; private ContentBlock? messageEntryBeforeEdit;
// We get the form reference from Blazor code to validate it manually: // We get the form reference from Blazor code to validate it manually:
private MudForm form = null!; private MudForm form = null!;
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Configure the spellchecking for the instance name input: // Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names: // Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList(); this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data: // When editing, we need to load the data:
if(this.IsEditing) if(this.IsEditing)
{ {
@ -108,7 +111,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
this.dataExampleConversation = this.ExistingChatThread.Blocks.Select(n => n.DeepClone(true)).ToList(); this.dataExampleConversation = this.ExistingChatThread.Blocks.Select(n => n.DeepClone(true)).ToList();
this.DataName = this.ExistingChatThread.Name; this.DataName = this.ExistingChatThread.Name;
} }
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -118,7 +121,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
// We don't want to show validation errors when the user opens the dialog. // We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender) if(!this.IsEditing && firstRender)
this.form.ResetValidation(); this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
@ -128,28 +131,34 @@ public partial class ChatTemplateDialog : MSGComponentBase
{ {
Num = this.DataNum, Num = this.DataNum,
Id = this.DataId, Id = this.DataId,
Name = this.DataName, Name = this.DataName,
SystemPrompt = this.DataSystemPrompt, SystemPrompt = this.DataSystemPrompt,
PredefinedUserPrompt = this.PredefinedUserPrompt, PredefinedUserPrompt = this.PredefinedUserPrompt,
ExampleConversation = this.dataExampleConversation, ExampleConversation = this.dataExampleConversation,
FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(), FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(),
AllowProfileUsage = this.AllowProfileUsage, AllowProfileUsage = this.AllowProfileUsage,
EnterpriseConfigurationPluginId = Guid.Empty, EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false, IsEnterpriseConfiguration = false,
}; };
private void RemoveMessage(ContentBlock item) private void RemoveMessage(ContentBlock item)
{ {
if (this.IsReadOnly)
return;
this.dataExampleConversation.Remove(item); this.dataExampleConversation.Remove(item);
} }
private void AddMessageToEnd() private void AddMessageToEnd()
{ {
if (this.IsReadOnly)
return;
var newEntry = new ContentBlock var newEntry = new ContentBlock
{ {
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(), Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
Content = new ContentText(), Content = new ContentText(),
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
HideFromUser = true, HideFromUser = true,
@ -161,6 +170,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
private void AddMessageBelow(ContentBlock currentItem) private void AddMessageBelow(ContentBlock currentItem)
{ {
if (this.IsReadOnly)
return;
var insertedEntry = new ContentBlock var insertedEntry = new ContentBlock
{ {
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(), Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
@ -169,7 +181,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
HideFromUser = true, HideFromUser = true,
Time = DateTimeOffset.Now, Time = DateTimeOffset.Now,
}; };
// The rest of the method remains the same: // The rest of the method remains the same:
var index = this.dataExampleConversation.IndexOf(currentItem); var index = this.dataExampleConversation.IndexOf(currentItem);
if (index >= 0) if (index >= 0)
@ -177,71 +189,83 @@ public partial class ChatTemplateDialog : MSGComponentBase
else else
this.dataExampleConversation.Add(insertedEntry); this.dataExampleConversation.Add(insertedEntry);
} }
private void BackupItem(object? element) private void BackupItem(object? element)
{ {
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = true; this.isInlineEditOnGoing = true;
this.messageEntryBeforeEdit = element switch this.messageEntryBeforeEdit = element switch
{ {
ContentBlock block => block.DeepClone(), ContentBlock block => block.DeepClone(),
_ => null, _ => null,
}; };
this.StateHasChanged(); this.StateHasChanged();
} }
private void ResetItem(object? element) private void ResetItem(object? element)
{ {
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = false; this.isInlineEditOnGoing = false;
switch (element) switch (element)
{ {
case ContentBlock block: case ContentBlock block:
if (this.messageEntryBeforeEdit is null) if (this.messageEntryBeforeEdit is null)
return; // No backup to restore from return; // No backup to restore from
block.Content = this.messageEntryBeforeEdit.Content?.DeepClone(); block.Content = this.messageEntryBeforeEdit.Content?.DeepClone();
block.Role = this.messageEntryBeforeEdit.Role; block.Role = this.messageEntryBeforeEdit.Role;
break; break;
} }
this.StateHasChanged(); this.StateHasChanged();
} }
private void CommitInlineEdit(object? element) private void CommitInlineEdit(object? element)
{ {
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = false; this.isInlineEditOnGoing = false;
this.StateHasChanged(); this.StateHasChanged();
} }
private async Task Store() private async Task Store()
{ {
if (this.IsReadOnly)
return;
await this.form.Validate(); await this.form.Validate();
// When the data is not valid, we don't store it: // When the data is not valid, we don't store it:
if (!this.dataIsValid) if (!this.dataIsValid)
return; return;
// When an inline edit is ongoing, we cannot store the data: // When an inline edit is ongoing, we cannot store the data:
if (this.isInlineEditOnGoing) if (this.isInlineEditOnGoing)
return; return;
// Use the data model to store the chat template. // Use the data model to store the chat template.
// We just return this data to the parent component: // We just return this data to the parent component:
var addedChatTemplateSettings = this.CreateChatTemplateSettings(); var addedChatTemplateSettings = this.CreateChatTemplateSettings();
if(this.IsEditing) if(this.IsEditing)
this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'."); this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'.");
else else
this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'."); this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings)); this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings));
} }
private string? ValidateExampleTextMessage(string message) private string? ValidateExampleTextMessage(string message)
{ {
if (string.IsNullOrWhiteSpace(message)) if (string.IsNullOrWhiteSpace(message))
return T("Please enter a message for the example conversation."); return T("Please enter a message for the example conversation.");
return null; return null;
} }
@ -249,20 +273,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
return T("Please enter a name for the chat template."); return T("Please enter a name for the chat template.");
if (name.Length > 40) if (name.Length > 40)
return T("The chat template name must not exceed 40 characters."); return T("The chat template name must not exceed 40 characters.");
// The instance name must be unique: // The instance name must be unique:
var lowerName = name.ToLowerInvariant(); var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName)) if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return T("The chat template name must be unique; the chosen name is already in use."); return T("The chat template name must be unique; the chosen name is already in use.");
return null; return null;
} }
private void UseDefaultSystemPrompt() private void UseDefaultSystemPrompt()
{ {
if (this.IsReadOnly)
return;
this.DataSystemPrompt = SystemPrompts.DEFAULT; this.DataSystemPrompt = SystemPrompts.DEFAULT;
} }

View File

@ -0,0 +1,26 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudText Typo="Typo.body1" Class="mb-3">
@string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name)
</MudText>
<MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start">
@foreach (var mode in this.availableUsernamePasswordModes)
{
<MudSelectItem Value="@mode">
@this.GetUsernamePasswordModeText(mode)
</MudSelectItem>
}
</MudSelect>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary">
@T("Export")
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -0,0 +1,37 @@
using AIStudio.Components;
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class DataSourceERIV1UsernamePasswordExportDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public DataSourceERI_V1 DataSource { get; set; }
private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes =
[
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD,
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD
];
private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode);
private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch
{
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"),
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"),
_ => T("User-managed username and password"),
};
private void Cancel() => this.MudDialog.Cancel();
private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1UsernamePasswordExportDialogResult(this.usernamePasswordMode)));
}

View File

@ -0,0 +1,5 @@
using AIStudio.Settings.DataModel;
namespace AIStudio.Dialogs;
public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode);

View File

@ -116,7 +116,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD) if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
{ {
// Load the secret: // Load the secret:
var requestedSecret = await this.RustService.GetSecret(this); var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE);
if (requestedSecret.Success) if (requestedSecret.Success)
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption); this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
else else
@ -169,6 +169,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
AuthMethod = this.dataAuthMethod, AuthMethod = this.dataAuthMethod,
Username = this.dataUsername, Username = this.dataUsername,
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED,
Type = DataSourceType.ERI_V1, Type = DataSourceType.ERI_V1,
SecurityPolicy = this.dataSecurityPolicy, SecurityPolicy = this.dataSecurityPolicy,
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id, SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
@ -323,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
if (!string.IsNullOrWhiteSpace(this.dataSecret)) if (!string.IsNullOrWhiteSpace(this.dataSecret))
{ {
// Store the secret in the OS secure storage: // Store the secret in the OS secure storage:
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret); var storeResponse = await this.RustService.SetSecret(this, this.dataSecret, SecretStoreType.DATA_SOURCE);
if (!storeResponse.Success) if (!storeResponse.Success)
{ {
this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue); this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue);

View File

@ -21,7 +21,7 @@
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) @if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
{ {
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.DataSource.Username" ClipboardTooltipSubject="@T("the username")"/> <TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.effectiveUsername" ClipboardTooltipSubject="@T("the username")"/>
} }
<TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/> <TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/>

View File

@ -41,6 +41,7 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
private readonly List<string> dataIssues = []; private readonly List<string> dataIssues = [];
private string serverDescription = string.Empty; private string serverDescription = string.Empty;
private string effectiveUsername = string.Empty;
private ProviderType securityRequirements = ProviderType.NONE; private ProviderType securityRequirements = ProviderType.NONE;
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = []; private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
private RetrievalInfo selectedRetrievalInfo; private RetrievalInfo selectedRetrievalInfo;
@ -51,6 +52,27 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}"; private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
private async Task<(bool Success, DataSourceERI_V1 EffectiveDataSource)> CreateEffectiveDataSource()
{
this.effectiveUsername = this.DataSource.Username;
if (this.DataSource is not { AuthMethod: AuthMethod.USERNAME_PASSWORD, UsernamePasswordMode: DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD })
return (true, this.DataSource);
var osUsername = await this.RustService.ReadUserName();
if (string.IsNullOrWhiteSpace(osUsername))
{
this.dataIssues.Add(T("Failed to read the user's username from the operating system."));
return (false, this.DataSource);
}
this.effectiveUsername = osUsername;
return (true, this.DataSource with
{
Username = osUsername,
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD,
});
}
private string RetrievalName(RetrievalInfo retrievalInfo) private string RetrievalName(RetrievalInfo retrievalInfo)
{ {
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id); var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
@ -91,15 +113,19 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
{ {
this.IsOperationInProgress = true; this.IsOperationInProgress = true;
this.StateHasChanged(); this.StateHasChanged();
var effectiveDataSourceResult = await this.CreateEffectiveDataSource();
if (!effectiveDataSourceResult.Success)
return;
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource); using var client = ERIClientFactory.Get(ERIVersion.V1, effectiveDataSourceResult.EffectiveDataSource);
if(client is null) if(client is null)
{ {
this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported.")); this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported."));
return; return;
} }
var loginResult = await client.AuthenticateAsync(this.RustService); var loginResult = await client.AuthenticateAsync(this.RustService, cancellationToken: this.cts.Token);
if (!loginResult.Successful) if (!loginResult.Successful)
{ {
this.dataIssues.Add(loginResult.Message); this.dataIssues.Add(loginResult.Message);

View File

@ -96,7 +96,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
#endregion #endregion
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false; private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
private DataSourceLocalDirectory CreateDataSource() => new() private DataSourceLocalDirectory CreateDataSource() => new()
{ {

View File

@ -56,7 +56,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsy
private bool IsOperationInProgress { get; set; } = true; private bool IsOperationInProgress { get; set; } = true;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
private bool IsDirectoryAvailable => this.directoryInfo.Exists; private bool IsDirectoryAvailable => this.directoryInfo.Exists;

View File

@ -96,7 +96,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
#endregion #endregion
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false; private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
private DataSourceLocalFile CreateDataSource() => new() private DataSourceLocalFile CreateDataSource() => new()
{ {

View File

@ -28,7 +28,7 @@ public partial class DataSourceLocalFileInfoDialog : MSGComponentBase
private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE; private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE;
private FileInfo fileInfo = null!; private FileInfo fileInfo = null!;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
private bool IsFileAvailable => this.fileInfo.Exists; private bool IsFileAvailable => this.fileInfo.Exists;

View File

@ -7,6 +7,12 @@ public static class DialogOptions
CloseOnEscapeKey = true, CloseOnEscapeKey = true,
FullWidth = true, MaxWidth = MaxWidth.Medium, FullWidth = true, MaxWidth = MaxWidth.Medium,
}; };
public static readonly MudBlazor.DialogOptions FULLSCREEN_MANUAL_ESCAPE = new()
{
CloseOnEscapeKey = false,
FullWidth = true, MaxWidth = MaxWidth.Medium,
};
public static readonly MudBlazor.DialogOptions FULLSCREEN_NO_HEADER = new() public static readonly MudBlazor.DialogOptions FULLSCREEN_NO_HEADER = new()
{ {

View File

@ -203,7 +203,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId #region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName(); public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName; public string SecretName => this.DataName;

View File

@ -27,6 +27,7 @@
AdornmentColor="Color.Info" AdornmentColor="Color.Info"
Validation="@this.ValidateName" Validation="@this.ValidateName"
Variant="Variant.Outlined" Variant="Variant.Outlined"
ReadOnly="@this.IsReadOnly"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
/> />
@ -44,8 +45,9 @@
MaxLines="12" MaxLines="12"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")" HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")"
ReadOnly="@this.IsReadOnly"
/> />
<ReadFileContent @bind-FileContent="@this.DataNeedToKnow"/> <ReadFileContent @bind-FileContent="@this.DataNeedToKnow" Disabled="@this.IsReadOnly"/>
<MudTextField <MudTextField
T="string" T="string"
@ -62,8 +64,9 @@
Class="mt-10" Class="mt-10"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.")" HelperText="@T("Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.")"
ReadOnly="@this.IsReadOnly"
/> />
<ReadFileContent @bind-FileContent="@this.DataActions"/> <ReadFileContent @bind-FileContent="@this.DataActions" Disabled="@this.IsReadOnly"/>
<MudJustifiedText Typo="Typo.body2" Class="mb-3 mt-3"> <MudJustifiedText Typo="Typo.body2" Class="mb-3 mt-3">
@T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.") @T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.")
@ -73,18 +76,27 @@
<Issues IssuesData="@this.dataIssues"/> <Issues IssuesData="@this.dataIssues"/>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled"> @if (this.IsReadOnly)
@T("Cancel") {
</MudButton> <MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary"> @T("Close")
@if(this.IsEditing) </MudButton>
{ }
@T("Update") else
} {
else <MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
{ @T("Cancel")
@T("Add") </MudButton>
} <MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
</MudButton> @if(this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
}
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>

View File

@ -15,19 +15,19 @@ public partial class ProfileDialog : MSGComponentBase
/// </summary> /// </summary>
[Parameter] [Parameter]
public uint DataNum { get; set; } public uint DataNum { get; set; }
/// <summary> /// <summary>
/// The profile's ID. /// The profile's ID.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString(); public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary> /// <summary>
/// The profile name chosen by the user. /// The profile name chosen by the user.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataName { get; set; } = string.Empty; public string DataName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// What should the LLM know about you? /// What should the LLM know about you?
/// </summary> /// </summary>
@ -39,27 +39,30 @@ public partial class ProfileDialog : MSGComponentBase
/// </summary> /// </summary>
[Parameter] [Parameter]
public string DataActions { get; set; } = string.Empty; public string DataActions { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Should the dialog be in editing mode? /// Should the dialog be in editing mode?
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool IsEditing { get; init; } public bool IsEditing { get; init; }
[Parameter]
public bool IsReadOnly { get; init; }
[Inject] [Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!; private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary> /// <summary>
/// The list of used profile names. We need this to check for uniqueness. /// The list of used profile names. We need this to check for uniqueness.
/// </summary> /// </summary>
private List<string> UsedNames { get; set; } = []; private List<string> UsedNames { get; set; } = [];
private bool dataIsValid; private bool dataIsValid;
private string[] dataIssues = []; private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty; private string dataEditingPreviousName = string.Empty;
// We get the form reference from Blazor code to validate it manually: // We get the form reference from Blazor code to validate it manually:
private MudForm form = null!; private MudForm form = null!;
@ -70,7 +73,7 @@ public partial class ProfileDialog : MSGComponentBase
Name = this.DataName, Name = this.DataName,
NeedToKnow = this.DataNeedToKnow, NeedToKnow = this.DataNeedToKnow,
Actions = this.DataActions, Actions = this.DataActions,
EnterpriseConfigurationPluginId = Guid.Empty, EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false, IsEnterpriseConfiguration = false,
}; };
@ -81,16 +84,16 @@ public partial class ProfileDialog : MSGComponentBase
{ {
// Configure the spellchecking for the instance name input: // Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names: // Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList(); this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data: // When editing, we need to load the data:
if(this.IsEditing) if(this.IsEditing)
{ {
this.dataEditingPreviousName = this.DataName.ToLowerInvariant(); this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
} }
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -100,37 +103,40 @@ public partial class ProfileDialog : MSGComponentBase
// We don't want to show validation errors when the user opens the dialog. // We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender) if(!this.IsEditing && firstRender)
this.form.ResetValidation(); this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
#endregion #endregion
private async Task Store() private async Task Store()
{ {
if (this.IsReadOnly)
return;
await this.form.Validate(); await this.form.Validate();
// When the data is not valid, we don't store it: // When the data is not valid, we don't store it:
if (!this.dataIsValid) if (!this.dataIsValid)
return; return;
// Use the data model to store the profile. // Use the data model to store the profile.
// We just return this data to the parent component: // We just return this data to the parent component:
var addedProfileSettings = this.CreateProfileSettings(); var addedProfileSettings = this.CreateProfileSettings();
if(this.IsEditing) if(this.IsEditing)
this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'."); this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'.");
else else
this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'."); this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedProfileSettings)); this.MudDialog.Close(DialogResult.Ok(addedProfileSettings));
} }
private string? ValidateNeedToKnow(string text) private string? ValidateNeedToKnow(string text)
{ {
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions)) if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take."); return T("Please enter what the LLM should know about you and/or what actions it should take.");
return null; return null;
} }
@ -138,7 +144,7 @@ public partial class ProfileDialog : MSGComponentBase
{ {
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions)) if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take."); return T("Please enter what the LLM should know about you and/or what actions it should take.");
return null; return null;
} }
@ -146,15 +152,15 @@ public partial class ProfileDialog : MSGComponentBase
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
return T("Please enter a profile name."); return T("Please enter a profile name.");
if (name.Length > 40) if (name.Length > 40)
return T("The profile name must not exceed 40 characters."); return T("The profile name must not exceed 40 characters.");
// The instance name must be unique: // The instance name must be unique:
var lowerName = name.ToLowerInvariant(); var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName)) if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return T("The profile name must be unique; the chosen name is already in use."); return T("The profile name must be unique; the chosen name is already in use.");
return null; return null;
} }

View File

@ -71,7 +71,7 @@
@* ReSharper restore Asp.Entity *@ @* ReSharper restore Asp.Entity *@
} }
@if (!this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) @if (!this.IsLLMModelSelectionHidden)
{ {
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3"> <MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End"> <MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">

View File

@ -104,6 +104,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
private string dataAPIKeyStorageIssue = string.Empty; private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty; private string dataEditingPreviousInstanceName = string.Empty;
private string dataLoadingModelsIssue = string.Empty; private string dataLoadingModelsIssue = string.Empty;
private bool usesLegacySystemModelFallback;
private bool showExpertSettings; private bool showExpertSettings;
// We get the form reference from Blazor code to validate it manually: // We get the form reference from Blazor code to validate it manually:
@ -123,6 +124,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
GetUsedInstanceNames = () => this.UsedInstanceNames, GetUsedInstanceNames = () => this.UsedInstanceNames,
GetHost = () => this.DataHost, GetHost = () => this.DataHost,
IsModelProvidedManually = () => this.DataLLMProvider.IsLLMModelProvidedManually(), IsModelProvidedManually = () => this.DataLLMProvider.IsLLMModelProvidedManually(),
IsModelSelectionHidden = () => this.IsLLMModelSelectionHidden,
}; };
} }
@ -132,9 +134,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
// Determine the model based on the provider and host configuration: // Determine the model based on the provider and host configuration:
Model model; Model model;
if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) if (this.IsLLMModelSelectionHidden)
{ {
// Use system model placeholder for hosts that don't support model selection (e.g., llama.cpp): // Use system model placeholder for legacy hosts that don't support model selection:
model = Model.SYSTEM_MODEL; model = Model.SYSTEM_MODEL;
} }
else if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) else if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE)
@ -229,7 +231,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId #region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName(); public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataInstanceName; public string SecretName => this.DataInstanceName;
@ -300,6 +302,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
this.dataManuallyModel = string.Empty; this.dataManuallyModel = string.Empty;
this.availableModels.Clear(); this.availableModels.Clear();
this.dataLoadingModelsIssue = string.Empty; this.dataLoadingModelsIssue = string.Empty;
this.usesLegacySystemModelFallback = false;
} }
private async Task ReloadModels() private async Task ReloadModels()
@ -321,6 +324,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
this.availableModels.Clear(); this.availableModels.Clear();
this.availableModels.AddRange(orderedModels); this.availableModels.AddRange(orderedModels);
this.UpdateModelSelectionAfterLoading();
} }
catch (Exception e) catch (Exception e)
{ {
@ -334,6 +338,34 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
LLMProviders.SELF_HOSTED => T("(Optional) API Key"), LLMProviders.SELF_HOSTED => T("(Optional) API Key"),
_ => T("API Key"), _ => T("API Key"),
}; };
private bool IsLLMModelSelectionHidden => this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost) ||
this.DataLLMProvider is LLMProviders.SELF_HOSTED &&
this.DataHost is Host.LLAMA_CPP &&
this.usesLegacySystemModelFallback;
private void UpdateModelSelectionAfterLoading()
{
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP)
return;
this.usesLegacySystemModelFallback = this.availableModels.Count is 1 && this.availableModels[0].IsSystemModel;
if (this.usesLegacySystemModelFallback)
{
this.DataModel = Model.SYSTEM_MODEL;
return;
}
var availableModel = this.availableModels.FirstOrDefault(model =>
string.Equals(model.Id, this.DataModel.Id, StringComparison.OrdinalIgnoreCase));
if (availableModel != default)
{
this.DataModel = availableModel;
return;
}
this.DataModel = this.availableModels.Count is 1 ? this.availableModels[0] : default;
}
private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings; private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings;

View File

@ -18,6 +18,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
[Inject] [Inject]
protected RustService RustService { get; init; } = null!; protected RustService RustService { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
protected readonly List<ConfigurationSelectData<string>> AvailableLLMProviders = new(); protected readonly List<ConfigurationSelectData<string>> AvailableLLMProviders = new();
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new(); protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();
@ -62,6 +65,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
switch (triggeredEvent) switch (triggeredEvent)
{ {
case Event.CONFIGURATION_CHANGED: case Event.CONFIGURATION_CHANGED:
case Event.PLUGINS_RELOADED:
this.UpdateProviders();
this.UpdateEmbeddingProviders();
this.StateHasChanged(); this.StateHasChanged();
break; break;
} }

View File

@ -16,10 +16,10 @@
<ConfigurationSelect OptionDescription="@T("Provider selection when loading a chat and sending assistant results to chat")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetLoadingChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for loaded chats is selected and when assistant results are sent to chat.")"/> <ConfigurationSelect OptionDescription="@T("Provider selection when loading a chat and sending assistant results to chat")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetLoadingChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for loaded chats is selected and when assistant results are sent to chat.")"/>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")"/> <ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/> <ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")"/> <ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")"/> <ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
</MudPaper> </MudPaper>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))

View File

@ -33,9 +33,14 @@
<MudTd> <MudTd>
@if (context.IsEnterpriseConfiguration) @if (context.IsEnterpriseConfiguration)
{ {
<MudTooltip Text="@T("This template is managed by your organization.")"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/> <MudTooltip Text="@T("This template is managed by your organization.")">
</MudTooltip> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudTooltip Text="@T("View")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="@(() => this.ViewChatTemplate(context))"/>
</MudTooltip>
</MudStack>
} }
else else
{ {
@ -43,6 +48,28 @@
<MudTooltip Text="@T("Edit")"> <MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
</MudTooltip> </MudTooltip>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
{
@if (context.FileAttachments.Count == 0)
{
<MudTooltip Text="@T("Export configuration")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
</MudTooltip>
}
else
{
<MudTooltip Text="@T("Export configuration")">
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
@T("Use shared attachment paths")
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Folder" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
@T("Copy attachments into plugin")
</MudMenuItem>
</MudMenu>
</MudTooltip>
}
}
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
</MudTooltip> </MudTooltip>

View File

@ -6,24 +6,24 @@ namespace AIStudio.Dialogs.Settings;
public partial class SettingsDialogChatTemplate : SettingsDialogBase public partial class SettingsDialogChatTemplate : SettingsDialogBase
{ {
[Parameter] [Parameter]
public bool CreateTemplateFromExistingChatThread { get; set; } public bool CreateTemplateFromExistingChatThread { get; set; }
[Parameter] [Parameter]
public ChatThread? ExistingChatThread { get; set; } public ChatThread? ExistingChatThread { get; set; }
#region Overrides of ComponentBase #region Overrides of ComponentBase
/// <inheritdoc /> /// <inheritdoc />
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
if (this.CreateTemplateFromExistingChatThread) if (this.CreateTemplateFromExistingChatThread)
await this.AddChatTemplate(); await this.AddChatTemplate();
} }
#endregion #endregion
private async Task AddChatTemplate() private async Task AddChatTemplate()
{ {
var dialogParameters = new DialogParameters<ChatTemplateDialog> var dialogParameters = new DialogParameters<ChatTemplateDialog>
@ -41,21 +41,21 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var addedChatTemplate = (ChatTemplate)dialogResult.Data!; var addedChatTemplate = (ChatTemplate)dialogResult.Data!;
addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ }; addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ };
this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate); this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate);
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task EditChatTemplate(ChatTemplate chatTemplate) private async Task EditChatTemplate(ChatTemplate chatTemplate)
{ {
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
return; return;
var dialogParameters = new DialogParameters<ChatTemplateDialog> var dialogParameters = new DialogParameters<ChatTemplateDialog>
{ {
{ x => x.DataNum, chatTemplate.Num }, { x => x.DataNum, chatTemplate.Num },
@ -68,34 +68,115 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
{ x => x.FileAttachments, chatTemplate.FileAttachments }, { x => x.FileAttachments, chatTemplate.FileAttachments },
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage }, { x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
}; };
var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var editedChatTemplate = (ChatTemplate)dialogResult.Data!; var editedChatTemplate = (ChatTemplate)dialogResult.Data!;
this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate; this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate;
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ViewChatTemplate(ChatTemplate chatTemplate)
{
var dialogParameters = new DialogParameters<ChatTemplateDialog>
{
{ x => x.DataNum, chatTemplate.Num },
{ x => x.DataId, chatTemplate.Id },
{ x => x.DataName, chatTemplate.Name },
{ x => x.DataSystemPrompt, chatTemplate.SystemPrompt },
{ x => x.PredefinedUserPrompt, chatTemplate.PredefinedUserPrompt },
{ x => x.IsEditing, true },
{ x => x.IsReadOnly, true },
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
{ x => x.FileAttachments, chatTemplate.FileAttachments },
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
};
await this.DialogService.ShowAsync<ChatTemplateDialog>(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task DeleteChatTemplate(ChatTemplate chatTemplate) private async Task DeleteChatTemplate(ChatTemplate chatTemplate)
{ {
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
{ {
{ x => x.Message, string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) }, { x => x.Message, string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) },
}; };
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate); this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate);
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ExportChatTemplateWithSharedAttachmentPaths(ChatTemplate chatTemplate)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
return;
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
}
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
return;
if (chatTemplate.FileAttachments.Count == 0)
{
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
return;
}
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
if (pluginDirectoryResponse.UserCancelled)
return;
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
}
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
{
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
{
await this.DialogService.ShowMessageBox(
T("Export Chat Template"),
issue,
T("Close"));
return;
}
if (!string.IsNullOrWhiteSpace(luaCode))
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
}
private async Task CopyPackagedChatTemplateLuaToClipboard(ChatTemplate chatTemplate, string pluginDirectory)
{
if (!chatTemplate.TryExportAsConfigurationSectionWithPackagedAttachments(pluginDirectory, out var luaCode, out var issue))
{
await this.DialogService.ShowMessageBox(
T("Export Chat Template"),
issue,
T("Close"));
return;
}
if (!string.IsNullOrWhiteSpace(luaCode))
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
}
} }

View File

@ -38,12 +38,27 @@
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/> <MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)"> @if (context.IsEnterpriseConfiguration)
@T("Edit") {
</MudButton> <MudTooltip Text="@T("This data source is managed by your organization.")">
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)"> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
@T("Delete") </MudTooltip>
</MudButton> }
else
{
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
@T("Edit")
</MudButton>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
{
<MudTooltip Text="@T("Export configuration")">
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
</MudTooltip>
}
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
@T("Delete")
</MudButton>
}
</MudStack> </MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>

View File

@ -1,6 +1,7 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Dialogs.Settings; namespace AIStudio.Dialogs.Settings;
@ -86,9 +87,106 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ExportDataSource(IDataSource dataSource)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (dataSource is not DataSourceERI_V1 eriDataSource)
return;
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
{
await this.DialogService.ShowMessageBox(
T("Export ERI Data Source"),
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
T("Close"));
return;
}
var needsSecret = eriDataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
if (!needsSecret)
{
var publicLuaCode = eriDataSource.ExportAsConfigurationSection();
if (!string.IsNullOrWhiteSpace(publicLuaCode))
await this.RustService.CopyText2Clipboard(this.Snackbar, publicLuaCode);
return;
}
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
if (!secretResponse.Success)
{
await this.DialogService.ShowMessageBox(
T("Export ERI Data Source"),
string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue),
T("Close"));
return;
}
var encryption = PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable != true)
{
await this.DialogService.ShowMessageBox(
T("Export ERI Data Source"),
T("Cannot export this ERI data source because no enterprise encryption secret is configured."),
T("Close"));
return;
}
var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED;
if (eriDataSource.AuthMethod is AuthMethod.TOKEN)
{
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, T("This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.") },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Access Token?"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
}
else if (eriDataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
{
var dialogParameters = new DialogParameters<DataSourceERIV1UsernamePasswordExportDialog>
{
{ x => x.DataSource, eriDataSource },
};
var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1UsernamePasswordExportDialog>(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1UsernamePasswordExportDialogResult exportResult)
return;
usernamePasswordMode = exportResult.UsernamePasswordMode;
}
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret))
{
await this.DialogService.ShowMessageBox(
T("Export ERI Data Source"),
T("Cannot export this ERI data source because the authentication secret could not be encrypted."),
T("Close"));
return;
}
var luaCode = eriDataSource.ExportAsConfigurationSection(
encryptedSecret,
usernamePasswordMode);
if (string.IsNullOrWhiteSpace(luaCode))
return;
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
}
private async Task EditDataSource(IDataSource dataSource) private async Task EditDataSource(IDataSource dataSource)
{ {
if (dataSource.IsEnterpriseConfiguration)
return;
IDataSource? editedDataSource = null; IDataSource? editedDataSource = null;
switch (dataSource) switch (dataSource)
{ {
@ -151,6 +249,9 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
private async Task DeleteDataSource(IDataSource dataSource) private async Task DeleteDataSource(IDataSource dataSource)
{ {
if (dataSource.IsEnterpriseConfiguration)
return;
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
{ {
{ x => x.Message, string.Format(T("Are you sure you want to delete the data source '{0}' of type {1}?"), dataSource.Name, dataSource.Type.GetDisplayName()) }, { x => x.Message, string.Format(T("Are you sure you want to delete the data source '{0}' of type {1}?"), dataSource.Name, dataSource.Type.GetDisplayName()) },
@ -174,7 +275,7 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
// All other auth methods require a secret, which we need to delete now: // All other auth methods require a secret, which we need to delete now:
else else
{ {
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource); var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE);
if (deleteSecretResponse.Success) if (deleteSecretResponse.Success)
applyChanges = true; applyChanges = true;
} }

View File

@ -32,9 +32,14 @@
<MudTd> <MudTd>
@if (context.IsEnterpriseConfiguration) @if (context.IsEnterpriseConfiguration)
{ {
<MudTooltip Text="@T("This profile is managed by your organization.")"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/> <MudTooltip Text="@T("This profile is managed by your organization.")">
</MudTooltip> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudTooltip Text="@T("View")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="() => this.ViewProfile(context)"/>
</MudTooltip>
</MudStack>
} }
else else
{ {
@ -42,6 +47,12 @@
<MudTooltip Text="@T("Edit")"> <MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
</MudTooltip> </MudTooltip>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
{
<MudTooltip Text="@T("Export configuration")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
</MudTooltip>
}
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
</MudTooltip> </MudTooltip>

View File

@ -10,21 +10,21 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
{ {
{ x => x.IsEditing, false }, { x => x.IsEditing, false },
}; };
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Add Profile"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Add Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var addedProfile = (Profile)dialogResult.Data!; var addedProfile = (Profile)dialogResult.Data!;
addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ }; addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ };
this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile); this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile);
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task EditProfile(Profile profile) private async Task EditProfile(Profile profile)
{ {
var dialogParameters = new DialogParameters<ProfileDialog> var dialogParameters = new DialogParameters<ProfileDialog>
@ -36,34 +36,63 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
{ x => x.DataActions, profile.Actions }, { x => x.DataActions, profile.Actions },
{ x => x.IsEditing, true }, { x => x.IsEditing, true },
}; };
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Edit Profile"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Edit Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var editedProfile = (Profile)dialogResult.Data!; var editedProfile = (Profile)dialogResult.Data!;
this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile; this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile;
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ViewProfile(Profile profile)
{
var dialogParameters = new DialogParameters<ProfileDialog>
{
{ x => x.DataNum, profile.Num },
{ x => x.DataId, profile.Id },
{ x => x.DataName, profile.Name },
{ x => x.DataNeedToKnow, profile.NeedToKnow },
{ x => x.DataActions, profile.Actions },
{ x => x.IsEditing, true },
{ x => x.IsReadOnly, true },
};
await this.DialogService.ShowAsync<ProfileDialog>(T("View Profile"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task ExportProfile(Profile profile)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (profile == Profile.NO_PROFILE || profile.IsEnterpriseConfiguration)
return;
var luaCode = profile.ExportAsConfigurationSection();
if (!string.IsNullOrWhiteSpace(luaCode))
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
}
private async Task DeleteProfile(Profile profile) private async Task DeleteProfile(Profile profile)
{ {
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
{ {
{ x => x.Message, string.Format(T("Are you sure you want to delete the profile '{0}'?"), profile.Name) }, { x => x.Message, string.Format(T("Are you sure you want to delete the profile '{0}'?"), profile.Name) },
}; };
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Profile"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
this.SettingsManager.ConfigurationData.Profiles.Remove(profile); this.SettingsManager.ConfigurationData.Profiles.Remove(profile);
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
} }

View File

@ -34,6 +34,7 @@ public partial class ShortcutDialog : MSGComponentBase
private string currentShortcut = string.Empty; private string currentShortcut = string.Empty;
private string originalShortcut = string.Empty; private string originalShortcut = string.Empty;
private string currentDisplayName = string.Empty;
private string validationMessage = string.Empty; private string validationMessage = string.Empty;
private Severity validationSeverity = Severity.Info; private Severity validationSeverity = Severity.Info;
private bool hasValidationError; private bool hasValidationError;
@ -115,6 +116,7 @@ public partial class ShortcutDialog : MSGComponentBase
{ {
this.UpdateModifiers(e); this.UpdateModifiers(e);
this.currentKey = null; this.currentKey = null;
this.currentDisplayName = string.Empty;
this.UpdateShortcutString(); this.UpdateShortcutString();
return; return;
} }
@ -123,10 +125,12 @@ public partial class ShortcutDialog : MSGComponentBase
// Get the key: // Get the key:
this.currentKey = TranslateKeyCode(e.Code); this.currentKey = TranslateKeyCode(e.Code);
this.currentDisplayName = this.BuildDisplayShortcut(e.Key);
// Validate: must have at least one modifier + a key // Validate: must have at least one modifier + a key
if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta) if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta)
{ {
this.currentDisplayName = string.Empty;
this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."); this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).");
this.validationSeverity = Severity.Warning; this.validationSeverity = Severity.Warning;
this.hasValidationError = true; this.hasValidationError = true;
@ -216,6 +220,9 @@ public partial class ShortcutDialog : MSGComponentBase
private string GetDisplayShortcut() private string GetDisplayShortcut()
{ {
if (!string.IsNullOrWhiteSpace(this.currentDisplayName))
return this.currentDisplayName;
// Convert internal format to display format: // Convert internal format to display format:
return this.currentShortcut return this.currentShortcut
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl") .Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
@ -225,6 +232,7 @@ public partial class ShortcutDialog : MSGComponentBase
private void ClearShortcut() private void ClearShortcut()
{ {
this.currentShortcut = string.Empty; this.currentShortcut = string.Empty;
this.currentDisplayName = string.Empty;
this.currentKey = null; this.currentKey = null;
this.hasCtrl = false; this.hasCtrl = false;
this.hasShift = false; this.hasShift = false;
@ -237,7 +245,17 @@ public partial class ShortcutDialog : MSGComponentBase
private void Cancel() => this.MudDialog.Cancel(); private void Cancel() => this.MudDialog.Cancel();
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut)); private void Confirm()
{
var displaySource = string.IsNullOrWhiteSpace(this.currentDisplayName)
? string.Empty
: this.currentShortcut;
this.MudDialog.Close(DialogResult.Ok(new ShortcutDialogResult(
this.currentShortcut,
this.currentDisplayName,
displaySource)));
}
/// <summary> /// <summary>
/// Checks if the key code represents a modifier key. /// Checks if the key code represents a modifier key.
@ -377,6 +395,36 @@ public partial class ShortcutDialog : MSGComponentBase
_ => code, _ => code,
}; };
private string BuildDisplayShortcut(string? key)
{
var displayKey = GetDisplayKey(key);
if (string.IsNullOrWhiteSpace(displayKey))
return string.Empty;
var parts = new List<string>();
if (this.hasCtrl)
parts.Add(OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
if (this.hasShift)
parts.Add("Shift");
if (this.hasAlt)
parts.Add("Alt");
parts.Add(displayKey);
return string.Join("+", parts);
}
private static string GetDisplayKey(string? key) => key switch
{
null or "" => string.Empty,
" " => "Space",
"Control" or "Shift" or "Alt" or "Meta" => string.Empty,
_ when key.Length == 1 && key[0] >= 'a' && key[0] <= 'z' => key.ToUpperInvariant(),
_ => key,
};
private void HandleBlur() private void HandleBlur()
{ {
// Re-focus the input field to keep capturing keys: // Re-focus the input field to keep capturing keys:

View File

@ -0,0 +1,3 @@
namespace AIStudio.Dialogs;
public readonly record struct ShortcutDialogResult(string Shortcut, string DisplayName, string DisplaySource);

View File

@ -31,6 +31,9 @@ public partial class SingleInputDialog : MSGComponentBase
[Parameter] [Parameter]
public string EmptyInputErrorMessage { get; set; } = string.Empty; public string EmptyInputErrorMessage { get; set; } = string.Empty;
[Parameter]
public Func<string?, string?>? AdditionalValidation { get; set; }
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private MudForm form = null!; private MudForm form = null!;
@ -52,8 +55,8 @@ public partial class SingleInputDialog : MSGComponentBase
{ {
if (!this.AllowEmptyInput && string.IsNullOrWhiteSpace(value)) if (!this.AllowEmptyInput && string.IsNullOrWhiteSpace(value))
return string.IsNullOrWhiteSpace(this.EmptyInputErrorMessage) ? T("Please enter a value.") : this.EmptyInputErrorMessage; return string.IsNullOrWhiteSpace(this.EmptyInputErrorMessage) ? T("Please enter a value.") : this.EmptyInputErrorMessage;
return null; return this.AdditionalValidation?.Invoke(value);
} }
private void Cancel() => this.MudDialog.Cancel(); private void Cancel() => this.MudDialog.Cancel();

View File

@ -218,7 +218,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId #region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName(); public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName; public string SecretName => this.DataName;

View File

@ -5,18 +5,57 @@
@this.Message @this.Message
</MudText> </MudText>
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace"> <MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
@foreach (var (workspaceName, workspaceId) in this.workspaces) @foreach (var workspace in this.workspaces)
{ {
<MudListItem Text="@workspaceName" Icon="@Icons.Material.Filled.Description" Value="@workspaceId" /> <MudListItem Text="@workspace.Name" Icon="@Icons.Material.Filled.Description" Value="@workspace.WorkspaceId" />
} }
</MudList> </MudList>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled"> <MudStack Style="width: 100%;" Spacing="2">
@T("Cancel") <MudDivider/>
</MudButton>
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info"> @if (this.showCreateWorkspaceForm)
@this.ConfirmText {
</MudButton> <MudForm @ref="this.createWorkspaceForm">
<MudTextField T="string"
@ref="@this.newWorkspaceNameField"
@bind-Text="@this.newWorkspaceName"
Variant="Variant.Outlined"
AutoGrow="@false"
Lines="1"
Label="@T("Workspace name")"
AutoFocus="@true"
Immediate="@true"
Disabled="@this.isCreatingWorkspace"
OnKeyDown="@this.HandleNewWorkspaceNameKeyDown"
Validation="@this.ValidateNewWorkspaceName" />
</MudForm>
}
else
{
<MudButton StartIcon="@Icons.Material.Filled.LibraryAdd" Variant="Variant.Filled" OnClick="@this.ShowCreateWorkspaceForm">
@T("Create new workspace")
</MudButton>
}
<MudStack Row="@true" Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="2">
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (this.showCreateWorkspaceForm)
{
<MudButton OnClick="@this.CreateWorkspaceAsync" Variant="Variant.Filled" Color="Color.Info" Disabled="@this.isCreatingWorkspace">
@T("Add workspace")
</MudButton>
}
else
{
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info" Disabled="@(this.selectedWorkspace == Guid.Empty)">
@this.ConfirmText
</MudButton>
}
</MudStack>
</MudStack>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>

View File

@ -1,14 +1,20 @@
using AIStudio.Components; using AIStudio.Components;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace AIStudio.Dialogs; namespace AIStudio.Dialogs;
public partial class WorkspaceSelectionDialog : MSGComponentBase public partial class WorkspaceSelectionDialog : MSGComponentBase
{ {
private readonly record struct WorkspaceSelectionItem(Guid WorkspaceId, string Name);
[CascadingParameter] [CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!; private IMudDialogInstance MudDialog { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Parameter] [Parameter]
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
@ -18,8 +24,18 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
[Parameter] [Parameter]
public string ConfirmText { get; set; } = "OK"; public string ConfirmText { get; set; } = "OK";
private readonly Dictionary<string, Guid> workspaces = new(); private readonly List<WorkspaceSelectionItem> workspaces = [];
private readonly string escapeHandlerId = $"workspace-selection-dialog-{Guid.NewGuid():N}";
private MudForm? createWorkspaceForm;
private MudTextField<string>? newWorkspaceNameField;
private DotNetObjectReference<WorkspaceSelectionDialog>? dotNetReference;
private Guid selectedWorkspace; private Guid selectedWorkspace;
private string newWorkspaceName = string.Empty;
private bool isCreatingWorkspace;
private bool showCreateWorkspaceForm;
private bool shouldFocusNewWorkspaceName;
private string? createWorkspaceError;
private string? createWorkspaceErrorName;
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -29,15 +45,156 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
foreach (var workspace in snapshot.Workspaces) foreach (var workspace in snapshot.Workspaces)
this.workspaces[workspace.Name] = workspace.WorkspaceId; this.workspaces.Add(new(workspace.WorkspaceId, workspace.Name));
this.StateHasChanged(); this.StateHasChanged();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
this.dotNetReference = DotNetObjectReference.Create(this);
await this.JsRuntime.InvokeVoidAsync("registerEscapeHandler", this.escapeHandlerId, this.dotNetReference);
}
if (this.shouldFocusNewWorkspaceName && this.newWorkspaceNameField is not null)
{
this.shouldFocusNewWorkspaceName = false;
await this.newWorkspaceNameField.FocusAsync();
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion #endregion
private void Cancel() => this.MudDialog.Cancel(); private string? ValidateNewWorkspaceName(string? workspaceName)
{
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return T("Please enter a workspace name.");
if (this.IsWorkspaceNameExisting(normalizedWorkspaceName))
return T("There is already a workspace with this name. Please choose a different name.");
if (this.createWorkspaceError is not null && string.Equals(this.createWorkspaceErrorName, normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
return this.createWorkspaceError;
return null;
}
private bool IsWorkspaceNameExisting(string normalizedWorkspaceName)
{
return this.workspaces.Any(workspace =>
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
}
private async Task HandleNewWorkspaceNameKeyDown(KeyboardEventArgs keyEvent)
{
var key = keyEvent.Key.ToLowerInvariant();
var code = keyEvent.Code.ToLowerInvariant();
if (key is not "enter" && code is not "enter" and not "numpadenter")
return;
if (keyEvent is { AltKey: true } or { CtrlKey: true } or { MetaKey: true })
return;
await this.CreateWorkspaceAsync();
}
private void ShowCreateWorkspaceForm()
{
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty;
this.showCreateWorkspaceForm = true;
this.shouldFocusNewWorkspaceName = true;
}
private async Task CreateWorkspaceAsync()
{
if (this.createWorkspaceForm is null)
return;
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
await this.createWorkspaceForm.Validate();
if (!this.createWorkspaceForm.IsValid)
return;
this.isCreatingWorkspace = true;
try
{
var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(this.newWorkspaceName);
if (!result.Success)
{
this.createWorkspaceError = T("There is already a workspace with this name. Please choose a different name.");
this.createWorkspaceErrorName = WorkspaceBehaviour.NormalizeWorkspaceName(this.newWorkspaceName);
await this.createWorkspaceForm.Validate();
return;
}
this.workspaces.Add(new(result.Workspace.WorkspaceId, result.Workspace.Name));
this.selectedWorkspace = result.Workspace.WorkspaceId;
this.newWorkspaceName = string.Empty;
this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false;
await this.SendMessage(Event.WORKSPACE_CREATED, result.Workspace.WorkspaceId);
}
finally
{
this.isCreatingWorkspace = false;
}
}
private void Cancel()
{
if (!this.showCreateWorkspaceForm)
{
this.MudDialog.Cancel();
return;
}
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty;
this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false;
this.shouldFocusNewWorkspaceName = false;
}
[JSInvokable]
public async Task HandleEscapeKeyAsync()
{
await this.InvokeAsync(() =>
{
this.Cancel();
this.StateHasChanged();
});
}
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace)); private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace));
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
_ = this.JsRuntime.InvokeVoidAsync("unregisterEscapeHandler", this.escapeHandlerId).AsTask();
}
catch
{
// Ignore JS cleanup errors while the dialog is being disposed.
}
this.dotNetReference?.Dispose();
this.dotNetReference = null;
base.DisposeResources();
}
#endregion
} }

View File

@ -17,7 +17,7 @@
<MudNavMenu> <MudNavMenu>
@foreach (var navBarItem in this.navItems) @foreach (var navBarItem in this.navItems)
{ {
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color"> <MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">
@navBarItem.Name @navBarItem.Name
</MudNavLink> </MudNavLink>
} }

View File

@ -1,6 +1,7 @@
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.AIJobs;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -26,6 +27,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
[Inject] [Inject]
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject]
private AIJobService AIJobService { get; init; } = null!;
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
@ -54,6 +58,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private MudThemeProvider themeProvider = null!; private MudThemeProvider themeProvider = null!;
private bool useDarkMode; private bool useDarkMode;
private bool startupCompleted; private bool startupCompleted;
private bool settingsWriteProtectionWarningShown;
private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1); private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1);
private IReadOnlyCollection<NavBarItem> navItems = []; private IReadOnlyCollection<NavBarItem> navItems = [];
@ -83,7 +88,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
// Read the user language from Rust: // Read the user language from Rust:
// //
var userLanguage = await this.RustService.ReadUserLanguage(); var userLanguage = await this.RustService.ReadUserLanguage();
var userName = await this.RustService.ReadUserName();
this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language."); this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language.");
this.Logger.LogInformation($"The OS says '{userName}' is the username.");
// Ensure that all settings are loaded: // Ensure that all settings are loaded:
await this.SettingsManager.LoadSettings(); await this.SettingsManager.LoadSettings();
@ -94,7 +101,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
[ [
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR, Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED, Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
Event.CHAT_GENERATION_CHANGED,
]); ]);
// Set the snackbar for the update service: // Set the snackbar for the update service:
@ -120,6 +128,39 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
#endregion #endregion
private void ShowSettingsWriteProtectionWarning()
{
if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown)
return;
this.settingsWriteProtectionWarningShown = true;
var reason = this.SettingsManager.SettingsWriteBlockReason;
var message = reason switch
{
SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."),
SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."),
SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."),
_ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."),
};
message = $"{message} {T("Reason")}: {reason}";
this.Snackbar.Add(message, Severity.Warning, config =>
{
config.Icon = Icons.Material.Filled.WarningAmber;
config.IconSize = Size.Large;
config.VisibleStateDuration = 32_000;
config.HideTransitionDuration = 600;
config.Action = T("Check for updates");
config.ActionVariant = Variant.Filled;
config.OnClick = async _ =>
{
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
};
});
}
#region Implementation of ILang #region Implementation of ILang
/// <inheritdoc /> /// <inheritdoc />
@ -184,6 +225,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
this.StateHasChanged(); this.StateHasChanged();
break; break;
case Event.AI_JOB_CHANGED:
case Event.AI_JOB_FINISHED:
case Event.CHAT_GENERATION_CHANGED:
this.LoadNavItems();
this.StateHasChanged();
break;
case Event.SHOW_SUCCESS: case Event.SHOW_SUCCESS:
if (data is DataSuccessMessage success) if (data is DataSuccessMessage success)
success.Show(this.Snackbar); success.Show(this.Snackbar);
@ -262,6 +310,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
case Event.PLUGINS_RELOADED: case Event.PLUGINS_RELOADED:
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin(); this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
I18N.Init(this.Lang); I18N.Init(this.Lang);
this.ShowSettingsWriteProtectionWarning();
this.LoadNavItems(); this.LoadNavItems();
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
@ -294,7 +343,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager); var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true); yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true);
yield return new(T("Chat"), Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false); yield return new(T("Chat"), this.AIJobService.HasActiveJobs ? Icons.Material.Filled.Chat : Icons.Material.Outlined.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false); yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false);
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager)) if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))

View File

@ -50,12 +50,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" /> <PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.15" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.17" />
<PackageReference Include="MudBlazor" Version="8.15.0" /> <PackageReference Include="MudBlazor" Version="8.15.0" />
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" /> <PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
<PackageReference Include="Qdrant.Client" Version="1.17.0" />
<PackageReference Include="ReverseMarkdown" Version="5.0.0" /> <PackageReference Include="ReverseMarkdown" Version="5.0.0" />
<PackageReference Include="LuaCSharp" Version="0.5.3" /> <PackageReference Include="LuaCSharp" Version="0.5.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -88,7 +87,7 @@
<MetaAppCommitHash>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ])</MetaAppCommitHash> <MetaAppCommitHash>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ])</MetaAppCommitHash>
<MetaArchitecture>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ])</MetaArchitecture> <MetaArchitecture>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ])</MetaArchitecture>
<MetaPdfiumVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ])</MetaPdfiumVersion> <MetaPdfiumVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ])</MetaPdfiumVersion>
<MetaQdrantVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaQdrantVersion> <MetaVectorStoreVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaVectorStoreVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
@ -116,8 +115,8 @@
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataLibraries"> <AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataLibraries">
<_Parameter1>$(MetaPdfiumVersion)</_Parameter1> <_Parameter1>$(MetaPdfiumVersion)</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataDatabases"> <AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataVectorStore">
<_Parameter1>$(MetaQdrantVersion)</_Parameter1> <_Parameter1>$(MetaVectorStoreVersion)</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
</ItemGroup> </ItemGroup>

View File

@ -51,13 +51,16 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/> <MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
<MudTooltip Text="@T("Hide your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Hide your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Medium" Icon="@this.WorkspaceSidebarToggleIcon" Class="me-1" OnClick="@(() => this.ToggleWorkspaceSidebar())"/> <MudIconButton Size="Size.Medium" Icon="@this.WorkspaceSidebarToggleIcon" Class="me-1" OnClick="@(() => this.ToggleWorkspaceSidebar())"/>
</MudTooltip> </MudTooltip>
</MudStack> </MudStack>
</HeaderContent> </HeaderContent>
<ChildContent> <ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</ChildContent> </ChildContent>
</InnerScrolling> </InnerScrolling>
} }
@ -77,10 +80,13 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/> <MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
</MudStack> </MudStack>
</HeaderContent> </HeaderContent>
<ChildContent> <ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</ChildContent> </ChildContent>
</InnerScrolling> </InnerScrolling>
} }
@ -89,6 +95,7 @@
<ChatComponent <ChatComponent
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
ComposerState="@this.composerState"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.UpdateWorkspaceName(name)"/> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
</EndContent> </EndContent>
@ -115,6 +122,7 @@
<ChatComponent <ChatComponent
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
ComposerState="@this.composerState"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.UpdateWorkspaceName(name)"/> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
</MudStack> </MudStack>
@ -125,6 +133,7 @@
<ChatComponent <ChatComponent
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
ComposerState="@this.composerState"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.UpdateWorkspaceName(name)"/> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
} }
@ -146,11 +155,14 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/> <MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
<MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Error" Size="Size.Medium" OnClick="@(() => this.ToggleWorkspacesOverlay())"/> <MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Error" Size="Size.Medium" OnClick="@(() => this.ToggleWorkspacesOverlay())"/>
</MudStack> </MudStack>
</MudDrawerHeader> </MudDrawerHeader>
<MudDrawerContainer Class="ml-6"> <MudDrawerContainer Class="ml-6">
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</MudDrawerContainer> </MudDrawerContainer>
</MudDrawer> </MudDrawer>
} }

View File

@ -23,9 +23,11 @@ public partial class Chat : MSGComponentBase
private ChatThread? chatThread; private ChatThread? chatThread;
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE; private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
private bool workspaceOverlayVisible; private bool workspaceOverlayVisible;
private bool workspaceSearchVisible;
private string currentWorkspaceName = string.Empty; private string currentWorkspaceName = string.Empty;
private Workspaces? workspaces; private Workspaces? workspaces;
private double splitterPosition = 30; private double splitterPosition = 30;
private readonly ChatComposerState composerState = new();
private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6)); private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
@ -50,6 +52,10 @@ public partial class Chat : MSGComponentBase
private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight; private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
private string WorkspaceSearchIcon => this.workspaceSearchVisible ? Icons.Material.Filled.SearchOff : Icons.Material.Filled.Search;
private string WorkspaceSearchTooltip => this.workspaceSearchVisible ? T("Hide search") : T("Search your workspaces");
private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) && ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|| this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE); || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
@ -106,6 +112,14 @@ public partial class Chat : MSGComponentBase
await this.workspaces.ForceRefreshFromDiskAsync(); await this.workspaces.ForceRefreshFromDiskAsync();
} }
private async Task ToggleWorkspaceSearch()
{
if (this.workspaces is null)
return;
await this.workspaces.ToggleSearchAsync();
}
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
protected override void DisposeResources() protected override void DisposeResources()

View File

@ -8,36 +8,52 @@
</MudText> </MudText>
<InnerScrolling> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels @key="@this.expansionPanelsRenderKey" Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true"> @if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
<MudText Typo="Typo.h5" Class="mb-3"> {
@T("Welcome to MindWork AI Studio!") <ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION, isExpanded))">
</MudText> <MudText Typo="Typo.h5" Class="mb-3">
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> @T("Welcome to MindWork AI Studio!")
@T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.") </MudText>
</MudText> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
<MudText Typo="Typo.body1" Class="mb-3"> @T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.")
@T("Here's what makes MindWork AI Studio stand out:") </MudText>
</MudText> <MudText Typo="Typo.body1" Class="mb-3">
<MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/> @T("Here's what makes MindWork AI Studio stand out:")
<MudText Typo="Typo.body1" Class="mb-3"> </MudText>
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") <MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/>
</MudText> <MudText Typo="Typo.body1" Class="mb-3">
</ExpansionPanel> @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
</MudText>
</ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")"> @foreach (var introduction in this.introductions)
{
<ExpansionPanel @key="@introduction.Id" HeaderIcon="@Icons.Material.Filled.Info" HeaderText="@introduction.Title" IsExpanded="@this.IsPanelExpanded(IntroductionPanelId(introduction))" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(IntroductionPanelId(introduction), isExpanded))">
<MudText Typo="Typo.body2" Class="mb-3">
@T("Version"): @introduction.VersionText
</MudText>
<MudJustifiedMarkdown Value="@introduction.Markdown" />
</ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_LAST_CHANGELOG)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_LAST_CHANGELOG, isExpanded))">
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/> <MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_VISION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_VISION, isExpanded))">
<Vision/> <Vision/>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")"> @if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide)
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/> {
</ExpansionPanel> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_QUICK_START_GUIDE)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_QUICK_START_GUIDE, isExpanded))">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
}
</MudExpansionPanels> </MudExpansionPanels>
</InnerScrolling> </InnerScrolling>
</div> </div>

View File

@ -1,5 +1,6 @@
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -18,13 +19,25 @@ public partial class Home : MSGComponentBase
private string LastChangeContent { get; set; } = string.Empty; private string LastChangeContent { get; set; } = string.Empty;
private TextItem[] itemsAdvantages = []; private TextItem[] itemsAdvantages = [];
private List<DataIntroduction> introductions = [];
private string expandedPanelId = string.Empty;
private int expansionPanelsRenderKey;
private const string PANEL_ID_BUILT_IN_INTRODUCTION = "built-in-introduction";
private const string PANEL_ID_LAST_CHANGELOG = "last-changelog";
private const string PANEL_ID_VISION = "vision";
private const string PANEL_ID_QUICK_START_GUIDE = "quick-start-guide";
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync(); await base.OnInitializedAsync();
this.InitializeAdvantagesItems(); this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
// Read the last change content asynchronously // Read the last change content asynchronously
// without blocking the UI thread: // without blocking the UI thread:
@ -69,6 +82,14 @@ public partial class Home : MSGComponentBase
{ {
case Event.PLUGINS_RELOADED: case Event.PLUGINS_RELOADED:
this.InitializeAdvantagesItems(); this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged);
break;
case Event.CONFIGURATION_CHANGED:
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
break; break;
} }
@ -76,6 +97,42 @@ public partial class Home : MSGComponentBase
#endregion #endregion
private void RefreshIntroductionPanels()
{
this.introductions = PluginFactory.GetIntroductions().ToList();
}
private string GetDefaultExpandedPanelId()
{
if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
return PANEL_ID_BUILT_IN_INTRODUCTION;
var firstIntroduction = this.introductions.FirstOrDefault();
return firstIntroduction is not null
? IntroductionPanelId(firstIntroduction)
: PANEL_ID_LAST_CHANGELOG;
}
private void EnsureDefaultExpandedPanel()
{
this.expandedPanelId = this.GetDefaultExpandedPanelId();
this.expansionPanelsRenderKey++;
}
private bool IsPanelExpanded(string panelId) => string.Equals(this.expandedPanelId, panelId, StringComparison.Ordinal);
private Task SetPanelExpanded(string panelId, bool isExpanded)
{
if (isExpanded)
this.expandedPanelId = panelId;
else if (this.IsPanelExpanded(panelId))
this.expandedPanelId = string.Empty;
return Task.CompletedTask;
}
private static string IntroductionPanelId(DataIntroduction introduction) => $"introduction:{introduction.Id}";
private async Task ReadLastChangeAsync() private async Task ReadLastChangeAsync()
{ {
var latest = Changelog.LOGS.MaxBy(n => n.Build); var latest = Changelog.LOGS.MaxBy(n => n.Build);

View File

@ -21,11 +21,11 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionRust"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionRust"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage"> <MudListItem T="string" Icon="@Icons.Material.Outlined.Storage">
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
@this.VersionDatabase @this.VersionVectorStore
</MudText> </MudText>
<MudCollapse Expanded="@this.showDatabaseDetails"> <MudCollapse Expanded="@this.showVectorStoreDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2"> <MudText Typo="Typo.body1" Class="mt-2 mb-2">
@foreach (var item in this.databaseDisplayInfo) @foreach (var item in this.vectorStoreDisplayInfo)
{ {
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/> <MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
@ -35,11 +35,11 @@
} }
</MudText> </MudText>
</MudCollapse> </MudCollapse>
<MudButton StartIcon="@(this.showDatabaseDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" <MudButton StartIcon="@(this.showVectorStoreDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small" Size="Size.Small"
Variant="Variant.Text" Variant="Variant.Text"
OnClick="@this.ToggleDatabaseDetails"> OnClick="@this.ToggleVectorStoreDetails">
@(this.showDatabaseDetails ? T("Hide Details") : T("Show Details")) @(this.showVectorStoreDetails ? T("Hide Details") : T("Show Details"))
</MudButton> </MudButton>
</MudListItem> </MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/>
@ -47,6 +47,27 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.AccountCircle" Text="@this.OSUserName"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder">
<div style="display: flex; align-items: center; gap: 8px;">
<MudText Typo="Typo.body1">
@this.WorkingDirectory
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the working directory to the clipboard"))" StringContent="@this.runtimeInfo.WorkingDirectory"/>
</div>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.InsertDriveFile">
<div style="display: flex; align-items: center; gap: 8px;">
<MudText Typo="Typo.body1">
@this.ExecutablePath
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the executable path to the clipboard"))" StringContent="@this.runtimeInfo.ExecutablePath"/>
</div>
</MudListItem>
@if (OperatingSystem.IsLinux())
{
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.LinuxPackageType"/>
}
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business"> <MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
@switch (HasAnyActiveEnvironment) @switch (HasAnyActiveEnvironment)
{ {
@ -88,18 +109,7 @@
{ {
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom" <ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")" HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([ Items="@this.BuildEnterpriseConfigurationItems(env)"/>
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
} }
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)" <EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
@ -129,41 +139,13 @@
{ {
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom" <ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")" HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([ Items="@this.BuildEnterpriseConfigurationItems(env)"/>
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
continue; continue;
} }
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension" <ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
HeaderText="@matchingPlugin.Name" HeaderText="@matchingPlugin.Name"
Items="@([ Items="@this.BuildEnterpriseConfigurationItems(env, matchingPlugin)"
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
matchingPlugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;")
])"
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)" ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/> WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
} }
@ -185,9 +167,32 @@
</MudButton> </MudButton>
} }
</MudListItem> </MudListItem>
@if (ExternalHttpClientTimeout.CustomRootCertificateState.IsEnabled)
{
<MudListItem T="string" Icon="@Icons.Material.Outlined.Security">
<MudText Typo="Typo.body1">
@(ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable
? T("External HTTPS custom root certificates are active.")
: T("External HTTPS custom root certificates are configured but not active."))
</MudText>
<MudCollapse Expanded="@this.showExternalHttpCustomRootCertificateDetails">
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Security"
HeaderText="@T("External HTTPS custom root certificates")"
Items="@this.BuildExternalHttpCustomRootCertificateItems()"
ShowWarning="@(!ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable)"
WarningText="@this.ExternalHttpCustomRootCertificateWarningText"/>
</MudCollapse>
<MudButton StartIcon="@(this.showExternalHttpCustomRootCertificateDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
Variant="Variant.Text"
OnClick="@this.ToggleExternalHttpCustomRootCertificateDetails">
@(this.showExternalHttpCustomRootCertificateDetails ? T("Hide Details") : T("Show Details"))
</MudButton>
</MudListItem>
}
</MudList> </MudList>
<MudStack Row="true"> <MudStack Row="true">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@(() => this.CheckForUpdate())"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@this.CheckForUpdate">
@T("Check for updates") @T("Check for updates")
</MudButton> </MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" StartIcon="@Icons.Material.Filled.Download" OnClick="@(async () => await this.ShowPandocDialog())"> <MudButton Variant="Variant.Filled" Color="Color.Default" StartIcon="@Icons.Material.Filled.Download" OnClick="@(async () => await this.ShowPandocDialog())">
@ -278,13 +283,19 @@
<ThirdPartyComponent Name="CodeBeam.MudBlazor.Extensions" Developer="Mehmet Can Karagöz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions/blob/dev/LICENSE" RepositoryUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions" UseCase="@T("This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.")"/> <ThirdPartyComponent Name="CodeBeam.MudBlazor.Extensions" Developer="Mehmet Can Karagöz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions/blob/dev/LICENSE" RepositoryUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions" UseCase="@T("This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.")"/>
<ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="@T("The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.")"/> <ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="@T("The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.")"/>
<ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/> <ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/>
<ThirdPartyComponent Name="Qdrant" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
@if (OperatingSystem.IsLinux())
{
<ThirdPartyComponent Name="GStreamer" Developer="GStreamer contributors & Open Source Community" LicenseName="LGPL-2.1" LicenseUrl="https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html" RepositoryUrl="https://gitlab.freedesktop.org/gstreamer/gstreamer" UseCase="@T("Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.")"/>
}
<ThirdPartyComponent Name="Qdrant Edge" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
<ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/> <ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/>
<ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/> <ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/>
<ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/> <ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/>
<ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/> <ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/>
<ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.")"/> <ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.")"/>
<ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.")"/> <ThirdPartyComponent Name="keyring-core" Developer="Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/open-source-cooperative/keyring-core/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/open-source-cooperative/keyring-core" UseCase="@T("AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.")"/>
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/> <ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/>
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/> <ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/>
<ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/> <ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/>
@ -301,8 +312,9 @@
<ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/> <ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/>
<ThirdPartyComponent Name="whoami" Developer="Ardaku Systems, Jeryn Aldaron Lau, Chase Johnson & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ardaku/whoami/blob/stable/LICENSE_MIT" RepositoryUrl="https://github.com/ardaku/whoami" UseCase="@T("This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.")"/>
<ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/> <ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/>
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.")"/> <ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders in runtime tests and supporting filesystem operations.")"/>
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" /> <ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" />
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="@T("We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.")"/> <ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="@T("We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/> <ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>

View File

@ -4,6 +4,7 @@ using AIStudio.Components;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Databases; using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.VectorStore;
using AIStudio.Tools.Metadata; using AIStudio.Tools.Metadata;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
@ -29,17 +30,19 @@ public partial class Information : MSGComponentBase
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
[Inject] [Inject]
private DatabaseClient DatabaseClient { get; init; } = null!; private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!; private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!; private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!; private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!;
private static readonly MetaDataDatabasesAttribute META_DATA_DATABASES = ASSEMBLY.GetCustomAttribute<MetaDataDatabasesAttribute>()!; private static readonly MetaDataVectorStoreAttribute META_DATA_VECTOR_STORE = ASSEMBLY.GetCustomAttribute<MetaDataVectorStoreAttribute>()!;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
private string osLanguage = string.Empty; private string osLanguage = string.Empty;
private string osUserName = string.Empty;
private RuntimeInfoResponse runtimeInfo;
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})"; private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})";
@ -49,6 +52,22 @@ public partial class Information : MSGComponentBase
private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'"; private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'";
private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'";
private string WorkingDirectory => $"{T("Working directory")}: {this.runtimeInfo.WorkingDirectory}";
private string ExecutablePath => $"{T("Executable path")}: {this.runtimeInfo.ExecutablePath}";
private string LinuxPackageType => $"{T("Linux package")}: {this.LinuxPackageTypeDisplayName}";
private string LinuxPackageTypeDisplayName => this.runtimeInfo.LinuxPackageType switch
{
"appimage" => "AppImage",
"flatpak" => "Flatpak",
"unknown" => T("unknown"),
_ => T("not applicable")
};
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}"; private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}"; private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
@ -59,9 +78,21 @@ public partial class Information : MSGComponentBase
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
private string VersionDatabase => this.DatabaseClient.IsAvailable private string VersionVectorStore
? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}" {
: $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}"; get
{
if (this.vectorStore is null)
return $"{T("Vector store")}: {T("checking availability")}";
return this.vectorStore.Status switch
{
DatabaseClientStatus.AVAILABLE => $"{T("Vector store version")}: {this.vectorStore.Name} v{META_DATA_VECTOR_STORE.VectorStoreVersion}",
DatabaseClientStatus.STARTING => $"{T("Vector store")}: {this.vectorStore.Name} - {T("starting")}",
_ => $"{T("Vector store")}: {this.vectorStore.Name} - {T("not available")}"
};
}
}
private string versionPandoc = TB("Determine Pandoc version, please wait..."); private string versionPandoc = TB("Determine Pandoc version, please wait...");
private PandocInstallation pandocInstallation; private PandocInstallation pandocInstallation;
@ -70,7 +101,8 @@ public partial class Information : MSGComponentBase
private bool showEnterpriseConfigDetails; private bool showEnterpriseConfigDetails;
private bool showDatabaseDetails; private bool showVectorStoreDetails;
private bool showExternalHttpCustomRootCertificateDetails;
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
.Where(x => x.Type is PluginType.CONFIGURATION) .Where(x => x.Type is PluginType.CONFIGURATION)
@ -80,12 +112,13 @@ public partial class Information : MSGComponentBase
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList(); private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
private List<MandatoryInfoPanelData> mandatoryInfoPanels = []; private List<MandatoryInfoPanelData> mandatoryInfoPanels = [];
private sealed record DatabaseDisplayInfo(string Label, string Value);
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance); private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new(); private sealed record VectorStoreDisplayInfo(string Label, string Value);
private readonly List<VectorStoreDisplayInfo> vectorStoreDisplayInfo = new();
private DatabaseClient? vectorStore;
private CancellationTokenSource? vectorStoreRefreshCancellationTokenSource;
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive); private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
@ -128,12 +161,13 @@ public partial class Information : MSGComponentBase
this.RefreshEnterpriseConfigurationState(); this.RefreshEnterpriseConfigurationState();
this.osLanguage = await this.RustService.ReadUserLanguage(); this.osLanguage = await this.RustService.ReadUserLanguage();
this.osUserName = await this.RustService.ReadUserName();
this.runtimeInfo = await this.RustService.GetRuntimeInfo();
this.logPaths = await this.RustService.GetLogPaths(); this.logPaths = await this.RustService.GetLogPaths();
await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo()) await this.RefreshVectorStoreInfo(CancellationToken.None);
{ if (this.vectorStore?.Status is DatabaseClientStatus.STARTING)
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value)); this.StartShortVectorStoreRefreshLoop();
}
// Determine the Pandoc version may take some time, so we start it here // Determine the Pandoc version may take some time, so we start it here
// without waiting for the result: // without waiting for the result:
@ -231,10 +265,78 @@ public partial class Information : MSGComponentBase
{ {
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails; this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
} }
private void ToggleDatabaseDetails() private void ToggleExternalHttpCustomRootCertificateDetails()
{ {
this.showDatabaseDetails = !this.showDatabaseDetails; this.showExternalHttpCustomRootCertificateDetails = !this.showExternalHttpCustomRootCertificateDetails;
}
private void ToggleVectorStoreDetails()
{
this.showVectorStoreDetails = !this.showVectorStoreDetails;
}
private async Task RefreshVectorStoreInfo(CancellationToken cancellationToken)
{
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
this.vectorStore = refreshedClient;
this.vectorStoreDisplayInfo.Clear();
try
{
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
{
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception e)
{
this.vectorStore = new NoVectorStoreClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
await foreach (var (label, value) in this.vectorStore.GetDisplayInfo().WithCancellation(cancellationToken))
{
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
}
}
}
private void StartShortVectorStoreRefreshLoop()
{
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
this.vectorStoreRefreshCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = this.vectorStoreRefreshCancellationTokenSource.Token;
_ = Task.Run(async () =>
{
const int MAX_TRIES = 12;
for (var attempt = 0; attempt < MAX_TRIES; attempt++)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
await this.InvokeAsync(async () =>
{
await this.RefreshVectorStoreInfo(cancellationToken);
this.StateHasChanged();
});
if (this.vectorStore?.Status is not DatabaseClientStatus.STARTING)
return;
}
catch (OperationCanceledException)
{
return;
}
catch
{
return;
}
}
}, cancellationToken);
} }
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId) private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
@ -244,11 +346,139 @@ public partial class Information : MSGComponentBase
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId); ?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
} }
private IReadOnlyList<ConfigInfoRowItem> BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null)
{
var items = new List<ConfigInfoRowItem>
{
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {environment.ConfigurationId}",
environment.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {environment.ConfigurationServerUrl}",
environment.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration source:")} {environment.Source}",
environment.Source,
T("Copies the configuration source to the clipboard"),
"margin-top: 4px;"),
};
if (!string.IsNullOrWhiteSpace(environment.SourceDetail))
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration origin:")} {environment.SourceDetail}",
environment.SourceDetail,
T("Copies the configuration origin to the clipboard"),
"margin-top: 4px;"));
}
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration slot:")} {environment.Slot}",
environment.Slot,
T("Copies the configuration slot to the clipboard"),
"margin-top: 4px;"));
if (plugin is not null)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {plugin.Id}",
plugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;"));
}
return items;
}
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId) private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
{ {
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId; return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
} }
private string ExternalHttpCustomRootCertificateWarningText
{
get
{
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
return string.IsNullOrWhiteSpace(state.Issue)
? T("The configured root certificates could not be used.")
: state.Issue;
}
}
private IReadOnlyList<ConfigInfoRowItem> BuildExternalHttpCustomRootCertificateItems()
{
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
var items = new List<ConfigInfoRowItem>
{
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Status:")} {(state.IsUsable ? T("active") : T("not active"))}",
state.IsUsable ? T("active") : T("not active"),
T("Copies the status to the clipboard")),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration source:")} {state.Source}",
state.Source,
T("Copies the configuration source to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Certificate bundle:")} {state.BundlePath}",
state.BundlePath,
T("Copies the certificate bundle path to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Loaded root certificates:")} {state.CertificateCount}",
state.CertificateCount.ToString(),
T("Copies the number of loaded root certificates to the clipboard"),
"margin-top: 4px;")
};
if (state.AllowedHostPatterns.Count == 0)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
T("Allowed hosts: none configured"),
string.Empty,
T("Copies the allowed host configuration to the clipboard"),
"margin-top: 4px;"));
}
else
{
foreach (var allowedHostPattern in state.AllowedHostPatterns)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Dns,
$"{T("Allowed host:")} {allowedHostPattern}",
allowedHostPattern,
T("Copies the allowed host pattern to the clipboard"),
"margin-top: 4px;"));
}
}
foreach (var fingerprint in state.CertificateFingerprints)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Fingerprint,
$"{T("Root certificate fingerprint:")} {fingerprint}",
fingerprint,
T("Copies the root certificate fingerprint to the clipboard"),
"margin-top: 4px;"));
}
return items;
}
protected override void DisposeResources()
{
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
base.DisposeResources();
}
private async Task CopyStartupLogPath() private async Task CopyStartupLogPath()
{ {
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath); await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);

View File

@ -8,6 +8,7 @@
<InnerScrolling> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<SettingsPanelConfidence/>
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders"/> <SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders"/>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))

View File

@ -10,6 +10,7 @@ namespace AIStudio.Pages;
public partial class Writer : MSGComponentBase public partial class Writer : MSGComponentBase
{ {
private static readonly ILogger<Writer> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Writer>();
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500)); private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
@ -106,22 +107,38 @@ public partial class Writer : MSGComponentBase
InitialRemoteWait = true, InitialRemoteWait = true,
}; };
this.chatThread?.Blocks.Add(new ContentBlock var aiBlock = new ContentBlock
{ {
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
Role = ChatRole.AI, Role = ChatRole.AI,
Content = aiText, Content = aiText,
}); };
this.chatThread?.Blocks.Add(aiBlock);
this.isStreaming = true; this.isStreaming = true;
this.StateHasChanged(); this.StateHasChanged();
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread); try
this.suggestion = aiText.Text; {
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
this.isStreaming = false; this.suggestion = aiText.Text;
this.StateHasChanged(); }
catch (ProviderRequestException e)
{
LOGGER.LogError(e, "The provider request failed for writer suggestions. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", e.StatusCode, e.ReasonPhrase, e.ResponseBody);
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
this.suggestion = string.Empty;
if (string.IsNullOrWhiteSpace(aiText.Text))
this.chatThread?.Blocks.Remove(aiBlock);
}
finally
{
this.isStreaming = false;
this.StateHasChanged();
}
} }
private void AcceptEntireSuggestion() private void AcceptEntireSuggestion()

View File

@ -136,6 +136,54 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
-- } -- }
-- } -- }
-- ERI v1 data sources for retrieval-augmented generation:
CONFIG["DATA_SOURCES"] = {}
-- Example: ERI v1 data source with a shared access token.
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
-- ["Name"] = "<user-friendly data source name>",
-- ["Type"] = "ERI_V1",
-- ["Hostname"] = "<https address of the ERI server>",
-- ["Port"] = 443,
-- ["AuthMethod"] = "TOKEN",
-- ["Token"] = "ENC:v1:<base64-encoded encrypted token>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
-- }
-- Example: ERI v1 data source with a shared username and password.
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
-- ["Name"] = "<user-friendly data source name>",
-- ["Type"] = "ERI_V1",
-- ["Hostname"] = "<https address of the ERI server>",
-- ["Port"] = 443,
-- ["AuthMethod"] = "USERNAME_PASSWORD",
-- ["UsernamePasswordMode"] = "SHARED_USERNAME_AND_PASSWORD",
-- ["Username"] = "<shared username>",
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
-- }
-- Example: ERI v1 data source using the user's username and a shared password.
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
-- ["Name"] = "<user-friendly data source name>",
-- ["Type"] = "ERI_V1",
-- ["Hostname"] = "<https address of the ERI server>",
-- ["Port"] = 443,
-- ["AuthMethod"] = "USERNAME_PASSWORD",
-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD",
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
-- }
CONFIG["SETTINGS"] = {} CONFIG["SETTINGS"] = {}
-- Configure the update check interval: -- Configure the update check interval:
@ -156,12 +204,16 @@ CONFIG["SETTINGS"] = {}
-- but users can still choose another start page in the app settings. -- but users can still choose another start page in the app settings.
-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true -- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true
-- Configure whether the quick start guide is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false
-- Configure whether the built-in introduction is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowIntroduction"] = false
-- Configure the user permission to add providers: -- Configure the user permission to add providers:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
-- Configure whether administration settings are visible in the UI: -- Configure whether administration settings are visible in the UI:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true -- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
-- Configure the visibility of preview features: -- Configure the visibility of preview features:
@ -172,9 +224,9 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE" -- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
-- Configure the enabled preview features: -- Configure the enabled preview features:
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs -- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026. -- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" } -- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
-- Configure the preselected provider. -- Configure the preselected provider.
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"]. -- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
@ -186,6 +238,32 @@ CONFIG["SETTINGS"] = {}
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found. -- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000" -- CONFIG["SETTINGS"]["DataApp.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
-- Configure chat-specific preselected options.
-- This must be enabled for the chat-specific provider, profile, and chat template to take effect.
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions"] = true
--
-- Configure the preselected provider for chats.
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected profile for chats.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- Please note: using an empty string ("") means chats will use the app default profile.
-- Please note: using "00000000-0000-0000-0000-000000000000" means chats will use no profile.
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected chat template for chats.
-- It must be one of the chat template IDs defined in CONFIG["CHAT_TEMPLATES"].
-- Please note: using an empty string ("") or "00000000-0000-0000-0000-000000000000" means chats will use no chat template.
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate"] = "00000000-0000-0000-0000-000000000000"
--
-- Allow users to change any configured chat default locally.
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate.AllowUserOverride"] = true
-- Configure the transcription provider for voice-to-text functionality. -- Configure the transcription provider for voice-to-text functionality.
-- It must be one of the transcription provider IDs defined in CONFIG["TRANSCRIPTION_PROVIDERS"]. -- It must be one of the transcription provider IDs defined in CONFIG["TRANSCRIPTION_PROVIDERS"].
-- Without a selected transcription provider, dictation and transcription features will be disabled. -- Without a selected transcription provider, dictation and transcription features will be disabled.
@ -212,6 +290,89 @@ CONFIG["SETTINGS"] = {}
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8" -- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1" -- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
-- Configure the HTTP timeout for external requests, in seconds.
-- The default is 3600 (1 hour).
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
-- Configure additional root certificates for external HTTPS requests.
--
-- This is intended for managed Linux/Flatpak deployments where organization-internal
-- HTTPS certificates chain to a private root CA that is not visible inside the sandbox.
-- The file must be a PEM bundle with one or more root CA certificates and must be
-- readable by AI Studio.
--
-- IMPORTANT: A configuration plugin cannot fix the very first download of that same
-- configuration plugin. For bootstrapping enterprise configuration downloads, deploy
-- the equivalent environment variables before AI Studio starts:
--
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;data.example.org
--
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
-- Configure provider confidence settings.
-- These settings apply to LLM providers, embedding providers, and transcription providers.
--
-- Configure a predefined confidence scheme.
-- Allowed values are: TRUST_ALL, TRUST_USA_EUROPE, TRUST_USA, TRUST_EUROPE, TRUST_ASIA, LOCAL_TRUST_ONLY, CUSTOM
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme"] = "TRUST_EUROPE"
--
-- Configure whether users can still change the confidence scheme locally.
-- Allowed values are: true, false
-- When set to true, the configured confidence scheme becomes the organization default,
-- but users can still choose another scheme in the app settings.
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme.AllowUserOverride"] = true
--
-- Configure whether confidence levels are shown in the UI.
-- CONFIG["SETTINGS"]["DataConfidence.ShowProviderConfidence"] = true
--
-- Configure an app-wide minimum confidence level.
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence"] = true
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence"] = "MEDIUM"
--
-- Configure whether users can change the app-wide minimum confidence level locally.
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence.AllowUserOverride"] = false
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence.AllowUserOverride"] = false
--
-- Configure a custom confidence scheme.
-- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM.
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme"] = {
-- ["OPEN_AI"] = "MODERATE",
-- ["ANTHROPIC"] = "MODERATE",
-- ["MISTRAL"] = "HIGH",
-- ["GOOGLE"] = "LOW",
-- ["X"] = "LOW",
-- ["DEEP_SEEK"] = "LOW",
-- ["ALIBABA_CLOUD"] = "LOW",
-- ["PERPLEXITY"] = "MODERATE",
-- ["OPEN_ROUTER"] = "MODERATE",
-- ["FIREWORKS"] = "MODERATE",
-- ["GROQ"] = "MODERATE",
-- ["HUGGINGFACE"] = "MODERATE",
-- ["SELF_HOSTED"] = "HIGH",
-- ["HELMHOLTZ"] = "HIGH",
-- ["GWDG"] = "HIGH",
-- }
--
-- Configure whether users can change the custom confidence scheme locally.
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme.AllowUserOverride"] = false
--
-- Configure provider instances trusted by your organization for data-source security checks.
-- These IDs may refer to LLM providers, embedding providers, or transcription providers
-- defined in this configuration. Trusted providers are treated like self-hosted providers
-- only for data-source security checks and related local data warnings.
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- "00000000-0000-0000-0000-000000000001",
-- }
-- Example chat templates for this configuration: -- Example chat templates for this configuration:
CONFIG["CHAT_TEMPLATES"] = {} CONFIG["CHAT_TEMPLATES"] = {}
@ -246,7 +407,8 @@ CONFIG["CHAT_TEMPLATES"] = {}
-- ["AllowProfileUsage"] = true, -- ["AllowProfileUsage"] = true,
-- -- Optional: Pre-attach files that will be automatically included when using this template. -- -- Optional: Pre-attach files that will be automatically included when using this template.
-- -- These files will be loaded when the user selects this chat template. -- -- These files will be loaded when the user selects this chat template.
-- -- Note: File paths must be absolute paths and accessible to all users. -- -- Note: File paths can be absolute paths that are accessible to all users, or relative paths
-- -- inside this plugin folder, for example "attachments/00000000-0000-0000-0000-000000000001/Guidelines.pdf".
-- ["FileAttachments"] = { -- ["FileAttachments"] = {
-- "G:\\Company\\Documents\\Guidelines.pdf", -- "G:\\Company\\Documents\\Guidelines.pdf",
-- "G:\\Company\\Documents\\CompanyPolicies.docx" -- "G:\\Company\\Documents\\CompanyPolicies.docx"
@ -263,6 +425,26 @@ CONFIG["CHAT_TEMPLATES"] = {}
-- } -- }
-- } -- }
-- Introduction texts shown as expansion panels on the welcome page:
CONFIG["INTRODUCTIONS"] = {}
-- An example introduction:
-- CONFIG["INTRODUCTIONS"][#CONFIG["INTRODUCTIONS"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
-- ["Title"] = "Welcome to Your Organization's AI Studio",
-- ["Version"] = "1",
-- ["Index"] = 1,
-- ["Markdown"] = [===[
-- ## Getting Started
--
-- This AI Studio installation is managed by your organization.
-- Please use the preconfigured providers and follow your internal
-- AI usage guidelines.
--
-- Further information is available in the [internal wiki](https://example.org/wiki).
-- ]===]
-- }
-- Mandatory infos that users must explicitly accept before using AI Studio: -- Mandatory infos that users must explicitly accept before using AI Studio:
-- AI Studio asks users again when Version, Title, or Markdown change. -- AI Studio asks users again when Version, Title, or Markdown change.
-- Changing Version additionally allows the UI to communicate that a new version is available. -- Changing Version additionally allows the UI to communicate that a new version is available.

View File

@ -2,7 +2,7 @@ using AIStudio.Agents;
using AIStudio.Agents.AssistantAudit; using AIStudio.Agents.AssistantAudit;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Databases; using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.Qdrant; using AIStudio.Tools.AIJobs;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -28,7 +28,7 @@ internal sealed class Program
public static string API_TOKEN = null!; public static string API_TOKEN = null!;
public static IServiceProvider SERVICE_PROVIDER = null!; public static IServiceProvider SERVICE_PROVIDER = null!;
public static ILoggerFactory LOGGER_FACTORY = null!; public static ILoggerFactory LOGGER_FACTORY = null!;
public static DatabaseClient DATABASE_CLIENT = null!; public static DatabaseClientProvider DATABASE_CLIENT_PROVIDER = null!;
public static async Task Main() public static async Task Main()
{ {
@ -87,48 +87,6 @@ internal sealed class Program
return; return;
} }
var qdrantInfo = await rust.GetQdrantInfo();
DatabaseClient databaseClient;
if (!qdrantInfo.IsAvailable)
{
Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'.");
databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason);
}
else
{
if (qdrantInfo.Path == string.Empty)
{
Console.WriteLine("Error: Failed to get the Qdrant path from Rust.");
return;
}
if (qdrantInfo.PortHttp == 0)
{
Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust.");
return;
}
if (qdrantInfo.PortGrpc == 0)
{
Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust.");
return;
}
if (qdrantInfo.Fingerprint == string.Empty)
{
Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust.");
return;
}
if (qdrantInfo.ApiToken == string.Empty)
{
Console.WriteLine("Error: Failed to get the Qdrant API token from Rust.");
return;
}
databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
}
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions => builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
{ {
@ -171,6 +129,7 @@ internal sealed class Program
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>(); builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>(); builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>(); builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddSingleton<AIJobService>();
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>(); builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
builder.Services.AddSingleton<DataSourceService>(); builder.Services.AddSingleton<DataSourceService>();
builder.Services.AddScoped<PandocAvailabilityService>(); builder.Services.AddScoped<PandocAvailabilityService>();
@ -183,7 +142,7 @@ internal sealed class Program
builder.Services.AddHostedService<UpdateService>(); builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>(); builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddHostedService<EnterpriseEnvironmentService>(); builder.Services.AddHostedService<EnterpriseEnvironmentService>();
builder.Services.AddSingleton(databaseClient); builder.Services.AddSingleton<DatabaseClientProvider>();
builder.Services.AddHostedService<GlobalShortcutService>(); builder.Services.AddHostedService<GlobalShortcutService>();
builder.Services.AddHostedService<RustAvailabilityMonitorService>(); builder.Services.AddHostedService<RustAvailabilityMonitorService>();
@ -242,10 +201,7 @@ internal sealed class Program
RUST_SERVICE = rust; RUST_SERVICE = rust;
ENCRYPTION = encryption; ENCRYPTION = encryption;
DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>();
var databaseLogger = app.Services.GetRequiredService<ILogger<DatabaseClient>>();
databaseClient.SetLogger(databaseLogger);
DATABASE_CLIENT = databaseClient;
programLogger.LogInformation("Initialize internal file system."); programLogger.LogInformation("Initialize internal file system.");
app.Use(Redirect.HandlerContentAsync); app.Use(Redirect.HandlerContentAsync);
@ -283,7 +239,7 @@ internal sealed class Program
await serverTask; await serverTask;
RUST_SERVICE.Dispose(); RUST_SERVICE.Dispose();
DATABASE_CLIENT.Dispose(); DATABASE_CLIENT_PROVIDER.Dispose();
PluginFactory.Dispose(); PluginFactory.Dispose();
programLogger.LogInformation("The AI Studio server was stopped."); programLogger.LogInformation("The AI Studio server was stopped.");
} }

View File

@ -6,14 +6,14 @@ using AIStudio.Settings;
namespace AIStudio.Provider.AlibabaCloud; namespace AIStudio.Provider.AlibabaCloud;
public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/", LOGGER) public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, new Uri("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{ {
private static readonly ILogger<ProviderAlibabaCloud> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAlibabaCloud>(); private static readonly ILogger<ProviderAlibabaCloud> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAlibabaCloud>();
#region Implementation of IProvider #region Implementation of IProvider
/// <inheritdoc /> /// <inheritdoc />
public override string Id => LLMProviders.ALIBABA_CLOUD.ToName(); public override string Id => LLMProviders.ALIBABA_CLOUD.ToSecretId();
/// <inheritdoc /> /// <inheritdoc />
public override string InstanceName { get; set; } = "AlibabaCloud"; public override string InstanceName { get; set; } = "AlibabaCloud";
@ -60,15 +60,15 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
{ {
return Task.FromResult(string.Empty); return Task.FromResult(TranscriptionResult.Failure());
} }
/// <inhertidoc /> /// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{ {
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
} }

View File

@ -8,14 +8,14 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic; namespace AIStudio.Provider.Anthropic;
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "https://api.anthropic.com/v1/", LOGGER) public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, new Uri("https://api.anthropic.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{ {
private static readonly ILogger<ProviderAnthropic> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAnthropic>(); private static readonly ILogger<ProviderAnthropic> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAnthropic>();
#region Implementation of IProvider #region Implementation of IProvider
/// <inheritdoc /> /// <inheritdoc />
public override string Id => LLMProviders.ANTHROPIC.ToName(); public override string Id => LLMProviders.ANTHROPIC.ToSecretId();
/// <inheritdoc /> /// <inheritdoc />
public override string InstanceName { get; set; } = "Anthropic"; public override string InstanceName { get; set; } = "Anthropic";
@ -27,7 +27,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{ {
// Get the API key: // Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
if(!requestedSecret.Success) if(!requestedSecret.Success)
yield break; yield break;
@ -93,7 +93,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
var request = new HttpRequestMessage(HttpMethod.Post, "messages"); var request = new HttpRequestMessage(HttpMethod.Post, "messages");
// Set the authorization header: // Set the authorization header:
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
// Set the Anthropic version: // Set the Anthropic version:
request.Headers.Add("anthropic-version", "2023-06-01"); request.Headers.Add("anthropic-version", "2023-06-01");
@ -116,9 +116,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
{ {
return Task.FromResult(string.Empty); return Task.FromResult(TranscriptionResult.Failure());
} }
/// <inhertidoc /> /// <inhertidoc />
@ -179,6 +179,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
{ {
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR, System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE, _ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
}, },
requestConfigurator: (request, secretKey) => requestConfigurator: (request, secretKey) =>

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