Compare commits

...
57 Commits
Author SHA1 Message Date
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
137 changed files with 9744 additions and 1652 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
+3 -3
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"
}
@@ -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' }}
+53 -7
View File
@@ -1,4 +1,48 @@
# MindWork AI Studio
## 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 Data API (EDI)](https://github.com/MindWorkAI/EDI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/EDI/pull/1))~~
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
- [ ] App: Add an option to show preview features
- [ ] App: Configure embedding providers
- [ ] App: Management of data sources (local & external data via [EDI](https://github.com/MindWorkAI/EDI))
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] App: Implement 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 an 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. Related PR: [#167](https://github.com/MindWorkAI/AI-Studio/pull/167).
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.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.
- v0.9.12: Added a job posting assistant to the business category and improved grammar & spelling check and rewrite assistants.
- v0.9.11: Added enforcement of minimal confidence levels & dark mode.
- v0.9.10: Added support for the OpenAI `o1` model.
## What is AI Studio?
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png)
![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png)
@@ -6,7 +50,7 @@ MindWork AI Studio is a desktop application available for macOS, Windows, and Li
**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 etc.), Mistral, Anthropic (Claude), Google Gemini, 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/).
- **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.
@@ -25,16 +69,18 @@ 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 should be able to integrate your data into AI Studio. For example, your PDF or Office files, or your Markdown notes.
- **Integration of enterprise data:** Soon, it will also be possible to integrate data from the corporate network using an interface that we have specified ([External Data API](https://github.com/MindWorkAI/EDI), EDI for short).
- **Writing mode:** We want to integrate a writing mode that should support you in creating extensive works. We are thinking of comprehensive project proposals, tenders, or your next fantasy novel.
- **Browser usage:** We're trying to offer the features from AI Studio to you in the browser via a plugin, so we could use spell-checking or rewriting text directly in the browser.
- **Voice control:** You should be able to interact with the AI systems using your voice as well. 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.
- **Email monitoring:** You should have the option to connect your email inboxes with AI Studio. The AI reads your emails and sends you a notification when something important happens. At the same time, you can access knowledge from your emails in your chats.
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>
+1
View File
@@ -3,5 +3,6 @@
<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
@@ -32,6 +32,9 @@
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.ShowResult && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
@@ -45,17 +48,17 @@
@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" Style="@this.GetSendToColor()" Class="rounded">
@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>
@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))
{
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
}
@foreach (var button in this.FooterButtons)
@@ -78,11 +81,8 @@
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>().OrderBy(n => n.Name().Length))
@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)">
@assistant.Name()
</MudMenuItem>
@@ -92,13 +92,19 @@
}
}
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
@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)
{
@@ -37,8 +37,9 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
[Inject]
private MessageBus MessageBus { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
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; }
@@ -73,6 +74,12 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
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();
@@ -172,14 +179,36 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
Blocks = [],
};
}
protected Guid CreateChatThread(Guid workspaceId, string name)
{
var chatId = Guid.NewGuid();
this.chatThread = new()
{
WorkspaceId = workspaceId,
ChatId = chatId,
Name = name,
Seed = this.RNG.Next(),
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
$"""
{this.SystemPrompt}
{this.currentProfile.ToSystemPrompt()}
""",
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
{
@@ -236,9 +265,12 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
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
@@ -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.Components;
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 ResetFrom()
{
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 = Workspaces.WORKSPACE_ID_BIAS,
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(Workspaces.WORKSPACE_ID_BIAS, 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);
}
}
@@ -24,8 +24,6 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
@@ -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 ResetFrom()
{
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,3 +1,5 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore
@@ -30,6 +32,11 @@ public partial class AssistantLegalCheck : AssistantBaseCore
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputLegalDocument = string.Empty;
@@ -1,3 +1,4 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Assistants.MyTasks;
@@ -34,6 +35,11 @@ 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()
{
@@ -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;
+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; }
}
@@ -64,7 +64,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 +83,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);
});
}
+1 -1
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.
@@ -13,6 +13,15 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
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"),
@@ -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,6 +1,12 @@
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="@this.Classes" style="@this.Styles">
@if (this.HeaderContent is not null)
{
<div>
@this.HeaderContent
</div>
}
<div class="flex-auto overflow-auto">
@this.ChildContent
@@ -6,6 +6,9 @@ namespace AIStudio.Components;
public partial class InnerScrolling : MSGComponentBase
{
[Parameter]
public bool FillEntireHorizontalSpace { get; set; }
/// <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
@@ -14,6 +17,9 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter]
public string HeaderHeight { get; set; } = "3em";
[Parameter]
public RenderFragment? HeaderContent { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
@@ -22,6 +28,9 @@ public partial class InnerScrolling : MSGComponentBase
/// </summary>
[Parameter]
public RenderFragment? FooterContent { get; set; }
[Parameter]
public string Class { get; set; } = string.Empty;
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
@@ -62,7 +71,9 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0;" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0;";
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
public async Task ScrollToBottom()
{
@@ -0,0 +1,18 @@
<MudListItem T="string" Icon="@this.Icon">
@if (string.IsNullOrWhiteSpace(this.URL))
{
<MudText Typo="Typo.body1" Inline="@true">
@this.Name
</MudText>
}
else
{
<MudLink Href="@this.URL" Target="_blank">
@this.Name
</MudLink>
}
<MudText Typo="Typo.body1" Class="ml-3" Inline="@true">
@this.Acknowledgment
</MudText>
</MudListItem>
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class Supporter : ComponentBase
{
[Parameter]
public string Name { get; set; } = string.Empty;
[Parameter]
public string Acknowledgment { get; set; } = string.Empty;
[Parameter]
public string? URL { get; set; }
[Parameter]
public SupporterType Type { get; set; }
private string Icon => this.Type switch
{
SupporterType.INDIVIDUAL => Icons.Material.Filled.Person,
SupporterType.ORGANIZATION => Icons.Material.Filled.Business,
_ => Icons.Material.Filled.Person4,
};
}
@@ -0,0 +1,8 @@
namespace AIStudio.Components;
public enum SupporterType
{
UNKNOWN,
INDIVIDUAL,
ORGANIZATION
}
@@ -6,11 +6,11 @@ public partial class Vision : ComponentBase
{
private static readonly TextItem[] ITEMS_VISION =
[
new TextItem("Meet your needs", "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer."),
new TextItem("One stop shop", "The app will strive to fulfill all your AI needs: text-generation AI (LLM), image-generation AI, audio-generation AI (text-to-speech, potentially even text-to-music), and audio input (transcription, dictation). When there's a provider and an API available, we'll try to integrate it."),
new TextItem("Local file system", "When you wish, we aim to integrate your local system. Local documents could be incorporated using Retrieval-Augmented Generation (RAG), and we could directly save AI-generated content to your file system."),
new TextItem("Local AI systems", "Want to use AI systems locally and offline? We aim to make that possible too."),
new TextItem("Your AI systems", "Prefer to run your AI systems with providers like replicate.com? We plan to support that!"),
new TextItem("Assistants", "We aim to integrate specialized user interfaces as assistants. For example, a UI specifically for writing emails, or one designed for translating and correcting text, and more."),
new("Meet your needs", "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer."),
new("One stop shop", "The app will strive to fulfill all your AI needs: text-generation AI (LLM), image-generation AI, audio-generation AI (text-to-speech, potentially even text-to-music), and audio input (transcription, dictation). When there's a provider and an API available, we'll try to integrate it."),
new("Local file system", "When you wish, we aim to integrate your local system. Local documents could be incorporated using Retrieval-Augmented Generation (RAG), and we could directly save AI-generated content to your file system."),
new("Local AI systems", "Want to use AI systems locally and offline? We aim to make that possible too."),
new("Your AI systems", "Prefer to run your AI systems with providers like replicate.com? We plan to support that!"),
new("Assistants", "We aim to integrate specialized user interfaces as assistants. For example, a UI specifically for writing emails, or one designed for translating and correcting text, and more."),
];
}
@@ -1,4 +1,4 @@
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true">
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
<ItemTemplate Context="item">
@switch (item.Value)
{
@@ -11,7 +11,7 @@
case TreeItemData treeItem:
@if (treeItem.Type is TreeItemType.CHAT)
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="() => this.LoadChat(treeItem.Path, true)">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="() => this.LoadChat(treeItem.Path, true)">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">
@@ -44,7 +44,7 @@
}
else if (treeItem.Type is TreeItemType.WORKSPACE)
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
@@ -63,7 +63,7 @@
}
else
{
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
@@ -1,6 +1,5 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Chat;
using AIStudio.Dialogs;
@@ -35,20 +34,12 @@ public partial class Workspaces : ComponentBase
[Parameter]
public Func<Task> LoadedChatWasChanged { get; set; } = () => Task.CompletedTask;
[Parameter]
public bool ExpandRootNodes { get; set; } = true;
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
}
};
public static readonly Guid WORKSPACE_ID_BIAS = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
private readonly List<TreeItemData<ITreeItem>> treeItems = new();
@@ -63,6 +54,7 @@ public partial class Workspaces : ComponentBase
// - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item
//
await this.EnsureBiasWorkspace();
await this.LoadTreeItems();
await base.OnInitializedAsync();
}
@@ -71,9 +63,9 @@ public partial class Workspaces : ComponentBase
private async Task LoadTreeItems()
{
this.treeItems.Clear();
this.treeItems.Add(new TreeItemData<ITreeItem>
var workspacesNode = new TreeItemData<ITreeItem>
{
Expanded = this.ExpandRootNodes,
Expandable = true,
Value = new TreeItemData
{
@@ -85,16 +77,11 @@ public partial class Workspaces : ComponentBase
Path = "root",
Children = await this.LoadWorkspaces(),
},
});
};
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeDivider(),
});
this.treeItems.Add(new TreeItemData<ITreeItem>
var tempChatNode = new TreeItemData<ITreeItem>
{
Expanded = this.ExpandRootNodes,
Expandable = true,
Value = new TreeItemData
{
@@ -106,7 +93,16 @@ public partial class Workspaces : ComponentBase
Path = "temp",
Children = await this.LoadTemporaryChats(),
},
};
this.treeItems.Clear();
this.treeItems.Add(workspacesNode);
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeDivider(),
});
this.treeItems.Add(tempChatNode);
}
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadTemporaryChats()
@@ -151,16 +147,6 @@ public partial class Workspaces : ComponentBase
return result;
}
public async Task<string> LoadWorkspaceName(Guid workspaceId)
{
if(workspaceId == Guid.Empty)
return string.Empty;
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
return await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
}
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaces()
{
var workspaces = new List<TreeItemData<ITreeItem>>();
@@ -250,27 +236,14 @@ public partial class Workspaces : ComponentBase
return result;
}
public async Task StoreChat(ChatThread chat)
public async Task StoreChat(ChatThread chat, bool reloadTreeItems = true)
{
string chatDirectory;
if (chat.WorkspaceId == Guid.Empty)
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
else
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
// Ensure the directory exists:
Directory.CreateDirectory(chatDirectory);
// Save the chat name:
var chatNamePath = Path.Join(chatDirectory, "name");
await File.WriteAllTextAsync(chatNamePath, chat.Name);
// Save the thread as thread.json:
var chatPath = Path.Join(chatDirectory, "thread.json");
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
await WorkspaceBehaviour.StoreChat(chat);
// Reload the tree items:
await this.LoadTreeItems();
if(reloadTreeItems)
await this.LoadTreeItems();
this.StateHasChanged();
}
@@ -299,7 +272,7 @@ public partial class Workspaces : ComponentBase
try
{
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS);
if (switchToChat)
{
this.CurrentChatThread = chat;
@@ -325,7 +298,7 @@ public partial class Workspaces : ComponentBase
if (askForConfirmation)
{
var workspaceName = await this.LoadWorkspaceName(chat.WorkspaceId);
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(chat.WorkspaceId);
var dialogParameters = new DialogParameters
{
{
@@ -390,7 +363,7 @@ public partial class Workspaces : ComponentBase
return;
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
var workspaceName = await this.LoadWorkspaceName(workspaceId);
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId);
var dialogParameters = new DialogParameters
{
{ "Message", $"Please enter a new or edit the name for your workspace '{workspaceName}':" },
@@ -434,6 +407,18 @@ public partial class Workspaces : ComponentBase
await this.LoadTreeItems();
}
private async Task EnsureBiasWorkspace()
{
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", WORKSPACE_ID_BIAS.ToString());
if(Path.Exists(workspacePath))
return;
Directory.CreateDirectory(workspacePath);
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, "Bias of the Day", Encoding.UTF8);
}
private async Task DeleteWorkspace(string? workspacePath)
{
@@ -441,7 +426,7 @@ public partial class Workspaces : ComponentBase
return;
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
var workspaceName = await this.LoadWorkspaceName(workspaceId);
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId);
// Determine how many chats are in the workspace:
var chatCount = Directory.EnumerateDirectories(workspacePath).Count();
@@ -531,7 +516,7 @@ public partial class Workspaces : ComponentBase
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = "You are a helpful assistant!",
SystemPrompt = SystemPrompts.DEFAULT,
Blocks = [],
};
@@ -19,7 +19,7 @@
<MudTextField
T="string"
@bind-Text="@this.dataAPIKey"
Label="API Key"
Label="@this.APIKeyText"
Disabled="@(!this.NeedAPIKey)"
Class="mb-3"
Adornment="Adornment.Start"
@@ -99,17 +99,21 @@ public partial class ProviderDialog : ComponentBase
private readonly List<Model> availableModels = new();
private readonly Encryption encryption = Program.ENCRYPTION;
private Settings.Provider CreateProviderSettings() => new()
private Settings.Provider CreateProviderSettings()
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedLLMProvider = this.DataLLMProvider,
Model = this.DataLLMProvider is LLMProviders.FIREWORKS ? new Model(this.dataManuallyModel) : this.DataModel,
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = this.DataHostname.EndsWith('/') ? this.DataHostname[..^1] : this.DataHostname,
Host = this.DataHost,
};
var cleanedHostname = this.DataHostname.Trim();
return new()
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedLLMProvider = this.DataLLMProvider,
Model = this.DataLLMProvider is LLMProviders.FIREWORKS ? new Model(this.dataManuallyModel, null) : this.DataModel,
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
};
}
#region Overrides of ComponentBase
@@ -133,7 +137,7 @@ public partial class ProviderDialog : ComponentBase
//
// We cannot load the API key for self-hosted providers:
//
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED)
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA)
{
await this.ReloadModels();
await base.OnInitializedAsync();
@@ -149,7 +153,7 @@ public partial class ProviderDialog : ComponentBase
}
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(provider);
var requestedSecret = await this.RustService.GetAPIKey(provider, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if(requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
@@ -159,8 +163,15 @@ public partial class ProviderDialog : ComponentBase
}
else
{
this.dataAPIKeyStorageIssue = $"Failed to load the API key from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the API key again.";
await this.form.Validate();
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
{
this.dataAPIKeyStorageIssue = $"Failed to load the API key from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the API key again.";
await this.form.Validate();
}
// We still try to load the models. Some local hosts don't need an API key:
await this.ReloadModels();
}
}
@@ -192,7 +203,7 @@ public partial class ProviderDialog : ComponentBase
// Use the data model to store the provider.
// We just return this data to the parent component:
var addedProviderSettings = this.CreateProviderSettings();
if (addedProviderSettings.UsedLLMProvider != LLMProviders.SELF_HOSTED)
if (!string.IsNullOrWhiteSpace(this.dataAPIKey))
{
// We need to instantiate the provider to store the API key:
var provider = addedProviderSettings.CreateProvider(this.Logger);
@@ -350,7 +361,9 @@ public partial class ProviderDialog : ComponentBase
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
LLMProviders.ANTHROPIC => true,
LLMProviders.GOOGLE => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
_ => false,
@@ -361,11 +374,21 @@ public partial class ProviderDialog : ComponentBase
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
LLMProviders.ANTHROPIC => true,
LLMProviders.GOOGLE => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
LLMProviders.SELF_HOSTED => this.DataHost is Host.OLLAMA,
_ => false,
};
private string APIKeyText => this.DataLLMProvider switch
{
LLMProviders.SELF_HOSTED => "(Optional) API Key",
_ => "API Key",
};
private bool NeedHostname => this.DataLLMProvider switch
{
@@ -397,7 +420,9 @@ public partial class ProviderDialog : ComponentBase
LLMProviders.OPEN_AI => "https://platform.openai.com/signup",
LLMProviders.MISTRAL => "https://console.mistral.ai/",
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
LLMProviders.GROQ => "https://console.groq.com/",
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
_ => string.Empty,
@@ -30,7 +30,7 @@
}
<MudMainContent Class="mud-height-full pt-1" Style="@this.PaddingLeft">
<MudContainer Fixed="@true" Class="mud-height-full" Style="margin-left: 5em; width: calc(100% - 5em);">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mud-height-full" Style="margin-left: 5em; width: calc(100% - 5em);">
@if (!this.performingUpdate && this.IsUpdateAlertVisible)
{
<MudAlert NoIcon="@true" Severity="Severity.Info" Variant="Variant.Filled" ShowCloseIcon="@true" Dense="@true" CloseIconClicked="() => this.DismissUpdate()" Class="mt-2 mb-2">
@@ -45,10 +45,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.65" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
<PackageReference Include="MudBlazor" Version="7.8.0" />
<PackageReference Include="MudBlazor.Markdown" Version="7.8.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageReference Include="MudBlazor" Version="7.15.0" />
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
</ItemGroup>
@@ -24,10 +24,18 @@
<AssistantBlock Name="E-Mail" Description="Generate an e-mail for a given context." Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
<AssistantBlock Name="My Tasks" Description="Analyze a text or an email for tasks you need to complete." Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
<AssistantBlock Name="Agenda Planner" Description="Generate an agenda for a given meeting, seminar, etc." Icon="@Icons.Material.Filled.CalendarToday" Link="@Routes.ASSISTANT_AGENDA"/>
<AssistantBlock Name="Job Posting" Description="Generate a job posting for a given job description." Icon="@Icons.Material.Filled.Work" Link="@Routes.ASSISTANT_JOB_POSTING"/>
<AssistantBlock Name="Legal Check" Description="Ask a question about a legal document." Icon="@Icons.Material.Filled.Gavel" Link="@Routes.ASSISTANT_LEGAL_CHECK"/>
<AssistantBlock Name="Icon Finder" Description="Using a LLM to find an icon for a given context." Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
</MudStack>
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
Learning
</MudText>
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
<AssistantBlock Name="Bias of the Day" Description="Learn about one cognitive bias every day." Icon="@Icons.Material.Filled.Psychology" Link="@Routes.ASSISTANT_BIAS"/>
</MudStack>
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
Software Engineering
</MudText>
+101 -55
View File
@@ -16,79 +16,125 @@
</MudText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
<InnerScrolling @ref="@this.scrollingArea" HeaderHeight="12.3em">
<ChildContent>
@if (this.chatThread is not null)
<MudStack Row="@true" Style="width: 100%; overflow: hidden;">
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR
&& !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
{
<MudPaper Class="border border-solid rounded-lg">
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
</MudTooltip>
</MudPaper>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
@if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE)
{
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
{
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
<HeaderContent>
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
</MudTooltip>
</HeaderContent>
<ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
</ChildContent>
</InnerScrolling>
}
else
{
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
<ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
</ChildContent>
</InnerScrolling>
}
}
</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)
}
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em">
<ChildContent>
@if (this.chatThread is not null)
{
<MudTooltip Text="Your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaces()"/>
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"/>
}
}
}
</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 (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>
}
@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>
}
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</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 (!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.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.providerSettings.UsedLLMProvider"/>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
}
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
</MudToolBar>
</FooterContent>
</InnerScrolling>
</MudStack>
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
</MudToolBar>
</FooterContent>
</InnerScrolling>
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES)
@if (
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
{
<MudDrawer @bind-Open="@this.workspacesVisible" Width="40em" Height="100%" Anchor="Anchor.Start" Variant="DrawerVariant.Temporary" Elevation="1">
<MudDrawer @bind-Open="@this.workspaceOverlayVisible" Width="40em" Height="100%" Anchor="Anchor.Start" Variant="DrawerVariant.Temporary" Elevation="1">
<MudDrawerHeader>
<MudStack Row="@true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="mr-3">
Your workspaces
</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaces()"/>
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaceOverlay()"/>
</MudStack>
</MudDrawerHeader>
<MudDrawerContainer Class="ml-6">
+71 -19
View File
@@ -42,10 +42,14 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private string userInput = string.Empty;
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private bool workspacesVisible;
private bool workspaceOverlayVisible;
private Workspaces? workspaces;
private bool mustScrollToBottomAfterRender;
private bool mustStoreChat;
private bool mustLoadChat;
private LoadChat loadChat;
private byte scrollRenderCountdown;
private bool autoSaveEnabled;
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
@@ -55,7 +59,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE ]);
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE ]);
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
@@ -68,14 +72,23 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
this.chatThread = deferredContent;
if (this.chatThread is not null)
{
var firstUserBlock = this.chatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
if (firstUserBlock is not null)
if (string.IsNullOrWhiteSpace(this.chatThread.Name))
{
this.chatThread.Name = firstUserBlock.Content switch
var firstUserBlock = this.chatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
if (firstUserBlock is not null)
{
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
_ => "Thread"
};
this.chatThread.Name = firstUserBlock.Content switch
{
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
_ => "Thread"
};
}
}
if(this.chatThread.WorkspaceId != Guid.Empty)
{
this.autoSaveEnabled = true;
this.mustStoreChat = true;
}
}
@@ -86,12 +99,34 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
this.StateHasChanged();
}
}
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
if (deferredLoading != default)
{
this.loadChat = deferredLoading;
this.mustLoadChat = true;
}
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && this.chatThread is not null && this.mustStoreChat)
{
this.mustStoreChat = false;
await WorkspaceBehaviour.StoreChat(this.chatThread);
this.currentWorkspaceId = this.chatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
}
if (firstRender && this.mustLoadChat)
{
this.mustLoadChat = false;
this.chatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
this.StateHasChanged();
}
if(this.mustScrollToBottomAfterRender)
{
if (--this.scrollRenderCountdown == 0)
@@ -123,6 +158,8 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
private void ProfileWasChanged(Profile profile)
{
@@ -274,23 +311,35 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
}
}
private void ToggleWorkspaces()
private void ToggleWorkspaceOverlay()
{
this.workspacesVisible = !this.workspacesVisible;
this.workspaceOverlayVisible = !this.workspaceOverlayVisible;
}
private async Task ToggleWorkspaceSidebar()
{
this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible = !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
await this.SettingsManager.StoreSettings();
}
private async Task SaveThread()
{
if(this.workspaces is null)
return;
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);
await this.workspaces.StoreChat(this.chatThread);
this.hasUnsavedChanges = false;
}
@@ -420,7 +469,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
await this.SaveThread();
this.currentWorkspaceId = this.chatThread.WorkspaceId;
this.currentWorkspaceName = await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId);
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
}
private async Task LoadedChatChanged()
@@ -432,7 +481,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty;
this.currentWorkspaceName = this.chatThread is null ? string.Empty : await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId);
this.currentWorkspaceName = this.chatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
this.userInput = string.Empty;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
@@ -455,16 +504,19 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
case Event.RESET_CHAT_STATE:
this.ResetState();
break;
case Event.CHAT_STREAMING_DONE:
if(this.autoSaveEnabled)
await this.SaveThread();
break;
}
return Task.CompletedTask;
}
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
+7 -7
View File
@@ -32,13 +32,13 @@ public partial class Home : ComponentBase
private static readonly TextItem[] ITEMS_ADVANTAGES =
[
new TextItem("Free of charge", "The app is free to use, both for personal and commercial purposes."),
new TextItem("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using llama.cpp, ollama, LM Studio, or Fireworks. Support for Google Gemini and Replicate is planned."),
new TextItem("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
new TextItem("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."),
new TextItem("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
new TextItem("Flexibility", "Choose the provider and model best suited for your current task."),
new TextItem("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."),
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
new("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."),
new("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
new("Flexibility", "Choose the provider and model best suited for your current task."),
new("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."),
];
private const string QUICK_START_GUIDE =
+53 -1
View File
@@ -3,6 +3,7 @@
@using AIStudio.Assistants.TextSummarizer
@using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@using Host = AIStudio.Provider.SelfHosted.Host
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
@@ -201,7 +202,12 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Work" HeaderText="Workspace Options">
<ConfigurationSelect OptionDescription="Workspace behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior = selectedValue)" OptionHelp="Should we store your chats?"/>
<ConfigurationSelect OptionDescription="Workspace maintenance" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageTemporaryMaintenancePolicyData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy = selectedValue)" OptionHelp="If and when should we delete your temporary chats?"/>
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<ConfigurationSelect OptionDescription="Workspace maintenance" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageTemporaryMaintenancePolicyData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy = selectedValue)" OptionHelp="If and when should we delete your temporary chats?"/>
<ConfigurationSelect OptionDescription="Workspace display behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior)" Data="@ConfigurationSelectDataFactory.GetWorkspaceDisplayBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior = selectedValue)" OptionHelp="How should we display your workspaces?"/>
}
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.FindInPage" HeaderText="Assistant: Icon Finder Options">
@@ -342,6 +348,26 @@
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Work" HeaderText="Assistant: Job Postings">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect job posting options?" LabelOn="Job posting options are preselected" LabelOff="No job posting options are preselected" State="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect some job posting options. This is might be useful when you prefer a specific LLM model."/>
<ConfigurationText OptionDescription="Preselect the company name?" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Warehouse" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedCompanyName)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedCompanyName = updatedText)" />
<ConfigurationText OptionDescription="Preselect some mandatory information about the job posting?" NumLines="6" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.TextSnippet" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedMandatoryInformation)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedMandatoryInformation = updatedText)" />
<ConfigurationText OptionDescription="Preselect the job description?" NumLines="3" MaxLines="6" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Settings" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedJobDescription)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedJobDescription = updatedText)" />
<ConfigurationText OptionDescription="Preselect the job qualifications?" NumLines="3" MaxLines="6" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Settings" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedQualifications)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedQualifications = updatedText)" />
<ConfigurationText OptionDescription="Preselect the job responsibilities?" NumLines="3" MaxLines="6" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Settings" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedResponsibilities)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedResponsibilities = updatedText)" />
<ConfigurationText OptionDescription="Preselect the work location?" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.MyLocation" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedWorkLocation)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedWorkLocation = updatedText)" />
<ConfigurationText OptionDescription="Preselect the country (legal framework)?" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Flag" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedCountryLegalFramework)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectedCountryLegalFramework = updatedText)" />
<ConfigurationSelect OptionDescription="Preselect the language" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.JobPostings.PreselectOtherLanguage = updatedText)"/>
}
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Gavel" HeaderText="Assistant: Legal Check">
<ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
@@ -380,6 +406,32 @@
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Psychology" HeaderText="Assistant: Bias of the Day">
<ConfigurationOption OptionDescription="Restrict to one bias a day?" LabelOn="Yes, you can only retrieve one bias per day" LabelOff="No restriction. You can retrieve as many biases as you want per day." State="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.RestrictOneBiasPerDay)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.BiasOfTheDay.RestrictOneBiasPerDay = updatedState)"/>
<MudField Label="Statistics" Variant="Variant.Outlined" Class="mb-2">
<MudText Typo="Typo.body1">
You have learned about @this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias.Count out of @BiasCatalog.ALL_BIAS.Count biases.
</MudText>
<MudButton Size="Size.Small" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Restore" Color="Color.Error" OnClick="@(() => this.ResetBiasOfTheDayHistory())">
Reset
</MudButton>
</MudField>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model."/>
<ConfigurationSelect OptionDescription="Preselect the language" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another language" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="Agent: Text Content Cleaner Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
@@ -131,7 +131,9 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
LLMProviders.ANTHROPIC => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
LLMProviders.GOOGLE => true,
_ => false,
};
@@ -141,6 +143,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
LLMProviders.OPEN_AI => "https://platform.openai.com/usage",
LLMProviders.MISTRAL => "https://console.mistral.ai/usage/",
LLMProviders.ANTHROPIC => "https://console.anthropic.com/settings/plans",
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
_ => string.Empty,
@@ -249,6 +253,29 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
#endregion
#region Bias-of-the-day related
private async Task ResetBiasOfTheDayHistory()
{
var dialogParameters = new DialogParameters
{
{ "Message", "Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again." },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Reset your bias-of-the-day statistics", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias.Clear();
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.MinValue;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
#endregion
#region Implementation of IMessageBusReceiver
+16 -1
View File
@@ -17,7 +17,7 @@
</MudButton>
</div>
<MudGrid Justify="Justify.Center" Spacing="2" Style="width: 100%;" Class="align-center ma-0">
<MudGrid Justify="Justify.Center" Spacing="2" Style="width: 100%;" Class="ma-0">
<MudItem Class="pa-0 pr-2" xs="6">
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
@@ -28,6 +28,21 @@
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.People" Variant="Variant.Filled" Target="_blank">
Become a contributor
</MudButton>
<MudText Typo="Typo.h6" Class="mt-8">
The first 10 supporters who make a monthly contribution:
</MudText>
<MudList T="string">
<Supporter Name="richard-stanton" Type="SupporterType.INDIVIDUAL" URL="https://github.com/richard-stanton" Acknowledgment="Thank you, Richard, for being the first."/>
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thank you, Peer, for your courage in being the second person to support the project financially."/>
</MudList>
<MudText Typo="Typo.h6" Class="mt-8">
The first 10 supporters who make a one-time contribution:
</MudText>
<MudList T="string">
<Supporter Name="Bessieres" Type="SupporterType.INDIVIDUAL" URL="https://github.com/Bessieres" Acknowledgment="Thank you for being the first to contribute a one-time donation."/>
</MudList>
</MudPaper>
</MudItem>
@@ -147,10 +147,18 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
{
return Task.FromResult(new[]
{
new Model("claude-3-5-sonnet-20240620"),
new Model("claude-3-opus-20240229"),
new Model("claude-3-sonnet-20240229"),
new Model("claude-3-haiku-20240307"),
new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (latest)"),
new Model("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet (20. June 2024)"),
new Model("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet (22. October 2024)"),
new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (latest)"),
new Model("claude-3-5-heiku-20241022", "Claude 3.5 Haiku (22. October 2024)"),
new Model("claude-3-opus-20240229", "Claude 3.0 Opus (29. February 2024)"),
new Model("claude-3-opus-latest", "Claude 3.0 Opus (latest)"),
new Model("claude-3-sonnet-20240229", "Claude 3.0 Sonnet (29. February 2024)"),
new Model("claude-3-haiku-20240307", "Claude 3.0 Haiku (7. March 2024)"),
}.AsEnumerable());
}
@@ -0,0 +1,15 @@
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Google;
/// <summary>
/// The Google chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream
);
@@ -0,0 +1,3 @@
namespace AIStudio.Provider.Google;
public readonly record struct Model(string Name, string DisplayName);
@@ -0,0 +1,7 @@
namespace AIStudio.Provider.Google;
/// <summary>
/// A data model for the response from the model endpoint.
/// </summary>
/// <param name="Models"></param>
public readonly record struct ModelsResponse(IList<Model> Models);
@@ -0,0 +1,188 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Google;
public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativelanguage.googleapis.com/v1beta/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
#region Implementation of IProvider
/// <inheritdoc />
public string Id => "Google";
/// <inheritdoc />
public string InstanceName { get; set; } = "Google Gemini";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
{
Role = "system",
Content = chatThread.SystemPrompt,
};
// Prepare the Google HTTP chat request:
var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
{
Role = n.Role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",
},
Content = n.Content switch
{
ContentText text => text.Text,
_ => string.Empty,
}
}).ToList()],
// Right now, we only support streaming completions:
Stream = true,
}, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json");
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
// Open the response stream:
var geminiStream = await response.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(geminiStream);
// Read the stream, line by line:
while(!streamReader.EndOfStream)
{
// Check if the token is canceled:
if(token.IsCancellationRequested)
yield break;
// Read the next line:
var line = await streamReader.ReadLineAsync(token);
// Skip empty lines:
if(string.IsNullOrWhiteSpace(line))
continue;
// Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines:
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
continue;
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
yield break;
ResponseStreamLine geminiResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
geminiResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
}
catch
{
// Skip invalid JSON data:
continue;
}
// Skip empty responses:
if(geminiResponse == default || geminiResponse.Choices.Count == 0)
continue;
// Yield the response:
yield return geminiResponse.Choices[0].Delta.Content;
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels(token, apiKeyProvisional);
}
/// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Provider.Model>());
}
#endregion
private async Task<IEnumerable<Provider.Model>> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
{
var secretKey = apiKeyProvisional switch
{
not null => apiKeyProvisional,
_ => await RUST_SERVICE.GetAPIKey(this) switch
{
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null,
}
};
if (secretKey is null)
return [];
var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
return modelResponse.Models.Where(model =>
model.Name.StartsWith("models/gemini-", StringComparison.InvariantCultureIgnoreCase))
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName));
}
}
@@ -0,0 +1,17 @@
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Groq;
/// <summary>
/// The Groq chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
/// <param name="Seed">The seed for the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream,
int Seed
);
@@ -0,0 +1,191 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Groq;
public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/openai/v1/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
#region Implementation of IProvider
/// <inheritdoc />
public string Id => "Groq";
/// <inheritdoc />
public string InstanceName { get; set; } = "Groq";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
{
Role = "system",
Content = chatThread.SystemPrompt,
};
// Prepare the OpenAI HTTP chat request:
var groqChatRequest = JsonSerializer.Serialize(new ChatRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
{
Role = n.Role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",
},
Content = n.Content switch
{
ContentText text => text.Text,
_ => string.Empty,
}
}).ToList()],
Seed = chatThread.Seed,
// Right now, we only support streaming completions:
Stream = true,
}, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json");
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
// Open the response stream:
var groqStream = await response.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(groqStream);
// Read the stream, line by line:
while(!streamReader.EndOfStream)
{
// Check if the token is canceled:
if(token.IsCancellationRequested)
yield break;
// Read the next line:
var line = await streamReader.ReadLineAsync(token);
// Skip empty lines:
if(string.IsNullOrWhiteSpace(line))
continue;
// Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines:
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
continue;
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
yield break;
ResponseStreamLine groqResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
groqResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
}
catch
{
// Skip invalid JSON data:
continue;
}
// Skip empty responses:
if(groqResponse == default || groqResponse.Choices.Count == 0)
continue;
// Yield the response:
yield return groqResponse.Choices[0].Delta.Content;
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels(token, apiKeyProvisional);
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult<IEnumerable<Model>>(Array.Empty<Model>());
}
#endregion
private async Task<IEnumerable<Model>> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
{
var secretKey = apiKeyProvisional switch
{
not null => apiKeyProvisional,
_ => await RUST_SERVICE.GetAPIKey(this) switch
{
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null,
}
};
if (secretKey is null)
return [];
var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
return modelResponse.Data.Where(n =>
!n.Id.StartsWith("whisper-", StringComparison.InvariantCultureIgnoreCase) &&
!n.Id.StartsWith("distil-", StringComparison.InvariantCultureIgnoreCase));
}
}
@@ -10,8 +10,10 @@ public enum LLMProviders
OPEN_AI = 1,
ANTHROPIC = 2,
MISTRAL = 3,
GOOGLE = 7,
FIREWORKS = 5,
GROQ = 6,
SELF_HOSTED = 4,
}
@@ -1,5 +1,7 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Google;
using AIStudio.Provider.Groq;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
@@ -7,7 +9,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider;
public static class ProvidersExtensions
public static class LLMProvidersExtensions
{
/// <summary>
/// Returns the human-readable name of the provider.
@@ -21,7 +23,9 @@ public static class ProvidersExtensions
LLMProviders.OPEN_AI => "OpenAI",
LLMProviders.ANTHROPIC => "Anthropic",
LLMProviders.MISTRAL => "Mistral",
LLMProviders.GOOGLE => "Google",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
LLMProviders.SELF_HOSTED => "Self-hosted",
@@ -48,6 +52,10 @@ public static class ProvidersExtensions
"https://openai.com/enterprise-privacy/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.GOOGLE => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://ai.google.dev/gemini-api/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.GROQ => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://wow.groq.com/terms-of-use/").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
@@ -57,6 +65,35 @@ public static class ProvidersExtensions
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
};
/// <summary>
/// Determines if the specified provider supports embeddings.
/// </summary>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>True if the provider supports embeddings; otherwise, false.</returns>
public static bool ProvideEmbeddings(this LLMProviders llmProvider) => llmProvider switch
{
//
// Providers that support embeddings:
//
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
LLMProviders.GOOGLE => true,
//
// Providers that do not support embeddings:
//
LLMProviders.GROQ => false,
LLMProviders.ANTHROPIC => false,
LLMProviders.FIREWORKS => false,
//
// Self-hosted providers are treated as a special case anyway.
//
LLMProviders.SELF_HOSTED => false,
_ => false,
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
@@ -72,7 +109,9 @@ public static class ProvidersExtensions
LLMProviders.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
@@ -173,7 +173,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
return modelResponse.Data.Where(n =>
!n.Id.StartsWith("code", StringComparison.InvariantCulture) &&
!n.Id.Contains("embed", StringComparison.InvariantCulture))
.Select(n => new Provider.Model(n.Id));
.Select(n => new Provider.Model(n.Id, null));
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+24 -2
View File
@@ -4,11 +4,33 @@ namespace AIStudio.Provider;
/// The data model for the model to use.
/// </summary>
/// <param name="Id">The model's ID.</param>
public readonly record struct Model(string Id)
/// <param name="DisplayName">The model's display name.</param>
public readonly record struct Model(string Id, string? DisplayName)
{
#region Overrides of ValueType
public override string ToString() => string.IsNullOrWhiteSpace(this.Id) ? "no model selected" : this.Id;
public override string ToString()
{
if(!string.IsNullOrWhiteSpace(this.DisplayName))
return this.DisplayName;
if(!string.IsNullOrWhiteSpace(this.Id))
return this.Id;
return "no model selected";
}
#endregion
#region Implementation of IEquatable<Model?>
public bool Equals(Model? other)
{
if(other is null)
return false;
return this.Id == other.Value.Id;
}
#endregion
}
@@ -1,3 +1,4 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
@@ -23,6 +24,9 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true);
// Prepare the system prompt:
var systemPrompt = new Message
{
@@ -62,68 +66,83 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
MaxTokens = -1,
}, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
// Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
// Open the response stream:
var providerStream = await response.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(providerStream);
// Read the stream, line by line:
while(!streamReader.EndOfStream)
StreamReader? streamReader = default;
try
{
// Check if the token is canceled:
if(token.IsCancellationRequested)
yield break;
// Read the next line:
var line = await streamReader.ReadLineAsync(token);
// Skip empty lines:
if(string.IsNullOrWhiteSpace(line))
continue;
// Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines:
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
continue;
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
yield break;
// Set the authorization header:
if (requestedSecret.Success)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
ResponseStreamLine providerResponse;
try
// Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
// Open the response stream:
var providerStream = await response.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line:
streamReader = new StreamReader(providerStream);
}
catch(Exception e)
{
this.logger.LogError($"Failed to stream chat completion from self-hosted provider '{this.InstanceName}': {e.Message}");
}
if (streamReader is not null)
{
// Read the stream, line by line:
while (!streamReader.EndOfStream)
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
// Check if the token is canceled:
if (token.IsCancellationRequested)
yield break;
// Read the next line:
var line = await streamReader.ReadLineAsync(token);
// Skip empty lines:
if (string.IsNullOrWhiteSpace(line))
continue;
// Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines:
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
continue;
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
yield break;
ResponseStreamLine providerResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
}
catch
{
// Skip invalid JSON data:
continue;
}
// Skip empty responses:
if (providerResponse == default || providerResponse.Choices.Count == 0)
continue;
// Yield the response:
yield return providerResponse.Choices[0].Delta.Content;
}
catch
{
// Skip invalid JSON data:
continue;
}
// Skip empty responses:
if(providerResponse == default || providerResponse.Choices.Count == 0)
continue;
// Yield the response:
yield return providerResponse.Choices[0].Delta.Content;
}
}
@@ -145,17 +164,31 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
case Host.LLAMACPP:
// Right now, llama.cpp only supports one model.
// There is no API to list the model(s).
return [ new Provider.Model("as configured by llama.cpp") ];
return [ new Provider.Model("as configured by llama.cpp", null) ];
case Host.LM_STUDIO:
case Host.OLLAMA:
var secretKey = apiKeyProvisional switch
{
not null => apiKeyProvisional,
_ => await RUST_SERVICE.GetAPIKey(this, isTrying: true) switch
{
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null,
}
};
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
if(secretKey is not null)
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
if(!lmStudioResponse.IsSuccessStatusCode)
return [];
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
return lmStudioModelResponse.Data.Select(n => new Provider.Model(n.Id));
return lmStudioModelResponse.Data.Select(n => new Provider.Model(n.Id, null));
}
return [];
+2
View File
@@ -21,5 +21,7 @@ public sealed partial class Routes
public const string ASSISTANT_LEGAL_CHECK = "/assistant/legal-check";
public const string ASSISTANT_SYNONYMS = "/assistant/synonyms";
public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks";
public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting";
public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day";
// ReSharper restore InconsistentNaming
}
@@ -58,6 +58,13 @@ public static class ConfigurationSelectDataFactory
yield return new("Delete temporary chats older than 1 year", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_365_DAYS);
}
public static IEnumerable<ConfigurationSelectData<WorkspaceDisplayBehavior>> GetWorkspaceDisplayBehaviorData()
{
yield return new("Toggle the overlay: the chat uses all the space, workspaces are temporarily shown", WorkspaceDisplayBehavior.TOGGLE_OVERLAY);
yield return new("Toggle the sidebar: show the workspaces next to the chat when desired", WorkspaceDisplayBehavior.TOGGLE_SIDEBAR);
yield return new("Sidebar is always visible: show the workspaces next to the chat all the time", WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
}
public static IEnumerable<ConfigurationSelectData<NavBehavior>> GetNavBehaviorData()
{
yield return new("Navigation expands on mouse hover", NavBehavior.EXPAND_ON_HOVER);
@@ -0,0 +1,34 @@
namespace AIStudio.Settings.DataModel;
public sealed class Bias
{
/// <summary>
/// The unique identifier of the bias.
/// </summary>
public Guid Id { get; init; } = Guid.Empty;
/// <summary>
/// In which category the bias is located.
/// </summary>
public BiasCategory Category { get; set; } = BiasCategory.NONE;
/// <summary>
/// The bias name.
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// The bias description.
/// </summary>
public string Description { get; init; } = string.Empty;
/// <summary>
/// Related bias.
/// </summary>
public IReadOnlyList<Guid> Related { get; init; } = [];
/// <summary>
/// Related links.
/// </summary>
public IReadOnlyList<string> Links { get; init; } = [];
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,11 @@
namespace AIStudio.Settings.DataModel;
public enum BiasCategory
{
NONE,
WHAT_SHOULD_WE_REMEMBER,
TOO_MUCH_INFORMATION,
NOT_ENOUGH_MEANING,
NEED_TO_ACT_FAST,
}
@@ -0,0 +1,55 @@
namespace AIStudio.Settings.DataModel;
public static class BiasCategoryExtensions
{
public static string ToName(this BiasCategory biasCategory) => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning",
BiasCategory.NEED_TO_ACT_FAST => "Need to act fast",
_ => "Unknown category",
};
public static string GetThoughts(this BiasCategory biasCategory) => biasCategory switch
{
BiasCategory.WHAT_SHOULD_WE_REMEMBER =>
"""
- We store memories differently based on how they were experienced
- We reduce events and lists to their key elements
- We discard specifics to form generalities
- We edit and reinforce some memories after the fact
""",
BiasCategory.TOO_MUCH_INFORMATION =>
"""
- We notice things already primed in memory or repeated often
- Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things
- We notice when something has changed
- We are drawn to details that confirm our own existing beliefs
- We notice flaws in others more easily than we notice flaws in ourselves
""",
BiasCategory.NOT_ENOUGH_MEANING =>
"""
- We tend to find stories and patterns even when looking at sparse data
- We fill in characteristics from stereotypes, generalities, and prior histories
- We imagine things and people we’re familiar with or fond of as better
- We simplify probabilities and numbers to make them easier to think about
- We project our current mindset and assumptions onto the past and future
- We think we know what other people are thinking
""",
BiasCategory.NEED_TO_ACT_FAST =>
"""
- We favor simple-looking options and complete information over complex, ambiguous options
- To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions
- To get things done, we tend to complete things we’ve invested time & energy in
- To stay focused, we favor the immediate, relatable thing in front of us
- To act, we must be confident we can make an impact and feel what we do is important
""",
_ => string.Empty,
};
}
@@ -58,11 +58,15 @@ public sealed class Data
public DataRewriteImprove RewriteImprove { get; init; } = new();
public DataEMail EMail { get; set; } = new();
public DataEMail EMail { get; init; } = new();
public DataLegalCheck LegalCheck { get; set; } = new();
public DataLegalCheck LegalCheck { get; init; } = new();
public DataSynonyms Synonyms { get; set; } = new();
public DataSynonyms Synonyms { get; init; } = new();
public DataMyTasks MyTasks { get; set; } = new();
public DataMyTasks MyTasks { get; init; } = new();
public DataJobPostings JobPostings { get; init; } = new();
public DataBiasOfTheDay BiasOfTheDay { get; init; } = new();
}
@@ -0,0 +1,61 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataBiasOfTheDay
{
/// <summary>
/// A list of bias IDs that have been used.
/// </summary>
public List<int> UsedBias { get; set; } = new();
/// <summary>
/// When was the last bias drawn?
/// </summary>
public DateOnly DateLastBiasDrawn { get; set; } = DateOnly.MinValue;
/// <summary>
/// Which bias is the bias of the day? This isn't the bias id, but rather the chat id in the bias workspace.
/// </summary>
public Guid BiasOfTheDayChatId { get; set; } = Guid.Empty;
/// <summary>
/// Which bias is the bias of the day?
/// </summary>
public Guid BiasOfTheDayId { get; set; } = Guid.Empty;
/// <summary>
/// Restrict to one bias per day?
/// </summary>
public bool RestrictOneBiasPerDay { get; set; } = true;
/// <summary>
/// Preselect any rewrite options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// Preselect the language?
/// </summary>
public CommonLanguages PreselectedTargetLanguage { get; set; }
/// <summary>
/// Preselect any other language?
/// </summary>
public string PreselectedOtherLanguage { get; set; } = string.Empty;
/// <summary>
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
/// <summary>
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
@@ -0,0 +1,66 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataJobPostings
{
/// <summary>
/// Do we want to preselect any translator options?
/// </summary>
public bool PreselectOptions { get; set; }
/// <summary>
/// The mandatory information for the job posting.
/// </summary>
public string PreselectedMandatoryInformation { get; set; } = string.Empty;
/// <summary>
/// The job description.
/// </summary>
public string PreselectedJobDescription { get; set; } = string.Empty;
/// <summary>
/// The qualifications required for the job.
/// </summary>
public string PreselectedQualifications { get; set; } = string.Empty;
/// <summary>
/// What are the responsibilities of the job?
/// </summary>
public string PreselectedResponsibilities { get; set; } = string.Empty;
/// <summary>
/// Which company name should be preselected?
/// </summary>
public string PreselectedCompanyName { get; set; } = string.Empty;
/// <summary>
/// Where should the work be done?
/// </summary>
public string PreselectedWorkLocation { get; set; } = string.Empty;
/// <summary>
/// The preselected country legal framework.
/// </summary>
public string PreselectedCountryLegalFramework { get; set; } = string.Empty;
/// <summary>
/// Preselect the target language?
/// </summary>
public CommonLanguages PreselectedTargetLanguage { get; set; } = CommonLanguages.EN_US;
/// <summary>
/// Preselect any other language?
/// </summary>
public string PreselectOtherLanguage { get; set; } = string.Empty;
/// <summary>
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
/// <summary>
/// The preselected translator provider.
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
@@ -11,4 +11,14 @@ public sealed class DataWorkspace
/// The chat storage maintenance behavior.
/// </summary>
public WorkspaceStorageTemporaryMaintenancePolicy StorageTemporaryMaintenancePolicy { get; set; } = WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_90_DAYS;
/// <summary>
/// The behavior used for displaying the workspace.
/// </summary>
public WorkspaceDisplayBehavior DisplayBehavior { get; set; } = WorkspaceDisplayBehavior.TOGGLE_SIDEBAR;
/// <summary>
/// Indicates whether the sidebar is currently visible.
/// </summary>
public bool IsSidebarVisible { get; set; } = true;
}
@@ -0,0 +1,9 @@
namespace AIStudio.Settings.DataModel;
public enum WorkspaceDisplayBehavior
{
TOGGLE_OVERLAY,
TOGGLE_SIDEBAR,
SIDEBAR_ALWAYS_VISIBLE,
}
@@ -15,6 +15,8 @@ public enum Components
LEGAL_CHECK_ASSISTANT,
SYNONYMS_ASSISTANT,
MY_TASKS_ASSISTANT,
JOB_POSTING_ASSISTANT,
BIAS_DAY_ASSISTANT,
CHAT,
}
@@ -5,6 +5,14 @@ namespace AIStudio.Tools;
public static class ComponentsExtensions
{
public static bool AllowSendTo(this Components component) => component switch
{
Components.NONE => false,
Components.BIAS_DAY_ASSISTANT => false,
_ => true,
};
public static string Name(this Components component) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => "Grammar & Spelling Assistant",
@@ -18,6 +26,7 @@ public static class ComponentsExtensions
Components.LEGAL_CHECK_ASSISTANT => "Legal Check Assistant",
Components.SYNONYMS_ASSISTANT => "Synonym Assistant",
Components.MY_TASKS_ASSISTANT => "My Tasks Assistant",
Components.JOB_POSTING_ASSISTANT => "Job Posting Assistant",
Components.CHAT => "New Chat",
@@ -37,6 +46,7 @@ public static class ComponentsExtensions
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
@@ -56,6 +66,8 @@ public static class ComponentsExtensions
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
_ => default,
};
@@ -73,6 +85,8 @@ public static class ComponentsExtensions
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : default,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default,
@@ -86,6 +100,7 @@ public static class ComponentsExtensions
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProfile) : default,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) : default,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) : default,
+3
View File
@@ -16,6 +16,8 @@ public enum Event
// Chat events:
HAS_CHAT_UNSAVED_CHANGES,
RESET_CHAT_STATE,
LOAD_CHAT,
CHAT_STREAMING_DONE,
// Send events:
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
@@ -30,4 +32,5 @@ public enum Event
SEND_TO_LEGAL_CHECK_ASSISTANT,
SEND_TO_SYNONYMS_ASSISTANT,
SEND_TO_MY_TASKS_ASSISTANT,
SEND_TO_JOB_POSTING_ASSISTANT,
}
@@ -6,7 +6,7 @@ public static class JsRuntimeExtensions
{
public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2)
{
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantBase.RESULT_DIV_ID, AssistantBase.AFTER_RESULT_DIV_ID);
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantBase.RESULT_DIV_ID, AssistantBase.BEFORE_RESULT_DIV_ID);
}
public static async Task ClearDiv(this IJSRuntime jsRuntime, string divId)
+3
View File
@@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct LoadChat(Guid WorkspaceId, Guid ChatId);
@@ -1,3 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct SelectSecretRequest(string Destination, string UserName);
public readonly record struct SelectSecretRequest(string Destination, string UserName, bool IsTrying);
+8 -6
View File
@@ -253,24 +253,26 @@ public sealed class RustService : IDisposable
throw;
}
}
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to get the API key for.</param>
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IProvider provider)
public async Task<RequestedSecret> GetAPIKey(IProvider provider, bool isTrying = false)
{
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, isTrying);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
if(!isTrying)
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
}
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
if (!secret.Success)
if (!secret.Success && !isTrying)
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
return secret;
@@ -307,7 +309,7 @@ public sealed class RustService : IDisposable
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
{
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, false);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
@@ -0,0 +1,84 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Tools;
public static class WorkspaceBehaviour
{
public static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
}
};
public static bool IsChatExisting(LoadChat loadChat)
{
var chatPath = loadChat.WorkspaceId == Guid.Empty
? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString())
: Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString());
return Directory.Exists(chatPath);
}
public static async Task StoreChat(ChatThread chat)
{
string chatDirectory;
if (chat.WorkspaceId == Guid.Empty)
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
else
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
// Ensure the directory exists:
Directory.CreateDirectory(chatDirectory);
// Save the chat name:
var chatNamePath = Path.Join(chatDirectory, "name");
await File.WriteAllTextAsync(chatNamePath, chat.Name);
// Save the thread as thread.json:
var chatPath = Path.Join(chatDirectory, "thread.json");
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
}
public static async Task<ChatThread?> LoadChat(LoadChat loadChat)
{
var chatPath = loadChat.WorkspaceId == Guid.Empty
? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString())
: Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString());
if(!Directory.Exists(chatPath))
return null;
try
{
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
return chat;
}
catch (Exception)
{
return null;
}
}
public static async Task<string> LoadWorkspaceName(Guid workspaceId)
{
if(workspaceId == Guid.Empty)
return string.Empty;
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
return await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
}
}
+5 -2
View File
@@ -63,8 +63,11 @@ def "main fix_web_assets" []: nothing -> nothing {
mkdir wwwroot/system
# Copy the web assets from the first RID to the source project:
let source_path: glob = $"bin/release/net8.0/($rid)/publish/wwwroot/_content/*"
cp --recursive --force --update $source_path wwwroot/system/
let source_paths = glob --depth 99 bin/release/net8.0/($rid)/publish/wwwroot/_content/*
for source_path in $source_paths {
cp --recursive --force --update $source_path wwwroot/system/
}
}
def "main publish" []: nothing -> nothing {
+17 -17
View File
@@ -4,30 +4,30 @@
"net8.0": {
"HtmlAgilityPack": {
"type": "Direct",
"requested": "[1.11.65, )",
"resolved": "1.11.65",
"contentHash": "Skse4MVlRcHHyP88x+v/uQrMxTPaBIwQodiQ/y59jxs8vHlqTR5M3jt5Wk+csYFnR4rX6I4ndqDji2M8p9kAzw=="
"requested": "[1.11.71, )",
"resolved": "1.11.71",
"contentHash": "HlEThQBnob4/29DIiyrPSYlGXXSZUtOH7571PRev+yu8FW8n5pdxqHyun+PMm17cp3ovCrxFhXedC+1HUSdRMA=="
},
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
"requested": "[8.0.8, )",
"resolved": "8.0.8",
"contentHash": "fefQMlaOIQnc7cn7R/GWCwnA6X0tv5pB9ZKIR6I1JZPmYcoCNY7HtLt0bJPMj+F0StrMkwTk2cPtMGtr9zhkoA==",
"requested": "[8.0.11, )",
"resolved": "8.0.11",
"contentHash": "1tn+c7d628+dASqo0UezdeWdKrEpp2XA376MUkr4nFYDj295iPWeWOV8rOvgR0Gma1xfOEjNJur1QvS6AVnjng==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.8, )",
"resolved": "8.0.8",
"contentHash": "P8wR6MUWwYXIjPJuBaZgo5zlI/GWI6QEAo6NyVIbPefa9CCkohYu7dP2rD/mrqnjEqfRHyl+h9VZrDoGpELqYg=="
"requested": "[8.0.11, )",
"resolved": "8.0.11",
"contentHash": "zk5lnZrYJgtuJG8L4v17Ej8rZ3PUcR2iweNV08BaO5LbYHIi2wNaVNcJoLxvqgQdnjLlKnCCfVGLDr6QHeAarQ=="
},
"MudBlazor": {
"type": "Direct",
"requested": "[7.8.0, )",
"resolved": "7.8.0",
"contentHash": "DivLaZKVkL4z8jVeqa1P2n3u5Dny8l/K1nCLrzp8hI+efdE2Rd1KDkMqoaGdjgV9jCEJ3D35r1+/w6V4yPzuaw==",
"requested": "[7.15.0, )",
"resolved": "7.15.0",
"contentHash": "b2+hO7ZTGwsGhu9SoowuNe2ETeF1GzExpg/hIYR++rSh+vy4v/QD0iOsrATWv3LdFJXSgk3d1+XIr5srW3jBiA==",
"dependencies": {
"Microsoft.AspNetCore.Components": "8.0.8",
"Microsoft.AspNetCore.Components.Web": "8.0.8",
@@ -36,12 +36,12 @@
},
"MudBlazor.Markdown": {
"type": "Direct",
"requested": "[7.8.0, )",
"resolved": "7.8.0",
"contentHash": "XaspHsS5I9osnna46Q/tH7cy3OlgG7dnycZgn7KhkTBNqpFzelrBoMkf/YFjRWSWEHEeO2o8AKjrG6uo99OYsw==",
"requested": "[7.14.0, )",
"resolved": "7.14.0",
"contentHash": "c6kFfMgUxKxYK1AkGHRNg5B9jw+iwxelBuDpaI5N+guOleT37BxYetYlD0iOew/n1plhobynUJn3Kk4MXhv6RA==",
"dependencies": {
"Markdig": "0.37.0",
"MudBlazor": "7.8.0"
"MudBlazor": "7.14.0"
}
},
"ReverseMarkdown": {
@@ -178,6 +178,6 @@
"contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
}
},
"net8.0/osx-arm64": {}
"net8.0/osx-x64": {}
}
}
+15
View File
@@ -54,4 +54,19 @@
.custom-icon-color > a > svg {
color: var(--custom-icon-color) !important;
}
.improvedDiff .wikEdDiffFragment {
background-color: initial !important;
}
.improvedDiff .wikEdDiffNoChange {
background-color: initial !important;
}
.improvedDiff .wikEdDiffDelete {
text-decoration-line: line-through;
text-decoration-style: double;
text-decoration-color: red;
text-decoration-thickness: 2px;
}
+1 -1
View File
@@ -2,7 +2,7 @@ window.generateDiff = function (text1, text2, divDiff, divLegend) {
let wikEdDiff = new WikEdDiff();
let targetDiv = document.getElementById(divDiff)
targetDiv.innerHTML = wikEdDiff.diff(text1, text2);
targetDiv.classList.add('mud-typography-body1');
targetDiv.classList.add('mud-typography-body1', 'improvedDiff');
let legend = document.getElementById(divLegend);
legend.innerHTML = `
@@ -0,0 +1,8 @@
# v0.9.12, build 187 (2024-09-15 20:49 UTC)
- Added a job posting assistant to the business category.
- Improved text diff viewer by line-through deleted text.
- Improved grammar & spelling check and rewrite assistants by showing the final text as well.
- Fixed the dark theme for the text diff viewer, e.g., in the grammar check assistant.
- Fixed margin-related issue in the `ConfigurationText` component.
- Fixed chat conversion of job posting, legal check, and my task assistants.
- Refactored the `ConfigurationText` component to debounce the input field to prevent unnecessary configuration updates. The component now also supports multiline text.
@@ -0,0 +1,2 @@
# v0.9.13, build 188 (2024-10-07 11:18 UTC)
- Allow the use of an API key for self-hosted `ollama` instances. Useful when using `ollama` with, e.g., Open WebUI.
@@ -0,0 +1,6 @@
# v0.9.14, build 189 (2024-10-18 08:48 UTC)
- Added our first contributor @richard-stanton to the supporter page.
- Upgraded to .NET 8.0.10
- Upgraded to Rust 1.82.0
- Upgraded to Tauri 1.8.1
- Upgraded to MudBlazor 7.13.0
@@ -0,0 +1,8 @@
# v0.9.15, build 190 (2024-10-28 15:04 UTC)
- Added bias-of-the-day assistant: the assistant will provide a cognitive bias every day. The bias is explained in detail, and the user can ask questions about it.
- Upgraded `MudBlazor` to v7.14.0.
- Upgraded `MudBlazor.Markdown` to v7.14.0.
- Upgraded `tokio` to v1.41.
- Upgraded `keyring` to v3.6.
- Upgraded `arboard` to v3.4.1.
- Upgraded `HtmlAgilityPack` to v1.11.70.
@@ -0,0 +1,6 @@
# v0.9.16, build 191 (2024-11-02 22:04 UTC)
- Added our second financial contributor: Thanks `peerschuett` for supporting AI Studio financially.
- Added multiple workspace display options: toggle the workspace overlay (this is the previous behavior), toggle the workspace sidebar, or permanently display the workspace sidebar.
- Improved the layout of the app window, for example, in full-screen mode on large or high-resolution monitors.
- Fixed the "send to" issue, where you could select the bias-of-the-day assistant as destination.
- Fixed the appearance of the "no diff" message for text diffs when the dark mode is enabled.
@@ -0,0 +1,2 @@
# v0.9.17, build 192 (2024-11-03 11:11 UTC)
- Added new Anthropic model `claude-3-5-sonnet-20241022` as well as the aliases `claude-3-5-sonnet-latest` and `claude-3-opus-latest`.
@@ -0,0 +1,7 @@
# v0.9.18, build 193 (2024-11-09 21:10 UTC)
- Added new Anthropic model `claude-3-5-heiku-20241022` as well as the alias `claude-3-5-heiku-latest`.
- Added [Groq](https://console.groq.com/) as a provider option.
- Added Google Gemini as a provider option.
- Improved model display while configuring a provider when the provider supports display names.
- Improved the bias catalog by cleaning up some artifacts.
- Refactored the runtime code as preparation for the integration of RAG (integrating your data and files in AI Studio).
@@ -0,0 +1,7 @@
# v0.9.19, build 194 (2024-11-14 05:58 UTC)
- Added provider's metadata: which providers offer embeddings for RAG? This is in preparation for integrating your data and files.
- Added `Bessieres` as a one-time contributor. Thank you for your support.
- Fixed a bug regarding hostnames of self-hosted providers, where it was possible to enter spaces at the end.
- Upgraded .NET to v8.0.11.
- Upgraded HtmlAgilityPack to v1.11.71.
- Upgraded MudBlazor to v7.15.0.
@@ -0,0 +1,2 @@
# v0.9.20, build 195 (2024-11-16 20:44 UTC)
- Fixed bugs related to storing and loading chats when the workspaces were docked to the left and were collapsed.
@@ -1 +1 @@
.mud-markdown-body code:not(.hljs){background:var(--mud-palette-action-disabled-background);color:var(--mud-palette-text-primary)!important;padding:2.5px 7.5px;border-radius:5px}pre code.hljs{display:block!important;overflow-x:auto;padding:1em}.mud-markdown-body .snippet-clipboard-content{position:relative!important}.mud-markdown-body .snippet-clipboard-content:hover>.snippet-clipboard-copy-icon{display:block!important}.mud-markdown-body .snippet-clipboard-content .snippet-clipboard-copy-icon{position:absolute;display:none;top:0;right:0}.mud-markdown-body blockquote{border-left:.25em solid var(--mud-palette-text-disabled);color:var(--mud-palette-text-secondary);background-color:var(--mud-palette-table-hover);padding:.25em 1em;margin:.5em 0 1.25em}.mud-markdown-body blockquote p{margin-bottom:0!important}.mud-markdown-body table{margin:1.25em 0}.mud-markdown-body .mud-link:hover{cursor:pointer!important}.mud-markdown-body ul{list-style-type:disc}.mud-markdown-body ol,.mud-markdown-body ul{padding-left:2em;margin-bottom:1.25em!important}.mud-markdown-body ul ul{list-style-type:circle;margin-bottom:0!important}.mud-markdown-body ul ul ul{list-style-type:square;margin-bottom:0!important}.mud-markdown-body li{display:list-item!important;text-align:-webkit-match-parent}.mud-markdown-body h1,.mud-markdown-body h2{border-bottom:1px solid var(--mud-palette-text-disabled);padding-bottom:.125em;margin-bottom:.4em}.mud-markdown-body h1,.mud-markdown-body h2,.mud-markdown-body h3,.mud-markdown-body h4,.mud-markdown-body h5,.mud-markdown-body h6{scroll-margin-top:5rem;margin-top:.25em;word-wrap:break-word;margin-bottom:.3em!important}.mud-markdown-body .mud-divider{margin:.5em 0;height:.25em}.mud-markdown-body p{margin-bottom:1.25em!important}.mud-markdown-body .mud-expand-panel p,.mud-markdown-body li p{margin-bottom:0!important}.mud-markdown-body img{max-width:100%}
.mud-markdown-body code:not(.hljs){background:var(--mud-palette-action-disabled-background);color:var(--mud-palette-text-primary)!important;padding:2.5px 7.5px;border-radius:5px}pre code.hljs{display:block!important;overflow-x:auto;padding:1em}.mud-markdown-body .snippet-clipboard-content{position:relative!important}.mud-markdown-body .snippet-clipboard-content:hover>.snippet-clipboard-copy-icon{display:block!important}.mud-markdown-body .snippet-clipboard-content .snippet-clipboard-copy-icon{position:absolute;display:none;top:0;right:0}.mud-markdown-body blockquote{border-left:.25em solid var(--mud-palette-text-disabled);color:var(--mud-palette-text-secondary);background-color:var(--mud-palette-table-hover);padding:.25em 1em;margin:.5em 0 1.25em}.mud-markdown-body blockquote p{margin-bottom:0!important}.mud-markdown-body table{margin:1.25em 0}.mud-markdown-body .mud-link:hover{cursor:pointer!important}.mud-markdown-body ul{list-style-type:disc}.mud-markdown-body ol,.mud-markdown-body ul{padding-left:2em;margin-bottom:1.25em!important}.mud-markdown-body ul ul{list-style-type:circle;margin-bottom:0!important}.mud-markdown-body ul ul ul{list-style-type:square;margin-bottom:0!important}.mud-markdown-body li{display:list-item!important;text-align:-webkit-match-parent}.mud-markdown-body h1,.mud-markdown-body h2{border-bottom:1px solid var(--mud-palette-text-disabled);padding-bottom:.125em;margin-bottom:.4em}.mud-markdown-body h1,.mud-markdown-body h2,.mud-markdown-body h3,.mud-markdown-body h4,.mud-markdown-body h5,.mud-markdown-body h6{scroll-margin-top:5rem;margin-top:.25em;word-wrap:break-word;margin-bottom:.3em!important}.mud-markdown-body .mud-divider{margin:.5em 0;height:.25em}.mud-markdown-body p{margin-bottom:1.25em!important}.mud-markdown-body .mud-expand-panel p,.mud-markdown-body li p{margin-bottom:0!important}.mud-markdown-body p .markdown-error{white-space:pre}.mud-markdown-body img{max-width:100%}
@@ -1 +1 @@
(()=>{var e={2689:e=>{e.exports=function(e){var t="built_in",a={},n={begin:"}",className:t,endsParent:!0},i={begin:"{",end:"}",contains:[e.QUOTE_STRING_MODE,"self"]},r=e.COMMENT("@\\*","\\*@",{relevance:10}),o={begin:"@[A-Za-z0-9\\._:-]+",returnBegin:!0,end:"(\\r|\\n|<|\\s|\"|')",subLanguage:"csharp",contains:[{begin:"@",className:t},{begin:"\\[",end:"\\]",skip:!0},{begin:"\\(",end:"\\)",skip:!0}],returnEnd:!0},s={begin:"[@]{0,1}<text>",returnBegin:!0,end:"</text>",returnEnd:!0,subLanguage:"cshtml-razor",contains:[{begin:"[@]{0,1}<text>",className:t},{begin:"</text>",className:t,endsParent:!0}]},l={begin:"@\\(",end:"\\)",returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@\\(",className:t},{begin:"\\(",end:"\\)",subLanguage:"csharp",contains:[e.QUOTE_STRING_MODE,"self",s]},s,{begin:"\\)",className:t,endsParent:!0}]},c=function(e,t){var a={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",variants:[{begin:/"/,end:/"/,contains:t},{begin:/'/,end:/'/,contains:t},{begin:/[^\s"'=<>`]+/}]}]}]};return[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[a],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[a],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},a]}].concat(t)}(e,[o,l]),_="^\\s*@(page|model|using|inherits|inject|layout)",d={begin:_+"[^\\r\\n{\\(]*$",end:"$",returnBegin:!0,returnEnd:!0,contains:[{begin:_,className:t},{variants:[{begin:"\\r|\\n",endsParent:!0},{begin:"\\s[^\\r\\n]+",end:"$"},{begin:"$"}],className:"type",endsParent:!0}]},m={variants:[{begin:"@\\{",end:"}"},{begin:"@code\\s*\\{",end:"}"}],returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@(code\\s*)?\\{",className:t},a,{begin:"{",end:"}",contains:["self"],skip:!0},n]},p={begin:"^\\s*@helper[\\s]*[^{]+[\\s]*{",returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:"@helper",className:t},{begin:"{",className:t},n]},u=[{begin:"@for[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@if[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@switch[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@while[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@using[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@lock[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@foreach[\\s]*\\([^{]+[\\s]*{",end:"}"}],g={variants:u,returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{variants:u.map((function(e){return{begin:e.begin}})),returnBegin:!0,contains:[{begin:"@",className:t},{variants:u.map((function(e){return{begin:e.begin.substr(1,e.begin.length-2)}})),subLanguage:"csharp"},{begin:"{",className:t}]},a,{variants:[{begin:"}[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{begin:"}[\\s]*else[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:t},{variants:[{begin:"[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{begin:"[\\s]*else[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:t}]},i,n]},E={begin:"@try[\\s]*{",end:"}",returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@",className:t},{begin:"try[\\s]*{",subLanguage:"csharp"},{variants:[{begin:"}[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*{"},{begin:"}[\\s]*finally[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:t},{variants:[{begin:"[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*"},{begin:"[\\s]*finally[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:t}]},a,i,n]},S="@section[\\s]+[a-zA-Z0-9]+[\\s]*{",b=[d,p,m,g,{begin:S,returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:S,className:t},i,n]},{begin:"@await ",returnBegin:!0,subLanguage:"csharp",end:"(\\r|\\n|<|\\s)",contains:[{begin:"@await ",className:t},{begin:"[<\\r\\n]",endsParent:!0}]},E,{variants:[{begin:"@@"},{begin:"[a-zA-Z]+@"}],skip:!0},s,r,l,{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10}].concat(c);return[m,g,E].forEach((function(e){var t=b.filter((function(t){return t!==e})),n=e.contains.indexOf(a);e.contains.splice.apply(e.contains,[n,1].concat(t))})),{aliases:["cshtml","razor","razor-cshtml","cshtml-razor"],contains:b}}},3390:e=>{function t(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((a=>{const n=e[a],i=typeof n;"object"!==i&&"function"!==i||Object.isFrozen(n)||t(n)})),e}class a{constructoLine truncated
(()=>{var e={2224:e=>{e.exports=function(e){var t="built_in",a={},n={begin:"}",className:t,endsParent:!0},r={begin:"{",end:"}",contains:[e.QUOTE_STRING_MODE,"self"]},i=e.COMMENT("@\\*","\\*@",{relevance:10}),o={begin:"@[A-Za-z0-9\\._:-]+",returnBegin:!0,end:"(\\r|\\n|<|\\s|\"|')",subLanguage:"csharp",contains:[{begin:"@",className:t},{begin:"\\[",end:"\\]",skip:!0},{begin:"\\(",end:"\\)",skip:!0}],returnEnd:!0},s={begin:"[@]{0,1}<text>",returnBegin:!0,end:"</text>",returnEnd:!0,subLanguage:"cshtml-razor",contains:[{begin:"[@]{0,1}<text>",className:t},{begin:"</text>",className:t,endsParent:!0}]},l={begin:"@\\(",end:"\\)",returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@\\(",className:t},{begin:"\\(",end:"\\)",subLanguage:"csharp",contains:[e.QUOTE_STRING_MODE,"self",s]},s,{begin:"\\)",className:t,endsParent:!0}]},c=function(e,t){var a={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",variants:[{begin:/"/,end:/"/,contains:t},{begin:/'/,end:/'/,contains:t},{begin:/[^\s"'=<>`]+/}]}]}]};return[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[a],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[a],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},a]}].concat(t)}(e,[o,l]),_="^\\s*@(page|model|using|inherits|inject|layout)",d={begin:_+"[^\\r\\n{\\(]*$",end:"$",returnBegin:!0,returnEnd:!0,contains:[{begin:_,className:t},{variants:[{begin:"\\r|\\n",endsParent:!0},{begin:"\\s[^\\r\\n]+",end:"$"},{begin:"$"}],className:"type",endsParent:!0}]},m={variants:[{begin:"@\\{",end:"}"},{begin:"@code\\s*\\{",end:"}"}],returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@(code\\s*)?\\{",className:t},a,{begin:"{",end:"}",contains:["self"],skip:!0},n]},p={begin:"^\\s*@helper[\\s]*[^{]+[\\s]*{",returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:"@helper",className:t},{begin:"{",className:t},n]},u=[{begin:"@for[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@if[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@switch[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@while[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@using[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@lock[\\s]*\\([^{]+[\\s]*{",end:"}"},{begin:"@foreach[\\s]*\\([^{]+[\\s]*{",end:"}"}],g={variants:u,returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{variants:u.map((function(e){return{begin:e.begin}})),returnBegin:!0,contains:[{begin:"@",className:t},{variants:u.map((function(e){return{begin:e.begin.substr(1,e.begin.length-2)}})),subLanguage:"csharp"},{begin:"{",className:t}]},a,{variants:[{begin:"}[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{begin:"}[\\s]*else[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:t},{variants:[{begin:"[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{begin:"[\\s]*else[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:t}]},r,n]},E={begin:"@try[\\s]*{",end:"}",returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@",className:t},{begin:"try[\\s]*{",subLanguage:"csharp"},{variants:[{begin:"}[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*{"},{begin:"}[\\s]*finally[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:t},{variants:[{begin:"[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*"},{begin:"[\\s]*finally[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:t}]},a,r,n]},S="@section[\\s]+[a-zA-Z0-9]+[\\s]*{",b=[d,p,m,g,{begin:S,returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:S,className:t},r,n]},{begin:"@await ",returnBegin:!0,subLanguage:"csharp",end:"(\\r|\\n|<|\\s)",contains:[{begin:"@await ",className:t},{begin:"[<\\r\\n]",endsParent:!0}]},E,{variants:[{begin:"@@"},{begin:"[a-zA-Z]+@"}],skip:!0},s,i,l,{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10}].concat(c);return[m,g,E].forEach((function(e){var t=b.filter((function(t){return t!==e})),n=e.contains.indexOf(a);e.contains.splice.apply(e.contains,[n,1].concat(t))})),{aliases:["cshtml","razor","razor-cshtml","cshtml-razor"],contains:b}}},8416:e=>{function t(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((a=>{const n=e[a],r=typeof n;"object"!==r&&"function"!==r||Object.isFrozen(n)||t(n)})),e}class a{constructoLine truncated
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more