Compare commits

..
122 Commits
Author SHA1 Message Date
Thorsten Sommer 9d71978feb Prepared release v0.9.27 (#272) 2025-01-21 19:28:09 +01:00
Thorsten Sommer 940459ca46 Fixed an issue with tooltips at the bottom of the app (#271) 2025-01-21 19:22:26 +01:00
Thorsten Sommer 09d67bc1c2 Fixed the workspace name after re-rendering the chat component (#270) 2025-01-21 19:12:36 +01:00
Thorsten Sommer 1681405fb9 Fixed an issue with the workspace title not being updated (#269) 2025-01-21 18:58:20 +01:00
Thorsten Sommer be408a6aee Fixed message bus issues (#268) 2025-01-21 15:36:22 +01:00
Thorsten Sommer cf975f2a6c Removed the "send to" button from the ERI server assistant (#267) 2025-01-21 14:20:03 +01:00
Thorsten Sommer 1df1d3f328 Updated changelog (#266) 2025-01-21 13:52:10 +01:00
Thorsten Sommer ed14525390 Fixed hostname validation message for ERI data sources (#265) 2025-01-21 13:49:44 +01:00
Thorsten Sommer 14caf550ec Improved the inner content scrolling to use the entire space available (#263) 2025-01-21 13:40:47 +01:00
Thorsten Sommer f50e041640 Updated introduction (#264) 2025-01-21 09:13:16 +01:00
Thorsten Sommer ebe00812e2 Prepared release v0.9.26 (#262) 2025-01-13 20:20:40 +01:00
Thorsten Sommer ae72c50236 Fixed the embedding dialog to store the selected embedding model (#261) 2025-01-13 20:10:58 +01:00
Thorsten Sommer 48874d617a Added a confirmation dialog for deleting ERI server presets (#260) 2025-01-13 19:58:52 +01:00
Thorsten Sommer 63be312bb4 Configure data sources (#259) 2025-01-13 19:51:26 +01:00
Thorsten Sommer 06f66fdab2 Refactored the settings page (#258) 2025-01-05 15:11:15 +01:00
Thorsten Sommer b9a65d9ca0 Prepared release v0.9.25 (#257) 2025-01-04 19:38:06 +01:00
Thorsten Sommer e4b1ea32fb Fixed chat loading by assistants (#256) 2025-01-04 19:27:45 +01:00
Thorsten Sommer e9975a5fbc Fixed the chat scrolling behavior after toggling the workspaces (#255) 2025-01-04 18:27:23 +01:00
Thorsten Sommer 2ef4d9d53f Added xAI as a new provider (#254) 2025-01-04 15:06:08 +01:00
Thorsten Sommer 33a2728644 Refactored network code (#253) 2025-01-04 14:11:32 +01:00
Thorsten Sommer a54c0bdbbf Prepared release v0.9.24 (#252) 2025-01-04 12:44:02 +01:00
Thorsten Sommer d6521850e9 Improved the stream handling for all providers (#251) 2025-01-04 12:37:49 +01:00
Thorsten Sommer 258bc7a338 Fixed a streaming bug (#250) 2025-01-04 12:30:05 +01:00
Thorsten Sommer 187663bbf2 Added the option to stop the current generation (#249) 2025-01-03 23:13:56 +01:00
Thorsten Sommer 8060fc01dd Added button to edit last user message (#248) 2025-01-03 21:58:31 +01:00
Thorsten Sommer b2ca49ab92 Added a button to regenerate the last AI response (#247) 2025-01-03 21:18:27 +01:00
Thorsten Sommer 5e445f09fa Implemented removal of chat messages (#246) 2025-01-03 18:01:22 +01:00
Thorsten Sommer 6f5aecd92b Fixed Ubuntu arm build pipeline (#245) 2025-01-02 21:29:20 +01:00
Thorsten Sommer 317d3c9eec Prepared release v0.9.23 (#244) 2025-01-02 20:47:44 +01:00
Thorsten Sommer 0acb3a20a2 Improved the navbar handling when configured as "never expands" (#243) 2025-01-02 20:26:04 +01:00
Thorsten Sommer 3b93841982 Fixed dark mode code blocks (#242) 2025-01-02 15:24:15 +01:00
Thorsten Sommer 38a45955fb Improved profile and system prompt handling (#241) 2025-01-02 14:50:54 +01:00
Thorsten Sommer caec3bfd2c Improved chat UI (#240) 2025-01-02 13:16:47 +01:00
Thorsten Sommer 12b3d6fc3d Fixed OpenAI o models (#239) 2025-01-01 20:11:42 +01:00
Thorsten Sommer 21927754b6 Upgraded dependencies (#238) 2025-01-01 18:05:26 +01:00
Thorsten Sommer 2131bf1817 Improved the save operation of settings by using a temporary file (#237) 2025-01-01 17:15:04 +01:00
Thorsten Sommer 8cbef49d89 Add ERI server assistant (#231) 2025-01-01 15:49:27 +01:00
Luc Bologna cdf717ad5f Update .gitignore and improve build.nu script 2024-12-26 14:39:29 +01:00
Thorsten Sommer 02b6f526da Added CFF (#234) 2024-12-13 15:16:26 +01:00
Thorsten Sommer a365539282 Renamed EDI to ERI (#233) 2024-12-13 14:33:38 +01:00
Thorsten Sommer 7250b270f5 Prepared release v0.9.22 (#230) 2024-12-04 12:04:39 +01:00
Thorsten Sommer d437d08640 Updated readme (#229) 2024-12-04 11:53:49 +01:00
Thorsten Sommer 61f7a3dd72 Fixed bugs related to the workspaces component (#228) 2024-12-04 11:44:12 +01:00
Thorsten Sommer 55d7895f58 Added preview feature configuration (#227) 2024-12-03 21:02:37 +01:00
Thorsten Sommer e2859b3d76 Define the writer mode as experimental preview (#226) 2024-12-03 15:52:45 +01:00
Thorsten Sommer 0a951ead3e Added the writer mode experiment (#167) 2024-12-03 15:32:38 +01:00
Thorsten Sommer 432ae30fdf Updated RAG tasks (#225) 2024-12-03 15:28:21 +01:00
Thorsten Sommer 799112eb9d Added embedding configurations (#224) 2024-12-03 15:24:40 +01:00
Thorsten Sommer f806ee28c5 Updated readme (#223) 2024-11-24 10:19:05 +01:00
Thorsten Sommer 32ff143b21 Added configuration for preview feature visibility (#222) 2024-11-24 10:11:56 +01:00
Thorsten Sommer 90a1f681c5 Readme: added link to o1 issue (#221) 2024-11-24 09:41:59 +01:00
Thorsten Sommer 99fe9f3a4f Updated RAG tasks (#220) 2024-11-23 13:35:42 +01:00
Thorsten Sommer cf8a9800f3 Prepared release v0.9.21 (#219) 2024-11-23 13:25:24 +01:00
Thorsten Sommer f57e971f80 Upgraded dependencies (#218) 2024-11-23 13:19:43 +01:00
Thorsten Sommer 0d104f5abc Remember chat provider (#217) 2024-11-23 13:04:02 +01:00
Thorsten Sommer ba6b8d2b3c Fixed auto-save when sending assistant results to a new chat (#216) 2024-11-23 10:54:58 +01:00
Thorsten Sommer 1c72c4d6ec Migrated to use the shared random instance (#214) 2024-11-16 12:20:04 +01:00
Thorsten Sommer 4ca5aba58f Fixed workspace titles (#213) 2024-11-16 11:59:09 +01:00
Thorsten Sommer c4d2574508 Prepared release v0.9.20 (#212) 2024-11-15 21:50:12 +01:00
Thorsten Sommer ceac9cefe5 Fixed bugs related to storing and loading chats (#211) 2024-11-15 21:22:57 +01:00
Thorsten Sommer ebe2ad62c6 Prepared release v0.9.19 (#210) 2024-11-14 07:03:01 +01:00
Thorsten Sommer 9c0dea9927 Updated supporters (#209) 2024-11-14 06:54:19 +01:00
Thorsten Sommer d2a2d658e6 Fixed a bug regarding hostnames of self-hosted providers (#208) 2024-11-14 06:39:35 +01:00
Thorsten Sommer d82427ba56 Upgraded dependencies (#207) 2024-11-14 06:24:29 +01:00
Thorsten Sommer 386121d198 Updated RAG news (#206) 2024-11-13 20:42:41 +01:00
Thorsten Sommer f9245d52c2 Added provider metadata regarding embeddings (#205) 2024-11-13 20:33:52 +01:00
Thorsten Sommer 9fa2a1a135 Updated readme (#203) 2024-11-10 10:41:20 +01:00
Thorsten Sommer 69288699b3 Prepared v0.9.18 (#202) 2024-11-09 22:16:37 +01:00
Thorsten Sommer 826d15729b Added the Google provider (#201) 2024-11-09 22:04:00 +01:00
Thorsten Sommer 2ae40c59a3 Added Groq provider (#200) 2024-11-09 20:13:14 +01:00
Thorsten Sommer 119100bbce Cleaned bias descriptions (#199) 2024-11-09 19:37:54 +01:00
Thorsten Sommer f92fab24b3 Updated RAG tasks (#198) 2024-11-08 22:49:09 +01:00
Thorsten Sommer 671abf3b07 Updated RAG tasks (#196) 2024-11-05 21:42:46 +01:00
Thorsten Sommer d0a366258a Refactored runtime (#192) 2024-11-05 21:39:21 +01:00
Thorsten Sommer 58d7478f3a Updated RAG tasks (#195) 2024-11-05 14:35:54 +01:00
Thorsten Sommer e8216b70a5 Added new Anthropic model claude-3-5-heiku-20241022 (#194) 2024-11-05 08:43:16 +01:00
Thorsten Sommer 152d5f9b4f Added PR regarding runtime refactoring (#193) 2024-11-04 20:48:25 +01:00
Thorsten Sommer f1104c5e09 Created RAG task list (#190) 2024-11-03 21:39:29 +01:00
Thorsten Sommer 3ccb097705 Updated news section (#189) 2024-11-03 20:52:31 +01:00
Thorsten Sommer b9416e66c3 Prepared release v0.9.17 (#188) 2024-11-03 12:17:04 +01:00
Thorsten Sommer 9f576e8572 Added new Anthropic models (#187) 2024-11-03 12:06:54 +01:00
Thorsten Sommer 629613f75a Prepared release v0.9.16 (#186) 2024-11-02 23:13:30 +01:00
Thorsten Sommer abbb8c1650 Fixed the appearance of the "no diff" message for text diffs (#185) 2024-11-02 23:03:07 +01:00
Thorsten Sommer d2ac4e26b0 Added workspace display options (#184) 2024-11-02 22:53:02 +01:00
Thorsten Sommer bd97c0fb4b Fixed the "send to" issue, where you could select the bias-of-the-day (#183) 2024-11-02 11:55:09 +01:00
Thorsten Sommer 8bd54e6d61 Added second supporter (#182) 2024-11-02 11:40:17 +01:00
Thorsten Sommer e419c5561b Improved the layout of the app window (#181) 2024-11-02 11:12:54 +01:00
Thorsten Sommer 24a73e92e1 Added media (#180) 2024-10-31 10:48:56 +01:00
Thorsten Sommer 832e054dbf Updated news (#179) 2024-10-28 20:19:38 +01:00
Thorsten Sommer 4703ab04ab Updated news (#178) 2024-10-28 16:53:18 +01:00
Thorsten Sommer c1d5be63cc Prepared release v0.9.15 (#177) 2024-10-28 16:10:49 +01:00
Thorsten Sommer 5d3ea3d57a Upgraded dependencies (#176) 2024-10-28 15:59:36 +01:00
Thorsten Sommer 217b9c4db3 Bias of the day (#173) 2024-10-28 15:41:00 +01:00
Thorsten Sommer e9c805f0df Updated news (#174) 2024-10-21 20:41:35 +02:00
Thorsten Sommer 11475ea4af Added bias names (#172) 2024-10-21 18:21:08 +02:00
Thorsten Sommer 46a7e98141 Defined bias catalog (#166) 2024-10-20 12:35:03 +02:00
Thorsten Sommer 0d6ccbd237 Prepare release v0.9.14 (#170) 2024-10-18 11:23:33 +02:00
Thorsten Sommer efb30cad87 Upgraded dependencies (#169) 2024-10-18 10:40:19 +02:00
Thorsten Sommer fe33867cb6 Updated news (#168) 2024-10-14 19:58:45 +02:00
Thorsten Sommer 1b1989e8eb Improved build introductions (#165) 2024-10-14 19:23:40 +02:00
Thorsten Sommer afa5df894e Add news section (#164) 2024-10-11 08:58:26 +02:00
Thorsten Sommer c8d67b1e4c Removed previous run configuration for JetBrains Rider (#163) 2024-10-11 08:33:19 +02:00
Thorsten Sommer e717b84fb4 Added @richard-stanton as the first supporter (#162) 2024-10-11 08:30:13 +02:00
Thorsten Sommer 2463c85b77 Updated build documentation (#161) 2024-10-11 07:43:45 +02:00
Thorsten Sommer cbf34580b3 Fixed media files to use LFS (#160) 2024-10-11 07:15:24 +02:00
Thorsten Sommer e60f815d77 Fixed LFS tracking for woff2, afpub, afdesign, afphoto files (#159) 2024-10-08 07:53:15 +02:00
Thorsten Sommer a19a6b2188 CI/CD: Use a specific Tauri version for the cli tools (#158) 2024-10-07 14:42:09 +02:00
Thorsten Sommer d2dd90d089 Update media (#157) 2024-10-07 13:37:17 +02:00
Thorsten Sommer 37e113af0e Allow the use of an API key for self-hosted ollama instances (#156) 2024-10-07 13:26:25 +02:00
Thorsten Sommer 776fa8ac58 Prepared release v0.9.12 (#155) 2024-09-15 22:53:09 +02:00
Thorsten Sommer 8715054dda Improved grammar & spelling check and rewrite assistants (#154) 2024-09-15 22:46:48 +02:00
Thorsten Sommer 95da4c6410 Improved text diff viewer by line-through deleted text (#153) 2024-09-15 22:33:39 +02:00
Thorsten Sommer 92f5b49543 Fixed the dark theme for the text diff viewer (#152) 2024-09-15 22:21:33 +02:00
Thorsten Sommer e6be9a593e Fixed chat conversion of job posting, legal check, and my task assistants (#151) 2024-09-15 22:06:22 +02:00
Thorsten Sommer 375be805a1 Add job posting assistant (#150) 2024-09-15 21:14:56 +02:00
Thorsten Sommer b99866b8fe Prepared release v0.9.11 (#149) 2024-09-15 12:37:56 +02:00
Thorsten Sommer e9998348c5 Added color themes (dark & light) (#148) 2024-09-15 12:30:07 +02:00
Thorsten Sommer 0ad7b8d4dd Added options to enforce minimum confidence levels for each assistant (#147) 2024-09-14 19:20:33 +02:00
Thorsten Sommer 4175387a93 Added optional enforcement for a min. confidence level (#145) 2024-09-13 23:29:19 +02:00
Thorsten Sommer 1cac1a7913 Renamed the Providers enum to LLMProviders for better clarity (#144) 2024-09-13 21:50:00 +02:00
Thorsten Sommer 5fa2f3c822 Added a tooltip to the confidence card button (#143) 2024-09-13 21:38:38 +02:00
Thorsten Sommer 5608b48a2d Allow OpenAI o1 models (#142) 2024-09-12 22:58:32 +02:00
373 changed files with 22279 additions and 4348 deletions

No files matched your search

+4
View File
@@ -11,3 +11,7 @@ media/**/*.pptx filter=lfs diff=lfs merge=lfs -text
media/**/*.png filter=lfs diff=lfs merge=lfs -text
media/**/*.ico filter=lfs diff=lfs merge=lfs -text
media/**/*.icns filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.afpub filter=lfs diff=lfs merge=lfs -text
*.afdesign filter=lfs diff=lfs merge=lfs -text
*.afphoto filter=lfs diff=lfs merge=lfs -text
+4 -4
View File
@@ -278,7 +278,7 @@ jobs:
if: matrix.platform != 'windows-latest'
run: |
if ! cargo tauri --version > /dev/null 2>&1; then
cargo install tauri-cli
cargo install --version 1.6.2 tauri-cli
else
echo "Tauri is already installed"
fi
@@ -287,7 +287,7 @@ jobs:
if: matrix.platform == 'windows-latest'
run: |
if (-not (cargo tauri --version 2>$null)) {
cargo install tauri-cli
cargo install --version 1.6.2 tauri-cli
} else {
Write-Output "Tauri is already installed"
}
@@ -370,7 +370,7 @@ jobs:
build_linux_arm64:
name: Build app (linux-arm64)
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: read_metadata
env:
SKIP: false # allows disabling this long-running job temporarily
@@ -500,7 +500,7 @@ jobs:
apt-get install --yes libwebkit2gtk-4.0-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
# Setup Tauri:
cargo install tauri-cli
cargo install --version 1.6.2 tauri-cli
- name: Add the built runner image to the cache
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
+1
View File
@@ -147,3 +147,4 @@ orleans.codegen.cs
**/.idea/**/dynamic.xml
**/.idea/**/uiDesigner.xml
**/.idea/**/dbnavigator.xml
**/.vs
+22
View File
@@ -0,0 +1,22 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: AI Studio
message: >-
When you want to cite AI Studio in your scientific work,
please use these metadata.
type: software
authors:
- given-names: Thorsten
family-names: Sommer
email: thorsten.sommer@dlr.de
affiliation: Deutsches Zentrum für Luft- und Raumfahrt (DLR)
orcid: 'https://orcid.org/0000-0002-3264-9934'
- name: Open Source Community
repository-code: 'https://github.com/MindWorkAI/AI-Studio'
url: 'https://mindworkai.org/'
keywords:
- LLM
- AI
- Orchestration
+1 -1
View File
@@ -6,7 +6,7 @@ FSL-1.1-MIT
## Notice
Copyright 2024 Thorsten Sommer
Copyright 2025 Thorsten Sommer
## Terms and Conditions
+65 -9
View File
@@ -1,15 +1,66 @@
# MindWork AI Studio
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
## News
Things we are currently working on:
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259))~~
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
- [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
- [ ] App: Implement the continuous process of vectorizing data
- [ ] App: Define a common retrieval context interface for the integration of RAG processes in chats
- [ ] App: Define a common augmentation interface for the integration of RAG processes in chats
- [ ] App: Integrate data sources in chats
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
Other News:
- October 2024: We've found the first two financial supporters. Huge thanks to `richard-stanton` and `peerschuett` for backing the project. Thanks for having the courage to be the first to support us.
- October 2024: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of one project and will also contribute to its further development. This is great news.
Features we have recently released:
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
- v0.9.16: Added workspace display options & improved the layout of the app window.
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
- v0.9.13: You can use `ollama` providers secured with API keys.
## What is AI Studio?
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png)
![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png)
MindWork AI Studio is a desktop application available for macOS, Windows, and Linux. It provides a unified chat interface for Large Language Models (LLMs). You bring your own API key for the respective LLM provider to use the models. The API keys are securely stored by the operating system.
MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provides a unified user interface for interaction with Large Language Models (LLM). AI Studio also offers so-called assistants, where prompting is not necessary. You can think of AI Studio like an email program: you bring your own API key for the LLM of your choice and can then use these AI systems with AI Studio. Whether you want to use Google Gemini, OpenAI o1, or even your own local AI models.
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
**Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), or [Fireworks](https://fireworks.ai/). Support for Google Gemini, and [Replicate](https://replicate.com/) is planned.
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/).
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API.
- **Privacy**: You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
- **Flexibility**: Choose the provider and model best suited for your current task.
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
@@ -25,16 +76,21 @@ We offer various ways you can support the project:
- **One-Time Contributions**: Make a one-time donation and have your name or company logo included in the app as a gesture of our gratitude.
For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
To view all available tiers, please visit our [GitHub Sponsors page](https://github.com/sponsors/MindWorkAI).
Your support, whether big or small, keeps the wheels turning and is deeply appreciated ❤️.
## Planned Features
Here's an exciting look at some of the features we're planning to add to MindWork AI Studio in future releases:
- **More providers**: We plan to add support for additional LLM providers, such as Google Gemini, giving you more options to choose from.
- **System prompts**: Integration of a system prompt library will allow you to control the behavior of the LLM with predefined prompts, ensuring consistency and efficiency.
- **Text replacement for better privacy**: Define keywords that will be replaced in your chats before sending content to the provider, enhancing your privacy.
- **Advanced interactions**: We're full of ideas for advanced interactions tailored for specific use cases, whether in a business context or for writers and other professionals.
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
- **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
- **Integration of enterprise data:** It will soon be possible to integrate data from the corporate network using a specified interface ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). This will likely require development work by the organization in question.
- **Useful assistants:** We'll develop more assistants for everyday tasks.
- **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
- **Specific requirements:** Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas.
- **Voice control:** You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
- **Content creation:** There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers.
- **Email monitoring:** You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
- **Browser usage:** We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser.
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.
-21
View File
@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="AI Studio" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net8.0/osx-arm64/mindworkAIStudio.dll" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="RUNTIME_TYPE" value="coreclr" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/MindWork AI Studio/MindWork AI Studio.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
-7
View File
@@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev" type="CompoundRunConfigurationType">
<toRun name="AI Studio" type="DotNetProject" />
<toRun name="Tauri Dev" type="ShConfigurationType" />
<method v="2" />
</configuration>
</component>
File diff suppressed because it is too large. Load diff
+10
View File
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+11
View File
@@ -2,6 +2,10 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ERIClients", "ERIClients", "{5C2AF789-287B-4FCB-B675-7273D8CD4579}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERIClientV1", "ERIClientV1\ERIClientV1.csproj", "{9E35A273-0FA6-4BD5-8880-A1DDAC106926}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +16,12 @@ Global
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9E35A273-0FA6-4BD5-8880-A1DDAC106926} = {5C2AF789-287B-4FCB-B675-7273D8CD4579}
EndGlobalSection
EndGlobal
+3
View File
@@ -1,7 +1,10 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
-1
View File
@@ -1 +0,0 @@
*.woff2 filter=lfs diff=lfs merge=lfs -text
@@ -18,7 +18,7 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
#region Overrides of AgentBase
public override Settings.Provider? ProviderSettings { get; set; }
public override AIStudio.Settings.Provider? ProviderSettings { get; set; }
protected override Type Type => Type.SYSTEM;
+2
View File
@@ -13,6 +13,7 @@
<link rel="icon" type="image/png" href="favicon.png"/>
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<link href="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
<link href="app.css" rel="stylesheet" />
<HeadOutlet/>
<script src="diff.js"></script>
@@ -24,6 +25,7 @@
<script src="boot.js"></script>
<script src="system/MudBlazor/MudBlazor.min.js"></script>
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
<script src="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
<script src="app.js"></script>
</body>
@@ -6,7 +6,7 @@ namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
protected override string Title => "Agenda Planner";
@@ -106,7 +106,7 @@ public partial class AssistantAgenda : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputContent = string.Empty;
this.contentLines.Clear();
@@ -1,112 +1,134 @@
@using AIStudio.Chat
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
<div class="inner-scrolling-context">
<InnerScrolling HeaderHeight="6em">
<ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
@if (this.Body is not null)
{
@this.Body
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
<InnerScrolling>
<ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
@if (this.ShowDedicatedProgress && this.isProcessing)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
@if (this.ShowResult && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
</ChildContent>
<FooterContent>
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
@foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
{
if (assistant is Components.NONE || this.Component == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
@foreach (var button in this.FooterButtons)
{
switch (button)
@if (this.Body is not null)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
<CascadingValue Value="@this">
@this.Body
</CascadingValue>
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
@if (this.ShowDedicatedProgress && this.isProcessing)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
case SendToButton sendToButton:
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
@foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
}
@if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null)
{
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
{
@if (!block.HideFromUser)
{
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
}
}
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
</ChildContent>
<FooterContent>
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
@if (this.ShowSendTo)
{
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{
if(assistant is Components.NONE || sendToButton.Self == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
}
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" Provider="@this.providerSettings.UsedProvider"/>
}
@foreach (var button in this.FooterButtons)
{
switch (button)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
</MudStack>
</FooterContent>
</InnerScrolling>
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
case SendToButton sendToButton:
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
@if (this.ShowCopyResult)
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
}
@if (this.ShowReset)
{
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
}
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
</MudStack>
</FooterContent>
</InnerScrolling>
</div>
@@ -4,11 +4,14 @@ using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
using MudBlazor.Utilities;
using RustService = AIStudio.Tools.RustService;
using Timer = System.Timers.Timer;
namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase
public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver, IDisposable
{
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;
@@ -31,8 +34,15 @@ public abstract partial class AssistantBase : ComponentBase
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
[Inject]
private MudTheme ColorTheme { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
protected abstract string Title { get; }
@@ -40,7 +50,7 @@ public abstract partial class AssistantBase : ComponentBase
protected abstract string SystemPrompt { get; }
protected abstract Tools.Components Component { get; }
public abstract Tools.Components Component { get; }
protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch
{
@@ -48,7 +58,7 @@ public abstract partial class AssistantBase : ComponentBase
_ => string.Empty,
};
protected abstract void ResetFrom();
protected abstract void ResetForm();
protected abstract bool MightPreselectValues();
@@ -61,12 +71,20 @@ public abstract partial class AssistantBase : ComponentBase
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual bool ShowEntireChatThread => false;
protected virtual bool AllowProfiles => true;
protected virtual bool ShowProfileSelection => true;
protected virtual bool ShowDedicatedProgress => false;
protected virtual bool ShowSendTo => true;
protected virtual bool ShowCopyResult => true;
protected virtual bool ShowReset => true;
protected virtual ChatThread ConvertToChatThread => this.chatThread ?? new();
@@ -78,8 +96,10 @@ public abstract partial class AssistantBase : ComponentBase
protected MudForm? form;
protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
private bool isProcessing;
@@ -88,9 +108,20 @@ public abstract partial class AssistantBase : ComponentBase
protected override async Task OnInitializedAsync()
{
this.formChangeTimer.AutoReset = false;
this.formChangeTimer.Elapsed += async (_, _) =>
{
this.formChangeTimer.Stop();
await this.OnFormChange();
};
this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
await base.OnInitializedAsync();
}
@@ -113,42 +144,109 @@ public abstract partial class AssistantBase : ComponentBase
}
#endregion
#region Implementation of IMessageBusReceiver
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedProvider.GetConfidence(this.SettingsManager).StyleBorder() : string.Empty;
public string ComponentName => nameof(AssistantBase);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.StateHasChanged();
break;
}
return Task.CompletedTask;
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
}
#endregion
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
protected string? ValidatingProvider(AIStudio.Settings.Provider provider)
{
if(provider.UsedProvider == Providers.NONE)
if(provider.UsedLLMProvider == LLMProviders.NONE)
return "Please select a provider.";
return null;
}
private void TriggerFormChange(FormFieldChangedEventArgs _)
{
this.formChangeTimer.Stop();
this.formChangeTimer.Start();
}
/// <summary>
/// This method is called after any form field has changed.
/// </summary>
/// <remarks>
/// This method is called after a delay of 1.6 seconds. This is to prevent
/// the method from being called too often. This method is called after
/// the user has stopped typing or selecting options.
/// </remarks>
protected virtual Task OnFormChange() => Task.CompletedTask;
/// <summary>
/// Add an issue to the UI.
/// </summary>
/// <param name="issue">The issue to add.</param>
protected void AddInputIssue(string issue)
{
Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1);
this.inputIssues[^1] = issue;
this.inputIsValid = false;
this.StateHasChanged();
}
protected void CreateChatThread()
{
this.chatThread = new()
{
SelectedProvider = this.providerSettings.Id,
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
SystemPrompt = this.SystemPrompt,
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Name = $"Assistant - {this.Title}",
Seed = this.RNG.Next(),
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
$"""
{this.SystemPrompt}
{this.currentProfile.ToSystemPrompt()}
""",
Blocks = [],
};
}
protected Guid CreateChatThread(Guid workspaceId, string name)
{
var chatId = Guid.NewGuid();
this.chatThread = new()
{
SelectedProvider = this.providerSettings.Id,
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
SystemPrompt = this.SystemPrompt,
WorkspaceId = workspaceId,
ChatId = chatId,
Name = name,
Seed = this.RNG.Next(),
Blocks = [],
};
return chatId;
}
protected DateTimeOffset AddUserRequest(string request)
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
{
var time = DateTimeOffset.Now;
this.chatThread!.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
HideFromUser = hideContentFromUser,
Role = ChatRole.USER,
Content = new ContentText
{
@@ -159,7 +257,7 @@ public abstract partial class AssistantBase : ComponentBase
return time;
}
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false)
{
var aiText = new ContentText
{
@@ -174,9 +272,15 @@ public abstract partial class AssistantBase : ComponentBase
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
HideFromUser = hideContentFromUser,
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
if (this.chatThread is not null)
{
this.chatThread.Blocks.Add(this.resultingContentBlock);
this.chatThread.SelectedProvider = this.providerSettings.Id;
}
this.isProcessing = true;
this.StateHasChanged();
@@ -205,9 +309,12 @@ public abstract partial class AssistantBase : ComponentBase
return icon;
}
private Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{
var contentToSend = sendToButton.UseResultingContentBlockData switch
if (!destination.AllowSendTo())
return Task.CompletedTask;
var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch
{
false => sendToButton.GetText(),
true => this.resultingContentBlock?.Content switch
@@ -221,7 +328,9 @@ public abstract partial class AssistantBase : ComponentBase
switch (destination)
{
case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
var convertedChatThread = this.ConvertToChatThread;
convertedChatThread = convertedChatThread with { SelectedProvider = this.providerSettings.Id };
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, convertedChatThread);
break;
default:
@@ -241,7 +350,7 @@ public abstract partial class AssistantBase : ComponentBase
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom();
this.ResetForm();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false;
@@ -251,4 +360,26 @@ public abstract partial class AssistantBase : ComponentBase
this.StateHasChanged();
this.form?.ResetValidation();
}
private string GetResetColor() => this.SettingsManager.IsDarkMode switch
{
true => $"background-color: #804000",
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).Warning.Value}",
};
private string GetSendToColor() => this.SettingsManager.IsDarkMode switch
{
true => $"background-color: #004080",
false => $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}",
};
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
this.formChangeTimer.Dispose();
}
#endregion
}
@@ -0,0 +1,14 @@
@attribute [Route(Routes.ASSISTANT_BIAS)]
@inherits AssistantBaseCore
<MudText Typo="Typo.body1">
<b>Links:</b>
</MudText>
<MudList T="string" Class="mb-6">
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://en.wikipedia.org/wiki/List_of_cognitive_biases">Wikipedia list of cognitive biases</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://commons.wikimedia.org/wiki/File:Cognitive_Bias_Codex_With_Definitions_1-2,_an_Extension_of_the_work_of_John_Manoogian_by_Brian_Rene_Morrissette.png">Extended bias poster</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://betterhumans.pub/cognitive-bias-cheat-sheet-55a472476b18">Blog post of Buster Benson: "Cognitive bias cheat sheet"</MudListItem>
</MudList>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -0,0 +1,164 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BiasDay;
public partial class BiasOfTheDayAssistant : AssistantBaseCore
{
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;
protected override string Title => "Bias of the Day";
protected override string Description =>
"""
Learn about a different cognitive bias every day. You can also ask the LLM your questions. The idea behind
"Bias of the Day" is based on work by Buster Benson, John Manoogian III, and Brian Rene Morrissette. Buster
Benson grouped the biases, and the original texts come from Wikipedia. Brian Rene Morrissette condensed them
into a shorter version. Finally, John Manoogian III created the original poster based on Benson's work and
Morrissette's texts. Thorsten Sommer compared all texts for integration into AI Studio with the current Wikipedia
versions, updated them, and added source references. The idea of learning about one bias each day based on John's
poster comes from Drew Nelson.
""";
protected override string SystemPrompt => $"""
You are a friendly, helpful expert on cognitive bias. You studied psychology and
have a lot of experience. You explain a bias every day. Today's bias belongs to
the category: "{this.biasOfTheDay.Category.ToName()}". We have the following
thoughts on this category:
{this.biasOfTheDay.Category.GetThoughts()}
Today's bias is:
{this.biasOfTheDay.Description}
{this.SystemPromptSources()}
Important: you use the following language: {this.SystemPromptLanguage()}. Please
ask the user a personal question at the end to encourage them to think about
this bias.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Show me the bias of the day";
protected override Func<Task> SubmitAction => this.TellBias;
protected override bool ShowSendTo => false;
protected override bool ShowCopyResult => false;
protected override bool ShowReset => false;
protected override void ResetForm()
{
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage;
return true;
}
return false;
}
private Bias biasOfTheDay = BiasCatalog.NONE;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
private string? ValidateTargetLanguage(CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Please select a target language for the bias.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptSources()
{
var sb = new StringBuilder();
if (this.biasOfTheDay.Links.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Please share the following sources with the user as a Markdown list:");
foreach (var link in this.biasOfTheDay.Links)
sb.AppendLine($"- {link}");
sb.AppendLine();
}
return sb.ToString();
}
private string SystemPromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private async Task TellBias()
{
bool useDrawnBias = false;
if(this.SettingsManager.ConfigurationData.BiasOfTheDay.RestrictOneBiasPerDay)
{
if(this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn == DateOnly.FromDateTime(DateTime.Now))
{
var biasChat = new LoadChat
{
WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID,
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
};
if (WorkspaceBehaviour.IsChatExisting(biasChat))
{
MessageBus.INSTANCE.DeferMessage(this, Event.LOAD_CHAT, biasChat);
this.NavigationManager.NavigateTo(Routes.CHAT);
return;
}
else
useDrawnBias = true;
}
}
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.biasOfTheDay = useDrawnBias ?
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias);
var chatId = this.CreateChatThread(KnownWorkspaces.BIAS_WORKSPACE_ID, this.biasOfTheDay.Name);
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id;
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId;
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);
await this.SettingsManager.StoreSettings();
var time = this.AddUserRequest(
"""
Please tell me about the bias of the day.
""", true);
// Start the AI response without waiting for it to finish:
_ = this.AddAIResponseAsync(time);
await this.SendToAssistant(Tools.Components.CHAT, default);
}
}
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
protected override string Title => "Coding Assistant";
@@ -32,7 +32,7 @@ public partial class AssistantCoding : AssistantBaseCore
protected override Func<Task> SubmitAction => this.GetSupport;
protected override void ResetFrom()
protected override void ResetForm()
{
this.codingContexts.Clear();
this.compilerMessages = string.Empty;
@@ -1,5 +1,5 @@
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudStack Row="@true" Class="mb-3">
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
{
@@ -6,7 +6,7 @@ namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
protected override string Title => "E-Mail";
@@ -33,7 +33,7 @@ public partial class AssistantEMail : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputBulletPoints = string.Empty;
this.bulletPointsLines.Clear();
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum AllowedLLMProviders
{
NONE,
ANY,
SELF_HOSTED,
}
@@ -0,0 +1,16 @@
namespace AIStudio.Assistants.ERI;
public static class AllowedLLMProvidersExtensions
{
public static string Description(this AllowedLLMProviders provider)
{
return provider switch
{
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
_ => "Unknown option was selected"
};
}
}
@@ -0,0 +1,349 @@
@attribute [Route(Routes.ASSISTANT_ERI)]
@using AIStudio.Settings.DataModel
@using MudExtensions
@inherits AssistantBaseCore
<MudText Typo="Typo.body1" Class="mb-3">
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the
vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to
significantly reduce the hallucination of the LLM in knowledge questions.
</MudText>
<MudText Typo="Typo.body1">
<b>Related links:</b>
</MudText>
<MudList T="string" Class="mb-6">
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://github.com/MindWorkAI/ERI">ERI repository with example implementation in .NET and C#</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://mindworkai.org/swagger-ui.html">Interactive documentation aka Swagger UI</MudListItem>
</MudList>
<PreviewPrototype/>
<div class="mb-6"></div>
<MudText Typo="Typo.h4" Class="mb-3">
ERI server presets
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if
you are responsible for multiple ERI servers.
</MudText>
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
{
<MudText Typo="Typo.body1" Class="mb-3">
You have not yet added any ERI server presets.
</MudText>
}
else
{
<MudList Disabled="@this.AreServerPresetsBlocked" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged">
@foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers)
{
<MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server">
@server.ServerName
</MudListItem>
}
</MudList>
}
<MudStack Row="@true" Class="mt-1">
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
Add ERI server preset
</MudButton>
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
Delete this server preset
</MudButton>
</MudStack>
@if(this.AreServerPresetsBlocked)
{
<MudText Typo="Typo.body1" Class="mb-3 mt-3">
Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings.
</MudText>
}
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
Auto save
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to
regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be
automatically saved. Would you like this?
</MudText>
@if(this.AreServerPresetsBlocked)
{
<MudText Typo="Typo.body1" Class="mb-3">
Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings.
</MudText>
}
<MudTextSwitch Label="Should we automatically save any input made?" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="Yes, please save my inputs" LabelOff="No, I will enter everything again or configure it manually in the settings" />
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Common ERI server settings
</MudText>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="ERI server name" HelperText="Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio." Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="ERI server description" HelperText="Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio." Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Programming language" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
@foreach (var language in Enum.GetValues<ProgrammingLanguages>())
{
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="ERI specification version" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion">
@foreach (var version in Enum.GetValues<ERIVersion>())
{
<MudSelectItem Value="@version">@version</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@(!this.selectedERIVersion.WasSpecificationSelected() || this.IsNoneERIServerSelected)" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> Download specification
</MudButton>
</MudStack>
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Data source settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="Data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged">
@foreach (var dataSource in Enum.GetValues<DataSources>())
{
<MudSelectItem Value="@dataSource">@dataSource.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedDataSource is DataSources.CUSTOM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="Describe your data source" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
@if(this.selectedDataSource > DataSources.FILE_SYSTEM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceProductName" Label="Data source: product name" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
}
@if (this.NeedHostnamePort())
{
<div class="mb-3">
<MudStack Row="@true">
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceHostname" Label="Data source: hostname" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudNumericField Disabled="@this.IsNoneERIServerSelected" Label="Data source: port" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/>
</MudStack>
@if (this.dataSourcePort < 1024)
{
<MudText Typo="Typo.body2">
<b>Warning:</b> Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
</MudText>
}
</div>
}
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Authentication settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-1">
<MudSelectExtended
T="Auth"
Disabled="@this.IsNoneERIServerSelected"
ShrinkLabel="@true"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionAuthText"
SelectedValues="@this.selectedAuthenticationMethods"
Validation="@this.ValidateAuthenticationMethods"
SelectedValuesChanged="@this.AuthenticationMethodWasChanged"
Label="Authentication method(s)"
Variant="Variant.Outlined"
Margin="Margin.Dense">
@foreach (var authMethod in Enum.GetValues<Auth>())
{
<MudSelectItemExtended Value="@authMethod">@authMethod.Name()</MudSelectItemExtended>
}
</MudSelectExtended>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</MudStack>
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
{
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="Operating system on which your ERI will run" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1">
@foreach (var os in Enum.GetValues<OperatingSystem>())
{
<MudSelectItem Value="@os">@os.Name()</MudSelectItem>
}
</MudSelect>
}
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Data protection settings
</MudText>
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="Allowed LLM providers for this data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1">
@foreach (var option in Enum.GetValues<AllowedLLMProviders>())
{
<MudSelectItem Value="@option">@option.Description()</MudSelectItem>
}
</MudSelect>
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Embedding settings
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector
space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't
have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You
store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't
need embeddings.
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries
or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
</MudText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.embeddings" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Type</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.EmbeddingName</MudTd>
<MudTd>@context.EmbeddingType</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.embeddings.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No embedding methods configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding">
Add Embedding Method
</MudButton>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Data retrieval settings
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process.
You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows
you to test with beta users which process works better. Or you might generally want to give users the choice so they can select
the process that best suits their circumstances.
</MudText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.retrievalProcesses" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditRetrievalProcess(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.retrievalProcesses.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No retrieval process configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess">
Add Retrieval Process
</MudButton>
<MudText Typo="Typo.body1" Class="mb-1">
You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text
mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here.
It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these
libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe
the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
</MudText>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="(Optional) Additional libraries" HelperText="Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them." Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Provider selection for generation
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task.
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
<b>Important:</b> The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number
of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.
<b>However, generating all the files takes a certain amount of time.</b> Local or self-hosted models may work without these limitations
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
</MudText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Write code to file system
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created
outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the
AI has made in which files.
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All
other files you have created remain. Then, the AI generates the new files. <b>But beware:</b> It may happen that the AI generates a
file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore,
you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization,
you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.
</MudText>
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum Auth
{
NONE,
KERBEROS,
USERNAME_PASSWORD,
TOKEN,
}
@@ -0,0 +1,26 @@
namespace AIStudio.Assistants.ERI;
public static class AuthExtensions
{
public static string Name(this Auth auth) => auth switch
{
Auth.NONE => "No login necessary: useful for public data sources",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users",
Auth.USERNAME_PASSWORD => "Login by username and password: simple to implement and to operate, useful for few users; easy to use for users",
Auth.TOKEN => "Login by token: simple to implement and to operate, useful for few users; unusual for many users",
_ => "Unknown login method"
};
public static string ToPrompt(this Auth auth) => auth switch
{
Auth.NONE => "No login is necessary, the data source is public.",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.",
Auth.USERNAME_PASSWORD => "Login by username and password.",
Auth.TOKEN => "Login by static token per user.",
_ => string.Empty,
};
}
@@ -0,0 +1,15 @@
namespace AIStudio.Assistants.ERI;
public enum DataSources
{
NONE,
CUSTOM,
FILE_SYSTEM,
OBJECT_STORAGE,
KEY_VALUE_STORE,
DOCUMENT_STORE,
RELATIONAL_DATABASE,
GRAPH_DATABASE,
}
@@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
public static class DataSourcesExtensions
{
public static string Name(this DataSources dataSource) => dataSource switch
{
DataSources.NONE => "No data source selected",
DataSources.CUSTOM => "Custom description",
DataSources.FILE_SYSTEM => "File system (local or network share)",
DataSources.OBJECT_STORAGE => "Object storage, like Amazon S3, MinIO, etc.",
DataSources.KEY_VALUE_STORE => "Key-Value store, like Redis, etc.",
DataSources.DOCUMENT_STORE => "Document store, like MongoDB, etc.",
DataSources.RELATIONAL_DATABASE => "Relational database, like MySQL, PostgreSQL, etc.",
DataSources.GRAPH_DATABASE => "Graph database, like Neo4j, ArangoDB, etc.",
_ => "Unknown data source"
};
}
@@ -0,0 +1,8 @@
namespace AIStudio.Assistants.ERI;
public enum ERIVersion
{
NONE,
V1,
}
@@ -0,0 +1,27 @@
namespace AIStudio.Assistants.ERI;
public static class ERIVersionExtensions
{
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
{
try
{
var url = version.SpecificationURL();
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
catch
{
return string.Empty;
}
}
public static string SpecificationURL(this ERIVersion version)
{
var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
}
@@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Represents information about the used embedding for a data source.
/// </summary>
/// <param name="EmbeddingType">What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
/// Embedding," "Graph Embedding," etc.</param>
/// <param name="EmbeddingName">Name the embedding used. This can be a library, a framework, or the name of the used
/// algorithm.</param>
/// <param name="Description">A short description of the embedding. Describe what the embedding is doing.</param>
/// <param name="UsedWhen">Describe when the embedding is used. For example, when the user prompt contains certain
/// keywords, or anytime?</param>
/// <param name="Link">A link to the embedding's documentation or the source code. Might be null.</param>
public readonly record struct EmbeddingInfo(
string EmbeddingType,
string EmbeddingName,
string Description,
string UsedWhen,
string? Link);
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum OperatingSystem
{
NONE,
WINDOWS,
LINUX,
}
@@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public static class OperatingSystemExtensions
{
public static string Name(this OperatingSystem os) => os switch
{
OperatingSystem.NONE => "No operating system specified",
OperatingSystem.WINDOWS => "Windows",
OperatingSystem.LINUX => "Linux",
_ => "Unknown operating system"
};
}
@@ -0,0 +1,20 @@
namespace AIStudio.Assistants.ERI;
public enum ProgrammingLanguages
{
NONE,
C,
CPP,
CSHARP,
GO,
JAVA,
JAVASCRIPT,
JULIA,
MATLAB,
PHP,
PYTHON,
RUST,
OTHER,
}
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.ERI;
public static class ProgrammingLanguagesExtensions
{
public static string Name(this ProgrammingLanguages language) => language switch
{
ProgrammingLanguages.NONE => "No programming language selected",
ProgrammingLanguages.C => "C",
ProgrammingLanguages.CPP => "C++",
ProgrammingLanguages.CSHARP => "C#",
ProgrammingLanguages.GO => "Go",
ProgrammingLanguages.JAVA => "Java",
ProgrammingLanguages.JAVASCRIPT => "JavaScript",
ProgrammingLanguages.JULIA => "Julia",
ProgrammingLanguages.MATLAB => "MATLAB",
ProgrammingLanguages.PHP => "PHP",
ProgrammingLanguages.PYTHON => "Python",
ProgrammingLanguages.RUST => "Rust",
ProgrammingLanguages.OTHER => "Other",
_ => "Unknown"
};
}
@@ -0,0 +1,18 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Information about a retrieval process, which this data source implements.
/// </summary>
/// <param name="Name">The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".</param>
/// <param name="Description">A short description of the retrieval process. What kind of retrieval process is it?</param>
/// <param name="Link">A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.</param>
/// <param name="ParametersDescription">A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.</param>
/// <param name="Embeddings">A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.</param>
public readonly record struct RetrievalInfo(
string Name,
string Description,
string? Link,
Dictionary<string, string>? ParametersDescription,
List<EmbeddingInfo>? Embeddings);
@@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public sealed class RetrievalParameter
{
/// <summary>
/// The name of the parameter.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the parameter.
/// </summary>
public string Description { get; set; } = string.Empty;
}
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
protected override string Title => "Grammar & Spelling Checker";
@@ -24,8 +24,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
@@ -50,7 +48,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.correctedText = string.Empty;
@@ -2,7 +2,7 @@ namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
protected override string Title => "Icon Finder";
@@ -33,7 +33,7 @@ public partial class AssistantIconFinder : AssistantBaseCore
protected override Func<Task> SubmitAction => this.FindIcon;
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputContext = string.Empty;
if (!this.MightPreselectValues())
@@ -0,0 +1,15 @@
@attribute [Route(Routes.ASSISTANT_JOB_POSTING)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/>
<MudTextField T="string" @bind-Text="@this.inputMandatoryInformation" Label="(Optional) Provide mandatory information" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.TextSnippet" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Mandatory information that your company requires for all job postings. This can include the company description, etc." />
<MudTextField T="string" @bind-Text="@this.inputJobDescription" Label="Job description" Validation="@this.ValidateJobDescription" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe what the person is supposed to do in the company. This might be just short bullet points." />
<MudTextField T="string" @bind-Text="@this.inputQualifications" Label="(Optional) Provide necessary job qualifications" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe what the person should bring to the table. This might be just short bullet points." />
<MudTextField T="string" @bind-Text="@this.inputResponsibilities" Label="(Optional) Provide job responsibilities" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the responsibilities the person should take on in the company." />
<MudTextField T="string" @bind-Text="@this.inputWorkLocation" Label="(Optional) Provide the work location" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.MyLocation" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputEntryDate" Label="(Optional) Provide the entry date" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputValidUntil" Label="(Optional) Provide the date until the job posting is valid" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DateRange" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -0,0 +1,290 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.JobPosting;
public partial class AssistantJobPostings : AssistantBaseCore
{
public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT;
protected override string Title => "Job Posting";
protected override string Description =>
"""
Provide some key points about the job you want to post. The AI will then
formulate a suggestion that you can finalize.
""";
protected override string SystemPrompt =>
$"""
You are an experienced specialist in personnel matters. You write job postings.
You follow the usual guidelines in the country of the posting. You ensure that
no individuals are discriminated against or excluded by the job posting. You do
not ask any questions and you do not repeat the task. You provide your response
in a Markdown format. Missing information necessary for the job posting, you try
to derive from the other details provided.
Structure of your response:
---
# <TITLE>
<Description of the job and the company>
# <TASKS>
<Describe what takes the job entails>
# <QUALIFICATIONS>
<What qualifications are required>
# <RESPONSIBILITIES>
<What responsibilities are associated with the job>
<When available, closing details such as the entry date etc.>
---
You write the job posting in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Create the job posting";
protected override Func<Task> SubmitAction => this.CreateJobPosting;
protected override bool SubmitDisabled => false;
protected override bool AllowProfiles => false;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetForm()
{
this.inputEntryDate = string.Empty;
this.inputValidUntil = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.inputMandatoryInformation = string.Empty;
this.inputJobDescription = string.Empty;
this.inputQualifications = string.Empty;
this.inputResponsibilities = string.Empty;
this.inputCompanyName = string.Empty;
this.inputWorkLocation = string.Empty;
this.inputCountryLegalFramework = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)
{
this.inputMandatoryInformation = this.SettingsManager.ConfigurationData.JobPostings.PreselectedMandatoryInformation;
if(string.IsNullOrWhiteSpace(this.inputJobDescription))
this.inputJobDescription = this.SettingsManager.ConfigurationData.JobPostings.PreselectedJobDescription;
this.inputQualifications = this.SettingsManager.ConfigurationData.JobPostings.PreselectedQualifications;
this.inputResponsibilities = this.SettingsManager.ConfigurationData.JobPostings.PreselectedResponsibilities;
this.inputCompanyName = this.SettingsManager.ConfigurationData.JobPostings.PreselectedCompanyName;
this.inputWorkLocation = this.SettingsManager.ConfigurationData.JobPostings.PreselectedWorkLocation;
this.inputCountryLegalFramework = this.SettingsManager.ConfigurationData.JobPostings.PreselectedCountryLegalFramework;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.JobPostings.PreselectOtherLanguage;
return true;
}
return false;
}
private string inputMandatoryInformation = string.Empty;
private string inputJobDescription = string.Empty;
private string inputQualifications = string.Empty;
private string inputResponsibilities = string.Empty;
private string inputCompanyName = string.Empty;
private string inputEntryDate = string.Empty;
private string inputValidUntil = string.Empty;
private string inputWorkLocation = string.Empty;
private string inputCountryLegalFramework = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_JOB_POSTING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputJobDescription = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom target language.";
return null;
}
private string? ValidateJobDescription(string jobDescription)
{
if(string.IsNullOrWhiteSpace(jobDescription))
return "Please provide a job description.";
return null;
}
private string? ValidateCountryLegalFramework(string countryLegalFramework)
{
if(string.IsNullOrWhiteSpace(countryLegalFramework))
return "Please provide the country where the job is posted (legal framework).";
return null;
}
private string SystemPromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.AS_IS)
return "Use the same language as the input";
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private string UserPromptMandatoryInformation()
{
if(string.IsNullOrWhiteSpace(this.inputMandatoryInformation))
return string.Empty;
return $"""
# Mandatory Information
{this.inputMandatoryInformation}
""";
}
private string UserPromptJobDescription()
{
if(string.IsNullOrWhiteSpace(this.inputJobDescription))
return string.Empty;
return $"""
# Job Description
{this.inputJobDescription}
""";
}
private string UserPromptQualifications()
{
if(string.IsNullOrWhiteSpace(this.inputQualifications))
return string.Empty;
return $"""
# Qualifications
{this.inputQualifications}
""";
}
private string UserPromptResponsibilities()
{
if(string.IsNullOrWhiteSpace(this.inputResponsibilities))
return string.Empty;
return $"""
# Responsibilities
{this.inputResponsibilities}
""";
}
private string UserPromptCompanyName()
{
if(string.IsNullOrWhiteSpace(this.inputCompanyName))
return string.Empty;
return $"""
# Company Name
{this.inputCompanyName}
""";
}
private string UserPromptEntryDate()
{
if(string.IsNullOrWhiteSpace(this.inputEntryDate))
return string.Empty;
return $"""
# Entry Date
{this.inputEntryDate}
""";
}
private string UserPromptValidUntil()
{
if(string.IsNullOrWhiteSpace(this.inputValidUntil))
return string.Empty;
return $"""
# Job Posting Valid Until
{this.inputValidUntil}
""";
}
private string UserPromptWorkLocation()
{
if(string.IsNullOrWhiteSpace(this.inputWorkLocation))
return string.Empty;
return $"""
# Work Location
{this.inputWorkLocation}
""";
}
private string UserPromptCountryLegalFramework()
{
if(string.IsNullOrWhiteSpace(this.inputCountryLegalFramework))
return string.Empty;
return $"""
# Country where the job is posted (legal framework)
{this.inputCountryLegalFramework}
""";
}
private async Task CreateJobPosting()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.UserPromptCompanyName()}
{this.UserPromptCountryLegalFramework()}
{this.UserPromptMandatoryInformation()}
{this.UserPromptJobDescription()}
{this.UserPromptQualifications()}
{this.UserPromptResponsibilities()}
{this.UserPromptWorkLocation()}
{this.UserPromptEntryDate()}
{this.UserPromptValidUntil()}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -1,8 +1,10 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
protected override string Title => "Legal Check";
@@ -30,7 +32,12 @@ public partial class AssistantLegalCheck : AssistantBaseCore
protected override bool SubmitDisabled => this.isAgentRunning;
protected override void ResetFrom()
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetForm()
{
this.inputLegalDocument = string.Empty;
this.inputQuestions = string.Empty;
@@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Assistants.MyTasks;
public partial class AssistantMyTasks : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;
protected override string Title => "My Tasks";
@@ -34,8 +35,13 @@ public partial class AssistantMyTasks : AssistantBaseCore
protected override Func<Task> SubmitAction => this.AnalyzeText;
protected override bool ShowProfileSelection => false;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
if (!this.MightPreselectValues())
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
protected override string Title => "Rewrite & Improve Text";
@@ -26,8 +26,6 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
protected override Func<string> Result2Copy => () => this.rewrittenText;
@@ -51,7 +49,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.rewrittenText = string.Empty;
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
protected override string Title => "Synonyms";
@@ -60,7 +60,7 @@ public partial class AssistantSynonyms : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.inputContext = string.Empty;
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
protected override string Title => "Text Summarizer";
@@ -40,7 +40,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
if(!this.MightPreselectValues())
@@ -4,7 +4,7 @@ namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
protected override string Title => "Translation";
@@ -36,7 +36,7 @@ public partial class AssistantTranslation : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.inputTextLastTranslation = string.Empty;
+104
View File
@@ -1,3 +1,5 @@
using AIStudio.Settings;
namespace AIStudio.Chat;
/// <summary>
@@ -15,6 +17,16 @@ public sealed record ChatThread
/// </summary>
public Guid WorkspaceId { get; set; }
/// <summary>
/// Specifies the provider selected for the chat thread.
/// </summary>
public string SelectedProvider { get; set; } = string.Empty;
/// <summary>
/// Specifies the profile selected for the chat thread.
/// </summary>
public string SelectedProfile { get; set; } = string.Empty;
/// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary>
@@ -34,4 +46,96 @@ public sealed record ChatThread
/// The content blocks of the chat thread.
/// </summary>
public List<ContentBlock> Blocks { get; init; } = [];
/// <summary>
/// Prepares the system prompt for the chat thread.
/// </summary>
/// <remarks>
/// The actual system prompt depends on the selected profile. If no profile is selected,
/// the system prompt is returned as is. When a profile is selected, the system prompt
/// is extended with the profile chosen.
/// </remarks>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <param name="chatThread">The chat thread to prepare the system prompt for.</param>
/// <param name="logger">The logger instance to use.</param>
/// <returns>The prepared system prompt.</returns>
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
{
//
// Prepare the system prompt:
//
string systemPromptText;
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
systemPromptText = chatThread.SystemPrompt;
else
{
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
systemPromptText = chatThread.SystemPrompt;
else
{
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = chatThread.SystemPrompt;
else
{
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
if(profile == default)
systemPromptText = chatThread.SystemPrompt;
else
{
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
systemPromptText = $"""
{chatThread.SystemPrompt}
{profile.ToSystemPrompt()}
""";
}
}
}
}
logger.LogInformation(logMessage);
return systemPromptText;
}
/// <summary>
/// Removes a content block from this chat thread.
/// </summary>
/// <param name="content">The content block to remove.</param>
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
/// which will not remove the previous user block if it is hidden from the user.</param>
public void Remove(IContent content, bool removeForRegenerate = false)
{
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
if(block is null)
return;
//
// Remove the previous user block if it is hidden from the user. Otherwise,
// the experience might be confusing for the user.
//
// Explanation, using the ERI assistant as an example:
// - The ERI assistant generates for every file a hidden user prompt.
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
// - Now, the user removes one AI response
// - The hidden user prompt is still there, but the user can't see it.
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
//
if (block.Role is ChatRole.AI && !removeForRegenerate)
{
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
var index = sortedBlocks.IndexOf(block);
if (index > 0)
{
var previousBlock = sortedBlocks[index - 1];
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
this.Blocks.Remove(previousBlock);
}
}
// Remove the block from the chat thread:
this.Blocks.Remove(block);
}
}
+6 -1
View File
@@ -18,10 +18,15 @@ public class ContentBlock
/// <summary>
/// The content of the block.
/// </summary>
public IContent? Content { get; init; } = null;
public IContent? Content { get; init; }
/// <summary>
/// The role of the content block in the chat thread, e.g., user, AI, etc.
/// </summary>
public ChatRole Role { get; init; } = ChatRole.NONE;
/// <summary>
/// Should the content block be hidden from the user?
/// </summary>
public bool HideFromUser { get; set; }
}
@@ -12,7 +12,33 @@
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard" />
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
{
<MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
</MudTooltip>
}
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
{
<MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
</MudTooltip>
}
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" OnClick="@this.RegenerateBlock"/>
</MudTooltip>
}
@if (this.RemoveBlockFunc is not null)
{
<MudTooltip Text="Removes this block" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
</MudTooltip>
}
<MudTooltip Text="Copies the content to the clipboard" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/>
</MudTooltip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
@@ -44,7 +70,7 @@
}
else
{
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
}
}
}
@@ -1,3 +1,5 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
@@ -39,11 +41,38 @@ public partial class ContentBlockComponent : ComponentBase
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public bool IsLastContentBlock { get; set; } = false;
[Parameter]
public bool IsSecondToLastBlock { get; set; } = false;
[Parameter]
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
[Parameter]
public Func<IContent, Task>? RegenerateFunc { get; set; }
[Parameter]
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
[Parameter]
public Func<IContent, Task>? EditLastUserBlockFunc { get; set; }
[Parameter]
public Func<bool> RegenerateEnabled { get; set; } = () => false;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private bool HideContent { get; set; }
@@ -64,7 +93,7 @@ public partial class ContentBlockComponent : ComponentBase
private async Task AfterStreaming()
{
// Might be called from a different thread, so we need to invoke the UI thread:
await this.InvokeAsync(() =>
await this.InvokeAsync(async () =>
{
//
// Issue we try to solve: When the content changes during streaming,
@@ -83,6 +112,9 @@ public partial class ContentBlockComponent : ComponentBase
// Let Blazor update the UI, i.e., to see the render tree diff:
this.StateHasChanged();
// Inform the chat that the streaming is done:
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.CHAT_STREAMING_DONE);
});
}
@@ -112,4 +144,68 @@ public partial class ContentBlockComponent : ComponentBase
}
private string CardClasses => $"my-2 rounded-lg {this.Class}";
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
private async Task RemoveBlock()
{
if (this.RemoveBlockFunc is null)
return;
var remove = await this.DialogService.ShowMessageBox(
"Remove Message",
"Do you really want to remove this message?",
"Yes, remove it",
"No, keep it");
if (remove.HasValue && remove.Value)
await this.RemoveBlockFunc(this.Content);
}
private async Task RegenerateBlock()
{
if (this.RegenerateFunc is null)
return;
if(this.Role is not ChatRole.AI)
return;
var regenerate = await this.DialogService.ShowMessageBox(
"Regenerate Message",
"Do you really want to regenerate this message?",
"Yes, regenerate it",
"No, keep it");
if (regenerate.HasValue && regenerate.Value)
await this.RegenerateFunc(this.Content);
}
private async Task EditLastBlock()
{
if (this.EditLastBlockFunc is null)
return;
if(this.Role is not ChatRole.USER)
return;
await this.EditLastBlockFunc(this.Content);
}
private async Task EditLastUserBlock()
{
if (this.EditLastUserBlockFunc is null)
return;
if(this.Role is not ChatRole.USER)
return;
var edit = await this.DialogService.ShowMessageBox(
"Edit Message",
"Do you really want to edit this message? In order to edit this message, the AI response will be deleted.",
"Yes, remove the AI response and edit it",
"No, keep it");
if (edit.HasValue && edit.Value)
await this.EditLastUserBlockFunc(this.Content);
}
}
+2 -2
View File
@@ -40,7 +40,7 @@ public sealed class ContentText : IContent
if(chatThread is null)
return;
// Store the last time we got a response. We use this ater
// Store the last time we got a response. We use this later
// to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode
// the user chose.
@@ -54,7 +54,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
{
// When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested)
@@ -0,0 +1,7 @@
namespace AIStudio.Chat;
public static class KnownWorkspaces
{
public static readonly Guid BIAS_WORKSPACE_ID = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
public static readonly Guid ERI_SERVER_WORKSPACE_ID = Guid.Parse("8ec09cd3-9da7-4736-b245-2d8b67fc342f");
}
@@ -1,4 +1,4 @@
<MudCard Outlined="@true" Style="border-width: 2px; border-color: #0d47a1; border-radius: 12px; border-style: solid; max-width: 20em;">
<MudCard Outlined="@true" Style="@this.BlockStyle">
<MudCardHeader>
<CardHeaderContent>
<MudStack AlignItems="AlignItems.Center" Row="@true">
@@ -1,8 +1,10 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class AssistantBlock : ComponentBase
public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDisposable
{
[Parameter]
public string Name { get; set; } = string.Empty;
@@ -18,4 +20,65 @@ public partial class AssistantBlock : ComponentBase
[Parameter]
public string Link { get; set; } = string.Empty;
[Inject]
private MudTheme ColorTheme { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBlock);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.StateHasChanged();
break;
}
return Task.CompletedTask;
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
}
#endregion
private string BorderColor => this.SettingsManager.IsDarkMode switch
{
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight,
false => this.ColorTheme.GetCurrentPalette(this.SettingsManager).Primary.Value,
};
private string BlockStyle => $"border-width: 2px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
}
#endregion
}
@@ -13,6 +13,24 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"),
new (198, "v0.9.23, build 198 (2025-01-02 19:39 UTC)", "v0.9.23.md"),
new (197, "v0.9.22, build 197 (2024-12-04 10:58 UTC)", "v0.9.22.md"),
new (196, "v0.9.21, build 196 (2024-11-23 12:22 UTC)", "v0.9.21.md"),
new (195, "v0.9.20, build 195 (2024-11-16 20:44 UTC)", "v0.9.20.md"),
new (194, "v0.9.19, build 194 (2024-11-14 05:58 UTC)", "v0.9.19.md"),
new (193, "v0.9.18, build 193 (2024-11-09 21:10 UTC)", "v0.9.18.md"),
new (192, "v0.9.17, build 192 (2024-11-03 11:11 UTC)", "v0.9.17.md"),
new (191, "v0.9.16, build 191 (2024-11-02 22:04 UTC)", "v0.9.16.md"),
new (190, "v0.9.15, build 190 (2024-10-28 15:04 UTC)", "v0.9.15.md"),
new (189, "v0.9.14, build 189 (2024-10-18 08:48 UTC)", "v0.9.14.md"),
new (188, "v0.9.13, build 188 (2024-10-07 11:18 UTC)", "v0.9.13.md"),
new (187, "v0.9.12, build 187 (2024-09-15 20:49 UTC)", "v0.9.12.md"),
new (186, "v0.9.11, build 186 (2024-09-15 10:33 UTC)", "v0.9.11.md"),
new (185, "v0.9.10, build 185 (2024-09-12 20:52 UTC)", "v0.9.10.md"),
new (184, "v0.9.9, build 184 (2024-09-11 21:10 UTC)", "v0.9.9.md"),
new (183, "v0.9.8, build 183 (2024-09-09 13:10 UTC)", "v0.9.8.md"),
new (182, "v0.9.7, build 182 (2024-09-08 20:55 UTC)", "v0.9.7.md"),
@@ -0,0 +1,113 @@
@using AIStudio.Settings.DataModel
@using AIStudio.Chat
@inherits MSGComponentBase
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" MinWidth="36em" Style="height: 100%">
<ChildContent>
@if (this.ChatThread is not null)
{
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
for (var i = 0; i < blocks.Count; i++)
{
var block = blocks[i];
var isLastBlock = i == blocks.Count - 1;
var isSecondLastBlock = i == blocks.Count - 2;
@if (!block.HideFromUser)
{
<ContentBlockComponent
Role="@block.Role"
Type="@block.ContentType"
Time="@block.Time"
Content="@block.Content"
RemoveBlockFunc="@this.RemoveBlock"
IsLastContentBlock="@isLastBlock"
IsSecondToLastBlock="@isSecondLastBlock"
RegenerateFunc="@this.RegenerateBlock"
RegenerateEnabled="@(() => this.IsProviderSelected)"
EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
}
}
}
</ChildContent>
<FooterContent>
<MudElement Style="flex: 0 0 auto;">
<MudTextField
T="string"
@ref="@this.inputField"
@bind-Text="@this.userInput"
Variant="Variant.Outlined"
AutoGrow="@true"
Lines="3"
MaxLines="12"
Label="@this.InputLabel"
Placeholder="@this.ProviderPlaceholder"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Send"
OnAdornmentClick="() => this.SendMessage()"
ReadOnly="!this.IsProviderSelected || this.isStreaming"
Immediate="@true"
OnKeyUp="this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Class="@this.UserInputClass"
Style="@this.UserInputStyle"/>
</MudElement>
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
@if (
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
{
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</MudTooltip>
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
{
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
}
@if (this.isStreaming && this.cancellationTokenSource is not null)
{
<MudTooltip Text="Stop generation" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
</MudTooltip>
}
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
</MudToolBar>
</FooterContent>
</InnerScrolling>
@@ -0,0 +1,732 @@
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
[Parameter]
public ChatThread? ChatThread { get; set; }
[Parameter]
public EventCallback<ChatThread?> ChatThreadChanged { get; set; }
[Parameter]
public AIStudio.Settings.Provider Provider { get; set; }
[Parameter]
public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; }
[Parameter]
public Action<string> WorkspaceName { get; set; } = _ => { };
[Parameter]
public Workspaces? Workspaces { get; set; }
[Inject]
private ILogger<ChatComponent> Logger { get; set; } = null!;
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private Profile currentProfile = Profile.NO_PROFILE;
private bool hasUnsavedChanges;
private bool mustScrollToBottomAfterRender;
private InnerScrolling scrollingArea = null!;
private byte scrollRenderCountdown;
private bool isStreaming;
private string userInput = string.Empty;
private bool mustStoreChat;
private bool mustLoadChat;
private LoadChat loadChat;
private bool autoSaveEnabled;
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private CancellationTokenSource? cancellationTokenSource;
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
private MudTextField<string> inputField = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
if (deferredContent is not null)
{
this.ChatThread = deferredContent;
this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
if (this.ChatThread is not null)
{
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
{
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
if (firstUserBlock is not null)
{
this.ChatThread.Name = firstUserBlock.Content switch
{
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
_ => "Thread"
};
}
}
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
this.autoSaveEnabled = true;
this.mustStoreChat = true;
// Ensure the workspace exists:
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureERIServerWorkspace();
else if (this.ChatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureBiasWorkspace();
}
}
}
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 4;
this.StateHasChanged();
}
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
if (deferredLoading != default)
{
this.loadChat = deferredLoading;
this.mustLoadChat = true;
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
}
if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
await this.SelectProviderWhenLoadingChat();
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && this.ChatThread is not null && this.mustStoreChat)
{
this.mustStoreChat = false;
if(this.Workspaces is not null)
await this.Workspaces.StoreChat(this.ChatThread);
else
await WorkspaceBehaviour.StoreChat(this.ChatThread);
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
if (firstRender && this.mustLoadChat)
{
this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now.");
this.mustLoadChat = false;
this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
if(this.ChatThread is not null)
{
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
await this.SelectProviderWhenLoadingChat();
}
else
this.Logger.LogWarning($"The chat '{this.loadChat.ChatId}' could not be loaded.");
this.StateHasChanged();
}
if(this.mustScrollToBottomAfterRender)
{
if (--this.scrollRenderCountdown == 0)
{
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
}
else
{
this.StateHasChanged();
}
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.Provider.InstanceName}', provider '{this.Provider.UsedLLMProvider.ToName()}')" : "Select a provider first";
private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0;
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
private string ExtractThreadName(string firstUserInput)
{
// We select the first 10 words of the user input:
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var threadName = string.Join(' ', words.Take(10));
// If the thread name is empty, we use a default name:
if (string.IsNullOrWhiteSpace(threadName))
threadName = "Thread";
return threadName;
}
private async Task ProfileWasChanged(Profile profile)
{
this.currentProfile = profile;
if(this.ChatThread is null)
return;
this.ChatThread = this.ChatThread with
{
SelectedProfile = this.currentProfile.Id,
};
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
{
this.hasUnsavedChanges = true;
var key = keyEvent.Code.ToLowerInvariant();
// Was the enter key (either enter or numpad enter) pressed?
var isEnter = key is "enter" or "numpadenter";
// Was a modifier key pressed as well?
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
// Depending on the user's settings, might react to shortcuts:
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
{
case SendBehavior.ENTER_IS_SENDING:
if (!isModifier && isEnter)
await this.SendMessage();
break;
case SendBehavior.MODIFER_ENTER_IS_SENDING:
if (isEnter && isModifier)
await this.SendMessage();
break;
}
}
private async Task SendMessage(bool reuseLastUserPrompt = false)
{
if (!this.IsProviderSelected)
return;
// We need to blur the focus away from the input field
// to be able to clear the field:
await this.inputField.BlurAsync();
// Create a new chat thread if necessary:
if (this.ChatThread is null)
{
this.ChatThread = new()
{
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(),
Name = this.ExtractThreadName(this.userInput),
Seed = this.RNG.Next(),
Blocks = [],
};
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
else
{
// Set the thread name if it is empty:
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
// Update provider and profile:
this.ChatThread.SelectedProvider = this.Provider.Id;
this.ChatThread.SelectedProfile = this.currentProfile.Id;
}
var time = DateTimeOffset.Now;
if (!reuseLastUserPrompt)
{
//
// Add the user message to the thread:
//
this.ChatThread?.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = this.userInput,
},
});
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
this.StateHasChanged();
}
}
//
// Add the AI response to the thread:
//
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
this.ChatThread?.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
});
// Clear the input field:
this.userInput = string.Empty;
// Enable the stream state for the chat component:
this.isStreaming = true;
this.hasUnsavedChanges = true;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
}
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
using (this.cancellationTokenSource = new())
{
this.StateHasChanged();
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.SettingsManager, this.Provider.Model, this.ChatThread, this.cancellationTokenSource.Token);
}
this.cancellationTokenSource = null;
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
}
// Disable the stream state:
this.isStreaming = false;
this.StateHasChanged();
}
private async Task CancelStreaming()
{
if (this.cancellationTokenSource is not null)
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
}
private async Task SaveThread()
{
if(this.ChatThread is null)
return;
if (!this.CanThreadBeSaved)
return;
//
// When the workspace component is visible, we store the chat
// through the workspace component. The advantage of this is that
// the workspace gets updated automatically when the chat is saved.
//
if (this.Workspaces is not null)
await this.Workspaces.StoreChat(this.ChatThread);
else
await WorkspaceBehaviour.StoreChat(this.ChatThread);
this.hasUnsavedChanges = false;
}
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
{
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
{
var dialogParameters = new DialogParameters
{
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
}
if (this.ChatThread is not null && deletePreviousChat)
{
string chatPath;
if (this.ChatThread.WorkspaceId == Guid.Empty)
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString());
else
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
if(this.Workspaces is null)
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
else
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
}
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
{
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
await this.ProviderChanged.InvokeAsync(this.Provider);
break;
default:
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
if(this.Provider == default)
{
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
await this.ProviderChanged.InvokeAsync(this.Provider);
}
break;
}
if (!useSameWorkspace)
{
this.ChatThread = null;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
}
else
{
this.ChatThread = new()
{
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
Blocks = [],
};
}
this.userInput = string.Empty;
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
private async Task MoveChatToWorkspace()
{
if(this.ChatThread is null)
return;
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
{
var confirmationDialogParameters = new DialogParameters
{
{ "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
};
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
var confirmationDialogResult = await confirmationDialogReference.Result;
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
return;
}
var dialogParameters = new DialogParameters
{
{ "Message", "Please select the workspace where you want to move the chat to." },
{ "SelectedWorkspace", this.ChatThread?.WorkspaceId },
{ "ConfirmText", "Move chat" },
};
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = dialogResult.Data is Guid id ? id : Guid.Empty;
if (workspaceId == Guid.Empty)
return;
// Delete the chat from the current workspace or the temporary storage:
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
this.ChatThread!.WorkspaceId = workspaceId;
await this.SaveThread();
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
private async Task LoadedChatChanged()
{
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
else
{
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
}
await this.SelectProviderWhenLoadingChat();
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
}
this.StateHasChanged();
}
private async Task ResetState()
{
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ChatThread = null;
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
private async Task SelectProviderWhenLoadingChat()
{
var chatProvider = this.ChatThread?.SelectedProvider;
var chatProfile = this.ChatThread?.SelectedProfile;
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
{
default:
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
if(this.Provider == default)
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
}
await this.ProviderChanged.InvokeAsync(this.Provider);
// Try to select the profile:
if (!string.IsNullOrWhiteSpace(chatProfile))
{
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile);
if(this.currentProfile == default)
this.currentProfile = Profile.NO_PROFILE;
}
}
private async Task ToggleWorkspaceOverlay()
{
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
}
private async Task RemoveBlock(IContent block)
{
if(this.ChatThread is null)
return;
this.ChatThread.Remove(block);
this.hasUnsavedChanges = true;
await this.SaveThread();
this.StateHasChanged();
}
private async Task RegenerateBlock(IContent aiBlock)
{
if(this.ChatThread is null)
return;
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
this.hasUnsavedChanges = true;
this.StateHasChanged();
await this.SendMessage(reuseLastUserPrompt: true);
}
private Task EditLastUserBlock(IContent block)
{
if(this.ChatThread is null)
return Task.CompletedTask;
if (block is not ContentText textBlock)
return Task.CompletedTask;
var lastBlock = this.ChatThread.Blocks.Last();
var lastBlockContent = lastBlock.Content;
if(lastBlockContent is null)
return Task.CompletedTask;
this.userInput = textBlock.Text;
this.ChatThread.Remove(block);
this.ChatThread.Remove(lastBlockContent);
this.hasUnsavedChanges = true;
this.StateHasChanged();
return Task.CompletedTask;
}
private Task EditLastBlock(IContent block)
{
if(this.ChatThread is null)
return Task.CompletedTask;
if (block is not ContentText textBlock)
return Task.CompletedTask;
this.userInput = textBlock.Text;
this.ChatThread.Remove(block);
this.hasUnsavedChanges = true;
this.StateHasChanged();
return Task.CompletedTask;
}
#region Overrides of MSGComponentBase
public override string ComponentName => nameof(ChatComponent);
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
case Event.RESET_CHAT_STATE:
await this.ResetState();
break;
case Event.CHAT_STREAMING_DONE:
if(this.autoSaveEnabled)
await this.SaveThread();
break;
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
await this.LoadedChatChanged();
break;
}
}
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
{
switch (triggeredEvent)
{
case Event.HAS_CHAT_UNSAVED_CHANGES:
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
return Task.FromResult((TResult?) (object) false);
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
}
return Task.FromResult(default(TResult));
}
#endregion
#region Implementation of IAsyncDisposable
public async ValueTask DisposeAsync()
{
this.MessageBus.Unregister(this);
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
}
if (this.cancellationTokenSource is not null)
{
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
this.cancellationTokenSource.Dispose();
}
}
#endregion
}
@@ -1,56 +1,58 @@
@using AIStudio.Provider
<div class="d-flex">
@if (this.Mode is ConfidenceInfoMode.ICON)
{
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.Provider.GetConfidence(this.SettingsManager).SetColorStyle()" OnClick="@(() => this.ToggleConfidence())"/>
}
else
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.Provider.GetConfidence(this.SettingsManager).SetColorStyle()" OnClick="@(() => this.ToggleConfidence())">
Confidence
</MudButton>
}
<MudPopover Open="@this.showConfidence" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" Style="@this.GetPopoverStyle()" DropShadow="@true" Class="border-solid border-4 rounded-lg">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Confidence Card</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.h6">Description</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/>
@if (this.currentConfidence.Sources.Count > 0)
{
<MudText Typo="Typo.h6">Sources</MudText>
<MudList T="@string">
@foreach (var sourceTuple in this.GetConfidenceSources())
{
<MudListItem Icon="@Icons.Material.Filled.Link" Href="@sourceTuple.Source" Target="_blank" Text="@sourceTuple.Index"/>
}
</MudList>
}
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
{
<MudText Typo="Typo.h6">Region</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
<b>@this.currentConfidence.Region</b>
</MudText>
}
<MudText Typo="Typo.h6">Confidence Level</MudText>
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
<b>@this.currentConfidence.Level.GetName()</b>
</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideConfidence())">
Close
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
@if (this.Mode is ConfidenceInfoMode.ICON)
{
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
}
else
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())">
Confidence
</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
}
</MudTooltip>
<MudPopover Open="@this.showConfidence" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" Style="@this.GetPopoverStyle()" DropShadow="@true" Class="border-solid border-4 rounded-lg">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Confidence Card</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.h6">Description</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/>
@if (this.currentConfidence.Sources.Count > 0)
{
<MudText Typo="Typo.h6">Sources</MudText>
<MudList T="@string">
@foreach (var sourceTuple in this.GetConfidenceSources())
{
<MudListItem Icon="@Icons.Material.Filled.Link" Href="@sourceTuple.Source" Target="_blank" Text="@sourceTuple.Index"/>
}
</MudList>
}
@if (!string.IsNullOrWhiteSpace(this.currentConfidence.Region))
{
<MudText Typo="Typo.h6">Region</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
<b>@this.currentConfidence.Region</b>
</MudText>
}
<MudText Typo="Typo.h6">Confidence Level</MudText>
<MudText Typo="Typo.body1" Style="@this.GetCurrentConfidenceColor()">
<b>@this.currentConfidence.Level.GetName()</b>
</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideConfidence())">
Close
</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
</div>
@@ -5,30 +5,36 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ConfidenceInfo : ComponentBase
public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
{
[Parameter]
public ConfidenceInfoMode Mode { get; set; } = ConfidenceInfoMode.BUTTON;
[Parameter]
public Providers Provider { get; set; }
public LLMProviders LLMProvider { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
private Confidence currentConfidence;
private bool showConfidence;
public ConfidenceInfo()
{
this.currentConfidence = Providers.NONE.GetConfidence(this.SettingsManager);
this.currentConfidence = LLMProviders.NONE.GetConfidence(this.SettingsManager);
}
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
this.currentConfidence = this.Provider.GetConfidence(this.SettingsManager);
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
this.currentConfidence = this.LLMProvider.GetConfidence(this.SettingsManager);
await base.OnParametersSetAsync();
}
@@ -51,7 +57,40 @@ public partial class ConfidenceInfo : ComponentBase
yield return ($"Source {++index}", source);
}
private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor()};";
private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};";
private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor()}; max-width: calc(35vw);";
private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor(this.SettingsManager)}; max-width: calc(35vw);";
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.showConfidence = false;
this.StateHasChanged();
break;
}
return Task.CompletedTask;
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
}
#endregion
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
}
#endregion
}
@@ -57,6 +57,8 @@ public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDi
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationBase);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{
switch (triggeredEvent)
@@ -0,0 +1,2 @@
@using AIStudio.Settings
<ConfigurationSelect Disabled="@this.Disabled" OptionDescription="Select a minimum confidence level" SelectedValue="@this.FilteredSelectedValue" Data="@ConfigurationSelectDataFactory.GetConfidenceLevelsData(this.SettingsManager, this.RestrictToGlobalMinimumConfidence)" SelectionUpdate="@this.SelectionUpdate" OptionHelp="Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."/>
@@ -0,0 +1,51 @@
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ConfigurationMinConfidenceSelection : ComponentBase
{
/// <summary>
/// The selected value.
/// </summary>
[Parameter]
public Func<ConfidenceLevel> SelectedValue { get; set; } = () => default!;
/// <summary>
/// An action that is called when the selection changes.
/// </summary>
[Parameter]
public Action<ConfidenceLevel> SelectionUpdate { get; set; } = _ => { };
/// <summary>
/// Is the selection component disabled?
/// </summary>
[Parameter]
public Func<bool> Disabled { get; set; } = () => false;
/// <summary>
/// Boolean value indicating whether the selection is restricted to a global minimum confidence level.
/// </summary>
[Parameter]
public bool RestrictToGlobalMinimumConfidence { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private ConfidenceLevel FilteredSelectedValue()
{
if (this.SelectedValue() is ConfidenceLevel.NONE)
return ConfidenceLevel.NONE;
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
{
var minimumLevel = this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence;
if(this.SelectedValue() < minimumLevel)
return minimumLevel;
}
return this.SelectedValue();
}
}
@@ -0,0 +1,23 @@
@inherits ConfigurationBase
@typeparam T
<MudSelectExtended
T="T"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionText"
SelectedValues="@this.SelectedValues()"
Strict="@true"
Disabled="@this.Disabled()"
Margin="Margin.Dense"
Label="@this.OptionDescription"
Class="@GetClass"
Variant="Variant.Outlined"
HelperText="@this.OptionHelp"
SelectedValuesChanged="@this.OptionChanged">
@foreach (var data in this.Data)
{
<MudSelectItemExtended Value="@data.Value">
@data.Name
</MudSelectItemExtended>
}
</MudSelectExtended>
@@ -0,0 +1,54 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
/// <summary>
/// Configuration component for selecting many values from a list.
/// </summary>
/// <typeparam name="T">The type of the value to select.</typeparam>
public partial class ConfigurationMultiSelect<T> : ConfigurationBase
{
/// <summary>
/// The data to select from.
/// </summary>
[Parameter]
public IEnumerable<ConfigurationSelectData<T>> Data { get; set; } = [];
/// <summary>
/// The selected values.
/// </summary>
[Parameter]
public Func<HashSet<T>> SelectedValues { get; set; } = () => [];
/// <summary>
/// An action that is called when the selection changes.
/// </summary>
[Parameter]
public Action<HashSet<T>> SelectionUpdate { get; set; } = _ => { };
private async Task OptionChanged(IEnumerable<T?>? updatedValues)
{
if(updatedValues is null)
this.SelectionUpdate([]);
else
this.SelectionUpdate(updatedValues.Where(n => n is not null).ToHashSet()!);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
private static string GetClass => $"{MARGIN_CLASS} rounded-lg";
private string GetMultiSelectionText(List<T?>? selectedValues)
{
if(selectedValues is null || selectedValues.Count == 0)
return "No preview features selected.";
if(selectedValues.Count == 1)
return $"You have selected 1 preview feature.";
return $"You have selected {selectedValues.Count} preview features.";
}
}
@@ -1 +1 @@
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
@@ -1,3 +1,4 @@
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
@@ -23,6 +24,9 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
[Parameter]
public Func<string> HelpText { get; set; } = () => "Select a provider that is preselected.";
[Parameter]
public Tools.Components Component { get; set; } = Tools.Components.NONE;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
@@ -43,8 +47,21 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#endregion
private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
foreach (var providerId in this.Data)
{
var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value);
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return providerId;
}
}
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationProviderSelection);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -1,7 +1,7 @@
@inherits ConfigurationBase
@typeparam T
<MudSelect T="T" Value="@this.SelectedValue()" Strict="@true" Disabled="@this.Disabled()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="@this.OptionChanged">
<MudSelect T="T" Value="@this.SelectedValue()" Strict="@true" ShrinkLabel="@true" Disabled="@this.Disabled()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="@this.OptionChanged">
@foreach (var data in this.Data)
{
<MudSelectItem Value="@data.Value">
@@ -3,16 +3,17 @@
<MudTextField
T="string"
Text="@this.Text()"
TextChanged="@this.OptionChanged"
TextChanged="@this.InternalUpdate"
Label="@this.OptionDescription"
Disabled="@this.Disabled()"
Class="mb-3"
Class="@MARGIN_CLASS"
Adornment="Adornment.Start"
AdornmentIcon="@this.Icon"
AdornmentColor="@this.IconColor"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Lines="1"
Immediate="@false"
DebounceInterval="500"
Lines="@this.NumLines"
AutoGrow="@this.AutoGrow"
MaxLines="@this.GetMaxLines"
Immediate="@true"
Variant="Variant.Outlined"
/>
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Components;
using Timer = System.Timers.Timer;
namespace AIStudio.Components;
public partial class ConfigurationText : ConfigurationBase
@@ -11,7 +13,7 @@ public partial class ConfigurationText : ConfigurationBase
public Func<string> Text { get; set; } = () => string.Empty;
/// <summary>
/// An action which is called when the option is changed.
/// An action which is called when the text was changed.
/// </summary>
[Parameter]
public Action<string> TextUpdate { get; set; } = _ => { };
@@ -27,6 +29,55 @@ public partial class ConfigurationText : ConfigurationBase
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Default;
/// <summary>
/// How many lines should the textfield have?
/// </summary>
[Parameter]
public int NumLines { get; set; } = 1;
/// <summary>
/// What is the maximum number of lines?
/// </summary>
[Parameter]
public int MaxLines { get; set; } = 12;
private string internalText = string.Empty;
private Timer timer = new(TimeSpan.FromMilliseconds(500))
{
AutoReset = false
};
#region Overrides of ConfigurationBase
protected override async Task OnInitializedAsync()
{
this.timer.Elapsed += async (_, _) => await this.InvokeAsync(async () => await this.OptionChanged(this.internalText));
await base.OnInitializedAsync();
}
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
this.internalText = this.Text();
await base.OnParametersSetAsync();
}
#endregion
#endregion
private bool AutoGrow => this.NumLines > 1;
private int GetMaxLines => this.AutoGrow ? this.MaxLines : 1;
private void InternalUpdate(string text)
{
this.timer.Stop();
this.internalText = text;
this.timer.Start();
}
private async Task OptionChanged(string updatedText)
{
@@ -1,7 +1,7 @@
@typeparam T
@inherits EnumSelectionBase
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudStack Row="@true" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection">
@foreach (var value in Enum.GetValues<T>())
{
@@ -1,17 +1,23 @@
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
<div class="@this.Classes" style="@this.Styles">
@if (this.HeaderContent is not null)
{
<div>
@this.HeaderContent
</div>
}
<div class="flex-auto overflow-auto mb-3">
@this.ChildContent
<div @ref="@this.AnchorAfterChildContent">
<div style="max-height: 0.1em;" @ref="@this.AnchorAfterChildContent">
&nbsp;
</div>
</div>
@if (this.FooterContent is not null)
{
<MudPaper Class="pa-3 border-solid border rounded-lg">
<MudPaper Class="pa-3 mb-3 border-solid border rounded-lg">
@this.FooterContent
</MudPaper>
}
@@ -6,13 +6,11 @@ namespace AIStudio.Components;
public partial class InnerScrolling : MSGComponentBase
{
/// <summary>
/// Set the height of anything above the scrolling content; usually a header.
/// What we do is calc(100vh - HeaderHeight). Means, you can use multiple measures like
/// 230px - 3em. Default is 3em.
/// </summary>
[Parameter]
public string HeaderHeight { get; set; } = "3em";
public bool FillEntireHorizontalSpace { get; set; }
[Parameter]
public RenderFragment? HeaderContent { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
@@ -22,6 +20,15 @@ public partial class InnerScrolling : MSGComponentBase
/// </summary>
[Parameter]
public RenderFragment? FooterContent { get; set; }
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public string? MinWidth { get; set; }
[Parameter]
public string Style { get; set; } = string.Empty;
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
@@ -43,7 +50,9 @@ public partial class InnerScrolling : MSGComponentBase
#region Overrides of MSGComponentBase
public override Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
public override string ComponentName => nameof(InnerScrolling);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
@@ -62,7 +71,13 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
private string TerminatedStyles => string.IsNullOrWhiteSpace(this.Style) ? string.Empty : $"{this.Style}; ";
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
private string Styles => $"flex-grow: 1; overflow: hidden; {this.TerminatedStyles}{this.MinWidthStyle}";
public async Task ScrollToBottom()
{
@@ -1,9 +1,14 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
@@ -19,7 +24,24 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
#region Implementation of IMessageBusReceiver
public abstract Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public abstract string ComponentName { get; }
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.StateHasChanged();
break;
default:
return this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
}
return Task.CompletedTask;
}
public abstract Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
@@ -46,6 +68,12 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
protected void ApplyFilters(ComponentBase[] components, Event[] events)
{
this.MessageBus.ApplyFilters(this, components, events);
// Append the color theme changed event to the list of events:
var eventsList = new List<Event>(events)
{
Event.COLOR_THEME_CHANGED
};
this.MessageBus.ApplyFilters(this, components, eventsList.ToArray());
}
}
@@ -0,0 +1,20 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
<ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3">
Alpha
</MudChip>
</ChildContent>
<TooltipContent>
<div style="max-width: 22em;">
<MudText Typo="Typo.body2" Class="mb-3">
This feature is currently in the alpha phase.
Expect bugs and unfinished work.
</MudText>
<MudText Typo="Typo.body2">
Alpha phase means that we are working on the
last details before the beta phase.
</MudText>
</div>
</TooltipContent>
</MudTooltip>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class PreviewAlpha : ComponentBase;
@@ -0,0 +1,19 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
<ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3">
Beta
</MudChip>
</ChildContent>
<TooltipContent>
<div style="max-width: 20em;">
<MudText Typo="Typo.body2" Class="mb-3">
This feature is currently in the beta phase.
It is still be possible that there are some bugs.
</MudText>
<MudText Typo="Typo.body2">
Beta phase means that we are testing the feature.
</MudText>
</div>
</TooltipContent>
</MudTooltip>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class PreviewBeta : ComponentBase;
@@ -0,0 +1,22 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
<ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3">
Experimental
</MudChip>
</ChildContent>
<TooltipContent>
<div style="max-width: 26em;">
<MudText Typo="Typo.body2" Class="mb-3">
This feature is currently in the experimental phase.
Expect bugs, unfinished work, changes in future
versions, and more.
</MudText>
<MudText Typo="Typo.body2">
Experimental phase means that we have a vision for a feature
but not a clear plan yet. We are still exploring the
possibilities.
</MudText>
</div>
</TooltipContent>
</MudTooltip>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class PreviewExperimental : ComponentBase;
@@ -0,0 +1,21 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
<ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3">
Prototype
</MudChip>
</ChildContent>
<TooltipContent>
<div style="max-width: 22em;">
<MudText Typo="Typo.body2" Class="mb-3">
This feature is currently in the prototype phase.
Expect bugs, unfinished work, changes in future
versions, and more.
</MudText>
<MudText Typo="Typo.body2">
Prototype phase means that we have a plan but we
are still working on it.
</MudText>
</div>
</TooltipContent>
</MudTooltip>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class PreviewPrototype : ComponentBase;
@@ -0,0 +1,19 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true">
<ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3">
Release Candidate
</MudChip>
</ChildContent>
<TooltipContent>
<div style="max-width: 20em;">
<MudText Typo="Typo.body2" Class="mb-3">
This feature is about to be released. We think it's ready for production.
There should be no more bugs.
</MudText>
<MudText Typo="Typo.body2">
Release candidates are the final step before a feature is proven to be stable.
</MudText>
</div>
</TooltipContent>
</MudTooltip>
@@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class PreviewReleaseCandidate : ComponentBase;
@@ -1,4 +1,4 @@
<MudTooltip Text="You can switch between your profiles here">
<MudTooltip Text="You can switch between your profiles here" Placement="Placement.Top">
<MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{
@@ -1,7 +1,7 @@
@using AIStudio.Settings
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
@foreach (var provider in this.GetAvailableProviders())
{
<MudSelectItem Value="@provider"/>
}
@@ -1,3 +1,5 @@
using AIStudio.Assistants;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
@@ -6,21 +8,32 @@ namespace AIStudio.Components;
public partial class ProviderSelection : ComponentBase
{
[Parameter]
public Settings.Provider ProviderSettings { get; set; }
[CascadingParameter]
public AssistantBase? AssistantBase { get; set; }
[Parameter]
public EventCallback<Settings.Provider> ProviderSettingsChanged { get; set; }
public AIStudio.Settings.Provider ProviderSettings { get; set; }
[Parameter]
public Func<Settings.Provider, string?> ValidateProvider { get; set; } = _ => null;
public EventCallback<AIStudio.Settings.Provider> ProviderSettingsChanged { get; set; }
[Parameter]
public Func<AIStudio.Settings.Provider, string?> ValidateProvider { get; set; } = _ => null;
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
private async Task SelectionChanged(Settings.Provider provider)
private async Task SelectionChanged(AIStudio.Settings.Provider provider)
{
this.ProviderSettings = provider;
await this.ProviderSettingsChanged.InvokeAsync(provider);
}
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
{
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return provider;
}
}
@@ -24,7 +24,7 @@ public partial class ReadWebContent : ComponentBase
public EventCallback<string> ContentChanged { get; set; }
[Parameter]
public Settings.Provider ProviderSettings { get; set; }
public AIStudio.Settings.Provider ProviderSettings { get; set; }
[Parameter]
public bool AgentIsRunning { get; set; }
@@ -47,7 +47,7 @@ public partial class ReadWebContent : ComponentBase
private bool urlIsValid;
private bool isProviderValid;
private Settings.Provider providerSettings;
private AIStudio.Settings.Provider providerSettings;
#region Overrides of ComponentBase
@@ -0,0 +1,17 @@
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.Directory"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenDirectoryDialog">
Choose Directory
</MudButton>
</MudStack>
@@ -0,0 +1,63 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class SelectDirectory : ComponentBase
{
[Parameter]
public string Directory { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> DirectoryChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string DirectoryDialogTitle { get; set; } = "Select Directory";
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public RustService RustService { get; set; } = null!;
[Inject]
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private void InternalDirectoryChanged(string directory)
{
this.Directory = directory;
this.DirectoryChanged.InvokeAsync(directory);
}
private async Task OpenDirectoryDialog()
{
var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory);
this.Logger.LogInformation($"The user selected the directory '{response.SelectedDirectory}'.");
if (!response.UserCancelled)
this.InternalDirectoryChanged(response.SelectedDirectory);
}
}
@@ -0,0 +1,17 @@
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
Choose File
</MudButton>
</MudStack>
@@ -0,0 +1,63 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class SelectFile : ComponentBase
{
[Parameter]
public string File { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> FileChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string FileDialogTitle { get; set; } = "Select File";
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public RustService RustService { get; set; } = null!;
[Inject]
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private void InternalFileChanged(string file)
{
this.File = file;
this.FileChanged.InvokeAsync(file);
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
if (!response.UserCancelled)
this.InternalFileChanged(response.SelectedFilePath);
}
}
@@ -0,0 +1,32 @@
@using AIStudio.Settings
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.CalendarToday" HeaderText="Assistant: Agenda Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect agenda options?" LabelOn="Agenda options are preselected" LabelOff="No agenda options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often."/>
<ConfigurationText OptionDescription="Preselect a name?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Tag" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectName)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectName = updatedText)" />
<ConfigurationText OptionDescription="Preselect a topic?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.EventNote" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectTopic)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectTopic = updatedText)" />
<ConfigurationText OptionDescription="Preselect an objective?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Flag" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectObjective)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectObjective = updatedText)" />
<ConfigurationText OptionDescription="Preselect a moderator?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Person3" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectModerator)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectModerator = updatedText)" />
<ConfigurationText OptionDescription="Preselect a duration?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Schedule" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectDuration)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectDuration = updatedText)" />
<ConfigurationText OptionDescription="Preselect a start time?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Schedule" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectStartTime)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectStartTime = updatedText)" />
<ConfigurationOption OptionDescription="Preselect whether the participants should get to know each other" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants should get to know each other" LabelOff="Participants do not need to get to know each other" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectIntroduceParticipants)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectIntroduceParticipants = updatedState)" />
<ConfigurationSelect OptionDescription="Preselect the number of participants" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants)" Data="@ConfigurationSelectDataFactory.GetNumberParticipantsData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants = selectedValue)" OptionHelp="How many participants should be preselected?"/>
<ConfigurationOption OptionDescription="Preselect whether the participants should actively involved" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants should be actively involved" LabelOff="Participants do not need to be actively involved" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether the meeting is virtual" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Meeting is virtual" LabelOff="Meeting is in person" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectIsMeetingVirtual)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectIsMeetingVirtual = updatedState)" />
<ConfigurationText OptionDescription="Preselect a location?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.MyLocation" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectLocation)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectLocation = updatedText)" />
<ConfigurationOption OptionDescription="Preselect whether there is a joint dinner" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="There is a joint dinner" LabelOff="There is no joint dinner" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectJointDinner)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectJointDinner = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether there is a social event" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="There is a social event" LabelOff="There is no social event" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectSocialActivity)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectSocialActivity = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether participants needs to arrive and depart" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants need to arrive and depart" LabelOff="Participants do not need to arrive and depart" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart = updatedState)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. lunch time" Min="30" Max="120" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime = updatedValue)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. break time" Min="10" Max="60" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime = updatedValue)" />
<ConfigurationSelect OptionDescription="Preselect the agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which agenda language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>
@@ -0,0 +1,3 @@
namespace AIStudio.Components.Settings;
public partial class SettingsPanelAgenda : SettingsPanelBase;
@@ -0,0 +1,12 @@
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="Agent: Text Content Cleaner Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3">
Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things,
and attempts to convert relative links into absolute links so that they can be used.
</MudText>
<ConfigurationOption OptionDescription="Preselect text content cleaner options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions = updatedState)" OptionHelp="When enabled, you can preselect some agent options. This is might be useful when you prefer a LLM."/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
@@ -0,0 +1,3 @@
namespace AIStudio.Components.Settings;
public partial class SettingsPanelAgentContentCleaner : SettingsPanelBase;
@@ -0,0 +1,24 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="App Options">
<ConfigurationSelect OptionDescription="Color theme" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="Choose the color theme that best suits for you."/>
<ConfigurationOption OptionDescription="Save energy?" LabelOn="Energy saving is enabled" LabelOff="Energy saving is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available."/>
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." />
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
<ConfigurationSelect OptionDescription="Preview feature visibility" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="Do you want to show preview features in the app?"/>
@if(this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
{
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
if (availablePreviewFeatures.Count > 0)
{
<ConfigurationMultiSelect OptionDescription="Select preview features" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures)" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="Which preview features would you like to enable?"/>
}
}
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "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.")"/>
<ConfigurationSelect OptionDescription="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="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."/>
</ExpansionPanel>
Loaded 100 of 373 files, more files were not shown because too many files have changed in this diff. Show more