Compare commits

..
24 Commits
Author SHA1 Message Date
Thorsten Sommer 09df19e6f5 Prepared release v26.2.2 (#676)
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 updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-02-22 15:20:05 +01:00
Thorsten Sommer 6c4507ef82 Improved API key retrieval for local embedding (#675) 2026-02-22 15:09:51 +01:00
Thorsten Sommer e7707ba482 Upgraded to Qdrant v1.17.0 (#673)
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) (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 deb updater) (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 updater) (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) (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 deb updater) (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 updater) (push) Has been cancelled
2026-02-20 15:46:05 +01:00
Paul KoudelkaandThorsten Sommer a8951fe58a Added embedding API (#654)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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 updater) (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) (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 deb updater) (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
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (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 deb updater) (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-02-20 15:32:54 +01:00
Thorsten Sommer af72a45035 Fixed handling of paths in Pandoc exports (#674)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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
Build and Release / Read metadata (push) Waiting to run
2026-02-20 14:13:10 +01:00
Peer SchüttandThorsten Sommer 6f76c845f1 Introduced additive configuration handling for managed preview features (#667)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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-02-20 12:40:38 +01:00
Peer Schütt ed8bd9d25c Added support for preselected provider in plugins (#668)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
2026-02-20 10:08:06 +01:00
Thorsten Sommer 6150d49997 Fixed Google Gemini model API (#672)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 updater) (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) (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 deb updater) (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
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
2026-02-20 09:10:53 +01:00
Thorsten Sommer 1aa27d217a Restrict export actions to admin settings visibility (#671)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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-02-19 21:31:59 +01:00
Thorsten Sommer 5e603f9f4c Localized profile name usage (#670) 2026-02-19 21:24:58 +01:00
Thorsten Sommer 21780ad481 Upgraded to .NET 9.0.13 & Rust 1.93.1 (#669)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (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 updater) (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 deb updater) (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
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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 deb updater) (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) (push) Blocked by required conditions
2026-02-19 20:57:56 +01:00
Thorsten Sommer b445600a52 Enhanced enterprise config support (#666) 2026-02-19 20:43:47 +01:00
Thorsten Sommer 6e33c361dc Improved single-input dialog (#665)
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 updater) (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) (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 deb updater) (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 updater) (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 deb updater) (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 / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Has been cancelled
2026-02-16 13:36:53 +01:00
Thorsten Sommer 4562ed1c6a Updated README and other texts (#664)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 updater) (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) (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 deb updater) (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
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
2026-02-16 13:04:16 +01:00
Thorsten Sommer 53f657da71 Improved the user language detection, especially for Linux (#663)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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 deb updater) (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 updater) (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) (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 deb updater) (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
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
2026-02-16 12:13:36 +01:00
Thorsten Sommer 3671444d28 Support multiple enterprise configurations (#662)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (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 updater) (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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) (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 deb updater) (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-02-15 18:11:57 +01:00
Oliver KuncandThorsten Sommer 48f8cb3285 Fixed OpenAI Responses API by not discarding whitespace (#661)
Build and Release / Read metadata (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 deb updater) (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 updater) (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) (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 deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-02-13 21:37:36 +01:00
Thorsten Sommer ea4e3f0199 Render source URL for plugins (#660)
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 updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-02-10 15:23:56 +01:00
Thorsten Sommer 891b90819b Localized database information (#658)
Build and Release / Read metadata (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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 / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Has been cancelled
2026-02-08 17:46:38 +01:00
Thorsten Sommer 4eb58eb56d Fixed manual chat saving during streaming (#657)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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
Build and Release / Read metadata (push) Waiting to run
2026-02-08 17:27:33 +01:00
Thorsten Sommer 50d4e7e6dc Fixed workspace delete color (#656)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (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 deb updater) (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 updater) (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) (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 deb updater) (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
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
2026-02-08 13:04:19 +01:00
Thorsten Sommer 7ae8f7b5ed Upgraded dependencies (#655)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (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-02-08 12:37:45 +01:00
Thorsten Sommer 4cf62672de Added settings & features for administrators in organizations (#653)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (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) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-02-07 22:59:41 +01:00
Paul KoudelkaandThorsten Sommer 5af6a8db3e Add Qdrant as vector database (#580)
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 updater) (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) (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 deb updater) (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 updater) (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) (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 deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-02-03 14:32:17 +01:00
139 changed files with 5088 additions and 800 deletions

No files matched your search

+134 -2
View File
@@ -173,6 +173,9 @@ 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:
qdrant_version="v$(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
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
@@ -185,6 +188,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
# Log the metadata: # Log the metadata:
echo "App version: '${formatted_app_version}'" echo "App version: '${formatted_app_version}'"
@@ -197,6 +201,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}'"
- name: Read and format metadata (Windows) - name: Read and format metadata (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
@@ -241,6 +246,9 @@ 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:
$qdrant_version = "v$($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
Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV
@@ -252,6 +260,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
# Log the metadata: # Log the metadata:
Write-Output "App version: '${formatted_app_version}'" Write-Output "App version: '${formatted_app_version}'"
@@ -264,6 +273,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}'"
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
@@ -334,7 +344,7 @@ jobs:
echo "Cleaning up ..." echo "Cleaning up ..."
rm -fr "$TMP" rm -fr "$TMP"
- name: Install PDFium (Windows) - name: Deploy PDFium (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
env: env:
PDFIUM_VERSION: ${{ env.PDFIUM_VERSION }} PDFIUM_VERSION: ${{ env.PDFIUM_VERSION }}
@@ -385,6 +395,128 @@ jobs:
Write-Host "Cleaning up ..." Write-Host "Cleaning up ..."
Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue 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: 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 to remove the temporary directory, but ignore errors if files are still in use
try { try {
Remove-Item $TMP -Recurse -Force -ErrorAction Stop Remove-Item $TMP -Recurse -Force -ErrorAction Stop
@@ -821,4 +953,4 @@ jobs:
name: "Release ${{ env.FORMATTED_VERSION }}" name: "Release ${{ env.FORMATTED_VERSION }}"
fail_on_unmatched_files: true fail_on_unmatched_files: true
files: | files: |
release/assets/* release/assets/*
+7
View File
@@ -6,6 +6,13 @@ libpdfium.dylib
libpdfium.so libpdfium.so
libpdfium.dll libpdfium.dll
# Ignore qdrant database:
qdrant-aarch64-apple-darwin
qdrant-x86_64-apple-darwin
qdrant-aarch64-unknown-linux-gnu
qdrant-x86_64-unknown-linux-gnu
qdrant-x86_64-pc-windows-msvc.exe
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo
+2 -1
View File
@@ -32,7 +32,7 @@ Since November 2024: Work on RAG (integration of your data and files) has begun.
- [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))~~
- [ ] App: Implement external embedding providers - [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings - [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) - [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
- [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))~~
@@ -105,6 +105,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
**Key advantages:** **Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes. - **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Democratization of AI**: We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support: - **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support:
- [OpenAI](https://openai.com/) (GPT5, GPT4.1, o1, o3, o4, etc.) - [OpenAI](https://openai.com/) (GPT5, GPT4.1, o1, o3, o4, etc.)
- [Perplexity](https://www.perplexity.ai/) - [Perplexity](https://www.perplexity.ai/)
+3
View File
@@ -0,0 +1,3 @@
namespace Build.Commands;
public record Database(string Path, string Filename);
+120
View File
@@ -0,0 +1,120 @@
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,
};
}
}
@@ -150,6 +150,9 @@ public sealed partial class UpdateMetadataCommands
var pdfiumVersion = await this.ReadPdfiumVersion(); var pdfiumVersion = await this.ReadPdfiumVersion();
await Pdfium.InstallAsync(rid, pdfiumVersion); await Pdfium.InstallAsync(rid, pdfiumVersion);
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()}");
@@ -364,6 +367,16 @@ 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;
+2
View File
@@ -27,4 +27,6 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=mime/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=mime/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Qdrant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=qdrant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -136,13 +136,16 @@ else
<ReadFileContent Text="@T("Load output rules from document")" @bind-FileContent="@this.policyOutputRules" Disabled="@this.IsNoPolicySelectedOrProtected"/> <ReadFileContent Text="@T("Load output rules from document")" @bind-FileContent="@this.policyOutputRules" Disabled="@this.IsNoPolicySelectedOrProtected"/>
<MudText Typo="Typo.h5" Class="mt-6 mb-1"> @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
@T("Preparation for enterprise distribution") {
</MudText> <MudText Typo="Typo.h5" Class="mt-6 mb-1">
@T("Preparation for enterprise distribution")
</MudText>
<MudButton StartIcon="@Icons.Material.Filled.ContentCopy" Disabled="@(this.IsNoPolicySelected || (this.selectedPolicy?.IsEnterpriseConfiguration ?? false))" Variant="Variant.Filled" Color="Color.Default" OnClick="@this.ExportPolicyAsConfiguration"> <MudButton StartIcon="@Icons.Material.Filled.ContentCopy" Disabled="@(this.IsNoPolicySelected || (this.selectedPolicy?.IsEnterpriseConfiguration ?? false))" Variant="Variant.Filled" Color="Color.Default" OnClick="@this.ExportPolicyAsConfiguration">
@T("Export policy as configuration section") @T("Export policy as configuration section")
</MudButton> </MudButton>
}
} }
</ExpansionPanel> </ExpansionPanel>
@@ -1804,21 +1804,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1086130692"] = "Limitations
-- Personal Needs and Limitations of Web Services -- Personal Needs and Limitations of Web Services
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Personal Needs and Limitations of Web Services" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Personal Needs and Limitations of Web Services"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1986314327"] = "Democratization of AI"
-- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations. -- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time. -- We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3569462457"] = "Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3672974243"] = "We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge."
-- Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3622193740"] = "Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices."
-- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge. -- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T483341611"] = "Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community."
-- Cross-Platform and Modern Development -- Cross-Platform and Modern Development
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development"
-- Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T904941692"] = "Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design."
-- Copies the content to the clipboard -- Copies the content to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard"
@@ -2080,12 +2086,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app. -- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
-- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization."
-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused. -- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."
-- Disable dictation and transcription -- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"
-- Enterprise Administration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2277116008"] = "Enterprise Administration"
-- Language behavior -- Language behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Language behavior" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Language behavior"
@@ -2095,6 +2107,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"]
-- Language -- Language
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language"
-- Administration settings are visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Administration settings are visible"
-- Save energy? -- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
@@ -2104,9 +2119,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"]
-- App Options -- App Options
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T362833"] = "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."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "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." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "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."
-- Show administration settings?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Show administration settings?"
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
-- Enable spellchecking? -- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?"
@@ -2140,9 +2164,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"]
-- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."
-- Generate an encryption secret and copy it to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Generate an encryption secret and copy it to the clipboard"
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
-- Delete -- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
-- Embed text
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Embed text"
-- Test Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider"
-- Add Embedding -- Add Embedding
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
@@ -2155,6 +2194,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider -- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
-- Embedding Vector (one dimension per line)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)"
-- Model -- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
@@ -2164,6 +2209,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
-- Name -- Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
-- No embedding was returned.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned."
-- Configured Embedding Providers -- Configured Embedding Providers
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
@@ -2173,6 +2221,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
-- Edit -- Edit
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
@@ -2194,9 +2245,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
-- Open Dashboard -- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
-- Test
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test"
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration"
-- Cannot export the encrypted API key: No enterprise encryption secret is configured.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T1832230847"] = "Cannot export the encrypted API key: No enterprise encryption secret is configured."
-- This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T3368145670"] = "This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key."
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "Export API Key?"
-- Show provider's confidence level? -- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?"
@@ -2302,6 +2371,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
-- No transcription provider configured yet. -- No transcription provider configured yet.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet."
@@ -2356,6 +2428,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration"
-- Copy {0} to the clipboard -- Copy {0} to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Copy {0} to the clipboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Copy {0} to the clipboard"
@@ -2392,9 +2467,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1648606751"] = "You'll be able t
-- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question. -- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question."
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1986314327"] = "Democratization of AI"
-- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer. -- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer."
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2201645589"] = "We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge."
-- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats. -- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats."
@@ -3274,6 +3355,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
-- Embedding Vector
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
@@ -4939,6 +5026,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what yo
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1628689293"] = "We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models."
-- Unrestricted usage -- Unrestricted usage
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unrestricted usage" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unrestricted usage"
@@ -4948,6 +5038,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Introduction"
-- Vision -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1986314327"] = "Democratization of AI"
-- Let's get started -- Let's get started
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started"
@@ -5002,18 +5095,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startup log file
-- Browse AI Studio's source code on GitHub — we welcome your contributions. -- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the plugin ID differs from the enterprise configuration ID."
-- This is a private AI Studio installation. It runs without an enterprise configuration. -- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available."
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Database version"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the configuration plugin..."
-- Encryption secret: is not configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secret: is not configured"
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active."
-- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "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."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "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." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "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."
@@ -5047,9 +5155,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
-- Copies the server URL to the clipboard -- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
@@ -5068,9 +5182,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Configuration se
-- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose."
-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2280402765"] = "AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management."
-- Configuration plugin ID: -- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:"
@@ -5095,6 +5206,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time -- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "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." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "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."
@@ -5131,6 +5245,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Changelog -- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@@ -5164,6 +5281,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3433065373"] = "Information abou
-- Used Rust compiler -- Used Rust compiler
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compiler" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compiler"
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management."
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
@@ -5173,9 +5293,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active."
-- this version does not met the requirements -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
@@ -5212,6 +5329,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
-- Used .NET SDK -- Used .NET SDK
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
-- Did you find a bug or are you experiencing issues? Report your concern here. -- Did you find a bug or are you experiencing issues? Report your concern here.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here."
@@ -5242,6 +5362,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
-- Disabled Plugins -- Disabled Plugins
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
-- Send a mail
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
-- Enable plugin -- Enable plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Enable plugin" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Enable plugin"
@@ -5254,6 +5377,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
-- Open website
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website"
-- Settings -- Settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings" UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
@@ -5770,6 +5896,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment. -- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment." UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."
@@ -5959,15 +6100,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2262604281"] = "The
-- The field DESCRIPTION does not exist or is not a valid string. -- The field DESCRIPTION does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "The field DESCRIPTION does not exist or is not a valid string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "The field DESCRIPTION does not exist or is not a valid string."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2320984047"] = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'."
-- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X). -- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2892057533"] = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'."
-- The table AUTHORS is empty. At least one author must be specified. -- The table AUTHORS is empty. At least one author must be specified.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "The table AUTHORS is empty. At least one author must be specified." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "The table AUTHORS is empty. At least one author must be specified."
-- The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3165663073"] = "The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient."
-- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string. -- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string."
@@ -13,6 +13,7 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"), new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
new (232, "v26.1.2, build 232 (2026-01-25 14:05 UTC)", "v26.1.2.md"), new (232, "v26.1.2, build 232 (2026-01-25 14:05 UTC)", "v26.1.2.md"),
new (231, "v26.1.1, build 231 (2026-01-11 15:53 UTC)", "v26.1.1.md"), new (231, "v26.1.1, build 231 (2026-01-11 15:53 UTC)", "v26.1.1.md"),
@@ -66,7 +66,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)"/> <MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.isStreaming)"/>
</MudTooltip> </MudTooltip>
} }
@@ -912,6 +912,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
break; break;
case Event.CHAT_STREAMING_DONE: case Event.CHAT_STREAMING_DONE:
// Streaming mutates the last AI block over time.
// In manual storage mode, a save during streaming must not
// mark the final streamed state as already persisted.
this.hasUnsavedChanges = true;
if(this.autoSaveEnabled) if(this.autoSaveEnabled)
await this.SaveThread(); await this.SaveThread();
break; break;
@@ -0,0 +1,10 @@
<div style="display: flex; align-items: center; gap: 8px; @this.Item.Style">
<MudIcon Icon="@this.Item.Icon"/>
<span>
@this.Item.Text
</span>
@if (!string.IsNullOrWhiteSpace(this.Item.CopyValue))
{
<MudCopyClipboardButton TooltipMessage="@this.Item.CopyTooltip" StringContent="@this.Item.CopyValue"/>
}
</div>
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ConfigInfoRow : ComponentBase
{
[Parameter]
public ConfigInfoRowItem Item { get; set; } = new(Icons.Material.Filled.ArrowRightAlt, string.Empty, string.Empty, string.Empty, string.Empty);
}
@@ -0,0 +1,9 @@
namespace AIStudio.Components;
public sealed record ConfigInfoRowItem(
string Icon,
string Text,
string CopyValue,
string CopyTooltip,
string Style = ""
);
@@ -0,0 +1,21 @@
<MudPaper Outlined="true" Class="@this.Class">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<MudIcon Icon="@this.HeaderIcon" Size="Size.Small"/>
<MudText Typo="Typo.subtitle2">
@this.HeaderText
</MudText>
</div>
@foreach (var item in this.Items)
{
<ConfigInfoRow Item="@item"/>
}
@if (this.ShowWarning)
{
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Size="Size.Small" Color="Color.Warning"/>
<MudText Typo="Typo.subtitle2">@this.WarningText</MudText>
</div>
}
</MudPaper>
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ConfigPluginInfoCard : ComponentBase
{
[Parameter]
public string HeaderIcon { get; set; } = Icons.Material.Filled.Extension;
[Parameter]
public string HeaderText { get; set; } = string.Empty;
[Parameter]
public IEnumerable<ConfigInfoRowItem> Items { get; set; } = [];
[Parameter]
public bool ShowWarning { get; set; }
[Parameter]
public string WarningText { get; set; } = string.Empty;
[Parameter]
public string Class { get; set; } = "pa-3 mt-2 mb-2";
}
@@ -14,8 +14,22 @@
SelectedValuesChanged="@this.OptionChanged"> SelectedValuesChanged="@this.OptionChanged">
@foreach (var data in this.Data) @foreach (var data in this.Data)
{ {
<MudSelectItemExtended Value="@data.Value"> var isLockedValue = this.IsLockedValue(data.Value);
@data.Name <MudSelectItemExtended Value="@data.Value" Disabled="@isLockedValue" Style="@(isLockedValue ? "pointer-events:auto !important;" : null)">
@if (isLockedValue)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexStart" Wrap="Wrap.NoWrap">
@* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@
<MudTooltip Text="@this.LockedTooltip()" Arrow="true" Placement="Placement.Right" RootStyle="display:inline-flex;">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" Class="mr-1"/>
</MudTooltip>
@data.Name
</MudStack>
}
else
{
@data.Name
}
</MudSelectItemExtended> </MudSelectItemExtended>
} }
</MudSelectExtended> </MudSelectExtended>
@@ -27,6 +27,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
/// </summary> /// </summary>
[Parameter] [Parameter]
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { }; public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
/// <summary>
/// Determines whether a specific item is locked by a configuration plugin.
/// </summary>
[Parameter]
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
#region Overrides of ConfigurationBase #region Overrides of ConfigurationBase
@@ -62,4 +68,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
return string.Format(T("You have selected {0} preview features."), selectedValues.Count); return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
} }
private bool IsLockedValue(TData value) => this.IsItemLocked(value);
private string LockedTooltip() =>
this.T(
"This feature is managed by your organization and has therefore been disabled.",
typeof(ConfigurationBase).Namespace,
nameof(ConfigurationBase));
} }
@@ -0,0 +1,15 @@
<MudText Typo="Typo.body1" Class="@this.Class">
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
@if (this.IsConfigured)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small"/>
<span>@this.ConfiguredText</span>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small"/>
<span>@this.NotConfiguredText</span>
}
</div>
</MudText>
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class EncryptionSecretInfo : ComponentBase
{
[Parameter]
public bool IsConfigured { get; set; }
[Parameter]
public string ConfiguredText { get; set; } = string.Empty;
[Parameter]
public string NotConfiguredText { get; set; } = string.Empty;
[Parameter]
public string Class { get; set; } = "mt-2 mb-2";
}
@@ -1,6 +1,6 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time.") @T("Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community.")
</MudText> </MudText>
<MudText Typo="Typo.h4"> <MudText Typo="Typo.h4">
@@ -28,5 +28,13 @@
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;"> <MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.") @T("Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design.")
</MudText> </MudText>
<MudText Typo="Typo.h4">
@T("Democratization of AI")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge.")
</MudText>
@@ -6,7 +6,7 @@
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{ {
<MudSelectItem Value="profile"> <MudSelectItem Value="profile">
@profile.Name @profile.GetSafeName()
</MudSelectItem> </MudSelectItem>
} }
</MudSelect> </MudSelect>
@@ -17,6 +17,7 @@
<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.")"/>
<ConfigurationOption OptionDescription="@T("Show administration settings?")" LabelOn="@T("Administration settings are visible")" LabelOff="@T("Administration settings are not visible")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowAdminSettings)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowAdminSettings = updatedState)" OptionHelp="@T("When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowAdminSettings, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreviewVisibility, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreviewVisibility, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE) @if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
@@ -24,11 +25,11 @@
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
if (availablePreviewFeatures.Count > 0) if (availablePreviewFeatures.Count > 0)
{ {
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/> <ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
} }
} }
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))"/> <ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
@@ -36,4 +37,25 @@
<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 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"/>
} }
</ExpansionPanel>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
{
<MudText Typo="Typo.h5" Class="mt-6 mb-3">
@T("Enterprise Administration")
</MudText>
<MudText Typo="Typo.body2" Class="mb-3">
@T("Generate a 256-bit encryption secret for encrypting API keys in configuration plugins. Deploy this secret to client machines via Group Policy (Windows Registry) or environment variables. Providers can then be exported with encrypted API keys using the export buttons in the provider settings.")
<MudLink Href="https://github.com/MindWorkAI/AI-Studio/blob/main/documentation/Enterprise%20IT.md" Target="_blank">
@T("Read the Enterprise IT documentation for details.")
</MudLink>
</MudText>
<MudButton StartIcon="@Icons.Material.Filled.Key"
Variant="Variant.Filled"
Color="Color.Primary"
OnClick="@this.GenerateEncryptionSecret">
@T("Generate an encryption secret and copy it to the clipboard")
</MudButton>
}
</ExpansionPanel>
@@ -6,6 +6,12 @@ namespace AIStudio.Components.Settings;
public partial class SettingsPanelApp : SettingsPanelBase public partial class SettingsPanelApp : SettingsPanelBase
{ {
private async Task GenerateEncryptionSecret()
{
var secret = EnterpriseEncryption.GenerateSecret();
await this.RustService.CopyText2Clipboard(this.Snackbar, secret);
}
private IEnumerable<ConfigurationSelectData<string>> GetFilteredTranscriptionProviders() private IEnumerable<ConfigurationSelectData<string>> GetFilteredTranscriptionProviders()
{ {
yield return new(T("Disable dictation and transcription"), string.Empty); yield return new(T("Disable dictation and transcription"), string.Empty);
@@ -21,7 +27,41 @@ public partial class SettingsPanelApp : SettingsPanelBase
private void UpdatePreviewFeatures(PreviewVisibility previewVisibility) private void UpdatePreviewFeatures(PreviewVisibility previewVisibility)
{ {
this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility; this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility;
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures); var filtered = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures);
filtered.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = filtered;
}
private HashSet<PreviewFeatures> GetPluginContributedPreviewFeatures()
{
if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution)
return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet();
return [];
}
private bool IsPluginContributedPreviewFeature(PreviewFeatures feature)
{
if (feature.IsReleased())
return false;
if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution)
return false;
return meta.PluginContribution.Contains(feature);
}
private HashSet<PreviewFeatures> GetSelectedPreviewFeatures()
{
var enabled = this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet();
enabled.UnionWith(this.GetPluginContributedPreviewFeatures());
return enabled;
}
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
{
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
} }
private async Task UpdateLangBehaviour(LangBehavior behavior) private async Task UpdateLangBehaviour(LangBehavior behavior)
@@ -15,4 +15,7 @@ public abstract class SettingsPanelBase : MSGComponentBase
[Inject] [Inject]
protected RustService RustService { get; init; } = null!; protected RustService RustService { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
} }
@@ -1,6 +1,6 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits SettingsPanelBase @inherits SettingsPanelProviderBase
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{ {
@@ -22,7 +22,7 @@
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col/> <col/>
<col style="width: 16em;"/> <col style="width: 22em;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>#</MudTh> <MudTh>#</MudTh>
@@ -53,9 +53,18 @@
<MudTooltip Text="@T("Edit")"> <MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditEmbeddingProvider(context))"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditEmbeddingProvider(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.ExportEmbeddingProvider(context))"/>
</MudTooltip>
}
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEmbeddingProvider(context))"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEmbeddingProvider(context))"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="@T("Test")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))"/>
</MudTooltip>
} }
</MudStack> </MudStack>
</MudTd> </MudTd>
@@ -73,4 +82,4 @@
@T("Add Embedding") @T("Add Embedding")
</MudButton> </MudButton>
</ExpansionPanel> </ExpansionPanel>
} }
@@ -1,4 +1,6 @@
using System.Globalization;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -7,7 +9,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components.Settings; namespace AIStudio.Components.Settings;
public partial class SettingsPanelEmbeddings : SettingsPanelBase public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
{ {
[Parameter] [Parameter]
public List<ConfigurationSelectData<string>> AvailableEmbeddingProviders { get; set; } = new(); public List<ConfigurationSelectData<string>> AvailableEmbeddingProviders { get; set; } = new();
@@ -114,6 +116,17 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase
await this.UpdateEmbeddingProviders(); await this.UpdateEmbeddingProviders();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ExportEmbeddingProvider(EmbeddingProvider provider)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (provider == EmbeddingProvider.NONE)
return;
await this.ExportProvider(provider, SecretStoreType.EMBEDDING_PROVIDER, provider.ExportAsConfigurationSection);
}
private async Task UpdateEmbeddingProviders() private async Task UpdateEmbeddingProviders()
{ {
@@ -123,4 +136,48 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase
await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders); await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders);
} }
}
private async Task TestEmbeddingProvider(EmbeddingProvider provider)
{
var dialogParameters = new DialogParameters<SingleInputDialog>
{
{ x => x.ConfirmText, T("Embed text") },
{ x => x.InputHeaderText, T("Add text that should be embedded:") },
{ x => x.UserInput, T("Example text to embed") },
};
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Test Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var inputText = dialogResult.Data as string;
if (string.IsNullOrWhiteSpace(inputText))
return;
var embeddingProvider = provider.CreateProvider();
var embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, default, new List<string> { inputText });
if (embeddings.Count == 0)
{
await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close"));
return;
}
var vector = embeddings.FirstOrDefault();
if (vector is null || vector.Count == 0)
{
await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close"));
return;
}
var resultText = string.Join(Environment.NewLine, vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture)));
var resultParameters = new DialogParameters<EmbeddingResultDialog>
{
{ x => x.ResultText, resultText },
{ x => x.ResultLabel, T("Embedding Vector (one dimension per line)") },
};
await this.DialogService.ShowAsync<EmbeddingResultDialog>(T("Embedding Result"), resultParameters, DialogOptions.FULLSCREEN);
}
}
@@ -0,0 +1,61 @@
using AIStudio.Dialogs;
using AIStudio.Tools.PluginSystem;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components.Settings;
public abstract class SettingsPanelProviderBase : SettingsPanelBase
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SettingsPanelProviderBase).Namespace, nameof(SettingsPanelProviderBase));
/// <summary>
/// Exports the provider configuration as Lua code, optionally including the encrypted API key if the provider has one
/// configured and the user agrees to include it. The exportFunc should generate the Lua code based on the provided
/// encrypted API key (which may be null if the user chose not to include it or if encryption is not available).
/// The generated Lua code is then copied to the clipboard for easy sharing.
/// </summary>
/// <param name="secretId">The secret ID of the provider to check for an API key.</param>
/// <param name="storeType">The type of secret store to check for the API key (e.g., LLM provider, transcription provider, etc.).</param>
/// <param name="exportFunc">The function that generates the Lua code for the provider configuration, given the optional encrypted API key.</param>
protected async Task ExportProvider(ISecretId secretId, SecretStoreType storeType, Func<string?, string> exportFunc)
{
string? encryptedApiKey = null;
// Check if the provider has an API key stored:
var apiKeyResponse = await this.RustService.GetAPIKey(secretId, storeType, isTrying: true);
if (apiKeyResponse.Success)
{
// Ask the user if they want to export the API key:
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, TB("This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key.") },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(TB("Export API Key?"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is { Canceled: false })
{
// User wants to export the API key - encrypt it:
var encryption = PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable == true)
{
var decryptedApiKey = await apiKeyResponse.Secret.Decrypt(Program.ENCRYPTION);
if (encryption.TryEncrypt(decryptedApiKey, out var encrypted))
encryptedApiKey = encrypted;
}
else
{
// No encryption secret available - inform the user:
this.Snackbar.Add(TB("Cannot export the encrypted API key: No enterprise encryption secret is configured."), Severity.Warning);
}
}
}
var luaCode = exportFunc(encryptedApiKey);
if (string.IsNullOrWhiteSpace(luaCode))
return;
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
}
}
@@ -1,6 +1,6 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Settings @using AIStudio.Settings
@inherits SettingsPanelBase @inherits SettingsPanelProviderBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Layers" HeaderText="@T("Configure LLM Providers")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Layers" HeaderText="@T("Configure LLM Providers")">
<MudText Typo="Typo.h4" Class="mb-3"> <MudText Typo="Typo.h4" Class="mb-3">
@@ -15,7 +15,7 @@
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col/> <col/>
<col style="width: 16em;"/> <col style="width: 22em;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>#</MudTh> <MudTh>#</MudTh>
@@ -45,6 +45,12 @@
<MudTooltip Text="@T("Edit")"> <MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditLLMProvider(context))"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditLLMProvider(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.ExportLLMProvider(context))"/>
</MudTooltip>
}
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteLLMProvider(context))"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteLLMProvider(context))"/>
</MudTooltip> </MudTooltip>
@@ -117,4 +123,4 @@
</MudTable> </MudTable>
} }
} }
</ExpansionPanel> </ExpansionPanel>
@@ -10,7 +10,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components.Settings; namespace AIStudio.Components.Settings;
public partial class SettingsPanelProviders : SettingsPanelBase public partial class SettingsPanelProviders : SettingsPanelProviderBase
{ {
[Parameter] [Parameter]
public List<ConfigurationSelectData<string>> AvailableLLMProviders { get; set; } = new(); public List<ConfigurationSelectData<string>> AvailableLLMProviders { get; set; } = new();
@@ -134,6 +134,17 @@ public partial class SettingsPanelProviders : SettingsPanelBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ExportLLMProvider(AIStudio.Settings.Provider provider)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (provider == AIStudio.Settings.Provider.NONE)
return;
await this.ExportProvider(provider, SecretStoreType.LLM_PROVIDER, provider.ExportAsConfigurationSection);
}
private string GetLLMProviderModelName(AIStudio.Settings.Provider provider) private string GetLLMProviderModelName(AIStudio.Settings.Provider provider)
{ {
// For system models, return localized text: // For system models, return localized text:
@@ -176,4 +187,4 @@ public partial class SettingsPanelProviders : SettingsPanelBase
this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[llmProvider] = level; this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[llmProvider] = level;
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
} }
} }
@@ -1,6 +1,6 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits SettingsPanelBase @inherits SettingsPanelProviderBase
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{ {
@@ -19,7 +19,7 @@
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col style="width: 12em;"/> <col style="width: 12em;"/>
<col/> <col/>
<col style="width: 16em;"/> <col style="width: 22em;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>#</MudTh> <MudTh>#</MudTh>
@@ -50,6 +50,12 @@
<MudTooltip Text="@T("Edit")"> <MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditTranscriptionProvider(context))"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditTranscriptionProvider(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.ExportTranscriptionProvider(context))"/>
</MudTooltip>
}
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteTranscriptionProvider(context))"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteTranscriptionProvider(context))"/>
</MudTooltip> </MudTooltip>
@@ -7,7 +7,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components.Settings; namespace AIStudio.Components.Settings;
public partial class SettingsPanelTranscription : SettingsPanelBase public partial class SettingsPanelTranscription : SettingsPanelProviderBase
{ {
[Parameter] [Parameter]
public List<ConfigurationSelectData<string>> AvailableTranscriptionProviders { get; set; } = new(); public List<ConfigurationSelectData<string>> AvailableTranscriptionProviders { get; set; } = new();
@@ -114,6 +114,17 @@ public partial class SettingsPanelTranscription : SettingsPanelBase
await this.UpdateTranscriptionProviders(); await this.UpdateTranscriptionProviders();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
private async Task ExportTranscriptionProvider(TranscriptionProvider provider)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
return;
if (provider == TranscriptionProvider.NONE)
return;
await this.ExportProvider(provider, SecretStoreType.TRANSCRIPTION_PROVIDER, provider.ExportAsConfigurationSection);
}
private async Task UpdateTranscriptionProviders() private async Task UpdateTranscriptionProviders()
{ {
@@ -123,4 +134,4 @@ public partial class SettingsPanelTranscription : SettingsPanelBase
await this.AvailableTranscriptionProvidersChanged.InvokeAsync(this.AvailableTranscriptionProviders); await this.AvailableTranscriptionProvidersChanged.InvokeAsync(this.AvailableTranscriptionProviders);
} }
} }
@@ -19,6 +19,7 @@ public partial class Vision : MSGComponentBase
this.itemsVision = this.itemsVision =
[ [
new(T("Meet your needs"), T("Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer.")), new(T("Meet your needs"), T("Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer.")),
new(T("Democratization of AI"), T("We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge.")),
new(T("Integrating your data"), T("You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.")), new(T("Integrating your data"), T("You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.")),
new(T("Integration of enterprise data"), T("It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question.")), new(T("Integration of enterprise data"), T("It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question.")),
new(T("Useful assistants"), T("We'll develop more assistants for everyday tasks.")), new(T("Useful assistants"), T("We'll develop more assistants for everyday tasks.")),
@@ -36,7 +36,7 @@
</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.Inherit" OnClick="@(() => this.DeleteChat(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChat(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
</div> </div>
</div> </div>
@@ -57,7 +57,7 @@
</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.Inherit" OnClick="@(() => this.DeleteWorkspace(treeItem.Path))"/> <MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspace(treeItem.Path))"/>
</MudTooltip> </MudTooltip>
</div> </div>
</div> </div>
@@ -90,4 +90,4 @@
break; break;
} }
</ItemTemplate> </ItemTemplate>
</MudTreeView> </MudTreeView>
@@ -0,0 +1,22 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudTextField
T="string"
Text="@this.ResultText"
Variant="Variant.Outlined"
Lines="10"
AutoGrow="@true"
MaxLines="25"
ReadOnly="true"
Label="@this.ResultLabelText"
AdornmentIcon="@Icons.Material.Filled.DataObject"
Adornment="Adornment.Start"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,21 @@
using AIStudio.Components;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class EmbeddingResultDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string ResultText { get; set; } = string.Empty;
[Parameter]
public string ResultLabel { get; set; } = string.Empty;
private string ResultLabelText => string.IsNullOrWhiteSpace(this.ResultLabel) ? T("Embedding Vector") : this.ResultLabel;
private void Close() => this.MudDialog.Close();
}
@@ -1,11 +1,21 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<MudDialog> <MudDialog DefaultFocus="DefaultFocus.FirstChild">
<DialogContent> <DialogContent>
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
@this.Message @this.Message
</MudText> </MudText>
<MudForm @ref="this.form" Class="mt-4"> <MudForm @ref="this.form" Class="mt-4">
<MudTextField T="string" @bind-Text="@this.UserInput" Variant="Variant.Outlined" AutoGrow="@false" Lines="1" Label="@this.GetInputHeaderText" AutoFocus="@true" UserAttributes="@USER_INPUT_ATTRIBUTES" Validation="@this.ValidateUserInput" /> <MudTextField T="string"
@bind-Text="@this.UserInput"
Variant="Variant.Outlined"
AutoGrow="@false"
Lines="1"
Label="@this.GetInputHeaderText"
AutoFocus="@true"
Immediate="@true"
OnKeyDown="@this.HandleUserInputKeyDown"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Validation="@this.ValidateUserInput" />
</MudForm> </MudForm>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -16,4 +26,4 @@
@this.ConfirmText @this.ConfirmText
</MudButton> </MudButton>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>
@@ -1,6 +1,7 @@
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;
@@ -57,6 +58,19 @@ public partial class SingleInputDialog : MSGComponentBase
private void Cancel() => this.MudDialog.Cancel(); private void Cancel() => this.MudDialog.Cancel();
private async Task HandleUserInputKeyDown(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.Confirm();
}
private async Task Confirm() private async Task Confirm()
{ {
await this.form.Validate(); await this.form.Validate();
@@ -65,4 +79,4 @@ public partial class SingleInputDialog : MSGComponentBase
this.MudDialog.Close(DialogResult.Ok(this.UserInput)); this.MudDialog.Close(DialogResult.Ok(this.UserInput));
} }
} }
@@ -211,9 +211,35 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
// //
// Check if there is an enterprise configuration plugin to download: // Check if there is an enterprise configuration plugin to download:
// //
var enterpriseEnvironment = this.MessageBus.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT).FirstOrDefault(); var enterpriseEnvironments = this.MessageBus
if (enterpriseEnvironment != default) .CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl); .Where(env => env != default)
.ToList();
var failedDeferredConfigIds = new HashSet<Guid>();
foreach (var env in enterpriseEnvironments)
{
var wasDownloadSuccessful = await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
if (!wasDownloadSuccessful)
{
failedDeferredConfigIds.Add(env.ConfigurationId);
this.Logger.LogWarning("Failed to download deferred enterprise configuration '{ConfigId}' during startup. Keeping managed plugins unchanged.", env.ConfigurationId);
}
}
if (EnterpriseEnvironmentService.HasValidEnterpriseSnapshot)
{
var activeConfigIds = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
.Select(env => env.ConfigurationId)
.ToHashSet();
PluginFactory.RemoveUnreferencedManagedConfigurationPlugins(activeConfigIds);
if (failedDeferredConfigIds.Count > 0)
this.Logger.LogWarning("Deferred startup updates failed for {FailedCount} enterprise configuration(s). Those configurations were kept unchanged.", failedDeferredConfigIds.Count);
}
// Initialize the enterprise encryption service for decrypting API keys:
await PluginFactory.InitializeEnterpriseEncryption(this.RustService);
// Load (but not start) all plugins without waiting for them: // Load (but not start) all plugins without waiting for them:
#if DEBUG #if DEBUG
@@ -52,6 +52,7 @@
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.12" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.12" />
<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.3" />
</ItemGroup> </ItemGroup>
@@ -81,6 +82,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>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
@@ -108,6 +110,9 @@
<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">
<_Parameter1>$(MetaQdrantVersion)</_Parameter1>
</AssemblyAttribute>
</ItemGroup> </ItemGroup>
</Target> </Target>
@@ -31,6 +31,7 @@ public partial class Home : MSGComponentBase
{ {
this.itemsAdvantages = [ this.itemsAdvantages = [
new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")), new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")),
new(this.T("Democratization of AI"), this.T("We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.")),
new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")), new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")),
new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")), new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")),
new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")), new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")),
+144 -68
View File
@@ -1,4 +1,5 @@
@attribute [Route(Routes.ABOUT)] @attribute [Route(Routes.ABOUT)]
@using AIStudio.Tools.PluginSystem
@using AIStudio.Tools.Services @using AIStudio.Tools.Services
@inherits MSGComponentBase @inherits MSGComponentBase
@@ -19,86 +20,158 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionDotnetSdk"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionDotnetSdk"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@this.VersionDotnetRuntime"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@this.VersionDotnetRuntime"/>
<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">
<MudText Typo="Typo.body1">
@this.VersionDatabase
</MudText>
<MudCollapse Expanded="@this.showDatabaseDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
@foreach (var item in this.databaseDisplayInfo)
{
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@item.Label: @item.Value</span>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the following to the clipboard")+": "+item.Value)" StringContent=@item.Value/>
</div>
}
</MudText>
</MudCollapse>
<MudButton StartIcon="@(this.showDatabaseDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
Variant="Variant.Text"
OnClick="@this.ToggleDatabaseDetails">
@(this.showDatabaseDetails ? T("Hide Details") : T("Show Details"))
</MudButton>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Article" Text="@this.versionPandoc"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Article" Text="@this.versionPandoc"/>
<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.Business"> <MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
@switch (EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive) @switch (HasAnyActiveEnvironment)
{ {
case false when this.configPlug is null: case false when this.configPlugins.Count == 0:
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
@T("This is a private AI Studio installation. It runs without an enterprise configuration.") @T("This is a private AI Studio installation. It runs without an enterprise configuration.")
</MudText> </MudText>
break; break;
case false: case false:
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.") @T("AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.")
</MudText> </MudText>
<MudCollapse Expanded="@this.showEnterpriseConfigDetails"> <MudCollapse Expanded="@this.showEnterpriseConfigDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2"> @foreach (var plug in this.configPlugins)
<div style="display: flex; align-items: center; gap: 8px;"> {
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/> <ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
<span>@T("Configuration plugin ID:") @this.configPlug!.Id</span> HeaderText="@plug.Name"
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@this.configPlug!.Id.ToString()/> Items="@([
</div> new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
</MudText> $"{T("Configuration plugin ID:")} {plug.Id}",
</MudCollapse> plug.Id.ToString(),
break; T("Copies the configuration plugin ID to the clipboard"))
])"/>
case true when this.configPlug is null: }
<MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.")
</MudText>
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId</span>
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId.ToString()/>
</div>
</MudText>
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl</span>
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl/>
</div>
</MudText>
</MudCollapse>
break;
case true:
<MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.")
</MudText>
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId</span>
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId.ToString()/>
</div>
</MudText>
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl</span>
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl/>
</div>
</MudText>
<MudText Typo="Typo.body1" Class="mt-2 mb-2"> <EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
<div style="display: flex; align-items: center; gap: 8px;"> ConfiguredText="@T("Encryption secret: is configured")"
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/> NotConfiguredText="@T("Encryption secret: is not configured")"/>
<span>@T("Configuration plugin ID:") @this.configPlug!.Id</span> </MudCollapse>
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@this.configPlug!.Id.ToString()/> break;
</div>
case true when this.configPlugins.Count == 0:
<MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.")
</MudText>
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
}
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
ConfiguredText="@T("Encryption secret: is configured")"
NotConfiguredText="@T("Encryption secret: is not configured")"/>
</MudCollapse>
break;
case true:
@if (this.HasAnyLoadedEnterpriseConfigurationPlugin)
{
<MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.")
</MudText> </MudText>
}
else
{
<MudText Typo="Typo.body1">
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.")
</MudText>
}
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
{
var matchingPlugin = this.FindManagedConfigurationPlugin(env.ConfigurationId);
if (matchingPlugin is null)
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
continue;
}
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
HeaderText="@matchingPlugin.Name"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
matchingPlugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;")
])"
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
}
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
ConfiguredText="@T("Encryption secret: is configured")"
NotConfiguredText="@T("Encryption secret: is not configured")"/>
</MudCollapse> </MudCollapse>
break; break;
} }
@@ -115,10 +188,10 @@
</MudListItem> </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())">
@this.PandocButtonText @this.PandocButtonText
</MudButton> </MudButton>
</MudStack> </MudStack>
@@ -126,7 +199,7 @@
<ExpansionPanel HeaderIcon="@Icons.Custom.Brands.GitHub" HeaderText="@T("Community & Code")"> <ExpansionPanel HeaderIcon="@Icons.Custom.Brands.GitHub" HeaderText="@T("Community & Code")">
<MudList T="string" Class="mb-1"> <MudList T="string" Class="mb-1">
<MudListItem T="string" Icon="@Icons.Material.Outlined.Home" Target="_blank" Href="http://mindworkai.org/"> <MudListItem T="string" Icon="@Icons.Material.Outlined.Home" Target="_blank" Href="https://mindworkai.org/">
@T("Discover MindWork AI's mission and vision on our official homepage.") @T("Discover MindWork AI's mission and vision on our official homepage.")
</MudListItem> </MudListItem>
<MudListItem T="string" Icon="@Icons.Custom.Brands.GitHub" Target="_blank" Href="https://github.com/MindWorkAI/AI-Studio"> <MudListItem T="string" Icon="@Icons.Custom.Brands.GitHub" Target="_blank" Href="https://github.com/MindWorkAI/AI-Studio">
@@ -167,14 +240,14 @@
@T("Startup log file") @T("Startup log file")
</MudText> </MudText>
<MudList T="string" Class="mb-3"> <MudList T="string" Class="mb-3">
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogStartupPath" OnClick="() => this.CopyStartupLogPath()"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogStartupPath" OnClick="@(() => this.CopyStartupLogPath())"/>
</MudList> </MudList>
<MudText Typo="Typo.h4"> <MudText Typo="Typo.h4">
@T("Usage log file") @T("Usage log file")
</MudText> </MudText>
<MudList T="string" Class="mb-3"> <MudList T="string" Class="mb-3">
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogAppPath" OnClick="() => this.CopyAppLogPath()"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogAppPath" OnClick="@(() => this.CopyAppLogPath())"/>
</MudList> </MudList>
</ExpansionPanel> </ExpansionPanel>
@@ -194,6 +267,7 @@
<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.")"/>
<ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/> <ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/>
<ThirdPartyComponent Name="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.")"/>
@@ -214,6 +288,8 @@
<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="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="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.")"/>
@@ -2,6 +2,7 @@ using System.Reflection;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Metadata; using AIStudio.Tools.Metadata;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
@@ -25,12 +26,16 @@ public partial class Information : MSGComponentBase
[Inject] [Inject]
private ISnackbar Snackbar { get; init; } = null!; private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private DatabaseClient DatabaseClient { 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 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;
@@ -53,6 +58,8 @@ 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 => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}";
private string versionPandoc = TB("Determine Pandoc version, please wait..."); private string versionPandoc = TB("Determine Pandoc version, please wait...");
private PandocInstallation pandocInstallation; private PandocInstallation pandocInstallation;
@@ -60,7 +67,22 @@ public partial class Information : MSGComponentBase
private bool showEnterpriseConfigDetails; private bool showEnterpriseConfigDetails;
private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); private bool showDatabaseDetails;
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
.Where(x => x.Type is PluginType.CONFIGURATION)
.OfType<IAvailablePlugin>()
.ToList();
private sealed record DatabaseDisplayInfo(string Label, string Value);
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
private static bool HasAnyActiveEnvironment => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Any(e => e.IsActive);
private bool HasAnyLoadedEnterpriseConfigurationPlugin => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
.Where(e => e.IsActive)
.Any(env => this.FindManagedConfigurationPlugin(env.ConfigurationId) is not null);
/// <summary> /// <summary>
/// Determines whether the enterprise configuration has details that can be shown/hidden. /// Determines whether the enterprise configuration has details that can be shown/hidden.
@@ -70,16 +92,16 @@ public partial class Information : MSGComponentBase
{ {
get get
{ {
return EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive switch return HasAnyActiveEnvironment switch
{ {
// Case 1: No enterprise config and no plugin - no details available // Case 1: No enterprise config and no plugin - no details available
false when this.configPlug is null => false, false when this.configPlugins.Count == 0 => false,
// Case 2: Enterprise config with plugin but no central management - has details // Case 2: Enterprise config with plugin but no central management - has details
false => true, false => true,
// Case 3: Enterprise config active but no plugin - has details // Case 3: Enterprise config active but no plugin - has details
true when this.configPlug is null => true, true when this.configPlugins.Count == 0 => true,
// Case 4: Enterprise config active with plugin - has details // Case 4: Enterprise config active with plugin - has details
true => true true => true
@@ -96,6 +118,11 @@ public partial class Information : MSGComponentBase
this.osLanguage = await this.RustService.ReadUserLanguage(); this.osLanguage = await this.RustService.ReadUserLanguage();
this.logPaths = await this.RustService.GetLogPaths(); this.logPaths = await this.RustService.GetLogPaths();
await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo())
{
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
}
// 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:
_ = this.DeterminePandocVersion(); _ = this.DeterminePandocVersion();
@@ -110,7 +137,10 @@ public partial class Information : MSGComponentBase
switch (triggeredEvent) switch (triggeredEvent)
{ {
case Event.PLUGINS_RELOADED: case Event.PLUGINS_RELOADED:
this.configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); this.configPlugins = PluginFactory.AvailablePlugins
.Where(x => x.Type is PluginType.CONFIGURATION)
.OfType<IAvailablePlugin>()
.ToList();
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
break; break;
} }
@@ -170,6 +200,23 @@ public partial class Information : MSGComponentBase
{ {
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails; this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
} }
private void ToggleDatabaseDetails()
{
this.showDatabaseDetails = !this.showDatabaseDetails;
}
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
{
return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId)
// Backward compatibility for already downloaded plugins without ManagedConfigurationId.
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
}
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
{
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
}
private async Task CopyStartupLogPath() private async Task CopyStartupLogPath()
{ {
+28 -8
View File
@@ -63,15 +63,35 @@
</MudStack> </MudStack>
</MudTd> </MudTd>
<MudTd> <MudTd>
@if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION }) <MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
{ @if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
var isEnabled = this.SettingsManager.IsPluginEnabled(context); {
<MudTooltip Text="@(isEnabled ? T("Disable plugin") : T("Enable plugin"))"> var isEnabled = this.SettingsManager.IsPluginEnabled(context);
<MudSwitch T="bool" Value="@isEnabled" ValueChanged="@(_ => this.PluginActivationStateChanged(context))"/> <MudTooltip Text="@(isEnabled ? T("Disable plugin") : T("Enable plugin"))">
</MudTooltip> <MudSwitch T="bool" Value="@isEnabled" ValueChanged="@(_ => this.PluginActivationStateChanged(context))"/>
} </MudTooltip>
}
@if (context is { IsInternal: false } && !string.IsNullOrWhiteSpace(context.SourceURL))
{
var sourceUrl = context.SourceURL;
var isSendingMail = IsSendingMail(sourceUrl);
if(isSendingMail)
{
<MudTooltip Text="@T("Send a mail")">
<MudIconButton Icon="@Icons.Material.Filled.Email" Href="@sourceUrl" Target="_blank" Size="Size.Medium"/>
</MudTooltip>
}
else
{
<MudTooltip Text="@T("Open website")">
<MudIconButton Icon="@Icons.Material.Filled.OpenInBrowser" Href="@sourceUrl" Target="_blank" Size="Size.Medium"/>
</MudTooltip>
}
}
</MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
</InnerScrolling> </InnerScrolling>
</div> </div>
@@ -49,6 +49,8 @@ public partial class Plugins : MSGComponentBase
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 static bool IsSendingMail(string sourceUrl) => sourceUrl.TrimStart().StartsWith("mailto:", StringComparison.OrdinalIgnoreCase);
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
@@ -63,4 +65,4 @@ public partial class Plugins : MSGComponentBase
} }
#endregion #endregion
} }
@@ -24,13 +24,21 @@ VERSION = "1.0.0"
-- The type of the plugin: -- The type of the plugin:
TYPE = "CONFIGURATION" TYPE = "CONFIGURATION"
-- True when this plugin is deployed by an enterprise configuration server:
DEPLOYED_USING_CONFIG_SERVER = false
-- The authors of the plugin: -- The authors of the plugin:
AUTHORS = {"<Company Name>"} AUTHORS = {"<Company Name>"}
-- The support contact for the plugin: -- The support contact for the plugin:
SUPPORT_CONTACT = "<IT Department of Company Name>" SUPPORT_CONTACT = "<IT Department of Company Name>"
-- The source URL for the plugin: -- The source URL for the plugin. Can be a HTTP(S) URL or an mailto link.
-- You may link to an internal documentation page, a Git repository, or
-- to a support or wiki page.
--
-- A mailto link could look like:
-- SOURCE_URL = "mailto:helpdesk@company.org?subject=Help"
SOURCE_URL = "<Any internal Git repository>" SOURCE_URL = "<Any internal Git repository>"
-- The categories for the plugin: -- The categories for the plugin:
@@ -64,6 +72,20 @@ CONFIG["LLM_PROVIDERS"] = {}
-- -- Could be something like ... \"temperature\": 0.5, \"max_tokens\": 1000 ... for multiple parameters. -- -- Could be something like ... \"temperature\": 0.5, \"max_tokens\": 1000 ... for multiple parameters.
-- -- Please do not add the enclosing curly braces {} here. Also, no trailing comma is allowed. -- -- Please do not add the enclosing curly braces {} here. Also, no trailing comma is allowed.
-- ["AdditionalJsonApiParameters"] = "", -- ["AdditionalJsonApiParameters"] = "",
--
-- -- Optional: Hugging Face inference provider. Only relevant for UsedLLMProvider = HUGGINGFACE.
-- -- Allowed values are: CEREBRAS, NEBIUS_AI_STUDIO, SAMBANOVA, NOVITA, HYPERBOLIC, TOGETHER_AI, FIREWORKS, HF_INFERENCE_API
-- -- ["HFInferenceProvider"] = "NOVITA",
--
-- -- Optional: Encrypted API key for cloud providers or secured on-premise models.
-- -- The API key must be encrypted using the enterprise encryption secret.
-- -- Format: "ENC:v1:<base64-encoded encrypted data>"
-- -- The encryption secret must be configured via:
-- -- Windows Registry: HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\config_encryption_secret
-- -- Environment variable: MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET
-- -- You can export an encrypted API key from an existing provider using the export button in the settings.
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
--
-- ["Model"] = { -- ["Model"] = {
-- ["Id"] = "<the model ID>", -- ["Id"] = "<the model ID>",
-- ["DisplayName"] = "<user-friendly name of the model>", -- ["DisplayName"] = "<user-friendly name of the model>",
@@ -82,6 +104,10 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {}
-- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, VLLM, and WHISPER_CPP -- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, VLLM, and WHISPER_CPP
-- ["Host"] = "WHISPER_CPP", -- ["Host"] = "WHISPER_CPP",
-- ["Hostname"] = "<https address of the server>", -- ["Hostname"] = "<https address of the server>",
--
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
--
-- ["Model"] = { -- ["Model"] = {
-- ["Id"] = "<the model ID>", -- ["Id"] = "<the model ID>",
-- ["DisplayName"] = "<user-friendly name of the model>", -- ["DisplayName"] = "<user-friendly name of the model>",
@@ -100,6 +126,10 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
-- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, and VLLM -- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, and VLLM
-- ["Host"] = "OLLAMA", -- ["Host"] = "OLLAMA",
-- ["Hostname"] = "<https address of the server>", -- ["Hostname"] = "<https address of the server>",
--
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
--
-- ["Model"] = { -- ["Model"] = {
-- ["Id"] = "<the model ID, e.g., nomic-embed-text>", -- ["Id"] = "<the model ID, e.g., nomic-embed-text>",
-- ["DisplayName"] = "<user-friendly name of the model>", -- ["DisplayName"] = "<user-friendly name of the model>",
@@ -120,6 +150,10 @@ CONFIG["SETTINGS"] = {}
-- Allowed values are: true, false -- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
-- Configure whether administration settings are visible in the UI:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
-- Configure the visibility of preview features: -- Configure the visibility of preview features:
-- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL -- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL
-- Please note: -- Please note:
@@ -132,6 +166,11 @@ CONFIG["SETTINGS"] = {}
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025. -- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025.
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" } -- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" }
-- Configure the preselected provider.
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- Please note: using an empty string ("") will lock the preselected provider selection, even though no valid preselected provider is found.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
-- Configure the preselected profile. -- Configure the preselected profile.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"]. -- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- 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.
@@ -260,4 +299,4 @@ CONFIG["PROFILES"] = {}
-- ["Name"] = "<user-friendly name of the profile>", -- ["Name"] = "<user-friendly name of the profile>",
-- ["NeedToKnow"] = "I like to cook in my free time. My favorite meal is ...", -- ["NeedToKnow"] = "I like to cook in my free time. My favorite meal is ...",
-- ["Actions"] = "Please always ensure the portion size is ..." -- ["Actions"] = "Please always ensure the portion size is ..."
-- } -- }
@@ -1798,7 +1798,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T527187983"] = "
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "Pandoc installieren" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "Pandoc installieren"
-- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications. -- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Da mein Arbeitgeber sowohl Windows als auch Linux am Arbeitsplatz nutzt, wollte ich eine plattformübergreifende Lösung, die nahtlos auf allen wichtigen Betriebssystemen, einschließlich macOS, funktioniert. Außerdem wollte ich zeigen, dass es möglich ist, moderne, effiziente und plattformübergreifende Anwendungen zu erstellen, ohne auf Software-Ballast, wie z.B. das Electron-Framework, zurückzugreifen. Die Kombination aus .NET und Rust mit Tauri hat sich dabei als hervorragender Technologiestapel für den Bau solch robuster Anwendungen erwiesen." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Da mein Arbeitgeber sowohl Windows als auch Linux am Arbeitsplatz nutzt, wollte ich eine plattformübergreifende Lösung, die nahtlos auf allen wichtigen Betriebssystemen, einschließlich macOS, funktioniert. Außerdem wollte ich zeigen, dass es möglich ist, moderne, effiziente und plattformübergreifende Anwendungen zu erstellen, ohne auf Software-Ballast, wie z.B. das Electron-Framework, zurückzugreifen. Die Kombination aus .NET und Rust mit Tauri hat sich dabei als hervorragender Technologie-Stack für den Bau solch robuster Anwendungen erwiesen."
-- Limitations of Existing Solutions -- Limitations of Existing Solutions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1086130692"] = "Einschränkungen bestehender Lösungen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1086130692"] = "Einschränkungen bestehender Lösungen"
@@ -1806,21 +1806,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1086130692"] = "Einschränku
-- Personal Needs and Limitations of Web Services -- Personal Needs and Limitations of Web Services
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Persönliche Bedürfnisse und Einschränkungen von Webdiensten" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Persönliche Bedürfnisse und Einschränkungen von Webdiensten"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1986314327"] = "Demokratisierung von KI"
-- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations. -- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "Während ich nach passenden Lösungen suchte, stieß ich auf eine Desktop-Anwendung namens Anything LLM. Leider konnte sie meine spezifischen Anforderungen nicht erfüllen und entsprach auch nicht dem Benutzeroberflächendesign, das ich mir vorgestellt hatte. Für macOS gab es zwar mehrere Apps, die meiner Vorstellung ähnelten, aber sie waren allesamt kostenpflichtige Lösungen mit unklarer Herkunft. Die Identität der Entwickler und die Ursprünge dieser Apps waren nicht ersichtlich, was erhebliche Sicherheitsbedenken hervorrief. Berichte von Nutzern über gestohlene API-Schlüssel und unerwünschte Abbuchungen verstärkten meine Bedenken zusätzlich." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "Während ich nach passenden Lösungen suchte, stieß ich auf eine Desktop-Anwendung namens Anything LLM. Leider konnte sie meine spezifischen Anforderungen nicht erfüllen und entsprach auch nicht dem Benutzeroberflächendesign, das ich mir vorgestellt hatte. Für macOS gab es zwar mehrere Apps, die meiner Vorstellung ähnelten, aber sie waren allesamt kostenpflichtige Lösungen mit unklarer Herkunft. Die Identität der Entwickler und die Ursprünge dieser Apps waren nicht ersichtlich, was erhebliche Sicherheitsbedenken hervorrief. Berichte von Nutzern über gestohlene API-Schlüssel und unerwünschte Abbuchungen verstärkten meine Bedenken zusätzlich."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time. -- We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3569462457"] = "Hallo, mein Name ist Thorsten Sommer und ich bin der ursprüngliche Entwickler von MindWork AI Studio. Die Motivation zur Entwicklung dieser App entstand aus mehreren wichtigen Bedürfnissen und Beobachtungen, die ich im Laufe der Zeit gemacht habe." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3672974243"] = "Wir möchten auch zur Demokratisierung von KI beitragen. MindWork AI Studio läuft selbst auf kostengünstiger Hardware, einschließlich Computern für rund 100 € wie dem Raspberry Pi. Dadurch sind die App und ihr voller Funktionsumfang auch für Menschen und Familien mit begrenztem Budget zugänglich. Für Ihre ersten Schritte können Sie mit lokalen LLMs beginnen oder günstige Cloud-Modelle nutzen. MindWork AI Studio selbst ist kostenlos erhältlich."
-- Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3622193740"] = "Mit MindWork AI Studio möchte ich ein sicheres, flexibles und benutzerfreundliches Werkzeug bereitstellen, das für ein breites Publikum geeignet ist, ohne Kompromisse bei Funktionalität oder Design einzugehen. Diese App ist das Ergebnis meines Wunsches, persönliche Anforderungen zu erfüllen, bestehende Lücken auf dem Markt zu schließen und innovative Entwicklungsmethoden zu präsentieren."
-- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge. -- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Sich auf Webdienste wie ChatGPT zu verlassen, war für mich keine nachhaltige Lösung. Ich brauchte eine KI, die auch direkt auf Dateien auf meinem Gerät zugreifen kann – eine Funktion, die Webdienste aus Sicherheits- und Datenschutzgründen grundsätzlich nicht bieten. Zwar hätte ich mir eine eigene Lösung in Python programmieren können, aber das wäre für den Alltag zu umständlich gewesen. Noch wichtiger war mir, eine Lösung zu entwickeln, die jeder nutzen kann, ganz ohne Programmierkenntnisse." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Sich auf Webdienste wie ChatGPT zu verlassen, war für mich keine nachhaltige Lösung. Ich brauchte eine KI, die auch direkt auf Dateien auf meinem Gerät zugreifen kann – eine Funktion, die Webdienste aus Sicherheits- und Datenschutzgründen grundsätzlich nicht bieten. Zwar hätte ich mir eine eigene Lösung in Python programmieren können, aber das wäre für den Alltag zu umständlich gewesen. Noch wichtiger war mir, eine Lösung zu entwickeln, die jeder nutzen kann, ganz ohne Programmierkenntnisse."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T483341611"] = "Hallo, mein Name ist Thorsten Sommer und ich bin der initiale Entwickler von MindWork AI Studio. Ich habe dieses Projekt auf Grundlage von Bedürfnissen und Beobachtungen gestartet, die ich im Laufe der Zeit gemacht habe. Heute haben wir ein Kernteam von Entwicklern und Unterstützung aus der Open-Source-Community."
-- Cross-Platform and Modern Development -- Cross-Platform and Modern Development
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Plattformübergreifende und moderne Entwicklung" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Plattformübergreifende und moderne Entwicklung"
-- Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T904941692"] = "Heute verfolgt unser Team das Ziel, ein sicheres, flexibles und benutzerfreundliches Tool bereitzustellen, das eine breite Zielgruppe anspricht, ohne dabei Kompromisse bei Funktionalität oder dem Design einzugehen."
-- Copies the content to the clipboard -- Copies the content to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Kopiert den Inhalt in die Zwischenablage" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Kopiert den Inhalt in die Zwischenablage"
@@ -2082,12 +2088,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app. -- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Wählen Sie die Sprache für die App aus." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Wählen Sie die Sprache für die App aus."
-- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "Wenn diese Option aktiviert ist, werden zusätzliche Optionen für die Administration angezeigt. Diese Optionen sind für IT-Mitarbeitende vorgesehen, um organisationsweite Einstellungen zu verwalten, z. B. Anbieter für eine gesamte Organisation zu konfigurieren und zu exportieren."
-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused. -- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "Der globale Tastaturkurzbefehl zum Ein- und Ausschalten der Sprachaufnahme. Dieser Kurzbefehl funktioniert systemweit, auch wenn die App nicht im Vordergrund ist." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "Der globale Tastaturkurzbefehl zum Ein- und Ausschalten der Sprachaufnahme. Dieser Kurzbefehl funktioniert systemweit, auch wenn die App nicht im Vordergrund ist."
-- Disable dictation and transcription -- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Diktieren und Transkribieren deaktivieren" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Diktieren und Transkribieren deaktivieren"
-- Enterprise Administration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2277116008"] = "Unternehmensverwaltung"
-- Language behavior -- Language behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Sprachverhalten" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Sprachverhalten"
@@ -2097,6 +2109,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"]
-- Language -- Language
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Sprache" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Sprache"
-- Administration settings are visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Die Optionen für die Administration sind sichtbar."
-- Save energy? -- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?"
@@ -2106,9 +2121,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"]
-- App Options -- App Options
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App-Einstellungen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App-Einstellungen"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T362833"] = "Generieren Sie ein 256‑Bit‑Geheimnis für die Verschlüsselung, um API‑Schlüssel in Konfigurations-Plugins zu verschlüsseln. Stellen Sie dieses Geheimnis über Gruppenrichtlinien (Windows-Registrierung) oder über Umgebungsvariablen auf Client-Geräten bereit. Anschließend können Anbieter über die Export-Schaltflächen in den Anbieter-Einstellungen mit verschlüsselten API‑Schlüsseln exportiert werden."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "Wenn aktiviert, wird gestreamter Inhalt von der KI alle drei Sekunden aktualisiert. Wenn deaktiviert, wird gestreamter Inhalt sofort aktualisiert, sobald er verfügbar ist." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "Wenn aktiviert, wird gestreamter Inhalt von der KI alle drei Sekunden aktualisiert. Wenn deaktiviert, wird gestreamter Inhalt sofort aktualisiert, sobald er verfügbar ist."
-- Show administration settings?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Optionen für die Administration anzeigen?"
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Lesen Sie die Enterprise-IT-Dokumentation für die Details."
-- Enable spellchecking? -- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Rechtschreibprüfung aktivieren?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Rechtschreibprüfung aktivieren?"
@@ -2142,9 +2166,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"]
-- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Möchten Sie einen Anbieter als Standard für die gesamte App festlegen? Wenn Sie einen anderen Anbieter für einen Assistenten konfigurieren, hat dieser immer Vorrang." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Möchten Sie einen Anbieter als Standard für die gesamte App festlegen? Wenn Sie einen anderen Anbieter für einen Assistenten konfigurieren, hat dieser immer Vorrang."
-- Generate an encryption secret and copy it to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Geheimnis für die Verschlüsselung generieren und in die Zwischenablage kopieren"
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Die Optionen für die Administration sind nicht sichtbar."
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Einbettungsergebnis"
-- Delete -- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen"
-- Embed text
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Text einbetten"
-- Test Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Anbieter für Einbettung testen"
-- Add Embedding -- Add Embedding
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen"
@@ -2157,6 +2196,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider -- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen"
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Text zum Einbetten eingeben:"
-- Embedding Vector (one dimension per line)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Einbettungsvektor (eine Dimension pro Zeile)"
-- Model -- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell"
@@ -2166,6 +2211,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
-- Name -- Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
-- No embedding was returned.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "Es wurde keine Einbettung zurückgegeben."
-- Configured Embedding Providers -- Configured Embedding Providers
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen"
@@ -2175,6 +2223,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
-- Edit -- Edit
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten"
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Schließen"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen"
@@ -2196,9 +2247,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
-- Open Dashboard -- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen"
-- Test
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Testen"
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Beispieltext zum Einbetten"
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Konfiguration exportieren"
-- Cannot export the encrypted API key: No enterprise encryption secret is configured.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T1832230847"] = "Der verschlüsselte API-Schlüssel kann nicht exportiert werden: Es ist kein Geheimnis für die Verschlüsselung konfiguriert."
-- This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T3368145670"] = "Für diesen Anbieter ist ein API-Schlüssel konfiguriert. Möchten Sie den verschlüsselten API-Schlüssel in den Export aufnehmen? Hinweis: Der Empfänger benötigt dasselbe Geheimnis für die Verschlüsselung, um den API-Schlüssel verwenden zu können."
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "API-Schlüssel exportieren?"
-- Show provider's confidence level? -- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Anzeigen, wie sicher sich der Anbieter ist?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Anzeigen, wie sicher sich der Anbieter ist?"
@@ -2304,6 +2373,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Anbieter" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Konfiguration exportieren"
-- No transcription provider configured yet. -- No transcription provider configured yet.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "Es ist bisher kein Anbieter für Transkriptionen konfiguriert." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "Es ist bisher kein Anbieter für Transkriptionen konfiguriert."
@@ -2358,6 +2430,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Anbieter" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Konfiguration exportieren"
-- Copy {0} to the clipboard -- Copy {0} to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Kopiere {0} in die Zwischenablage" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Kopiere {0} in die Zwischenablage"
@@ -2394,9 +2469,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1648606751"] = "Sie können ihre
-- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question. -- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "Bald wird es möglich sein, Daten aus dem Firmennetzwerk über eine festgelegte Schnittstelle (External Retrieval Interface, kurz ERI) zu integrieren. Dafür wird voraussichtlich Entwicklungsaufwand seitens der jeweiligen Organisation nötig sein." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "Bald wird es möglich sein, Daten aus dem Firmennetzwerk über eine festgelegte Schnittstelle (External Retrieval Interface, kurz ERI) zu integrieren. Dafür wird voraussichtlich Entwicklungsaufwand seitens der jeweiligen Organisation nötig sein."
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1986314327"] = "Demokratisierung von KI"
-- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer. -- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Was auch immer ihr Beruf oder ihre Aufgabe ist, MindWork AI Studio möchte ihre Bedürfnisse erfüllen: Egal, ob Sie Projektmanager, Wissenschaftler, Künstler, Autor, Softwareentwickler oder Spieleentwickler sind." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Was auch immer ihr Beruf oder ihre Aufgabe ist, MindWork AI Studio möchte ihre Bedürfnisse erfüllen: Egal, ob Sie Projektmanager, Wissenschaftler, Künstler, Autor, Softwareentwickler oder Spieleentwickler sind."
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2201645589"] = "Wir möchten zur Demokratisierung von KI beitragen. MindWork AI Studio läuft sogar auf kostengünstiger Hardware, einschließlich Computern für etwa 100 € wie dem Raspberry Pi. Dadurch werden die App und ihr voller Funktionsumfang auch für Menschen und Familien mit begrenztem Budget zugänglich. Sie können mit lokalen LLMs starten oder günstige Cloud-Modelle nutzen. MindWork AI Studio selbst ist kostenlos erhältlich."
-- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats. -- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "Sie können ihre E-Mail-Postfächer mit AI Studio verbinden. Die KI liest ihre E-Mails und benachrichtigt Sie über wichtige Ereignisse. Außerdem haben Sie in ihren Chats Zugriff auf das Wissen aus ihren E-Mails." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "Sie können ihre E-Mail-Postfächer mit AI Studio verbinden. Die KI liest ihre E-Mails und benachrichtigt Sie über wichtige Ereignisse. Außerdem haben Sie in ihren Chats Zugriff auf das Wissen aus ihren E-Mails."
@@ -3276,6 +3357,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Anb
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen"
-- Embedding Vector
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einbettungsvektor"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems."
@@ -4941,6 +5028,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "Sie zahlen nur für das,
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistenten" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistenten"
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1628689293"] = "Wir möchten zur Demokratisierung von KI beitragen. MindWork AI Studio läuft sogar auf kostengünstiger Hardware, einschließlich Computern für etwa 100 € wie dem Raspberry Pi. Dadurch werden die App und ihr vollständiger Funktionsumfang auch für Menschen und Familien mit begrenztem Budget zugänglich. Sie können mit lokalen LLMs starten oder günstige Cloud-Modelle nutzen."
-- Unrestricted usage -- Unrestricted usage
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unbeschränkte Nutzung" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unbeschränkte Nutzung"
@@ -4950,6 +5040,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Einführung"
-- Vision -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1986314327"] = "Demokratisierung von KI"
-- Let's get started -- Let's get started
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Los geht's" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Los geht's"
@@ -5004,18 +5097,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startprotokollda
-- Browse AI Studio's source code on GitHub — we welcome your contributions. -- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an – wir freuen uns über ihre Beiträge." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an – wir freuen uns über ihre Beiträge."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die Plugin-ID stimmt nicht mit der ID der Unternehmenskonfiguration überein."
-- This is a private AI Studio installation. It runs without an enterprise configuration. -- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio läuft mit einer Unternehmenskonfiguration und einem Konfigurationsserver. Das Konfigurations-Plugin ist noch nicht verfügbar."
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Datenbankversion"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind."
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Warten auf das Konfigurations-Plugin …"
-- Encryption secret: is not configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Geheimnis für die Verschlüsselung: ist nicht konfiguriert"
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind aktiv."
-- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant ist eine Vektordatenbank und Suchmaschine für Vektoren. Wir nutzen Qdrant, um lokales RAG (Retrieval-Augmented Generation) innerhalb von AI Studio zu realisieren. Vielen Dank für den Einsatz und die großartige Arbeit, die in Qdrant gesteckt wurde und weiterhin gesteckt wird."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek."
@@ -5049,9 +5157,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "Um ein beliebige
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust."
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage"
-- Copies the server URL to the clipboard -- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage"
@@ -5070,9 +5184,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Konfigurationsse
-- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "Wir müssen Zufallszahlen erzeugen, z. B. um die Kommunikation zwischen der Benutzeroberfläche und der Laufzeitumgebung abzusichern. Die rand-Bibliothek eignet sich dafür hervorragend." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "Wir müssen Zufallszahlen erzeugen, z. B. um die Kommunikation zwischen der Benutzeroberfläche und der Laufzeitumgebung abzusichern. Die rand-Bibliothek eignet sich dafür hervorragend."
-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2280402765"] = "AI Studio läuft mit einer Unternehmenskonfiguration über ein Konfigurations-Plugin, ohne zentrale Konfigurationsverwaltung."
-- Configuration plugin ID: -- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Konfigurations-Plugin-ID:" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Konfigurations-Plugin-ID:"
@@ -5097,6 +5208,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Verwendete Open-
-- Build time -- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "Diese Bibliothek wird verwendet, um temporäre Ordner zu erstellen, in denen das Zertifikat und der private Schlüssel für die Kommunikation mit Qdrant gespeichert werden."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "Dieses Crate stellt Derive-Makros für Rust-Enums bereit, die wir verwenden, um Boilerplate zu reduzieren, wenn wir String-Konvertierungen und Metadaten für Laufzeittypen implementieren. Das ist hilfreich für die Kommunikation zwischen unseren Rust- und .NET-Systemen." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "Dieses Crate stellt Derive-Makros für Rust-Enums bereit, die wir verwenden, um Boilerplate zu reduzieren, wenn wir String-Konvertierungen und Metadaten für Laufzeittypen implementieren. Das ist hilfreich für die Kommunikation zwischen unseren Rust- und .NET-Systemen."
@@ -5133,6 +5247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung"
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software."
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind noch nicht verfügbar."
-- Changelog -- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
@@ -5166,6 +5283,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3433065373"] = "Informationen ü
-- Used Rust compiler -- Used Rust compiler
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Verwendeter Rust-Compiler" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Verwendeter Rust-Compiler"
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird mit Unternehmenskonfigurationen unter Verwendung von Konfigurations-Plugins betrieben. Eine zentrale Konfigurationsverwaltung wird nicht eingesetzt."
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!"
@@ -5175,9 +5295,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio läuft mit einer Unternehmenskonfiguration und einem Konfigurationsserver. Das Konfigurations-Plugin ist aktiv."
-- this version does not met the requirements -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
@@ -5214,6 +5331,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bib
-- Used .NET SDK -- Used .NET SDK
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden."
-- Did you find a bug or are you experiencing issues? Report your concern here. -- Did you find a bug or are you experiencing issues? Report your concern here.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Haben Sie einen Fehler gefunden oder Probleme festgestellt? Melden Sie Ihr Anliegen hier." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Haben Sie einen Fehler gefunden oder Probleme festgestellt? Melden Sie Ihr Anliegen hier."
@@ -5244,6 +5364,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Interne Plugins"
-- Disabled Plugins -- Disabled Plugins
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Deaktivierte Plugins" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Deaktivierte Plugins"
-- Send a mail
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "E-Mail senden"
-- Enable plugin -- Enable plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Plugin aktivieren" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Plugin aktivieren"
@@ -5256,6 +5379,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Aktivierte Plugins"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Aktionen" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Aktionen"
-- Open website
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Website öffnen"
-- Settings -- Settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Einstellungen" UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Einstellungen"
@@ -5772,6 +5898,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP-Port"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Gemeldete Version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC-Port"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Anzahl der Collections"
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment. -- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "Die zugehörigen Daten dürfen an keinen LLM-Anbieter gesendet werden. Das bedeutet, dass diese Datenquelle momentan nicht verwendet werden kann." UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "Die zugehörigen Daten dürfen an keinen LLM-Anbieter gesendet werden. Das bedeutet, dass diese Datenquelle momentan nicht verwendet werden kann."
@@ -5961,15 +6102,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2262604281"] = "Das
-- The field DESCRIPTION does not exist or is not a valid string. -- The field DESCRIPTION does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "Das Feld DESCRIPTION existiert nicht oder ist keine gültige Zeichenkette." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "Das Feld DESCRIPTION existiert nicht oder ist keine gültige Zeichenkette."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2320984047"] = "Das Feld SOURCE_URL ist keine gültige URL. Die URL muss mit 'http://' oder 'https://' beginnen."
-- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X). -- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "Das Feld VERSION ist keine gültige Versionsnummer. Die Versionsnummer muss als Zeichenkette im Format major.minor.patch (X.X.X) angegeben werden." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "Das Feld VERSION ist keine gültige Versionsnummer. Die Versionsnummer muss als Zeichenkette im Format major.minor.patch (X.X.X) angegeben werden."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2892057533"] = "Das Feld SOURCE_URL ist keine gültige URL. Die URL muss mit „http://“, „https://“ oder „mailto:“ beginnen."
-- The table AUTHORS is empty. At least one author must be specified. -- The table AUTHORS is empty. At least one author must be specified.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "Die Tabelle AUTHORS ist leer. Es muss mindestens ein Autor angegeben werden." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "Die Tabelle AUTHORS ist leer. Es muss mindestens ein Autor angegeben werden."
-- The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3165663073"] = "Das Feld SOURCE_URL ist keine gültige URL. Wenn die URL mit „mailto:“ beginnt, muss sie eine gültige E-Mail-Adresse als Empfänger enthalten."
-- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string. -- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "Das Feld SUPPORT_CONTACT ist leer. Der Support-Kontakt muss eine nicht-leere Zeichenkette sein." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "Das Feld SUPPORT_CONTACT ist leer. Der Support-Kontakt muss eine nicht-leere Zeichenkette sein."
@@ -1806,21 +1806,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1086130692"] = "Limitations
-- Personal Needs and Limitations of Web Services -- Personal Needs and Limitations of Web Services
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Personal Needs and Limitations of Web Services" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1839655973"] = "Personal Needs and Limitations of Web Services"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1986314327"] = "Democratization of AI"
-- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations. -- While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3552777197"] = "While exploring available solutions, I found a desktop application called Anything LLM. Unfortunately, it fell short of meeting my specific requirements and lacked the user interface design I envisioned. For macOS, there were several apps similar to what I had in mind, but they were all commercial solutions shrouded in uncertainty. The developers' identities and the origins of these apps were unclear, raising significant security concerns. Reports from users about stolen API keys and unwanted charges only amplified my reservations."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time. -- We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3569462457"] = "Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. The motivation behind developing this app stems from several crucial needs and observations I made over time." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3672974243"] = "We also want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs for your first steps or use affordable cloud models. MindWork AI Studio itself is available free of charge."
-- Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T3622193740"] = "Through MindWork AI Studio, I aim to provide a secure, flexible, and user-friendly tool that caters to a wider audience without compromising on functionality or design. This app is the culmination of my desire to meet personal requirements, address existing gaps in the market, and showcase innovative development practices."
-- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge. -- Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on web services like ChatGPT was not a sustainable solution for me. I needed an AI that could also access files directly on my device, a functionality web services inherently lack due to security and privacy constraints. Although I could have scripted something in Python to meet my needs, this approach was too cumbersome for daily use. More importantly, I wanted to develop a solution that anyone could use without needing any programming knowledge."
-- Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T483341611"] = "Hello, my name is Thorsten Sommer, and I am the initial creator of MindWork AI Studio. I started this project based on several crucial needs and observations I made over time. Today, we have a core team of developers and support from the open-source community."
-- Cross-Platform and Modern Development -- Cross-Platform and Modern Development
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development"
-- Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T904941692"] = "Today, our team aims to provide a secure, flexible, and user-friendly tool that serves a broad audience without compromising on functionality or design."
-- Copies the content to the clipboard -- Copies the content to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard"
@@ -2082,12 +2088,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app. -- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
-- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization."
-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused. -- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."
-- Disable dictation and transcription -- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"
-- Enterprise Administration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2277116008"] = "Enterprise Administration"
-- Language behavior -- Language behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Language behavior" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] = "Language behavior"
@@ -2097,6 +2109,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"]
-- Language -- Language
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language"
-- Administration settings are visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Administration settings are visible"
-- Save energy? -- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
@@ -2106,9 +2121,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"]
-- App Options -- App Options
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
-- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T362833"] = "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."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "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." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"] = "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."
-- Show administration settings?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Show administration settings?"
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
-- Enable spellchecking? -- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?"
@@ -2142,9 +2166,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"]
-- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."
-- Generate an encryption secret and copy it to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Generate an encryption secret and copy it to the clipboard"
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
-- Delete -- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
-- Embed text
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Embed text"
-- Test Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider"
-- Add Embedding -- Add Embedding
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
@@ -2157,6 +2196,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider -- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
-- Embedding Vector (one dimension per line)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)"
-- Model -- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
@@ -2166,6 +2211,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
-- Name -- Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
-- No embedding was returned.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned."
-- Configured Embedding Providers -- Configured Embedding Providers
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
@@ -2175,6 +2223,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
-- Edit -- Edit
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
@@ -2196,9 +2247,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
-- Open Dashboard -- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
-- Test
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test"
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration"
-- Cannot export the encrypted API key: No enterprise encryption secret is configured.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T1832230847"] = "Cannot export the encrypted API key: No enterprise encryption secret is configured."
-- This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T3368145670"] = "This provider has an API key configured. Do you want to include the encrypted API key in the export? Note: The recipient will need the same encryption secret to use the API key."
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "Export API Key?"
-- Show provider's confidence level? -- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?"
@@ -2304,6 +2373,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
-- No transcription provider configured yet. -- No transcription provider configured yet.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet."
@@ -2358,6 +2430,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Provider -- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration"
-- Copy {0} to the clipboard -- Copy {0} to the clipboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Copy {0} to the clipboard" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TEXTINFOLINE::T2206391442"] = "Copy {0} to the clipboard"
@@ -2394,9 +2469,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1648606751"] = "You'll be able t
-- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question. -- It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1926587044"] = "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question."
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1986314327"] = "Democratization of AI"
-- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer. -- Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2144737937"] = "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer."
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2201645589"] = "We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models. MindWork AI Studio itself is available free of charge."
-- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats. -- You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2289234741"] = "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats."
@@ -3276,6 +3357,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
-- Embedding Vector
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
@@ -4941,6 +5028,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what yo
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"
-- We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1628689293"] = "We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models."
-- Unrestricted usage -- Unrestricted usage
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unrestricted usage" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1686815996"] = "Unrestricted usage"
@@ -4950,6 +5040,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Introduction"
-- Vision -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision"
-- Democratization of AI
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1986314327"] = "Democratization of AI"
-- Let's get started -- Let's get started
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started"
@@ -5004,18 +5097,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startup log file
-- Browse AI Studio's source code on GitHub — we welcome your contributions. -- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the plugin ID differs from the enterprise configuration ID."
-- This is a private AI Studio installation. It runs without an enterprise configuration. -- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available."
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Database version"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the configuration plugin..."
-- Encryption secret: is not configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secret: is not configured"
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active."
-- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "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."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "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." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "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."
@@ -5049,9 +5157,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
-- Copies the server URL to the clipboard -- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
@@ -5070,9 +5184,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Configuration se
-- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose."
-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2280402765"] = "AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management."
-- Configuration plugin ID: -- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:"
@@ -5097,6 +5208,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time -- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant."
-- 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. -- 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.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "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." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "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."
@@ -5133,6 +5247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Changelog -- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@@ -5166,6 +5283,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3433065373"] = "Information abou
-- Used Rust compiler -- Used Rust compiler
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compiler" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compiler"
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management."
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
@@ -5175,9 +5295,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active."
-- this version does not met the requirements -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
@@ -5214,6 +5331,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
-- Used .NET SDK -- Used .NET SDK
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
-- Did you find a bug or are you experiencing issues? Report your concern here. -- Did you find a bug or are you experiencing issues? Report your concern here.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here."
@@ -5244,6 +5364,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
-- Disabled Plugins -- Disabled Plugins
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
-- Send a mail
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
-- Enable plugin -- Enable plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Enable plugin" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2057806005"] = "Enable plugin"
@@ -5256,6 +5379,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
-- Actions -- Actions
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions" UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
-- Open website
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website"
-- Settings -- Settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings" UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
@@ -5772,6 +5898,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment. -- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment." UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."
@@ -5961,15 +6102,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2262604281"] = "The
-- The field DESCRIPTION does not exist or is not a valid string. -- The field DESCRIPTION does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "The field DESCRIPTION does not exist or is not a valid string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T229488255"] = "The field DESCRIPTION does not exist or is not a valid string."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2320984047"] = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'."
-- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X). -- The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2538827536"] = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)."
-- The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2892057533"] = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'."
-- The table AUTHORS is empty. At least one author must be specified. -- The table AUTHORS is empty. At least one author must be specified.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "The table AUTHORS is empty. At least one author must be specified." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T2981832540"] = "The table AUTHORS is empty. At least one author must be specified."
-- The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3165663073"] = "The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient."
-- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string. -- The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T3524814526"] = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string."
+42
View File
@@ -1,5 +1,7 @@
using AIStudio.Agents; using AIStudio.Agents;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.Qdrant;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@@ -24,6 +26,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 async Task Main() public static async Task Main()
{ {
@@ -82,6 +85,39 @@ internal sealed class Program
return; return;
} }
var qdrantInfo = await rust.GetQdrantInfo();
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;
}
var 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 =>
{ {
@@ -133,6 +169,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>(databaseClient);
builder.Services.AddHostedService<GlobalShortcutService>(); builder.Services.AddHostedService<GlobalShortcutService>();
builder.Services.AddHostedService<RustAvailabilityMonitorService>(); builder.Services.AddHostedService<RustAvailabilityMonitorService>();
@@ -191,6 +228,10 @@ internal sealed class Program
RUST_SERVICE = rust; RUST_SERVICE = rust;
ENCRYPTION = encryption; ENCRYPTION = encryption;
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);
@@ -228,6 +269,7 @@ internal sealed class Program
await serverTask; await serverTask;
RUST_SERVICE.Dispose(); RUST_SERVICE.Dispose();
DATABASE_CLIENT.Dispose();
PluginFactory.Dispose(); PluginFactory.Dispose();
programLogger.LogInformation("The AI Studio server was stopped."); programLogger.LogInformation("The AI Studio server was stopped.");
} }
@@ -86,6 +86,13 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -113,6 +113,12 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -98,6 +99,9 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <inheritdoc /> /// <inheritdoc />
public abstract Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); public abstract Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
/// <inheritdoc />
public abstract Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
/// <inheritdoc /> /// <inheritdoc />
public abstract Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default); public abstract Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
@@ -112,9 +116,14 @@ public abstract class BaseProvider : IProvider, ISecretId
#endregion #endregion
/// <summary>
/// Whether this provider was imported from an enterprise configuration plugin.
/// </summary>
public bool IsEnterpriseConfiguration { get; init; }
#region Implementation of ISecretId #region Implementation of ISecretId
public string SecretId => this.Id; public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id;
public string SecretName => this.InstanceName; public string SecretName => this.InstanceName;
@@ -640,6 +649,81 @@ public abstract class BaseProvider : IProvider, ISecretId
} }
} }
protected async Task<IReadOnlyList<IReadOnlyList<float>>> PerformStandardTextEmbeddingRequest(RequestedSecret requestedSecret, Model embeddingModel, Host host = Host.NONE, CancellationToken token = default, params List<string> texts)
{
try
{
//
// Add the model name to the form data. Ensure that a model name is always provided.
// Otherwise, the StringContent constructor will throw an exception.
//
var modelName = embeddingModel.Id;
if (string.IsNullOrWhiteSpace(modelName))
modelName = "placeholder";
// Prepare the HTTP embedding request:
var payload = new
{
model = modelName,
input = texts,
encoding_format = "float"
};
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL());
// Handle the authorization header based on the provider:
switch (this.Provider)
{
case LLMProviders.SELF_HOSTED:
if(requestedSecret.Success)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
break;
default:
if(!requestedSecret.Success)
{
this.logger.LogError("No valid API key available for embedding request.");
return [];
}
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
break;
}
// Set the content:
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
using var response = await this.httpClient.SendAsync(request, token);
var responseBody = response.Content.ReadAsStringAsync(token).Result;
if (!response.IsSuccessStatusCode)
{
this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
return [];
}
var embeddingResponse = JsonSerializer.Deserialize<EmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
if (embeddingResponse is { Data: not null })
{
return embeddingResponse.Data
.Select(d => d.Embedding?.ToArray() ?? [])
.Cast<IReadOnlyList<float>>()
.ToArray();
}
else
{
this.logger.LogError("Was not able to deserialize the embedding response.");
return [];
}
}
catch (Exception e)
{
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
return [];
}
}
/// <summary> /// <summary>
/// Parse and convert API parameters from a provided JSON string into a dictionary, /// Parse and convert API parameters from a provided JSON string into a dictionary,
/// optionally merging additional parameters and removing specific keys. /// optionally merging additional parameters and removing specific keys.
@@ -86,6 +86,12 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -0,0 +1,12 @@
// ReSharper disable CollectionNeverUpdated.Global
namespace AIStudio.Provider;
// ReSharper disable once ClassNeverInstantiated.Global
public sealed record EmbeddingData
{
public string? Object { get; set; }
public List<float>? Embedding { get; set; }
public int? Index { get; set; }
}
@@ -0,0 +1,14 @@
namespace AIStudio.Provider;
public sealed record EmbeddingResponse
{
public string? Id { get; init; }
public string? Object { get; init; }
public List<EmbeddingData>? Data { get; init; }
public string? Model { get; init; }
public EmbeddingUsage? Usage { get; init; }
}
@@ -0,0 +1,11 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace AIStudio.Provider;
public sealed record EmbeddingUsage
{
public int? PromptTokens { get; set; }
public int? TotalTokens { get; set; }
public int? CompletionTokens { get; set; }
}
@@ -88,6 +88,12 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -87,6 +87,12 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -0,0 +1,6 @@
namespace AIStudio.Provider.Google;
public sealed record GoogleEmbedding
{
public List<float>? Values { get; init; }
}
@@ -0,0 +1,30 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Google;
public sealed record GoogleEmbeddingResponse
{
[JsonConverter(typeof(GoogleEmbeddingListConverter))]
public List<GoogleEmbedding>? Embedding { get; init; }
private sealed class GoogleEmbeddingListConverter : JsonConverter<List<GoogleEmbedding>>
{
public override List<GoogleEmbedding> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject)
{
var single = JsonSerializer.Deserialize<GoogleEmbedding>(ref reader, options);
return single is null ? new() : new() { single };
}
if (reader.TokenType == JsonTokenType.StartArray)
return JsonSerializer.Deserialize<List<GoogleEmbedding>>(ref reader, options) ?? new();
throw new JsonException("Expected object or array for embedding.");
}
public override void Write(Utf8JsonWriter writer, List<GoogleEmbedding> value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value, options);
}
}
@@ -1,3 +0,0 @@
namespace AIStudio.Provider.Google;
public readonly record struct Model(string Name, string DisplayName);
@@ -1,7 +0,0 @@
namespace AIStudio.Provider.Google;
/// <summary>
/// A data model for the response from the model endpoint.
/// </summary>
/// <param name="Models"></param>
public readonly record struct ModelsResponse(IList<Model> Models);
@@ -22,7 +22,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
public override string InstanceName { get; set; } = "Google Gemini"; public override string InstanceName { get; set; } = "Google Gemini";
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.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 RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
@@ -76,57 +76,122 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{ {
yield break; yield break;
} }
#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(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inheritdoc /> /// <inhertidoc />
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{ {
var modelResponse = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional); var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
if(modelResponse == default) try
return []; {
var modelName = embeddingModel.Id;
if (string.IsNullOrWhiteSpace(modelName))
{
LOGGER.LogError("No model name provided for embedding request.");
return [];
}
if (modelName.StartsWith("models/", StringComparison.OrdinalIgnoreCase))
modelName = modelName.Substring("models/".Length);
if (!requestedSecret.Success)
{
LOGGER.LogError("No valid API key available for embedding request.");
return [];
}
// Prepare the Google Gemini embedding request:
var payload = new
{
content = new
{
parts = texts.Select(text => new { text }).ToArray()
},
taskType = "SEMANTIC_SIMILARITY"
};
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent";
using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl);
request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
using var response = await this.httpClient.SendAsync(request, token);
var responseBody = await response.Content.ReadAsStringAsync(token);
return modelResponse.Models.Where(model => if (!response.IsSuccessStatusCode)
model.Name.StartsWith("models/gemini-", StringComparison.OrdinalIgnoreCase) && !model.Name.Contains("embed")) {
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName)); LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
return [];
}
var embeddingResponse = JsonSerializer.Deserialize<GoogleEmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
if (embeddingResponse is { Embedding: not null })
{
return embeddingResponse.Embedding
.Select(d => d.Values?.ToArray() ?? [])
.Cast<IReadOnlyList<float>>()
.ToArray();
}
else
{
LOGGER.LogError("Was not able to deserialize the embedding response.");
return [];
}
}
catch (Exception e)
{
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
return [];
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Provider.Model>()); var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
return models.Where(model =>
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
!this.IsEmbeddingModel(model.Id))
.Select(this.WithDisplayNameFallback);
} }
public override async Task<IEnumerable<Provider.Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) /// <inheritdoc />
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
var modelResponse = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional); return Task.FromResult(Enumerable.Empty<Model>());
if(modelResponse == default) }
return [];
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
return modelResponse.Models.Where(model => {
model.Name.StartsWith("models/text-embedding-", StringComparison.OrdinalIgnoreCase) || var models = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
model.Name.StartsWith("models/gemini-embed", StringComparison.OrdinalIgnoreCase)) return models.Where(model => this.IsEmbeddingModel(model.Id))
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName)); .Select(this.WithDisplayNameFallback);
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Provider.Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
return Task.FromResult(Enumerable.Empty<Provider.Model>()); return Task.FromResult(Enumerable.Empty<Model>());
} }
#endregion #endregion
private async Task<ModelsResponse> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null) private async Task<IReadOnlyList<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
{ {
var secretKey = apiKeyProvisional switch var secretKey = apiKeyProvisional switch
{ {
@@ -138,16 +203,57 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
} }
}; };
if (secretKey is null) if (string.IsNullOrWhiteSpace(secretKey))
return default; return [];
using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}"); using var request = new HttpRequestMessage(HttpMethod.Get, "models");
using var response = await this.httpClient.SendAsync(request, token); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return default; {
LOGGER.LogError("Failed to load models with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, await response.Content.ReadAsStringAsync(token));
return [];
}
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token); try
return modelResponse; {
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
if (modelResponse == default || modelResponse.Data.Count is 0)
{
LOGGER.LogError("Google model list response did not contain a valid data array.");
return [];
}
return modelResponse.Data
.Where(model => !string.IsNullOrWhiteSpace(model.Id))
.Select(model => new Model(this.NormalizeModelId(model.Id), model.DisplayName))
.ToArray();
}
catch (Exception e)
{
LOGGER.LogError("Failed to parse Google model list response: '{Message}'.", e.Message);
return [];
}
}
private bool IsEmbeddingModel(string modelId)
{
return modelId.Contains("embedding", StringComparison.OrdinalIgnoreCase) ||
modelId.Contains("embed", StringComparison.OrdinalIgnoreCase);
}
private Model WithDisplayNameFallback(Model model)
{
return string.IsNullOrWhiteSpace(model.DisplayName)
? new Model(model.Id, model.Id)
: model;
}
private string NormalizeModelId(string modelId)
{
return modelId.StartsWith("models/", StringComparison.OrdinalIgnoreCase)
? modelId["models/".Length..]
: modelId;
} }
} }
@@ -87,6 +87,12 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -86,6 +86,13 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -91,6 +91,12 @@ public sealed class ProviderHuggingFace : BaseProvider
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -59,6 +59,16 @@ public interface IProvider
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>>The transcription result.</returns> /// <returns>>The transcription result.</returns>
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
/// <summary>
/// Embed a text file.
/// </summary>
/// <param name="embeddingModel">The model to use for embedding.</param>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <param name="token">The cancellation token.</param>
/// /// <param name="texts">A single string or a list of strings to embed.</param>
/// <returns>>The embedded text as a single vector or as a list of vectors.</returns>
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
/// <summary> /// <summary>
/// Load all possible text models that can be used with this provider. /// Load all possible text models that can be used with this provider.
@@ -186,7 +186,7 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns> /// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings)
{ {
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters); return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration);
} }
/// <summary> /// <summary>
@@ -196,7 +196,7 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns> /// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings)
{ {
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE); return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration);
} }
/// <summary> /// <summary>
@@ -206,34 +206,34 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns> /// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings)
{ {
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE); return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration);
} }
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string expertProviderApiParameter = "") private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false)
{ {
try try
{ {
return provider switch return provider switch
{ {
LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter }, LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
_ => new NoProvider(), _ => new NoProvider(),
}; };
} }
@@ -89,6 +89,13 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
@@ -40,6 +40,8 @@ public class NoProvider : IProvider
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty); public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty);
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) => Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
public IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => [ Capability.NONE ]; public IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => [ Capability.NONE ];
#endregion #endregion
@@ -224,6 +224,13 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -7,15 +7,15 @@ namespace AIStudio.Provider.OpenAI;
/// <param name="Delta">The delta content of the response.</param> /// <param name="Delta">The delta content of the response.</param>
public record ResponsesDeltaStreamLine( public record ResponsesDeltaStreamLine(
string Type, string Type,
string Delta) : IResponseStreamLine string? Delta) : IResponseStreamLine
{ {
#region Implementation of IResponseStreamLine #region Implementation of IResponseStreamLine
/// <inheritdoc /> /// <inheritdoc />
public bool ContainsContent() => !string.IsNullOrWhiteSpace(this.Delta); public bool ContainsContent() => this.Delta is not null;
/// <inheritdoc /> /// <inheritdoc />
public ContentStreamChunk GetContent() => new(this.Delta, this.GetSources()); public ContentStreamChunk GetContent() => new(this.Delta ?? string.Empty, this.GetSources());
// //
// Please note that there are multiple options where LLM providers might stream sources: // Please note that there are multiple options where LLM providers might stream sources:
@@ -94,6 +94,13 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -94,6 +94,12 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
{ {
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
@@ -30,6 +30,11 @@ public static class HostExtensions
_ => "audio/transcriptions", _ => "audio/transcriptions",
}; };
public static string EmbeddingURL(this Host host) => host switch
{
_ => "embeddings",
};
public static bool IsChatSupported(this Host host) public static bool IsChatSupported(this Host host)
{ {
switch (host) switch (host)
@@ -95,6 +95,13 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token);
} }
/// <inhertidoc />
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: true);
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, host, token: token, texts: texts);
}
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
try try
@@ -88,6 +88,12 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
return Task.FromResult(string.Empty); return Task.FromResult(string.Empty);
} }
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{ {
+55 -11
View File
@@ -28,14 +28,14 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
private Expression<Func<TClass, TValue>> PropertyExpression { get; } private Expression<Func<TClass, TValue>> PropertyExpression { get; }
/// <summary> /// <summary>
/// Indicates whether the configuration is managed by a plugin and is therefore locked. /// Indicates whether the configuration is locked by a configuration plugin.
/// </summary> /// </summary>
public bool IsLocked { get; private set; } public bool IsLocked { get; private set; }
/// <summary> /// <summary>
/// The ID of the plugin that manages this configuration. This is set when the configuration is locked. /// The ID of the plugin that locked this configuration.
/// </summary> /// </summary>
public Guid MangedByConfigPluginId { get; private set; } public Guid LockedByConfigPluginId { get; private set; }
/// <summary> /// <summary>
/// The default value for the configuration property. This is used when resetting the property to its default state. /// The default value for the configuration property. This is used when resetting the property to its default state.
@@ -43,30 +43,74 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
public required TValue Default { get; init; } public required TValue Default { get; init; }
/// <summary> /// <summary>
/// Locks the configuration state, indicating that it is managed by a specific plugin. /// Indicates whether a plugin contribution is available.
/// </summary> /// </summary>
/// <param name="pluginId">The ID of the plugin that is managing this configuration.</param> public bool HasPluginContribution { get; private set; }
public void LockManagedState(Guid pluginId)
/// <summary>
/// The additive value contribution provided by a configuration plugin.
/// </summary>
public TValue PluginContribution { get; private set; } = default!;
/// <summary>
/// The ID of the plugin that provided the additive value contribution.
/// </summary>
public Guid PluginContributionByConfigPluginId { get; private set; }
/// <summary>
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
/// </summary>
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
public void LockConfiguration(Guid pluginId)
{ {
this.IsLocked = true; this.IsLocked = true;
this.MangedByConfigPluginId = pluginId; this.LockedByConfigPluginId = pluginId;
} }
/// <summary> /// <summary>
/// Resets the managed state of the configuration, allowing it to be modified again. /// Resets the locked state of the configuration, allowing it to be modified again.
/// This will also reset the property to its default value. /// This will also reset the property to its default value.
/// </summary> /// </summary>
public void ResetManagedState() public void ResetLockedConfiguration()
{ {
this.IsLocked = false; this.IsLocked = false;
this.MangedByConfigPluginId = Guid.Empty; this.LockedByConfigPluginId = Guid.Empty;
this.Reset(); this.Reset();
} }
/// <summary>
/// Unlocks the configuration state without changing the current value.
/// </summary>
public void UnlockConfiguration()
{
this.IsLocked = false;
this.LockedByConfigPluginId = Guid.Empty;
}
/// <summary>
/// Stores an additive plugin contribution.
/// </summary>
public void SetPluginContribution(TValue value, Guid pluginId)
{
this.PluginContribution = value;
this.PluginContributionByConfigPluginId = pluginId;
this.HasPluginContribution = true;
}
/// <summary>
/// Clears the additive plugin contribution without changing the current value.
/// </summary>
public void ClearPluginContribution()
{
this.PluginContribution = default!;
this.PluginContributionByConfigPluginId = Guid.Empty;
this.HasPluginContribution = false;
}
/// <summary> /// <summary>
/// Resets the configuration property to its default value. /// Resets the configuration property to its default value.
/// </summary> /// </summary>
public void Reset() private void Reset()
{ {
var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
var memberExpression = this.PropertyExpression.GetMemberExpression(); var memberExpression = this.PropertyExpression.GetMemberExpression();
@@ -201,7 +201,7 @@ public static class ConfigurationSelectDataFactory
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles) public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
{ {
foreach (var profile in profiles.GetAllProfiles()) foreach (var profile in profiles.GetAllProfiles())
yield return new(profile.Name, profile.Id); yield return new(profile.GetSafeName(), profile.Id);
} }
public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders) public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders)
@@ -65,7 +65,7 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// <summary> /// <summary>
/// Should we preselect a provider for the entire app? /// Should we preselect a provider for the entire app?
/// </summary> /// </summary>
public string PreselectedProvider { get; set; } = string.Empty; public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary> /// <summary>
/// Should we preselect a profile for the entire app? /// Should we preselect a profile for the entire app?
@@ -93,9 +93,14 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// Should the user be allowed to add providers? /// Should the user be allowed to add providers?
/// </summary> /// </summary>
public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true); public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true);
/// <summary>
/// Should administration settings be visible in the UI?
/// </summary>
public bool ShowAdminSettings { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowAdminSettings, false);
/// <summary> /// <summary>
/// List of assistants that should be hidden from the UI. /// List of assistants that should be hidden from the UI.
/// </summary> /// </summary>
public HashSet<ConfigurableAssistant> HiddenAssistants { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HiddenAssistants, []); public HashSet<ConfigurableAssistant> HiddenAssistants { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HiddenAssistants, []);
} }
@@ -43,7 +43,7 @@ public sealed record EmbeddingProvider(
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
public string SecretId => this.Id; public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
@@ -56,43 +56,43 @@ public sealed record EmbeddingProvider(
provider = NONE; provider = NONE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid ID. The ID must be a valid GUID."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name)) if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid name."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider)) if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid LLM provider enum value."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host)) if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid host enum value."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname)) if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid hostname."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable)) if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model table."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!TryReadModelTable(idx, modelTable, out var model)) if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model configuration."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
return false; return false;
} }
@@ -110,25 +110,85 @@ public sealed record EmbeddingProvider(
Host = host, Host = host,
}; };
// Handle encrypted API key if present:
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
else
{
var encryption = PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable == true)
{
if (encryption.TryDecrypt(apiKeyText, out var decryptedApiKey))
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
name,
decryptedApiKey,
SecretStoreType.EMBEDDING_PROVIDER));
LOGGER.LogDebug($"Successfully decrypted API key for embedding provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"Failed to decrypt API key for embedding provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"The configured embedding provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
}
}
return true; return true;
} }
private static bool TryReadModelTable(int idx, LuaTable table, out Model model) private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
{ {
model = default; model = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model ID."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName)) if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
{ {
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model display name."); LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
return false; return false;
} }
model = new(id, displayName); model = new(id, displayName);
return true; return true;
} }
}
/// <summary>
/// Exports the embedding provider configuration as a Lua configuration section.
/// </summary>
/// <param name="encryptedApiKey">Optional encrypted API key to include in the export.</param>
/// <returns>A Lua configuration section string.</returns>
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
{
var apiKeyLine = string.Empty;
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
{
apiKeyLine = $"""
["APIKey"] = "{LuaTools.EscapeLuaString(encryptedApiKey)}",
""";
}
return $$"""
CONFIG["EMBEDDING_PROVIDERS"][#CONFIG["EMBEDDING_PROVIDERS"]+1] = {
["Id"] = "{{Guid.NewGuid().ToString()}}",
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
["Host"] = "{{this.Host}}",
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
{{apiKeyLine}}
["Model"] = {
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? string.Empty)}}",
},
}
""";
}
}
@@ -581,6 +581,90 @@ public static partial class ManagedConfiguration
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
} }
/// <summary>
/// Attempts to process additive plugin contributions for enum set settings from a Lua table.
/// The contributed values are merged into the existing set, and the setting remains unlocked
/// so users can add additional values.
/// </summary>
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
/// <param name="settings">The Lua table containing the settings to process.</param>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
/// elements, which must be an enum.</typeparam>
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
public static bool TryProcessConfigurationWithPluginContribution<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
where TValue : Enum
{
//
// Handle configured enum sets (additive merge)
//
// Check if that configuration was registered:
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = new HashSet<TValue>();
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
// Step 3 -- try to parse the string as the target type:
if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum))
set.Add((TValue)configuredEnum);
}
}
configuredValue = set;
successful = true;
}
if (dryRun)
return successful;
if (successful)
{
var configInstance = configSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
var currentValue = propertyExpression.Compile().Invoke(configInstance);
var merged = new HashSet<TValue>(currentValue);
merged.UnionWith(configuredValue);
configMeta.SetValue(merged);
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
}
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
{
configMeta.ClearPluginContribution();
}
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
configMeta.UnlockConfiguration();
return successful;
}
/// <summary> /// <summary>
/// Attempts to process the configuration settings from a Lua table for string set types. /// Attempts to process the configuration settings from a Lua table for string set types.
@@ -744,12 +828,12 @@ public static partial class ManagedConfiguration
// Case: the setting was configured, and we could read the value successfully. // Case: the setting was configured, and we could read the value successfully.
// //
// Set the configured value and lock the managed state: // Set the configured value and lock the configuration:
configMeta.SetValue(configuredValue); configMeta.SetValue(configuredValue);
configMeta.LockManagedState(configPluginId); configMeta.LockConfiguration(configPluginId);
break; break;
case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId:
// //
// Case: the setting was configured previously, but we could not read the value successfully. // Case: the setting was configured previously, but we could not read the value successfully.
// This happens when the setting was removed from the configuration plugin. We handle that // This happens when the setting was removed from the configuration plugin. We handle that
@@ -757,10 +841,10 @@ public static partial class ManagedConfiguration
// //
// The other case, when the setting was locked and managed by a different configuration plugin, // The other case, when the setting was locked and managed by a different configuration plugin,
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
// is still available. If it is not available, it resets the managed state of the // is still available. If it is not available, it resets the locked state of the
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
// //
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
break; break;
case false: case false:
@@ -9,6 +9,7 @@ namespace AIStudio.Settings;
public static partial class ManagedConfiguration public static partial class ManagedConfiguration
{ {
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new(); private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
/// <summary> /// <summary>
/// Attempts to retrieve the configuration metadata for a given configuration selection and /// Attempts to retrieve the configuration metadata for a given configuration selection and
@@ -251,13 +252,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@@ -272,13 +273,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@@ -296,13 +297,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@@ -319,13 +320,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@@ -340,13 +341,38 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true;
}
return false;
}
/// <summary>
/// Checks if a plugin contribution is left over from a configuration plugin that is no longer available.
/// If so, it clears the contribution and returns true.
/// </summary>
public static bool IsPluginContributionLeftOver<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
IEnumerable<IAvailablePlugin> availablePlugins)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
if (!configMeta.HasPluginContribution || configMeta.PluginContributionByConfigPluginId == Guid.Empty)
return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.PluginContributionByConfigPluginId);
if (plugin is null)
{
configMeta.ClearPluginContribution();
return true; return true;
} }
@@ -361,13 +387,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
+96 -15
View File
@@ -71,7 +71,7 @@ public sealed record Provider(
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
public string SecretId => this.Id; public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
@@ -94,50 +94,60 @@ public sealed record Provider(
provider = NONE; provider = NONE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName)) if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider)) if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host)) if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname)) if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
return false; return false;
} }
var hfInferenceProvider = HFInferenceProvider.NONE;
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
{
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
{
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})");
hfInferenceProvider = HFInferenceProvider.NONE;
}
}
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable)) if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!TryReadModelTable(idx, modelTable, out var model)) if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("AdditionalJsonApiParameters", out var additionalJsonApiParametersValue) || !additionalJsonApiParametersValue.TryRead<string>(out var additionalJsonApiParameters)) if (!table.TryGetValue("AdditionalJsonApiParameters", out var additionalJsonApiParametersValue) || !additionalJsonApiParametersValue.TryRead<string>(out var additionalJsonApiParameters))
{ {
// In this case, no reason exists to reject this provider, though. // In this case, no reason exists to reject this provider, though.
LOGGER.LogWarning($"The configured provider {idx} does not contain valid additional JSON API parameters."); LOGGER.LogWarning($"The configured provider {idx} does not contain valid additional JSON API parameters. (Plugin ID: {configPluginId})");
additionalJsonApiParameters = string.Empty; additionalJsonApiParameters = string.Empty;
} }
@@ -153,28 +163,99 @@ public sealed record Provider(
EnterpriseConfigurationPluginId = configPluginId, EnterpriseConfigurationPluginId = configPluginId,
Hostname = hostname, Hostname = hostname,
Host = host, Host = host,
HFInferenceProvider = hfInferenceProvider,
AdditionalJsonApiParameters = additionalJsonApiParameters, AdditionalJsonApiParameters = additionalJsonApiParameters,
}; };
// Handle encrypted API key if present:
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
else
{
var encryption = PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable == true)
{
if (encryption.TryDecrypt(apiKeyText, out var decryptedApiKey))
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
instanceName,
decryptedApiKey,
SecretStoreType.LLM_PROVIDER));
LOGGER.LogDebug($"Successfully decrypted API key for provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"Failed to decrypt API key for provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"The configured provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
}
}
return true; return true;
} }
private static bool TryReadModelTable(int idx, LuaTable table, out Model model) private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
{ {
model = default; model = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName)) if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
{ {
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name."); LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
return false; return false;
} }
model = new(id, displayName); model = new(id, displayName);
return true; return true;
} }
}
/// <summary>
/// Exports the provider configuration as a Lua configuration section.
/// </summary>
/// <param name="encryptedApiKey">Optional encrypted API key to include in the export.</param>
/// <returns>A Lua configuration section string.</returns>
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
{
var hfInferenceProviderLine = string.Empty;
if (this.HFInferenceProvider is not HFInferenceProvider.NONE)
{
hfInferenceProviderLine = $"""
["HFInferenceProvider"] = "{this.HFInferenceProvider}",
""";
}
var apiKeyLine = string.Empty;
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
{
apiKeyLine = $"""
["APIKey"] = "{LuaTools.EscapeLuaString(encryptedApiKey)}",
""";
}
return $$"""
CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
["Id"] = "{{Guid.NewGuid().ToString()}}",
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}",
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
["Host"] = "{{this.Host}}",
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
{{hfInferenceProviderLine}}
{{apiKeyLine}}
["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}",
["Model"] = {
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? string.Empty)}}",
},
}
""";
}
}
@@ -172,17 +172,31 @@ public sealed class SettingsManager
{ {
case LangBehavior.AUTO: case LangBehavior.AUTO:
var languageCode = await this.rustService.ReadUserLanguage(); var languageCode = await this.rustService.ReadUserLanguage();
var languagePlugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x is ILanguagePlugin langPlug && langPlug.IETFTag == languageCode); var languagePlugins = PluginFactory.RunningPlugins.OfType<ILanguagePlugin>().ToList();
if (languagePlugin is null)
if (!string.IsNullOrWhiteSpace(languageCode))
{ {
this.logger.LogWarning($"The language plugin for the language '{languageCode}' is not available."); var exactMatch = languagePlugins.FirstOrDefault(x => string.Equals(x.IETFTag, languageCode, StringComparison.OrdinalIgnoreCase));
return PluginFactory.BaseLanguage; if (exactMatch is not null)
return exactMatch;
var primaryLanguage = GetPrimaryLanguage(languageCode);
if (!string.IsNullOrWhiteSpace(primaryLanguage))
{
var primaryLanguageMatch = languagePlugins
.Where(x => string.Equals(GetPrimaryLanguage(x.IETFTag), primaryLanguage, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.IETFTag, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (primaryLanguageMatch is not null)
{
this.logger.LogWarning($"No exact language plugin found for '{languageCode}'. Use language fallback '{primaryLanguageMatch.IETFTag}'.");
return primaryLanguageMatch;
}
}
} }
if (languagePlugin is ILanguagePlugin langPlugin) this.logger.LogWarning($"The language plugin for the language '{languageCode}' (normalized='{languageCode}') is not available.");
return langPlugin;
this.logger.LogError("The language plugin is not a language plugin.");
return PluginFactory.BaseLanguage; return PluginFactory.BaseLanguage;
case LangBehavior.MANUAL: case LangBehavior.MANUAL:
@@ -204,6 +218,18 @@ public sealed class SettingsManager
this.logger.LogError("The language behavior is unknown."); this.logger.LogError("The language behavior is unknown.");
return PluginFactory.BaseLanguage; return PluginFactory.BaseLanguage;
} }
private static string GetPrimaryLanguage(string localeTag)
{
if (string.IsNullOrWhiteSpace(localeTag))
return string.Empty;
var separatorIndex = localeTag.IndexOf('-');
if (separatorIndex < 0)
return localeTag;
return localeTag[..separatorIndex];
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
@@ -365,4 +391,4 @@ public sealed class SettingsManager
// Return the full name of the property, including the class name: // Return the full name of the property, including the class name:
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; return $"{typeof(TIn).Name}.{memberExpr.Member.Name}";
} }
} }
@@ -43,7 +43,7 @@ public sealed record TranscriptionProvider(
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
public string SecretId => this.Id; public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
@@ -56,43 +56,43 @@ public sealed record TranscriptionProvider(
provider = NONE; provider = NONE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid ID. The ID must be a valid GUID."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name)) if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid name."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider)) if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid LLM provider enum value."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host)) if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid host enum value."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname)) if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid hostname."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable)) if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model table."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!TryReadModelTable(idx, modelTable, out var model)) if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model configuration."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
return false; return false;
} }
@@ -110,25 +110,85 @@ public sealed record TranscriptionProvider(
Host = host, Host = host,
}; };
// Handle encrypted API key if present:
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
else
{
var encryption = PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable == true)
{
if (encryption.TryDecrypt(apiKeyText, out var decryptedApiKey))
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
name,
decryptedApiKey,
SecretStoreType.TRANSCRIPTION_PROVIDER));
LOGGER.LogDebug($"Successfully decrypted API key for transcription provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"Failed to decrypt API key for transcription provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
}
else
LOGGER.LogWarning($"The configured transcription provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
}
}
return true; return true;
} }
private static bool TryReadModelTable(int idx, LuaTable table, out Model model) private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
{ {
model = default; model = default;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id)) if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model ID."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
return false; return false;
} }
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName)) if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
{ {
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model display name."); LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
return false; return false;
} }
model = new(id, displayName); model = new(id, displayName);
return true; return true;
} }
}
/// <summary>
/// Exports the transcription provider configuration as a Lua configuration section.
/// </summary>
/// <param name="encryptedApiKey">Optional encrypted API key to include in the export.</param>
/// <returns>A Lua configuration section string.</returns>
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
{
var apiKeyLine = string.Empty;
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
{
apiKeyLine = $"""
["APIKey"] = "{LuaTools.EscapeLuaString(encryptedApiKey)}",
""";
}
return $$"""
CONFIG["TRANSCRIPTION_PROVIDERS"][#CONFIG["TRANSCRIPTION_PROVIDERS"]+1] = {
["Id"] = "{{Guid.NewGuid().ToString()}}",
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
["Host"] = "{{this.Host}}",
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
{{apiKeyLine}}
["Model"] = {
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? string.Empty)}}",
},
}
""";
}
}
@@ -0,0 +1,52 @@
namespace AIStudio.Tools.Databases;
public abstract class DatabaseClient(string name, string path)
{
public string Name => name;
private string Path => path;
private ILogger<DatabaseClient>? logger;
public abstract IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo();
protected string GetStorageSize()
{
if (string.IsNullOrWhiteSpace(this.Path))
{
this.logger!.LogError($"Error: Database path '{this.Path}' cannot be null or empty.");
return "0 B";
}
if (!Directory.Exists(this.Path))
{
this.logger!.LogError($"Error: Database path '{this.Path}' does not exist.");
return "0 B";
}
var files = Directory.EnumerateFiles(this.Path, "*", SearchOption.AllDirectories)
.Where(file => !System.IO.Path.GetDirectoryName(file)!.Contains("cert", StringComparison.OrdinalIgnoreCase));
var size = files.Sum(file => new FileInfo(file).Length);
return FormatBytes(size);
}
private static string FormatBytes(long size)
{
string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" };
int suffixIndex = 0;
while (size >= 1024 && suffixIndex < suffixes.Length - 1)
{
size /= 1024;
suffixIndex++;
}
return $"{size:0##} {suffixes[suffixIndex]}";
}
public void SetLogger(ILogger<DatabaseClient> logService)
{
this.logger = logService;
}
public abstract void Dispose();
}
@@ -0,0 +1,66 @@
using Qdrant.Client;
using Qdrant.Client.Grpc;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases.Qdrant;
public class QdrantClientImplementation : DatabaseClient
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(QdrantClientImplementation).Namespace, nameof(QdrantClientImplementation));
private int HttpPort { get; }
private int GrpcPort { get; }
private QdrantClient GrpcClient { get; }
private string Fingerprint { get; }
private string ApiToken { get; }
public QdrantClientImplementation(string name, string path, int httpPort, int grpcPort, string fingerprint, string apiToken): base(name, path)
{
this.HttpPort = httpPort;
this.GrpcPort = grpcPort;
this.Fingerprint = fingerprint;
this.ApiToken = apiToken;
this.GrpcClient = this.CreateQdrantClient();
}
private const string IP_ADDRESS = "localhost";
private QdrantClient CreateQdrantClient()
{
var address = "https://" + IP_ADDRESS + ":" + this.GrpcPort;
var channel = QdrantChannel.ForAddress(address, new ClientConfiguration
{
ApiKey = this.ApiToken,
CertificateThumbprint = this.Fingerprint
});
var grpcClient = new QdrantGrpcClient(channel);
return new QdrantClient(grpcClient);
}
private async Task<string> GetVersion()
{
var operation = await this.GrpcClient.HealthAsync();
return "v"+operation.Version;
}
private async Task<string> GetCollectionsAmount()
{
var operation = await this.GrpcClient.ListCollectionsAsync();
return operation.Count.ToString();
}
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("HTTP port"), this.HttpPort.ToString());
yield return (TB("gRPC port"), this.GrpcPort.ToString());
yield return (TB("Reported version"), await this.GetVersion());
yield return (TB("Storage size"), $"{this.GetStorageSize()}");
yield return (TB("Number of collections"), await this.GetCollectionsAmount());
}
public override void Dispose() => this.GrpcClient.Dispose();
}
@@ -0,0 +1,211 @@
using System.Security.Cryptography;
using System.Text;
namespace AIStudio.Tools;
/// <summary>
/// Provides encryption and decryption functionality for enterprise configuration plugins.
/// This is used to encrypt/decrypt API keys in Lua configuration files.
/// </summary>
/// <remarks>
/// Important: This is obfuscation, not security. Users with administrative access
/// to their machines can potentially extract the decrypted API keys. This feature
/// is designed to prevent casual exposure of API keys in configuration files. It
/// also protects against accidental leaks while sharing configuration snippets,
/// as the encrypted values cannot be decrypted without the secret key.
/// </remarks>
public sealed class EnterpriseEncryption
{
/// <summary>
/// The number of iterations to derive the key and IV from the password.
/// We use a higher iteration count here because the secret is static
/// (not regenerated each startup like the IPC encryption).
/// </summary>
private const int ITERATIONS = 10_000;
/// <summary>
/// The length of the salt in bytes.
/// </summary>
private const int SALT_LENGTH = 16;
/// <summary>
/// The prefix for encrypted values.
/// </summary>
private const string PREFIX = "ENC:v1:";
private readonly ILogger<EnterpriseEncryption> logger;
private readonly byte[]? secretKey;
/// <summary>
/// Gets a value indicating whether the encryption service is available.
/// </summary>
public bool IsAvailable { get; }
/// <summary>
/// Creates a new instance of the enterprise encryption service.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="base64Secret">The base64-encoded 32-byte encryption secret.</param>
public EnterpriseEncryption(ILogger<EnterpriseEncryption> logger, string? base64Secret)
{
this.logger = logger;
if (string.IsNullOrWhiteSpace(base64Secret))
{
this.logger.LogWarning("No enterprise encryption secret configured. Encrypted API keys in configuration plugins will not be available.");
this.IsAvailable = false;
return;
}
try
{
this.secretKey = Convert.FromBase64String(base64Secret);
if (this.secretKey.Length != 32)
{
this.logger.LogWarning($"The enterprise encryption secret must be exactly 32 bytes (256 bits). Got {this.secretKey.Length} bytes.");
this.secretKey = null;
this.IsAvailable = false;
return;
}
this.IsAvailable = true;
this.logger.LogInformation("Enterprise encryption service initialized successfully.");
}
catch (FormatException ex)
{
this.logger.LogWarning(ex, "Failed to decode the enterprise encryption secret from base64.");
this.IsAvailable = false;
}
}
/// <summary>
/// Checks if the given value is encrypted (has the encryption prefix).
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the value starts with the encryption prefix; otherwise, false.</returns>
public static bool IsEncrypted(string? value) => value?.StartsWith(PREFIX, StringComparison.Ordinal) ?? false;
/// <summary>
/// Tries to decrypt an encrypted value.
/// </summary>
/// <param name="encryptedValue">The encrypted value (with ENC:v1: prefix).</param>
/// <param name="decryptedValue">When successful, contains the decrypted plaintext.</param>
/// <returns>True if decryption was successful; otherwise, false.</returns>
public bool TryDecrypt(string encryptedValue, out string decryptedValue)
{
decryptedValue = string.Empty;
if (!this.IsAvailable)
{
this.logger.LogWarning("Cannot decrypt: Enterprise encryption service is not available.");
return false;
}
if (!IsEncrypted(encryptedValue))
{
this.logger.LogWarning("Cannot decrypt: Value does not have the expected encryption prefix.");
return false;
}
try
{
// Extract the base64-encoded data after the prefix:
var base64Data = encryptedValue[PREFIX.Length..];
var encryptedBytes = Convert.FromBase64String(base64Data);
if (encryptedBytes.Length < SALT_LENGTH + 1)
{
this.logger.LogWarning("Cannot decrypt: Encrypted data is too short.");
return false;
}
// Extract salt and encrypted content:
var salt = encryptedBytes[..SALT_LENGTH];
var cipherText = encryptedBytes[SALT_LENGTH..];
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
var key = keyDerivation.GetBytes(32); // AES-256
var iv = keyDerivation.GetBytes(16); // AES block size
// Decrypt using AES-256-CBC:
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var decryptor = aes.CreateDecryptor();
var decryptedBytes = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
decryptedValue = Encoding.UTF8.GetString(decryptedBytes);
return true;
}
catch (FormatException ex)
{
this.logger.LogWarning(ex, "Failed to decode encrypted value from base64.");
return false;
}
catch (CryptographicException ex)
{
this.logger.LogWarning(ex, "Failed to decrypt value. The encryption secret may be incorrect.");
return false;
}
}
/// <summary>
/// Encrypts a plaintext value.
/// </summary>
/// <param name="plaintext">The plaintext to encrypt.</param>
/// <param name="encryptedValue">When successful, contains the encrypted value with prefix.</param>
/// <returns>True if encryption was successful; otherwise, false.</returns>
public bool TryEncrypt(string plaintext, out string encryptedValue)
{
encryptedValue = string.Empty;
if (!this.IsAvailable)
{
this.logger.LogWarning("Cannot encrypt: Enterprise encryption service is not available.");
return false;
}
try
{
// Generate a random salt:
var salt = RandomNumberGenerator.GetBytes(SALT_LENGTH);
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
var key = keyDerivation.GetBytes(32); // AES-256
var iv = keyDerivation.GetBytes(16); // AES block size
// Encrypt using AES-256-CBC:
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var encryptor = aes.CreateEncryptor();
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
var cipherText = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
// Combine salt and ciphertext
var combined = new byte[SALT_LENGTH + cipherText.Length];
Array.Copy(salt, 0, combined, 0, SALT_LENGTH);
Array.Copy(cipherText, 0, combined, SALT_LENGTH, cipherText.Length);
// Encode to base64 and add the prefix:
encryptedValue = PREFIX + Convert.ToBase64String(combined);
return true;
}
catch (CryptographicException ex)
{
this.logger.LogWarning(ex, "Failed to encrypt value.");
return false;
}
}
/// <summary>
/// Generates a new random 32-byte secret key and returns it as a base64 string.
/// </summary>
/// <returns>A base64-encoded 32-byte secret key.</returns>
public static string GenerateSecret() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
}
@@ -5,6 +5,13 @@ namespace AIStudio.Tools;
/// </summary> /// </summary>
public interface ISecretId public interface ISecretId
{ {
/// <summary>
/// Prefix used for secrets imported from enterprise configuration plugins.
/// This helps distinguish enterprise-managed keys from user-added keys
/// in the OS keyring.
/// </summary>
public const string ENTERPRISE_KEY_PREFIX = "config-plugin";
/// <summary> /// <summary>
/// The unique ID of the secret. /// The unique ID of the secret.
/// </summary> /// </summary>
+16
View File
@@ -0,0 +1,16 @@
namespace AIStudio.Tools;
public static class LuaTools
{
public static string EscapeLuaString(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n");
}
}
@@ -0,0 +1,6 @@
namespace AIStudio.Tools.Metadata;
public class MetaDataDatabasesAttribute(string databaseVersion) : Attribute
{
public string DatabaseVersion => databaseVersion;
}
@@ -1,6 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text;
using AIStudio.Tools.Metadata; using AIStudio.Tools.Metadata;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@@ -74,36 +73,49 @@ public sealed class PandocProcessBuilder
public async Task<PandocPreparedProcess> BuildAsync(RustService rustService) public async Task<PandocPreparedProcess> BuildAsync(RustService rustService)
{ {
var sbArguments = new StringBuilder();
if (this.useStandaloneMode)
sbArguments.Append(" --standalone ");
if(!string.IsNullOrWhiteSpace(this.providedInputFile))
sbArguments.Append(this.providedInputFile);
if(!string.IsNullOrWhiteSpace(this.providedInputFormat))
sbArguments.Append($" -f {this.providedInputFormat}");
if(!string.IsNullOrWhiteSpace(this.providedOutputFormat))
sbArguments.Append($" -t {this.providedOutputFormat}");
foreach (var additionalArgument in this.additionalArguments)
sbArguments.Append($" {additionalArgument}");
if(!string.IsNullOrWhiteSpace(this.providedOutputFile))
sbArguments.Append($" -o {this.providedOutputFile}");
var pandocExecutable = await PandocExecutablePath(rustService); var pandocExecutable = await PandocExecutablePath(rustService);
return new (new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = pandocExecutable.Executable, FileName = pandocExecutable.Executable,
Arguments = sbArguments.ToString(),
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
}, pandocExecutable.IsLocalInstallation); };
// Use argument tokens instead of a single command string so paths with spaces
// or Unicode characters are passed to Pandoc unchanged on all platforms.
if (this.useStandaloneMode)
startInfo.ArgumentList.Add("--standalone");
if (!string.IsNullOrWhiteSpace(this.providedInputFile))
startInfo.ArgumentList.Add(this.providedInputFile);
if (!string.IsNullOrWhiteSpace(this.providedInputFormat))
{
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add(this.providedInputFormat);
}
if (!string.IsNullOrWhiteSpace(this.providedOutputFormat))
{
startInfo.ArgumentList.Add("-t");
startInfo.ArgumentList.Add(this.providedOutputFormat);
}
foreach (var additionalArgument in this.additionalArguments)
{
if (!string.IsNullOrWhiteSpace(additionalArgument))
startInfo.ArgumentList.Add(additionalArgument);
}
if (!string.IsNullOrWhiteSpace(this.providedOutputFile))
{
startInfo.ArgumentList.Add("-o");
startInfo.ArgumentList.Add(this.providedOutputFile);
}
return new(startInfo, pandocExecutable.IsLocalInstallation);
} }
/// <summary> /// <summary>
@@ -3,4 +3,8 @@ namespace AIStudio.Tools.PluginSystem;
public interface IAvailablePlugin : IPluginMetadata public interface IAvailablePlugin : IPluginMetadata
{ {
public string LocalPath { get; } public string LocalPath { get; }
public bool IsManagedByConfigServer { get; }
public Guid? ManagedConfigurationId { get; }
} }
@@ -0,0 +1,49 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents a pending API key that needs to be stored in the OS keyring.
/// This is used during plugin loading to collect API keys from configuration plugins
/// before storing them asynchronously.
/// </summary>
/// <param name="SecretId">The secret ID (provider ID).</param>
/// <param name="SecretName">The secret name (provider instance name).</param>
/// <param name="ApiKey">The decrypted API key.</param>
/// <param name="StoreType">The type of secret store to use.</param>
public sealed record PendingEnterpriseApiKey(
string SecretId,
string SecretName,
string ApiKey,
SecretStoreType StoreType);
/// <summary>
/// Static container for pending API keys during plugin loading.
/// </summary>
public static class PendingEnterpriseApiKeys
{
private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = [];
private static readonly Lock LOCK = new();
/// <summary>
/// Adds a pending API key to the list.
/// </summary>
/// <param name="key">The pending API key to add.</param>
public static void Add(PendingEnterpriseApiKey key)
{
lock (LOCK)
PENDING_KEYS.Add(key);
}
/// <summary>
/// Gets and clears all pending API keys.
/// </summary>
/// <returns>A list of all pending API keys.</returns>
public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear()
{
lock (LOCK)
{
var keys = PENDING_KEYS.ToList();
PENDING_KEYS.Clear();
return keys;
}
}
}
@@ -332,16 +332,55 @@ public abstract partial class PluginBase : IPluginMetadata
return false; return false;
} }
if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) url = url.Trim();
if (!Uri.TryCreate(url, UriKind.Absolute, out var sourceUri))
{ {
url = string.Empty; url = string.Empty;
message = TB("The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'."); message = TB("The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'.");
return false; return false;
} }
var isHttp = sourceUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase);
var isHttps = sourceUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
var isMailTo = sourceUri.Scheme.Equals(Uri.UriSchemeMailto, StringComparison.OrdinalIgnoreCase);
if (!isHttp && !isHttps && !isMailTo)
{
url = string.Empty;
message = TB("The field SOURCE_URL is not a valid URL. The URL must start with 'http://', 'https://', or 'mailto:'.");
return false;
}
if (isMailTo)
{
var recipient = ExtractMailtoRecipient(url);
if (string.IsNullOrWhiteSpace(recipient))
{
url = string.Empty;
message = TB("The field SOURCE_URL is not a valid URL. When the URL starts with 'mailto:', it must contain a valid email address as recipient.");
return false;
}
}
url = sourceUri.ToString();
message = string.Empty; message = string.Empty;
return true; return true;
} }
private static string ExtractMailtoRecipient(string rawUrl)
{
var separatorIndex = rawUrl.IndexOf(':');
if (separatorIndex < 0 || separatorIndex + 1 >= rawUrl.Length)
return string.Empty;
var schemeSpecificPart = rawUrl[(separatorIndex + 1)..];
var queryStart = schemeSpecificPart.IndexOf('?');
var recipient = queryStart >= 0
? schemeSpecificPart[..queryStart]
: schemeSpecificPart;
return recipient.Trim();
}
/// <summary> /// <summary>
/// Tries to read the categories of the plugin. /// Tries to read the categories of the plugin.
@@ -1,4 +1,5 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
using Lua; using Lua;
@@ -8,13 +9,19 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration));
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
private List<PluginConfigurationObject> configObjects = []; private List<PluginConfigurationObject> configObjects = [];
/// <summary> /// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary> /// </summary>
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects; public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state);
public async Task InitializeAsync(bool dryRun) public async Task InitializeAsync(bool dryRun)
{ {
@@ -23,11 +30,58 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
if (!dryRun) if (!dryRun)
{ {
// Store any decrypted API keys from enterprise configuration in the OS keyring:
await StoreEnterpriseApiKeysAsync();
await SETTINGS_MANAGER.StoreSettings(); await SETTINGS_MANAGER.StoreSettings();
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED); await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
} }
} }
/// <summary>
/// Stores any pending enterprise API keys in the OS keyring.
/// </summary>
private static async Task StoreEnterpriseApiKeysAsync()
{
var pendingKeys = PendingEnterpriseApiKeys.GetAndClear();
if (pendingKeys.Count == 0)
return;
LOG.LogInformation($"Storing {pendingKeys.Count} enterprise API key(s) in the OS keyring.");
var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
foreach (var pendingKey in pendingKeys)
{
try
{
// Create a temporary secret ID object for storing the key:
var secretId = new TemporarySecretId(pendingKey.SecretId, pendingKey.SecretName);
var result = await rustService.SetAPIKey(secretId, pendingKey.ApiKey, pendingKey.StoreType);
if (result.Success)
LOG.LogDebug($"Successfully stored enterprise API key for '{pendingKey.SecretName}' in the OS keyring.");
else
LOG.LogWarning($"Failed to store enterprise API key for '{pendingKey.SecretName}': {result.Issue}");
}
catch (Exception ex)
{
LOG.LogError(ex, $"Exception while storing enterprise API key for '{pendingKey.SecretName}'.");
}
}
}
/// <summary>
/// Temporary implementation of ISecretId for storing enterprise API keys.
/// </summary>
private sealed record TemporarySecretId(string SecretId, string SecretName) : ISecretId;
private static bool? ReadDeployedUsingConfigServer(LuaState state)
{
if (state.Environment["DEPLOYED_USING_CONFIG_SERVER"].TryRead<bool>(out var deployedUsingConfigServer))
return deployedUsingConfigServer;
return null;
}
/// <summary> /// <summary>
/// Tries to initialize the UI text content of the plugin. /// Tries to initialize the UI text content of the plugin.
/// </summary> /// </summary>
@@ -60,12 +114,15 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: allow the user to add providers? // Config: allow the user to add providers?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
// Config: show administration settings?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
// Config: preview features visibility // Config: preview features visibility
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
// Config: enabled preview features // Config: enabled preview features (plugin contribution; users can enable additional features)
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfigurationWithPluginContribution(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun);
// Config: hide some assistants? // Config: hide some assistants?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun);
@@ -91,6 +148,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Handle configured document analysis policies: // Handle configured document analysis policies:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun); PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun);
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected profile? // Config: preselected profile?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -100,4 +160,4 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
message = string.Empty; message = string.Empty;
return true; return true;
} }
} }
@@ -2,6 +2,7 @@ using System.Linq.Expressions;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Lua; using Lua;
@@ -13,6 +14,7 @@ namespace AIStudio.Tools.PluginSystem;
/// </summary> /// </summary>
public sealed record PluginConfigurationObject public sealed record PluginConfigurationObject
{ {
private static readonly RustService RUST_SERVICE = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<PluginConfigurationObject>(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<PluginConfigurationObject>();
@@ -77,13 +79,13 @@ public sealed record PluginConfigurationObject
if (luaTableName is null) if (luaTableName is null)
{ {
LOG.LogError($"The configuration object type '{configObjectType}' is not supported yet."); LOG.LogError("The configuration object type '{ConfigObjectType}' is not supported yet (config plugin id: {ConfigPluginId}).", configObjectType, configPluginId);
return false; return false;
} }
if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable)) if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
{ {
LOG.LogWarning($"The {luaTableName} table does not exist or is not a valid table."); LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, configPluginId);
return false; return false;
} }
@@ -95,7 +97,7 @@ public sealed record PluginConfigurationObject
var luaObjectTableValue = luaTable[i]; var luaObjectTableValue = luaTable[i];
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable)) if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
{ {
LOG.LogWarning($"The {luaObjectTable} table at index {i} is not a valid table."); LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, i, configPluginId);
continue; continue;
} }
@@ -149,17 +151,17 @@ public sealed record PluginConfigurationObject
random ??= new ThreadSafeRandom(); random ??= new ThreadSafeRandom();
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
storedObjects.Add((TClass)configObject); storedObjects.Add((TClass)configObject);
LOG.LogWarning($"The next number for the configuration object '{configObject.Name}' (id={configObject.Id}) could not be incremented. Using a random number instead."); LOG.LogWarning("The next number for the configuration object '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
} }
} }
} }
else else
LOG.LogWarning($"The {luaObjectTable} table at index {i} does not contain a valid chat template configuration."); LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid configuration object (type={ConfigObjectType}, config plugin id: {ConfigPluginId}).", luaTableName, i, configObjectType, configPluginId);
} }
return true; return true;
} }
/// <summary> /// <summary>
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin. /// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
/// </summary> /// </summary>
@@ -168,37 +170,45 @@ public sealed record PluginConfigurationObject
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param> /// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
/// <param name="availablePlugins">A list of currently available plugins.</param> /// <param name="availablePlugins">A list of currently available plugins.</param>
/// <param name="configObjectList">A list of all existing configuration objects.</param> /// <param name="configObjectList">A list of all existing configuration objects.</param>
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns> /// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
public static bool CleanLeftOverConfigurationObjects<TClass>( public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
PluginConfigurationObjectType configObjectType, PluginConfigurationObjectType configObjectType,
Expression<Func<Data, List<TClass>>> configObjectSelection, Expression<Func<Data, List<TClass>>> configObjectSelection,
IList<IAvailablePlugin> availablePlugins, IList<IAvailablePlugin> availablePlugins,
IList<PluginConfigurationObject> configObjectList) where TClass : IConfigurationObject IList<PluginConfigurationObject> configObjectList,
SecretStoreType? secretStoreType = null) where TClass : IConfigurationObject
{ {
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
var leftOverObjects = new List<TClass>(); var leftOverObjects = new List<TClass>();
foreach (var configuredObject in configuredObjects) foreach (var configuredObject in configuredObjects)
{ {
// Only process objects that are based on enterprise configuration plugins (aka configuration plugins),
// as only those can be left over after a plugin was removed:
if(!configuredObject.IsEnterpriseConfiguration) if(!configuredObject.IsEnterpriseConfiguration)
continue; continue;
// From what plugin is this configuration object coming from?
var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId; var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
if(configObjectSourcePluginId == Guid.Empty) if(configObjectSourcePluginId == Guid.Empty)
continue; continue;
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
// over and should be removed:
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId); var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
if(templateSourcePlugin is null) if(templateSourcePlugin is null)
{ {
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings."); LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing this object from the settings.");
leftOverObjects.Add(configuredObject); leftOverObjects.Add(configuredObject);
} }
// Is the configuration object still present in the configuration plugin? If not, it is also left over and should be removed:
if(!configObjectList.Any(configObject => if(!configObjectList.Any(configObject =>
configObject.Type == configObjectType && configObject.Type == configObjectType &&
configObject.ConfigPluginId == configObjectSourcePluginId && configObject.ConfigPluginId == configObjectSourcePluginId &&
configObject.Id.ToString() == configuredObject.Id)) configObject.Id.ToString() == configuredObject.Id))
{ {
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings."); LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the object from the settings.");
leftOverObjects.Add(configuredObject); leftOverObjects.Add(configuredObject);
} }
} }
@@ -206,8 +216,20 @@ public sealed record PluginConfigurationObject
// Remove collected items after enumeration to avoid modifying the collection during iteration: // Remove collected items after enumeration to avoid modifying the collection during iteration:
var wasConfigurationChanged = leftOverObjects.Count > 0; var wasConfigurationChanged = leftOverObjects.Count > 0;
foreach (var item in leftOverObjects.Distinct()) foreach (var item in leftOverObjects.Distinct())
{
configuredObjects.Remove(item); configuredObjects.Remove(item);
// Delete the API key from the OS keyring if the removed object has one:
if(secretStoreType is not null && item is ISecretId secretId)
{
var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value);
if (deleteResult.Success)
LOG.LogInformation($"Successfully deleted API key for removed enterprise provider '{item.Name}' from the OS keyring.");
else
LOG.LogWarning($"Failed to delete API key for removed enterprise provider '{item.Name}' from the OS keyring: {deleteResult.Issue}");
}
}
return wasConfigurationChanged; return wasConfigurationChanged;
} }
} }
@@ -5,10 +5,10 @@ namespace AIStudio.Tools.PluginSystem;
public static partial class PluginFactory public static partial class PluginFactory
{ {
public static async Task<EntityTagHeaderValue?> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default) public static async Task<(bool Success, EntityTagHeaderValue? ETag, string? Issue)> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
{ {
if(configPlugId == Guid.Empty || string.IsNullOrWhiteSpace(configServerUrl)) if(configPlugId == Guid.Empty || string.IsNullOrWhiteSpace(configServerUrl))
return null; return (false, null, "Configuration ID or server URL is missing.");
try try
{ {
@@ -18,18 +18,24 @@ public static partial class PluginFactory
using var http = new HttpClient(); using var http = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl); using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
return response.Headers.ETag; if (!response.IsSuccessStatusCode)
{
LOG.LogError($"Failed to determine the ETag for configuration plugin '{configPlugId}'. HTTP Status: {response.StatusCode}");
return (false, null, $"HTTP status: {response.StatusCode}");
}
return (true, response.Headers.ETag, null);
} }
catch (Exception e) catch (Exception e)
{ {
LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin."); LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin.");
return null; return (false, null, e.Message);
} }
} }
public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default) public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
{ {
if(!IS_INITIALIZED) if(!IsInitialized)
{ {
LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin."); LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin.");
return false; return false;
@@ -40,36 +46,72 @@ public static partial class PluginFactory
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})"); LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
var tempDownloadFile = Path.GetTempFileName(); var tempDownloadFile = Path.GetTempFileName();
var stagedDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.staging-{Guid.NewGuid():N}");
string? backupDirectory = null;
var wasSuccessful = false;
try try
{ {
await LockHotReloadAsync(); await LockHotReloadAsync();
using var httpClient = new HttpClient(); using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(downloadUrl, cancellationToken); var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
if (response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
await using(var tempFileStream = File.Create(tempDownloadFile))
{
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
}
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if(Directory.Exists(configDirectory))
Directory.Delete(configDirectory, true);
Directory.CreateDirectory(configDirectory);
ZipFile.ExtractToDirectory(tempDownloadFile, configDirectory);
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
}
else
LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}"); LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}");
return false;
}
await using(var tempFileStream = File.Create(tempDownloadFile))
{
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
}
ZipFile.ExtractToDirectory(tempDownloadFile, stagedDirectory);
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if (Directory.Exists(configDirectory))
{
backupDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.backup-{Guid.NewGuid():N}");
Directory.Move(configDirectory, backupDirectory);
}
Directory.Move(stagedDirectory, configDirectory);
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory))
Directory.Delete(backupDirectory, true);
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
wasSuccessful = true;
} }
catch (Exception e) catch (Exception e)
{ {
LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin."); LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin.");
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !Directory.Exists(configDirectory))
{
try
{
Directory.Move(backupDirectory, configDirectory);
}
catch (Exception restoreException)
{
LOG.LogError(restoreException, "Failed to restore the previous configuration plugin after a failed update.");
}
}
} }
finally finally
{ {
if (Directory.Exists(stagedDirectory))
{
try
{
Directory.Delete(stagedDirectory, true);
}
catch (Exception e)
{
LOG.LogError(e, "Failed to delete the staged configuration plugin directory.");
}
}
if (File.Exists(tempDownloadFile)) if (File.Exists(tempDownloadFile))
{ {
try try
@@ -85,6 +127,6 @@ public static partial class PluginFactory
UnlockHotReload(); UnlockHotReload();
} }
return true; return wasSuccessful;
} }
} }
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more