Compare commits

...
17 Commits
Author SHA1 Message Date
Thorsten Sommer e973b51f75 Fixed Tauri TLS build issue caused by OpenSSL dependency (#280) 2025-02-09 19:01:51 +01:00
Thorsten Sommer c9474c55b7 Prepared release v0.9.28 (#279) 2025-02-09 18:06:24 +01:00
Thorsten Sommer c0b7016c3a Added the assistant option to select the default app provider (#278) 2025-02-09 16:59:26 +01:00
Thorsten Sommer 78315195cb Improved build script (#277) 2025-02-09 16:25:40 +01:00
Thorsten Sommer 6040e88dad Updated the supporter page (#276) 2025-02-09 15:04:07 +01:00
Thorsten Sommer ad3d8cbb3a Updated OpenSSL dependency to fix potential security issues (#275) 2025-02-09 13:10:20 +01:00
Thorsten Sommer c836bd7f33 Added an information view to all data sources (#273) 2025-02-09 12:36:37 +01:00
Thorsten Sommer 9d71978feb Prepared release v0.9.27 (#272) 2025-01-21 19:28:09 +01:00
Thorsten Sommer 940459ca46 Fixed an issue with tooltips at the bottom of the app (#271) 2025-01-21 19:22:26 +01:00
Thorsten Sommer 09d67bc1c2 Fixed the workspace name after re-rendering the chat component (#270) 2025-01-21 19:12:36 +01:00
Thorsten Sommer 1681405fb9 Fixed an issue with the workspace title not being updated (#269) 2025-01-21 18:58:20 +01:00
Thorsten Sommer be408a6aee Fixed message bus issues (#268) 2025-01-21 15:36:22 +01:00
Thorsten Sommer cf975f2a6c Removed the "send to" button from the ERI server assistant (#267) 2025-01-21 14:20:03 +01:00
Thorsten Sommer 1df1d3f328 Updated changelog (#266) 2025-01-21 13:52:10 +01:00
Thorsten Sommer ed14525390 Fixed hostname validation message for ERI data sources (#265) 2025-01-21 13:49:44 +01:00
Thorsten Sommer 14caf550ec Improved the inner content scrolling to use the entire space available (#263) 2025-01-21 13:40:47 +01:00
Thorsten Sommer f50e041640 Updated introduction (#264) 2025-01-21 09:13:16 +01:00
102 changed files with 2136 additions and 1581 deletions

No files matched your search

+17 -10
View File
@@ -1,4 +1,5 @@
# MindWork AI Studio # MindWork AI Studio
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
## News ## News
Things we are currently working on: Things we are currently working on:
@@ -11,7 +12,7 @@ Things we are currently working on:
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~ - [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ - [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~ - [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259))~~ - [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
- [ ] App: Implement external embedding providers - [ ] App: Implement external embedding providers
@@ -49,14 +50,17 @@ Features we have recently released:
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png) ![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png)
![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png) ![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png)
MindWork AI Studio is a desktop application available for macOS, Windows, and Linux. It provides a unified chat interface for Large Language Models (LLMs). You bring your own API key for the respective LLM provider to use the models. The API keys are securely stored by the operating system. MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provides a unified user interface for interaction with Large Language Models (LLM). AI Studio also offers so-called assistants, where prompting is not necessary. You can think of AI Studio like an email program: you bring your own API key for the LLM of your choice and can then use these AI systems with AI Studio. Whether you want to use Google Gemini, OpenAI o1, or even your own local AI models.
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
**Key advantages:** **Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes. - **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). - **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/).
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API. - **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. - **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API. - **Privacy**: You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
- **Flexibility**: Choose the provider and model best suited for your current task. - **Flexibility**: Choose the provider and model best suited for your current task.
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life. - **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
@@ -78,12 +82,15 @@ Your support, whether big or small, keeps the wheels turning and is deeply appre
## Planned Features ## Planned Features
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases: Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
- **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. - **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
- **Integration of enterprise data:** Soon, it will also be possible to integrate data from the corporate network using an interface that we have specified ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). - **Integration of enterprise data:** It will soon be possible to integrate data from the corporate network using a specified interface ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). This will likely require development work by the organization in question.
- **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. - **Useful assistants:** We'll develop more assistants for everyday tasks.
- **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. - **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
- **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. - **Specific requirements:** Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas.
- **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. - **Voice control:** You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
- **Content creation:** There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers.
- **Email monitoring:** You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
- **Browser usage:** We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser.
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩. Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.
File diff suppressed because it is too large. Load diff
-10
View File
@@ -1,10 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
-9
View File
@@ -2,10 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ERIClients", "ERIClients", "{5C2AF789-287B-4FCB-B675-7273D8CD4579}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERIClientV1", "ERIClientV1\ERIClientV1.csproj", "{9E35A273-0FA6-4BD5-8880-A1DDAC106926}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -16,12 +12,7 @@ Global
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{9E35A273-0FA6-4BD5-8880-A1DDAC106926} = {5C2AF789-287B-4FCB-B675-7273D8CD4579}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
@@ -1,10 +1,12 @@
@using AIStudio.Chat @using AIStudio.Chat
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <div class="inner-scrolling-context">
@(this.Title)
</MudText>
<InnerScrolling HeaderHeight="6em"> <MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
<InnerScrolling>
<ChildContent> <ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2"> <MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6"> <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@@ -128,4 +130,5 @@
} }
</MudStack> </MudStack>
</FooterContent> </FooterContent>
</InnerScrolling> </InnerScrolling>
</div>
@@ -147,6 +147,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBase);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -300,6 +300,8 @@ public partial class AssistantERI : AssistantBaseCore
protected override bool ShowEntireChatThread => true; protected override bool ShowEntireChatThread => true;
protected override bool ShowSendTo => false;
protected override string SubmitText => "Create the ERI server"; protected override string SubmitText => "Create the ERI server";
protected override Func<Task> SubmitAction => this.GenerateServer; protected override Func<Task> SubmitAction => this.GenerateServer;
@@ -7,7 +7,7 @@ public static class ERIVersionExtensions
try try
{ {
var url = version.SpecificationURL(); var url = version.SpecificationURL();
var response = await httpClient.GetAsync(url); using var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync(); return await response.Content.ReadAsStringAsync();
} }
catch catch
@@ -44,6 +44,8 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBlock);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -13,6 +13,8 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (203, "v0.9.28, build 203 (2025-02-09 16:33 UTC)", "v0.9.28.md"),
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"), new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"), new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"), new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"),
@@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase
private async Task ReadLogAsync() private async Task ReadLogAsync()
{ {
var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}"); using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
this.LogContent = await response.Content.ReadAsStringAsync(); this.LogContent = await response.Content.ReadAsStringAsync();
} }
} }
@@ -3,7 +3,7 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em" MinWidth="36em"> <InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" MinWidth="36em" Style="height: 100%">
<ChildContent> <ChildContent>
@if (this.ChatThread is not null) @if (this.ChatThread is not null)
{ {
@@ -40,7 +40,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private Profile currentProfile = Profile.NO_PROFILE; private Profile currentProfile = Profile.NO_PROFILE;
@@ -124,6 +124,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now."); this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
} }
if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@@ -525,17 +532,28 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.isStreaming = false; this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.userInput = string.Empty;
this.currentWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
this.currentWorkspaceName = this.ChatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName); this.WorkspaceName(this.currentWorkspaceName);
}
else
{
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
}
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{ {
this.mustScrollToBottomAfterRender = true; this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2; this.scrollRenderCountdown = 2;
this.StateHasChanged();
} }
this.StateHasChanged();
} }
private async Task ResetState() private async Task ResetState()
@@ -653,6 +671,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(ChatComponent);
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -692,6 +712,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
this.MessageBus.Unregister(this);
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
await this.SaveThread(); await this.SaveThread();
@@ -1,6 +1,6 @@
@using AIStudio.Provider @using AIStudio.Provider
<div class="d-flex"> <div class="d-flex">
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider."> <MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
@if (this.Mode is ConfidenceInfoMode.ICON) @if (this.Mode is ConfidenceInfoMode.ICON)
{ {
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/> <MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
@@ -63,6 +63,8 @@ public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDispo
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -57,6 +57,8 @@ public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDi
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationBase);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data) public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -49,6 +49,9 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
private IEnumerable<ConfigurationSelectData<string>> FilteredData() private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{ {
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
yield return new("Use app default", string.Empty);
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
foreach (var providerId in this.Data) foreach (var providerId in this.Data)
{ {
@@ -60,6 +63,8 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationProviderSelection);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -7,17 +7,17 @@
@this.HeaderContent @this.HeaderContent
</div> </div>
} }
<div class="flex-auto overflow-auto"> <div class="flex-auto overflow-auto mb-3">
@this.ChildContent @this.ChildContent
<div @ref="@this.AnchorAfterChildContent"> <div style="max-height: 0.1em;" @ref="@this.AnchorAfterChildContent">
&nbsp; &nbsp;
</div> </div>
</div> </div>
@if (this.FooterContent is not null) @if (this.FooterContent is not null)
{ {
<MudPaper Class="pa-3 border-solid border rounded-lg"> <MudPaper Class="pa-3 mb-3 border-solid border rounded-lg">
@this.FooterContent @this.FooterContent
</MudPaper> </MudPaper>
} }
@@ -9,14 +9,6 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter] [Parameter]
public bool FillEntireHorizontalSpace { get; set; } 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
/// 230px - 3em. Default is 3em.
/// </summary>
[Parameter]
public string HeaderHeight { get; set; } = "3em";
[Parameter] [Parameter]
public RenderFragment? HeaderContent { get; set; } public RenderFragment? HeaderContent { get; set; }
@@ -35,6 +27,9 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter] [Parameter]
public string? MinWidth { get; set; } public string? MinWidth { get; set; }
[Parameter]
public string Style { get; set; } = string.Empty;
[CascadingParameter] [CascadingParameter]
private MainLayout MainLayout { get; set; } = null!; private MainLayout MainLayout { get; set; } = null!;
@@ -55,6 +50,8 @@ public partial class InnerScrolling : MSGComponentBase
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(InnerScrolling);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -74,12 +71,14 @@ public partial class InnerScrolling : MSGComponentBase
#endregion #endregion
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth};"; private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0; {this.MinWidthStyle}" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0; {this.MinWidthStyle}"; private string TerminatedStyles => string.IsNullOrWhiteSpace(this.Style) ? string.Empty : $"{this.Style}; ";
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column"; private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
private string Styles => $"flex-grow: 1; overflow: hidden; {this.TerminatedStyles}{this.MinWidthStyle}";
public async Task ScrollToBottom() public async Task ScrollToBottom()
{ {
await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime); await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime);
@@ -24,6 +24,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public abstract string ComponentName { get; }
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -1,4 +1,4 @@
<MudTooltip Text="You can switch between your profiles here"> <MudTooltip Text="You can switch between your profiles here" Placement="Placement.Top">
<MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass"> <MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{ {
@@ -19,6 +19,6 @@
} }
} }
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/> <ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/> <ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
</ExpansionPanel> </ExpansionPanel>
@@ -9,7 +9,7 @@
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/> <ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/> <ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for chats?"/> <ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for chats?"/>
</MudPaper> </MudPaper>
</ExpansionPanel> </ExpansionPanel>
@@ -1,4 +1,3 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits SettingsPanelBase @inherits SettingsPanelBase
@@ -37,12 +36,7 @@
<MudTd>@this.GetEmbeddingName(context)</MudTd> <MudTd>@this.GetEmbeddingName(context)</MudTd>
<MudTd Style="text-align: left;"> <MudTd Style="text-align: left;">
@if (context is IERIDataSource) <MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"/>
{
@* <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"> *@
@* Show Information *@
@* </MudButton> *@
}
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)">
Edit Edit
</MudButton> </MudButton>
@@ -1,8 +1,7 @@
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel;
using ERI_Client.V1;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -216,10 +215,37 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
} }
} }
private Task ShowInformation(IDataSource dataSource) private async Task ShowInformation(IDataSource dataSource)
{ {
#warning Implement the information dialog for ERI data sources. switch (dataSource)
return Task.CompletedTask; {
case DataSourceLocalFile localFile:
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileInfoDialog>
{
{ x => x.DataSource, localFile },
};
await this.DialogService.ShowAsync<DataSourceLocalFileInfoDialog>("Local File Data Source Information", localFileDialogParameters, DialogOptions.FULLSCREEN);
break;
case DataSourceLocalDirectory localDirectory:
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryInfoDialog>
{
{ x => x.DataSource, localDirectory },
};
await this.DialogService.ShowAsync<DataSourceLocalDirectoryInfoDialog>("Local Directory Data Source Information", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
break;
case DataSourceERI_V1 eriV1DataSource:
var eriV1DialogParameters = new DialogParameters<DataSourceERI_V1InfoDialog>
{
{ x => x.DataSource, eriV1DataSource },
};
await this.DialogService.ShowAsync<DataSourceERI_V1InfoDialog>("ERI v1 Data Source Information", eriV1DialogParameters, DialogOptions.FULLSCREEN);
break;
}
} }
private async Task UpdateDataSources() private async Task UpdateDataSources()
@@ -0,0 +1,19 @@
<MudStack Row="@true" AlignItems="AlignItems.Center">
<MudTextField
T="string"
ReadOnly="@true"
Label="@this.Label"
Text="@this.Value"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Adornment="Adornment.Start"
AdornmentIcon="@this.Icon"
UserAttributes="@USER_INPUT_ATTRIBUTES" />
@if (this.ShowingCopyButton)
{
<MudTooltip Text="@this.ClipboardTooltip">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Size="Size.Medium" OnClick="@(() => this.CopyToClipboard(this.Value))"/>
</MudTooltip>
}
</MudStack>
@@ -0,0 +1,50 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class TextInfoLine : ComponentBase
{
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.Info;
[Parameter]
public string Value { get; set; } = string.Empty;
[Parameter]
public string ClipboardTooltipSubject { get; set; } = "the text";
[Parameter]
public bool ShowingCopyButton { get; set; } = true;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
}
@@ -0,0 +1,20 @@
<MudStack Row="@true" AlignItems="AlignItems.Start">
<MudTextField
T="string"
ReadOnly="@true"
Label="@this.Label"
Text="@this.Value"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="3"
MaxLines="@this.MaxLines"
AutoGrow="@true"
UserAttributes="@USER_INPUT_ATTRIBUTES" />
@if (this.ShowingCopyButton)
{
<MudTooltip Text="@this.ClipboardTooltip">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Size="Size.Medium" OnClick="@(() => this.CopyToClipboard(this.Value))"/>
</MudTooltip>
}
</MudStack>
@@ -0,0 +1,50 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class TextInfoLines : ComponentBase
{
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string Value { get; set; } = string.Empty;
[Parameter]
public string ClipboardTooltipSubject { get; set; } = "the text";
[Parameter]
public int MaxLines { get; set; } = 30;
[Parameter]
public bool ShowingCopyButton { get; set; } = true;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
}
@@ -7,10 +7,14 @@ public partial class Vision : ComponentBase
private static readonly TextItem[] ITEMS_VISION = private static readonly TextItem[] ITEMS_VISION =
[ [
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("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("Integrating your data", "You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes."),
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("Integration of enterprise data", "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question."),
new("Local AI systems", "Want to use AI systems locally and offline? We aim to make that possible too."), new("Useful assistants", "We'll develop more assistants for everyday tasks."),
new("Your AI systems", "Prefer to run your AI systems with providers like replicate.com? We plan to support that!"), new("Writing mode", "We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel."),
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."), new("Specific requirements", "Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas."),
new("Voice control", "You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation."),
new("Content creation", "There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers."),
new("Email monitoring", "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats."),
new("Browser usage", "We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser."),
]; ];
} }
@@ -0,0 +1,101 @@
@using AIStudio.Tools.ERIClient.DataModel
<MudDialog>
<DialogContent>
<MudText Typo="Typo.h5">
Common data source information
</MudText>
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
<TextInfoLine Icon="@Icons.Material.Filled.NetworkCheck" Label="ERI server hostname" Value="@this.DataSource.Hostname" ClipboardTooltipSubject="the ERI server hostname"/>
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="ERI server port" Value="@this.Port" ClipboardTooltipSubject="the ERI server port"/>
@if (!this.IsConnectionEncrypted())
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
Please note: the connection to the ERI v1 server is not encrypted. This means that all
data sent to the server is transmitted in plain text. Please ask the ERI server administrator
to enable encryption.
</MudJustifiedText>
}
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
{
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="Username" Value="@this.DataSource.Username" ClipboardTooltipSubject="the username"/>
}
<TextInfoLines Label="Server description" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="the server description"/>
<TextInfoLines Label="Security requirements" MaxLines="3" Value="@this.securityRequirements.Explain()" ClipboardTooltipSubject="the security requirements"/>
<MudText Typo="Typo.h5" Class="mt-6">
Retrieval information
</MudText>
@if (!this.retrievalInfoformation.Any())
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Info" Class="mb-3">
The data source does not provide any retrieval information.
</MudJustifiedText>
}
else
{
<MudExpansionPanels Class="mb-3">
@for (var index = 0; index < this.retrievalInfoformation.Count; index++)
{
var info = this.retrievalInfoformation[index];
<ExpansionPanel HeaderText="@this.RetrievalName(info)" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="index == 0">
<TextInfoLines Label="Description" MaxLines="14" Value="@info.Description" ClipboardTooltipSubject="the retrieval description"/>
<TextInfoLines Label="Parameters" MaxLines="14" Value="@this.RetrievalParameters(info)" ClipboardTooltipSubject="the retrieval parameters"/>
@if (!string.IsNullOrWhiteSpace(info.Link))
{
<MudButton Href="@info.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
Open web link, show more information
</MudButton>
}
<MudText Typo="Typo.h6" Class="mt-3">
Embeddings
</MudText>
@if (!info.Embeddings.Any())
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Info" Class="mb-3">
The data source does not provide any embedding information.
</MudJustifiedText>
}
else
{
<MudExpansionPanels>
@for (var embeddingIndex = 0; embeddingIndex < info.Embeddings.Count; embeddingIndex++)
{
var embedding = info.Embeddings[embeddingIndex];
<ExpansionPanel HeaderText="@embedding.EmbeddingName" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="embeddingIndex == 0">
<TextInfoLine Icon="@Icons.Material.Filled.FormatShapes" Label="Type" Value="@embedding.EmbeddingType" ClipboardTooltipSubject="the embedding type"/>
<TextInfoLines Label="Description" MaxLines="14" Value="@embedding.Description" ClipboardTooltipSubject="the embedding description"/>
<TextInfoLines Label="When to use" MaxLines="3" Value="@embedding.UsedWhen" ClipboardTooltipSubject="when is the embedding used"/>
@if (!string.IsNullOrWhiteSpace(embedding.Link))
{
<MudButton Href="@embedding.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
Open web link, show more information
</MudButton>
}
</ExpansionPanel>
}
</MudExpansionPanels>
}
</ExpansionPanel>
}
</MudExpansionPanels>
}
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
@if (this.IsOperationInProgress)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="ml-5 mr-5"/>
}
<MudButton OnClick="@this.GetERIMetadata" Variant="Variant.Filled" Color="Color.Info">Reload</MudButton>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,178 @@
// ReSharper disable InconsistentNaming
using System.Text;
using AIStudio.Assistants.ERI;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient;
using AIStudio.Tools.ERIClient.DataModel;
using Microsoft.AspNetCore.Components;
using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
namespace AIStudio.Dialogs;
public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposable, ISecretId
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public DataSourceERI_V1 DataSource { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.eriServerTasks.Add(this.GetERIMetadata());
await base.OnInitializedAsync();
}
#endregion
private readonly CancellationTokenSource cts = new();
private readonly List<Task> eriServerTasks = new();
private readonly List<string> dataIssues = [];
private string serverDescription = string.Empty;
private ProviderType securityRequirements = ProviderType.NONE;
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
private bool IsOperationInProgress { get; set; } = true;
private bool IsConnectionEncrypted() => this.DataSource.Hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
private string RetrievalName(RetrievalInfo retrievalInfo)
{
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
var hasName = !string.IsNullOrWhiteSpace(retrievalInfo.Name);
if (hasId && hasName)
return $"[{retrievalInfo.Id}] {retrievalInfo.Name}";
if (hasId)
return $"[{retrievalInfo.Id}] Unnamed retrieval process";
return hasName ? retrievalInfo.Name : "Unnamed retrieval process";
}
private string RetrievalParameters(RetrievalInfo retrievalInfo)
{
var parameters = retrievalInfo.ParametersDescription;
if (parameters is null || parameters.Count == 0)
return "This retrieval process has no parameters.";
var sb = new StringBuilder();
foreach (var (paramName, description) in parameters)
{
sb.Append("Parameter: ");
sb.AppendLine(paramName);
sb.AppendLine(description);
sb.AppendLine();
}
return sb.ToString();
}
private async Task GetERIMetadata()
{
this.dataIssues.Clear();
try
{
this.IsOperationInProgress = true;
this.StateHasChanged();
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
if(client is null)
{
this.dataIssues.Add("Failed to connect to the ERI v1 server. The server is not supported.");
return;
}
var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService);
if (!loginResult.Successful)
{
this.dataIssues.Add(loginResult.Message);
return;
}
var dataSourceInfo = await client.GetDataSourceInfoAsync(this.cts.Token);
if (!dataSourceInfo.Successful)
{
this.dataIssues.Add(dataSourceInfo.Message);
return;
}
this.serverDescription = dataSourceInfo.Data.Description;
var securityRequirementsResult = await client.GetSecurityRequirementsAsync(this.cts.Token);
if (!securityRequirementsResult.Successful)
{
this.dataIssues.Add(securityRequirementsResult.Message);
return;
}
this.securityRequirements = securityRequirementsResult.Data.AllowedProviderType;
var retrievalInfoResult = await client.GetRetrievalInfoAsync(this.cts.Token);
if (!retrievalInfoResult.Successful)
{
this.dataIssues.Add(retrievalInfoResult.Message);
return;
}
this.retrievalInfoformation = retrievalInfoResult.Data ?? [];
this.StateHasChanged();
}
catch (Exception e)
{
this.dataIssues.Add($"Failed to connect to the ERI v1 server. The message was: {e.Message}");
}
finally
{
this.IsOperationInProgress = false;
this.StateHasChanged();
}
}
private void Close()
{
this.cts.Cancel();
this.MudDialog.Close();
}
#region Implementation of ISecretId
public string SecretId => this.DataSource.Id;
public string SecretName => this.DataSource.Name;
#endregion
#region Implementation of IDisposable
public async ValueTask DisposeAsync()
{
try
{
await this.cts.CancelAsync();
await Task.WhenAll(this.eriServerTasks);
this.cts.Dispose();
}
catch
{
// ignored
}
}
#endregion
}
@@ -1,4 +1,4 @@
@using ERI_Client.V1 @using AIStudio.Tools.ERIClient.DataModel
<MudDialog> <MudDialog>
<DialogContent> <DialogContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues"> <MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@@ -1,9 +1,10 @@
using AIStudio.Assistants.ERI;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient;
using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.Validation; using AIStudio.Tools.Validation;
using ERI_Client.V1;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@@ -43,7 +44,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
private string[] dataIssues = []; private string[] dataIssues = [];
private string dataSecretStorageIssue = string.Empty; private string dataSecretStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty; private string dataEditingPreviousInstanceName = string.Empty;
private HttpClient? httpClient;
private List<AuthMethod> availableAuthMethods = []; private List<AuthMethod> availableAuthMethods = [];
private bool connectionTested; private bool connectionTested;
private bool connectionSuccessfulTested; private bool connectionSuccessfulTested;
@@ -167,32 +167,39 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
{ {
try try
{ {
this.httpClient = new HttpClient var cts = new CancellationTokenSource(TimeSpan.FromSeconds(14));
var dataSource = new DataSourceERI_V1
{ {
BaseAddress = new Uri($"{this.dataHostname}:{this.dataPort}"), Hostname = this.dataHostname,
Timeout = TimeSpan.FromSeconds(5), Port = this.dataPort
}; };
using (this.httpClient) using var client = ERIClientFactory.Get(ERIVersion.V1, dataSource);
{ if(client is null)
var client = new Client(this.httpClient);
var authSchemes = await client.GetAuthMethodsAsync();
if (authSchemes is null)
{ {
await this.form.Validate(); await this.form.Validate();
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server did not respond."; this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server is not supported.";
return; return;
} }
this.availableAuthMethods = authSchemes.Select(n => n.AuthMethod).ToList(); var authSchemes = await client.GetAuthMethodsAsync(cts.Token);
if (!authSchemes.Successful)
{
await this.form.Validate();
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
this.dataIssues[^1] = authSchemes.Message;
return;
}
this.availableAuthMethods = authSchemes.Data!.Select(n => n.AuthMethod).ToList();
this.connectionTested = true; this.connectionTested = true;
this.connectionSuccessfulTested = true; this.connectionSuccessfulTested = true;
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested."); this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
} }
}
catch (Exception e) catch (Exception e)
{ {
await this.form.Validate(); await this.form.Validate();
@@ -0,0 +1,52 @@
<MudDialog>
<DialogContent>
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
<TextInfoLine Icon="@Icons.Material.Filled.FolderOpen" Label="Path" Value="@this.DataSource.Path" ClipboardTooltipSubject="this path"/>
@if (!this.IsDirectoryAvailable)
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
The directory chosen for the data source does not exist anymore. Please edit the data source and correct the path.
</MudJustifiedText>
}
else
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
The directory chosen for the data source exists.
</MudJustifiedText>
}
<TextInfoLine Icon="@Icons.Material.Filled.Layers" Label="Embedding name" Value="@this.embeddingProvider.Name" ClipboardTooltipSubject="the embedding name"/>
@if (this.IsCloudEmbedding)
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
The embedding runs in the cloud. All your data from the folder '@this.DataSource.Path' and all its subdirectories
will be sent to the cloud.
</MudJustifiedText>
}
else
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
The embedding runs locally or in your organization. Your data is not sent to the cloud.
</MudJustifiedText>
}
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="Number of files" Value="@this.NumberFilesInDirectory" ClipboardTooltipSubject="the number of files in the directory"/>
<TextInfoLines Label="Files list" MaxLines="14" Value="@this.directoryFiles.ToString()" ClipboardTooltipSubject="the files list"/>
@if (this.directorySizeNumFiles > 100)
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Warning" Class="mb-3">
For performance reasons, only the first 100 files are shown. The directory contains @this.NumberFilesInDirectory files in total.
</MudJustifiedText>
}
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="Total directory size" Value="@this.directorySizeBytes.FileSize()" ClipboardTooltipSubject="the total directory size"/>
</DialogContent>
<DialogActions>
@if (this.IsOperationInProgress)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="ml-5 mr-5"/>
}
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,112 @@
using System.Text;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
using Timer = System.Timers.Timer;
namespace AIStudio.Dialogs;
public partial class DataSourceLocalDirectoryInfoDialog : ComponentBase, IAsyncDisposable
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public DataSourceLocalDirectory DataSource { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private readonly Timer refreshTimer = new(TimeSpan.FromSeconds(1.6))
{
AutoReset = true,
};
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
this.directoryInfo = new DirectoryInfo(this.DataSource.Path);
if (this.directoryInfo.Exists)
{
this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token);
this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged);
this.refreshTimer.Start();
}
await base.OnInitializedAsync();
}
#endregion
private const int MAX_FILES_TO_SHOW = 100;
private readonly CancellationTokenSource cts = new();
private EmbeddingProvider embeddingProvider;
private DirectoryInfo directoryInfo = null!;
private long directorySizeBytes;
private long directorySizeNumFiles;
private readonly StringBuilder directoryFiles = new();
private Task directorySizeTask = Task.CompletedTask;
private bool IsOperationInProgress { get; set; } = true;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
private bool IsDirectoryAvailable => this.directoryInfo.Exists;
private void UpdateFileList(string file)
{
this.directoryFiles.Append("- ");
this.directoryFiles.AppendLine(file);
}
private void UpdateDirectorySize(long size)
{
this.directorySizeBytes = size;
}
private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles;
private void DirectoryOperationDone()
{
this.refreshTimer.Stop();
this.IsOperationInProgress = false;
this.InvokeAsync(this.StateHasChanged);
}
private string NumberFilesInDirectory => $"{this.directorySizeNumFiles:###,###,###,###}";
private void Close()
{
this.cts.Cancel();
this.MudDialog.Close();
}
#region Implementation of IDisposable
public async ValueTask DisposeAsync()
{
try
{
await this.cts.CancelAsync();
await this.directorySizeTask;
this.cts.Dispose();
this.refreshTimer.Stop();
this.refreshTimer.Dispose();
}
catch
{
// ignored
}
}
#endregion
}
@@ -23,7 +23,6 @@ public partial class DataSourceLocalFileDialog : ComponentBase
[Inject] [Inject]
private SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
private readonly DataSourceValidation dataSourceValidation; private readonly DataSourceValidation dataSourceValidation;
@@ -0,0 +1,39 @@
<MudDialog>
<DialogContent>
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
<TextInfoLine Icon="@Icons.Material.Filled.FolderOpen" Label="File path" Value="@this.DataSource.FilePath" ClipboardTooltipSubject="this path"/>
@if (!this.IsFileAvailable)
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
The file chosen for the data source does not exist anymore. Please edit the data source and choose another file or correct the path.
</MudJustifiedText>
}
else
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
The file chosen for the data source exists.
</MudJustifiedText>
}
<TextInfoLine Icon="@Icons.Material.Filled.Layers" Label="Embedding name" Value="@this.embeddingProvider.Name" ClipboardTooltipSubject="the embedding name"/>
@if (this.IsCloudEmbedding)
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
The embedding runs in the cloud. All your data within the
file '@this.DataSource.FilePath' will be sent to the cloud.
</MudJustifiedText>
}
else
{
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
The embedding runs locally or in your organization. Your data is not sent to the cloud.
</MudJustifiedText>
}
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="File size" Value="@this.FileSize" ClipboardTooltipSubject="the file size"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,40 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class DataSourceLocalFileInfoDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public DataSourceLocalFile DataSource { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
this.fileInfo = new FileInfo(this.DataSource.FilePath);
await base.OnInitializedAsync();
}
#endregion
private EmbeddingProvider embeddingProvider;
private FileInfo fileInfo = null!;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
private bool IsFileAvailable => this.fileInfo.Exists;
private string FileSize => this.fileInfo.FileSize();
private void Close() => this.MudDialog.Close();
}
@@ -136,6 +136,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(MainLayout);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -47,16 +47,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" /> <PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.1" />
<PackageReference Include="MudBlazor" Version="7.15.0" /> <PackageReference Include="MudBlazor" Version="7.16.0" />
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" /> <PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
<PackageReference Include="ReverseMarkdown" Version="4.6.0" /> <PackageReference Include="ReverseMarkdown" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ERIClientV1\ERIClientV1.csproj" />
</ItemGroup>
<!-- Read the meta data file --> <!-- Read the meta data file -->
<Target Name="ReadMetaData" BeforeTargets="BeforeBuild"> <Target Name="ReadMetaData" BeforeTargets="BeforeBuild">
<Error Text="The ../../metadata.txt file was not found!" Condition="!Exists('../../metadata.txt')" /> <Error Text="The ../../metadata.txt file was not found!" Condition="!Exists('../../metadata.txt')" />
+5 -3
View File
@@ -1,8 +1,9 @@
@attribute [Route(Routes.ABOUT)] @attribute [Route(Routes.ABOUT)]
<MudText Typo="Typo.h3" Class="mb-2">About MindWork AI Studio</MudText> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-2">About MindWork AI Studio</MudText>
<InnerScrolling HeaderHeight="6em"> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Layers" HeaderText="Versions" IsExpanded="@true"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Layers" HeaderText="Versions" IsExpanded="@true">
<MudText> <MudText>
@@ -61,4 +62,5 @@
<MudMarkdown Value="@LICENSE" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/> <MudMarkdown Value="@LICENSE" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
</ExpansionPanel> </ExpansionPanel>
</MudExpansionPanels> </MudExpansionPanels>
</InnerScrolling> </InnerScrolling>
</div>
@@ -1,11 +1,12 @@
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@attribute [Route(Routes.ASSISTANTS)] @attribute [Route(Routes.ASSISTANTS)]
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
Assistants Assistants
</MudText> </MudText>
<InnerScrolling HeaderHeight="6em"> <InnerScrolling>
<MudText Typo="Typo.h4" Class="mb-2 mr-3"> <MudText Typo="Typo.h4" Class="mb-2 mr-3">
General General
@@ -48,4 +49,5 @@
} }
</MudStack> </MudStack>
</InnerScrolling> </InnerScrolling>
</div>
+32 -41
View File
@@ -2,7 +2,9 @@
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits MSGComponentBase @inherits MSGComponentBase
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty) @if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
{ {
@($"Chat in Workspace \"{this.currentWorkspaceName}\"") @($"Chat in Workspace \"{this.currentWorkspaceName}\"")
@@ -11,64 +13,51 @@
{ {
@("Temporary Chat") @("Temporary Chat")
} }
</MudText> </MudText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
@if (this.AreWorkspacesVisible) @if (this.AreWorkspacesVisible)
{ {
<MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;"> <MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;">
<StartContent> <StartContent>
@if (this.AreWorkspacesHidden)
{
<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)
{
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) @if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
{ {
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em"> // Case: Sidebar can be toggled and is currently visible
<InnerScrolling FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg mb-3" MinWidth="26em">
<HeaderContent> <HeaderContent>
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/> <MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
</MudTooltip> </MudTooltip>
</HeaderContent> </HeaderContent>
<ChildContent> <ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" /> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
</ChildContent> </ChildContent>
</InnerScrolling> </InnerScrolling>
} }
else else
{ {
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em"> // Case: Sidebar is always visible
<InnerScrolling FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg mb-3" MinWidth="26em">
<ChildContent> <ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" /> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
</ChildContent> </ChildContent>
</InnerScrolling> </InnerScrolling>
} }
}
}
</StartContent> </StartContent>
<EndContent> <EndContent>
<ChatComponent <ChatComponent
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.currentWorkspaceName = name" /> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
</EndContent> </EndContent>
</MudSplitter> </MudSplitter>
} }
else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR) else if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR)
{ {
<MudStack Row="@true" Style="width: 100%; overflow: hidden;"> // Case: Sidebar can be toggled and is currently hidden
<MudPaper Class="border border-solid rounded-lg"> <MudStack Row="@true" Style="width: 100%; overflow: hidden; height: 100%; flex-grow: 1; min-height: 0;">
<MudPaper Class="border border-solid rounded-lg mb-3">
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> <MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/> <MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
</MudTooltip> </MudTooltip>
@@ -78,22 +67,23 @@ else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.currentWorkspaceName = name"/> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
</MudStack> </MudStack>
} }
else else
{ {
// Case: Workspaces are disabled or shown in an overlay
<ChatComponent <ChatComponent
@bind-ChatThread="@this.chatThread" @bind-ChatThread="@this.chatThread"
@bind-Provider="@this.providerSettings" @bind-Provider="@this.providerSettings"
Workspaces="@this.workspaces" Workspaces="@this.workspaces"
WorkspaceName="name => this.currentWorkspaceName = name"/> WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
} }
@if ( @if (
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
{ {
<MudDrawer @bind-Open="@this.workspaceOverlayVisible" 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> <MudDrawerHeader>
<MudStack Row="@true" AlignItems="AlignItems.Center"> <MudStack Row="@true" AlignItems="AlignItems.Center">
@@ -104,7 +94,8 @@ else
</MudStack> </MudStack>
</MudDrawerHeader> </MudDrawerHeader>
<MudDrawerContainer Class="ml-6"> <MudDrawerContainer Class="ml-6">
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" /> <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
</MudDrawerContainer> </MudDrawerContainer>
</MudDrawer> </MudDrawer>
} }
</div>
@@ -28,6 +28,8 @@ public partial class Chat : MSGComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.ApplyFilters([], [ Event.WORKSPACE_TOGGLE_OVERLAY ]);
this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition; this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
this.splitterSaveTimer.AutoReset = false; this.splitterSaveTimer.AutoReset = false;
this.splitterSaveTimer.Elapsed += async (_, _) => this.splitterSaveTimer.Elapsed += async (_, _) =>
@@ -72,8 +74,16 @@ public partial class Chat : MSGComponentBase
private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition; private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition;
private void UpdateWorkspaceName(string workspaceName)
{
this.currentWorkspaceName = workspaceName;
this.StateHasChanged();
}
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(Chat);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
+6 -4
View File
@@ -1,9 +1,10 @@
@attribute [Route(Routes.HOME)] @attribute [Route(Routes.HOME)]
<MudImage Src="svg/banner.svg" /> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mt-2 mb-2">Let's get started</MudText> <MudImage Src="svg/banner.svg" Style="max-height: 16em; width: 100%; object-fit: cover;" />
<MudText Typo="Typo.h3" Class="mt-2 mb-2">Let's get started</MudText>
<InnerScrolling HeaderHeight="288px - 8em"> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="Introduction" IsExpanded="@true"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="Introduction" IsExpanded="@true">
@@ -37,4 +38,5 @@
</ExpansionPanel> </ExpansionPanel>
</MudExpansionPanels> </MudExpansionPanels>
</InnerScrolling> </InnerScrolling>
</div>
+4 -3
View File
@@ -26,17 +26,18 @@ public partial class Home : ComponentBase
private async Task ReadLastChangeAsync() private async Task ReadLastChangeAsync()
{ {
var latest = Changelog.LOGS.MaxBy(n => n.Build); var latest = Changelog.LOGS.MaxBy(n => n.Build);
var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}"); using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
this.LastChangeContent = await response.Content.ReadAsStringAsync(); this.LastChangeContent = await response.Content.ReadAsStringAsync();
} }
private static readonly TextItem[] ITEMS_ADVANTAGES = private static readonly TextItem[] ITEMS_ADVANTAGES =
[ [
new("Free of charge", "The app is free to use, both for personal and commercial purposes."), 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, xAI (Grok), and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."), 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, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
new("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."), 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("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("Privacy", "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems."),
new("Flexibility", "Choose the provider and model best suited for your current task."), 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."), 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."),
]; ];
+5 -3
View File
@@ -1,9 +1,10 @@
@attribute [Route(Routes.SETTINGS)] @attribute [Route(Routes.SETTINGS)]
@using AIStudio.Components.Settings @using AIStudio.Components.Settings
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
<InnerScrolling HeaderHeight="10em"> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" /> <SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" /> <SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" />
@@ -28,4 +29,5 @@
<SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" /> <SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" /> <SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
</MudExpansionPanels> </MudExpansionPanels>
</InnerScrolling> </InnerScrolling>
</div>
@@ -28,6 +28,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(Settings);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data) public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
+42 -3
View File
@@ -1,9 +1,12 @@
@attribute [Route(Routes.SUPPORTERS)] @attribute [Route(Routes.SUPPORTERS)]
<MudText Typo="Typo.h3" Class="mb-2">Supporters</MudText> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-2">Supporters</MudText>
<InnerScrolling>
<InnerScrolling HeaderHeight="6em"> <MudExpansionPanels>
<ExpansionPanel HeaderText="Financial Support" IsExpanded="@true" HeaderIcon="@Icons.Material.Filled.AttachMoney">
<div class="border-solid border-2 rounded-lg pa-3 mb-6"> <div class="border-solid border-2 rounded-lg pa-3 mb-6">
<MudText Typo="Typo.h4" Class="mb-2">Our Titans</MudText> <MudText Typo="Typo.h4" Class="mb-2">Our Titans</MudText>
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3"> <MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3">
@@ -35,6 +38,7 @@
<MudList T="string"> <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="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."/> <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."/>
<Supporter Name="donework" Type="SupporterType.INDIVIDUAL" URL="https://github.com/donework" Acknowledgment="Thanks Dominic for being the third supporter."/>
</MudList> </MudList>
<MudText Typo="Typo.h6" Class="mt-8"> <MudText Typo="Typo.h6" Class="mt-8">
@@ -59,5 +63,40 @@
</MudPaper> </MudPaper>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
</ExpansionPanel>
</InnerScrolling> <ExpansionPanel HeaderText="Content Contributors" HeaderIcon="@Icons.Material.Filled.Code">
<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">
<MudIcon Icon="@Icons.Material.Filled.Code" Size="Size.Large" class="mr-2"/>
Code Contributions
</MudText>
<MudList T="string">
<Supporter Name="DevNullx64" Type="SupporterType.INDIVIDUAL" URL="https://github.com/DevNullx64" Acknowledgment="Thanks Luc for your build script contribution."/>
<Supporter Name="SolsticeSpectrum" Type="SupporterType.INDIVIDUAL" URL="https://github.com/SolsticeSpectrum" Acknowledgment="Thanks for your build script contribution."/>
</MudList>
</MudPaper>
</MudItem>
<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-start">
<MudIcon Icon="@Icons.Material.Filled.Image" Size="Size.Large" class="mr-2"/>
Moderation, Design, Wiki, and Documentation
</MudText>
<MudList T="string">
<Supporter Name="KeSah001" Type="SupporterType.INDIVIDUAL" URL="https://github.com/KeSah001" Acknowledgment="Thank you very much, Kerstin, for taking care of creating the Wiki."/>
</MudList>
</MudPaper>
</MudItem>
</MudGrid>
</ExpansionPanel>
</MudExpansionPanels>
</InnerScrolling>
</div>
+8 -6
View File
@@ -1,14 +1,15 @@
@attribute [Route(Routes.WRITER)] @attribute [Route(Routes.WRITER)]
@inherits MSGComponentBase @inherits MSGComponentBase
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <div class="inner-scrolling-context">
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
Writer Writer
</MudText> </MudText>
<PreviewExperimental/> <PreviewExperimental/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
<InnerScrolling HeaderHeight="12.3em"> <InnerScrolling>
<ChildContent> <ChildContent>
<MudTextField <MudTextField
@ref="@this.textField" @ref="@this.textField"
@@ -54,4 +55,5 @@
FullWidth="@true" FullWidth="@true"
UserAttributes="@USER_INPUT_ATTRIBUTES"/> UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</FooterContent> </FooterContent>
</InnerScrolling> </InnerScrolling>
</div>
@@ -41,6 +41,8 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(Writer);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
return Task.CompletedTask; return Task.CompletedTask;
@@ -103,9 +103,14 @@ public abstract class BaseProvider : IProvider, ISecretId
{ {
using var request = await requestBuilder(); using var request = await requestBuilder();
//
// Send the request with the ResponseHeadersRead option. // Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received. // This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses. // This is important because we want to stream the responses.
//
// Please notice: We do not dispose the response here. The caller is responsible
// for disposing the response object. This is important because the response
// object is used to read the stream.
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
if (nextResponse.IsSuccessStatusCode) if (nextResponse.IsSuccessStatusCode)
{ {
@@ -136,8 +136,8 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
if (secretKey is null) if (secretKey is null)
return default; return default;
var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}"); using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
var response = await this.httpClient.SendAsync(request, token); using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return default; return default;
@@ -127,10 +127,10 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
if (secretKey is null) if (secretKey is null)
return []; return [];
var request = new HttpRequestMessage(HttpMethod.Get, "models"); using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
var response = await this.httpClient.SendAsync(request, token); using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return []; return [];
@@ -138,10 +138,10 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
if (secretKey is null) if (secretKey is null)
return default; return default;
var request = new HttpRequestMessage(HttpMethod.Get, "models"); using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
var response = await this.httpClient.SendAsync(request, token); using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return default; return default;
@@ -154,10 +154,10 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
if (secretKey is null) if (secretKey is null)
return []; return [];
var request = new HttpRequestMessage(HttpMethod.Get, "models"); using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
var response = await this.httpClient.SendAsync(request, token); using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return []; return [];
@@ -154,11 +154,11 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
} }
}; };
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models"); using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
if(secretKey is not null) if(secretKey is not null)
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional); lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token); using var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
if(!lmStudioResponse.IsSuccessStatusCode) if(!lmStudioResponse.IsSuccessStatusCode)
return []; return [];
@@ -127,10 +127,10 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v
if (secretKey is null) if (secretKey is null)
return []; return [];
var request = new HttpRequestMessage(HttpMethod.Get, "models"); using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
var response = await this.httpClient.SendAsync(request, token); using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
return []; return [];
@@ -1,6 +1,7 @@
using ERI_Client.V1;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
/// <summary> /// <summary>
@@ -24,23 +25,15 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
/// <inheritdoc /> /// <inheritdoc />
public DataSourceType Type { get; init; } = DataSourceType.NONE; public DataSourceType Type { get; init; } = DataSourceType.NONE;
/// <summary> /// <inheritdoc />
/// The hostname of the ERI server.
/// </summary>
public string Hostname { get; init; } = string.Empty; public string Hostname { get; init; } = string.Empty;
/// <summary> /// <inheritdoc />
/// The port of the ERI server.
/// </summary>
public int Port { get; init; } public int Port { get; init; }
/// <summary> /// <inheritdoc />
/// The authentication method to use.
/// </summary>
public AuthMethod AuthMethod { get; init; } = AuthMethod.NONE; public AuthMethod AuthMethod { get; init; } = AuthMethod.NONE;
/// <summary> /// <inheritdoc />
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
/// </summary>
public string Username { get; init; } = string.Empty; public string Username { get; init; } = string.Empty;
} }
@@ -1,12 +1,26 @@
using ERI_Client.V1; using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Settings; namespace AIStudio.Settings;
public interface IERIDataSource : IExternalDataSource public interface IERIDataSource : IExternalDataSource
{ {
/// <summary>
/// The hostname of the ERI server.
/// </summary>
public string Hostname { get; init; } public string Hostname { get; init; }
/// <summary>
/// The port of the ERI server.
/// </summary>
public int Port { get; init; } public int Port { get; init; }
/// <summary>
/// The authentication method to use.
/// </summary>
public AuthMethod AuthMethod { get; init; } public AuthMethod AuthMethod { get; init; }
/// <summary>
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
/// </summary>
public string Username { get; init; }
} }
@@ -1,4 +1,4 @@
using ERI_Client.V1; using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Tools; namespace AIStudio.Tools;
@@ -20,4 +20,5 @@ public enum Components
ERI_ASSISTANT, ERI_ASSISTANT,
CHAT, CHAT,
APP_SETTINGS,
} }
@@ -0,0 +1,66 @@
namespace AIStudio.Tools;
public static class DirectoryInfoExtensions
{
private static readonly EnumerationOptions ENUMERATION_OPTIONS = new()
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
ReturnSpecialDirectories = false,
};
/// <summary>
/// Determines the size of the directory and all its subdirectories, as well as the number of files. When desired,
/// it can report the found files up to a certain limit.
/// </summary>
/// <remarks>
/// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
/// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
/// to cancel slow operations, e.g., when the directory is on a slow network drive.
///
/// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
///
/// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
/// method to update the UI from a background thread.
/// </remarks>
/// <param name="directoryInfo">The root directory to determine the size of.</param>
/// <param name="reportCurrentTotalSize">The callback to report the current total size of the directory.</param>
/// <param name="reportCurrentNumFiles">The callback to report the current number of files found.</param>
/// <param name="reportNextFile">The callback to report the next file found. The file name is relative to the root directory.</param>
/// <param name="reportMaxFiles">The maximum number of files to report. A negative value reports all files.</param>
/// <param name="done">The callback to report that the operation is done.</param>
/// <param name="cancellationToken">The cancellation token to stop the operation.</param>
public static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action<long> reportCurrentTotalSize, Action<long> reportCurrentNumFiles, Action<string> reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default)
{
var rootDirectoryLen = directoryInfo.FullName.Length;
long totalSize = 0;
long numFiles = 0;
await Task.Factory.StartNew(() => {
foreach (var file in directoryInfo.EnumerateFiles("*", ENUMERATION_OPTIONS))
{
if (cancellationToken.IsCancellationRequested)
return;
totalSize += file.Length;
numFiles++;
if (numFiles % 100 == 0)
{
reportCurrentTotalSize(totalSize);
reportCurrentNumFiles(numFiles);
}
if (reportMaxFiles < 0 || numFiles <= reportMaxFiles)
reportNextFile(file.FullName[rootDirectoryLen..]);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
reportCurrentTotalSize(totalSize);
reportCurrentNumFiles(numFiles);
if(done is not null)
done();
}
}
@@ -0,0 +1,19 @@
namespace AIStudio.Tools.ERIClient;
public sealed class APIResponse<T>
{
/// <summary>
/// Was the API call successful?
/// </summary>
public bool Successful { get; set; }
/// <summary>
/// When the API call was not successful, this will contain the error message.
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// The data returned by the API call.
/// </summary>
public T? Data { get; set; }
}
@@ -0,0 +1,13 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// An authentication field.
/// </summary>
public enum AuthField
{
NONE,
USERNAME,
PASSWORD,
TOKEN,
KERBEROS_TICKET,
}
@@ -0,0 +1,8 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// The mapping between an AuthField and the field name in the authentication request.
/// </summary>
/// <param name="AuthField">The AuthField that is mapped to the field name.</param>
/// <param name="FieldName">The field name in the authentication request.</param>
public record AuthFieldMapping(AuthField AuthField, string FieldName);
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.ERIClient.DataModel;
public enum AuthMethod
{
NONE,
KERBEROS,
USERNAME_PASSWORD,
TOKEN,
}
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// The response to an authentication request.
/// </summary>
/// <param name="Success">True, when the authentication was successful.</param>
/// <param name="Token">The token to use for further requests.</param>
/// <param name="Message">When the authentication was not successful, this contains the reason.</param>
public readonly record struct AuthResponse(bool Success, string? Token, string? Message);
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Describes one authentication scheme for this data source.
/// </summary>
/// <param name="AuthMethod">The method used for authentication, e.g., "API Key," "Username/Password," etc.</param>
/// <param name="AuthFieldMappings">A list of field mappings for the authentication method. The client must know,
/// e.g., how the password field is named in the request.</param>
public readonly record struct AuthScheme(AuthMethod AuthMethod, List<AuthFieldMapping> AuthFieldMappings);
@@ -0,0 +1,7 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// A chat thread, which is a list of content blocks.
/// </summary>
/// <param name="ContentBlocks">The content blocks in this chat thread.</param>
public readonly record struct ChatThread(List<ContentBlock> ContentBlocks);
@@ -0,0 +1,12 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// A block of content of a chat thread.
/// </summary>
/// <remarks>
/// Images and other media are base64 encoded.
/// </remarks>
/// <param name="Content">The content of the block. Remember that images and other media are base64 encoded.</param>
/// <param name="Role">The role of the content in the chat thread.</param>
/// <param name="Type">The type of the content, e.g., text, image, video, etc.</param>
public readonly record struct ContentBlock(string Content, Role Role, ContentType Type);
@@ -0,0 +1,16 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// The type of content.
/// </summary>
public enum ContentType
{
NONE,
UNKNOWN,
TEXT,
IMAGE,
VIDEO,
AUDIO,
SPEECH,
}
@@ -0,0 +1,27 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Matching context returned by the data source as a result of a retrieval request.
/// </summary>
/// <param name="Name">The name of the source, e.g., a document name, database name,
/// collection name, etc.</param>
/// <param name="Category">What are the contents of the source? For example, is it a
/// dictionary, a book chapter, business concept, a paper, etc.</param>
/// <param name="Path">The path to the content, e.g., a URL, a file path, a path in a
/// graph database, etc.</param>
/// <param name="Type">The type of the content, e.g., text, image, video, audio, speech, etc.</param>
/// <param name="MatchedContent">The content that matched the user prompt. For text, you
/// return the matched text and, e.g., three words before and after it.</param>
/// <param name="SurroundingContent">The surrounding content of the matched content.
/// For text, you may return, e.g., one sentence or paragraph before and after
/// the matched content.</param>
/// <param name="Links">Links to related content, e.g., links to Wikipedia articles,
/// links to sources, etc.</param>
public readonly record struct Context(
string Name,
string Category,
string? Path,
ContentType Type,
string MatchedContent,
string[] SurroundingContent,
string[] Links);
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Information about the data source.
/// </summary>
/// <param name="Name">The name of the data source, e.g., "Internal Organization Documents."</param>
/// <param name="Description">A short description of the data source. What kind of data does it contain?
/// What is the data source used for?</param>
public readonly record struct DataSourceInfo(string Name, string Description);
@@ -0,0 +1,20 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Represents information about the used embedding for this data source. The purpose of this information is to give the
/// interested user an idea of what kind of embedding is used and what it does.
/// </summary>
/// <param name="EmbeddingType">What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
/// Embedding," "Graph Embedding," etc.</param>
/// <param name="EmbeddingName">Name the embedding used. This can be a library, a framework, or the name of the used
/// algorithm.</param>
/// <param name="Description">A short description of the embedding. Describe what the embedding is doing.</param>
/// <param name="UsedWhen">Describe when the embedding is used. For example, when the user prompt contains certain
/// keywords, or anytime?</param>
/// <param name="Link">A link to the embedding's documentation or the source code. Might be null.</param>
public readonly record struct EmbeddingInfo(
string EmbeddingType,
string EmbeddingName,
string Description,
string UsedWhen,
string? Link);
@@ -0,0 +1,22 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Known types of providers that can process data.
/// </summary>
public enum ProviderType
{
/// <summary>
/// The related data is not allowed to be sent to any provider.
/// </summary>
NONE,
/// <summary>
/// The related data can be sent to any provider.
/// </summary>
ANY,
/// <summary>
/// The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally.
/// </summary>
SELF_HOSTED,
}
@@ -0,0 +1,13 @@
namespace AIStudio.Tools.ERIClient.DataModel;
public static class ProviderTypeExtensions
{
public static string Explain(this ProviderType providerType) => providerType switch
{
ProviderType.NONE => "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.",
ProviderType.ANY => "The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted).",
ProviderType.SELF_HOSTED => "The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed.",
_ => "Unknown configuration. This data source cannot be used at the moment.",
};
}
@@ -0,0 +1,20 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Information about a retrieval process, which this data source implements.
/// </summary>
/// <param name="Id">A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer.</param>
/// <param name="Name">The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".</param>
/// <param name="Description">A short description of the retrieval process. What kind of retrieval process is it?</param>
/// <param name="Link">A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.</param>
/// <param name="ParametersDescription">A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.</param>
/// <param name="Embeddings">A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.</param>
public readonly record struct RetrievalInfo(
string Id,
string Name,
string Description,
string? Link,
Dictionary<string, string>? ParametersDescription,
List<EmbeddingInfo> Embeddings);
@@ -0,0 +1,25 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// The retrieval request sent by AI Studio.
/// </summary>
/// <remarks>
/// Images and other media are base64 encoded.
/// </remarks>
/// <param name="LatestUserPrompt">The latest user prompt that AI Studio received.</param>
/// <param name="LatestUserPromptType">The type of the latest user prompt, e.g., text, image, etc.</param>
/// <param name="Thread">The chat thread that the user is currently in.</param>
/// <param name="RetrievalProcessId">Optional. The ID of the retrieval process that the data source should use.
/// When null, the data source chooses an appropriate retrieval process. Selecting a retrieval process is optional
/// for AI Studio users. Most users do not specify a retrieval process.</param>
/// <param name="Parameters">A dictionary of parameters that the data source should use for the retrieval process.
/// Although each parameter will be sent as a string, the retrieval process specifies the expected type and range.</param>
/// <param name="MaxMatches">The maximum number of matches that the data source should return. AI Studio uses
/// any value below 1 to indicate that the data source should return as many matches as appropriate.</param>
public readonly record struct RetrievalRequest(
string LatestUserPrompt,
ContentType LatestUserPromptType,
ChatThread Thread,
string? RetrievalProcessId,
Dictionary<string, string>? Parameters,
int MaxMatches);
@@ -0,0 +1,15 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Possible roles of any chat thread.
/// </summary>
public enum Role
{
NONE,
UNKNOW,
SYSTEM,
USER,
AI,
AGENT,
}
@@ -0,0 +1,7 @@
namespace AIStudio.Tools.ERIClient.DataModel;
/// <summary>
/// Represents the security requirements for this data source.
/// </summary>
/// <param name="AllowedProviderType">Which provider types are allowed to process the data?</param>
public readonly record struct SecurityRequirements(ProviderType AllowedProviderType);
@@ -0,0 +1,36 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Tools.ERIClient;
public abstract class ERIClientBase(string baseAddress) : IDisposable
{
protected static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNamingPolicy = null,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
}
};
protected readonly HttpClient httpClient = new()
{
BaseAddress = new Uri(baseAddress),
};
protected string securityToken = string.Empty;
#region Implementation of IDisposable
public void Dispose()
{
this.httpClient.Dispose();
}
#endregion
}
@@ -0,0 +1,14 @@
using AIStudio.Assistants.ERI;
using AIStudio.Settings;
namespace AIStudio.Tools.ERIClient;
public static class ERIClientFactory
{
public static IERIClient? Get(ERIVersion version, IERIDataSource dataSource) => version switch
{
ERIVersion.V1 => new ERIClientV1($"{dataSource.Hostname}:{dataSource.Port}"),
_ => null
};
}
@@ -0,0 +1,344 @@
using System.Text;
using System.Text.Json;
using AIStudio.Settings;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Tools.ERIClient;
public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIClient
{
#region Implementation of IERIClient
public async Task<APIResponse<List<AuthScheme>>> GetAuthMethodsAsync(CancellationToken cancellationToken = default)
{
using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the authentication methods: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var authMethods = await response.Content.ReadFromJsonAsync<List<AuthScheme>>(JSON_OPTIONS, cancellationToken);
if(authMethods is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = authMethods
};
}
public async Task<APIResponse<AuthResponse>> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default)
{
var authMethod = dataSource.AuthMethod;
var username = dataSource.Username;
switch (dataSource.AuthMethod)
{
case AuthMethod.NONE:
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!noneAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {noneAuthResponse.StatusCode}, Reason: {noneAuthResponse.ReasonPhrase}"
};
}
var noneAuthResult = await noneAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(noneAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
};
}
this.securityToken = noneAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = noneAuthResult
};
}
case AuthMethod.USERNAME_PASSWORD:
var passwordResponse = await rustService.GetSecret(dataSource);
if (!passwordResponse.Success)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the password."
};
}
var password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
// We must send both values inside the header. The username field is named 'user'.
// The password field is named 'password'.
request.Headers.Add("user", username);
request.Headers.Add("password", password);
using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!usernamePasswordAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {usernamePasswordAuthResponse.StatusCode}, Reason: {usernamePasswordAuthResponse.ReasonPhrase}"
};
}
var usernamePasswordAuthResult = await usernamePasswordAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(usernamePasswordAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the server: the response was invalid."
};
}
this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = usernamePasswordAuthResult
};
}
case AuthMethod.TOKEN:
var tokenResponse = await rustService.GetSecret(dataSource);
if (!tokenResponse.Success)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the access token."
};
}
var token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
request.Headers.Add("Authorization", $"Bearer {token}");
using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!tokenAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {tokenAuthResponse.StatusCode}, Reason: {tokenAuthResponse.ReasonPhrase}"
};
}
var tokenAuthResult = await tokenAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(tokenAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
};
}
this.securityToken = tokenAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = tokenAuthResult
};
}
default:
this.securityToken = string.Empty;
return new()
{
Successful = false,
Message = "The authentication method is not supported yet."
};
}
}
public async Task<APIResponse<DataSourceInfo>> GetDataSourceInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the data source information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var dataSourceInfo = await response.Content.ReadFromJsonAsync<DataSourceInfo>(JSON_OPTIONS, cancellationToken);
if(dataSourceInfo == default)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the data source information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = dataSourceInfo
};
}
public async Task<APIResponse<List<EmbeddingInfo>>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the embedding information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var embeddingInfo = await response.Content.ReadFromJsonAsync<List<EmbeddingInfo>>(JSON_OPTIONS, cancellationToken);
if(embeddingInfo is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the embedding information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = embeddingInfo
};
}
public async Task<APIResponse<List<RetrievalInfo>>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the retrieval information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var retrievalInfo = await response.Content.ReadFromJsonAsync<List<RetrievalInfo>>(JSON_OPTIONS, cancellationToken);
if(retrievalInfo is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the retrieval information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = retrievalInfo
};
}
public async Task<APIResponse<List<Context>>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default)
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval");
requestMessage.Headers.Add("token", this.securityToken);
using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json");
requestMessage.Content = content;
using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to execute the retrieval request: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var contexts = await response.Content.ReadFromJsonAsync<List<Context>>(JSON_OPTIONS, cancellationToken);
if(contexts is null)
{
return new()
{
Successful = false,
Message = "Failed to execute the retrieval request: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = contexts
};
}
public async Task<APIResponse<SecurityRequirements>> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the security requirements: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var securityRequirements = await response.Content.ReadFromJsonAsync<SecurityRequirements>(JSON_OPTIONS, cancellationToken);
if(securityRequirements == default)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the security requirements: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = securityRequirements
};
}
#endregion
}
@@ -0,0 +1,62 @@
using AIStudio.Settings;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Tools.ERIClient;
public interface IERIClient : IDisposable
{
/// <summary>
/// Retrieves the available authentication methods from the ERI server.
/// </summary>
/// <remarks>
/// No authentication is required to retrieve the available authentication methods.
/// </remarks>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available authentication methods.</returns>
public Task<APIResponse<List<AuthScheme>>> GetAuthMethodsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Authenticate the user to the ERI server.
/// </summary>
/// <param name="dataSource">The data source to use.</param>
/// <param name="rustService">The Rust service.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The authentication response.</returns>
public Task<APIResponse<AuthResponse>> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the data source information from the ERI server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The data source information.</returns>
public Task<APIResponse<DataSourceInfo>> GetDataSourceInfoAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the embedding information from the ERI server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of embedding information.</returns>
public Task<APIResponse<List<EmbeddingInfo>>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the retrieval information from the ERI server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of retrieval information.</returns>
public Task<APIResponse<List<RetrievalInfo>>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Executes a retrieval request on the ERI server.
/// </summary>
/// <param name="request">The retrieval request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The retrieved contexts to use for augmentation and generation.</returns>
public Task<APIResponse<List<Context>>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the security requirements from the ERI server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The security requirements.</returns>
public Task<APIResponse<SecurityRequirements>> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,17 @@
namespace AIStudio.Tools;
public static class FileInfoExtensions
{
/// <summary>
/// Returns the file size in human-readable format.
/// </summary>
/// <param name="fileInfo">The file info object.</param>
/// <returns>The file size in human-readable format.</returns>
public static string FileSize(this FileInfo fileInfo)
{
if (!fileInfo.Exists)
return "N/A";
return fileInfo.Length.FileSize();
}
}
@@ -4,6 +4,8 @@ namespace AIStudio.Tools;
public interface IMessageBusReceiver public interface IMessageBusReceiver
{ {
public string ComponentName { get; }
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data); public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data); public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
@@ -0,0 +1,22 @@
namespace AIStudio.Tools;
public static class LongExtensions
{
/// <summary>
/// Formats the file size in a human-readable format.
/// </summary>
/// <param name="sizeBytes">The size in bytes.</param>
/// <returns>The formatted file size.</returns>
public static string FileSize(this long sizeBytes)
{
string[] sizes = { "B", "kB", "MB", "GB", "TB" };
var order = 0;
while (sizeBytes >= 1024 && order < sizes.Length - 1)
{
order++;
sizeBytes /= 1024;
}
return $"{sizeBytes:0.##} {sizes[order]}";
}
}
@@ -60,6 +60,8 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(UpdateService);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@@ -1,4 +1,4 @@
using ERI_Client.V1; using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Tools.Validation; namespace AIStudio.Tools.Validation;
@@ -23,7 +23,7 @@ public sealed class DataSourceValidation
public string? ValidatingHostname(string hostname) public string? ValidatingHostname(string hostname)
{ {
if(string.IsNullOrWhiteSpace(hostname)) if(string.IsNullOrWhiteSpace(hostname))
return "Please enter a hostname, e.g., http://localhost:1234"; return "Please enter a hostname, e.g., http://localhost";
if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
return "The hostname must start with either http:// or https://"; return "The hostname must start with either http:// or https://";
+23 -19
View File
@@ -2,11 +2,11 @@
def main [] {} def main [] {}
def are_assets_exist [rid: string]: string -> bool { def are_assets_exist [rid: string] {
$"bin/release/net8.0/($rid)/publish/wwwroot/_content/MudBlazor/MudBlazor.min.css" | path exists $"bin/release/net8.0/($rid)/publish/wwwroot/_content/MudBlazor/MudBlazor.min.css" | path exists
} }
def "main help" []: nothing -> nothing { def "main help" [] {
print "Usage: nu build.nu [action]" print "Usage: nu build.nu [action]"
print "" print ""
print "Optional Actions:" print "Optional Actions:"
@@ -26,7 +26,7 @@ def "main help" []: nothing -> nothing {
print "" print ""
} }
def "main prepare" [action: string]: string -> nothing { def "main prepare" [action: string] {
if (update_app_version $action) { if (update_app_version $action) {
main fix_web_assets main fix_web_assets
inc_build_number inc_build_number
@@ -36,7 +36,7 @@ def "main prepare" [action: string]: string -> nothing {
} }
} }
def "main metadata" []: nothing -> nothing { def "main metadata" [] {
update_dotnet_version update_dotnet_version
update_rust_version update_rust_version
update_mudblazor_version update_mudblazor_version
@@ -46,7 +46,7 @@ def "main metadata" []: nothing -> nothing {
update_license_year "Pages/About.razor.cs" update_license_year "Pages/About.razor.cs"
} }
def "main fix_web_assets" []: nothing -> nothing { def "main fix_web_assets" [] {
# Get the matching RIDs for the current OS: # Get the matching RIDs for the current OS:
let rids = get_rids let rids = get_rids
@@ -70,7 +70,7 @@ def "main fix_web_assets" []: nothing -> nothing {
} }
} }
def "main publish" []: nothing -> nothing { def "main publish" [] {
main metadata main metadata
@@ -139,7 +139,7 @@ def "main publish" []: nothing -> nothing {
print "Building done." print "Building done."
} }
def get_rids []: nothing -> list { def get_rids [] {
# Define the list of RIDs to build for, cf. https://learn.microsoft.com/en-us/dotnet/core/rid-catalog: # Define the list of RIDs to build for, cf. https://learn.microsoft.com/en-us/dotnet/core/rid-catalog:
let rids = ["win-x64", "win-arm64", "linux-x64", "linux-arm64", "osx-arm64", "osx-x64"] let rids = ["win-x64", "win-arm64", "linux-x64", "linux-arm64", "osx-arm64", "osx-x64"]
@@ -163,11 +163,15 @@ def get_rids []: nothing -> list {
$rids $rids
} }
def get_os []: nothing -> string { def get_os [] {
(sys host).name | str downcase let os = (sys host).name | str downcase
if $os =~ "linux" {
return "linux"
}
$os
} }
def update_build_time []: nothing -> nothing { def update_build_time [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut build_time = $meta_lines.1 mut build_time = $meta_lines.1
@@ -179,7 +183,7 @@ def update_build_time []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def inc_build_number []: nothing -> nothing { def inc_build_number [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut build_number = $meta_lines.2 | into int mut build_number = $meta_lines.2 | into int
@@ -191,7 +195,7 @@ def inc_build_number []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def update_dotnet_version []: nothing -> nothing { def update_dotnet_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut dotnet_sdk_version = $meta_lines.3 mut dotnet_sdk_version = $meta_lines.3
mut dotnet_version = $meta_lines.4 mut dotnet_version = $meta_lines.4
@@ -211,11 +215,11 @@ def update_dotnet_version []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def update_rust_version []: nothing -> nothing { def update_rust_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut rust_version = $meta_lines.5 mut rust_version = $meta_lines.5
let rust_data = (^rustc -Vv) | parse --regex 'rustc (?<version>[0-9.]+) \((?<commit>[a-zA-Z0-9]+)' let rust_data = (^rustc -Vv) | parse --regex 'rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)'
let version = $rust_data.version.0 let version = $rust_data.version.0
let commit = $rust_data.commit.0 let commit = $rust_data.commit.0
@@ -225,7 +229,7 @@ def update_rust_version []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def update_mudblazor_version []: nothing -> nothing { def update_mudblazor_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut mudblazor_version = $meta_lines.6 mut mudblazor_version = $meta_lines.6
@@ -238,7 +242,7 @@ def update_mudblazor_version []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def update_tauri_version []: nothing -> nothing { def update_tauri_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut tauri_version = $meta_lines.7 mut tauri_version = $meta_lines.7
@@ -272,7 +276,7 @@ def update_license_year [licence_file: string] {
$updated_license_text | save --raw --force $licence_file $updated_license_text | save --raw --force $licence_file
} }
def update_app_version [action: string]: string -> bool { def update_app_version [action: string] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut app_version = $meta_lines.0 mut app_version = $meta_lines.0
@@ -317,7 +321,7 @@ def update_app_version [action: string]: string -> bool {
return true return true
} }
def update_project_commit_hash []: nothing -> nothing { def update_project_commit_hash [] {
mut meta_lines = open --raw ../../metadata.txt | lines mut meta_lines = open --raw ../../metadata.txt | lines
mut commit_hash = $meta_lines.8 mut commit_hash = $meta_lines.8
@@ -352,7 +356,7 @@ def update_project_commit_hash []: nothing -> nothing {
$meta_lines | save --raw --force ../../metadata.txt $meta_lines | save --raw --force ../../metadata.txt
} }
def update_changelog []: nothing -> nothing { def update_changelog [] {
# Get all changelog files: # Get all changelog files:
let all_changelog_files = glob wwwroot/changelog/*.md let all_changelog_files = glob wwwroot/changelog/*.md
+16 -19
View File
@@ -24,24 +24,24 @@
}, },
"Microsoft.Extensions.FileProviders.Embedded": { "Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.0, )", "requested": "[9.0.1, )",
"resolved": "9.0.0", "resolved": "9.0.1",
"contentHash": "6Ev1goLIvggLF6uCs6oZvdr9JM+2b1Zj+4FLdBWNW5iw3tm2BymVIb0yMsjnQTBWL7YUmqVWH3u45hSqOfvuqg==", "contentHash": "Pnw9plviwRguhVxHjthooumWH8yUxfF54tuu/+VoVBOuqBmzu6VUnn4NGf3eVkEO1k5q2nihOIAQL/7UMemSmQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0" "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1"
} }
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[8.0.11, )", "requested": "[8.0.12, )",
"resolved": "8.0.11", "resolved": "8.0.12",
"contentHash": "zk5lnZrYJgtuJG8L4v17Ej8rZ3PUcR2iweNV08BaO5LbYHIi2wNaVNcJoLxvqgQdnjLlKnCCfVGLDr6QHeAarQ==" "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig=="
}, },
"MudBlazor": { "MudBlazor": {
"type": "Direct", "type": "Direct",
"requested": "[7.15.0, )", "requested": "[7.16.0, )",
"resolved": "7.15.0", "resolved": "7.16.0",
"contentHash": "b2+hO7ZTGwsGhu9SoowuNe2ETeF1GzExpg/hIYR++rSh+vy4v/QD0iOsrATWv3LdFJXSgk3d1+XIr5srW3jBiA==", "contentHash": "68bEFn7MLCyOB4yYxtvZG+AA+bZbxBDcIOhnSVFrHrAg8LLXVw1LxHCp9EyRktlREpnoAO1JxINp+5WeHc9Z4w==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Components": "8.0.8", "Microsoft.AspNetCore.Components": "8.0.8",
"Microsoft.AspNetCore.Components.Web": "8.0.8", "Microsoft.AspNetCore.Components.Web": "8.0.8",
@@ -147,10 +147,10 @@
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.0", "resolved": "9.0.1",
"contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", "contentHash": "DguZYt1DWL05+8QKWL3b6bW7A2pC5kYFMY5iXM6W2M23jhvcNa8v6AU8PvVJBcysxHwr9/jax0agnwoBumsSwg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "9.0.0" "Microsoft.Extensions.Primitives": "9.0.1"
} }
}, },
"Microsoft.Extensions.Localization": { "Microsoft.Extensions.Localization": {
@@ -188,8 +188,8 @@
}, },
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.0", "resolved": "9.0.1",
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
}, },
"Microsoft.JSInterop": { "Microsoft.JSInterop": {
"type": "Transitive", "type": "Transitive",
@@ -205,11 +205,8 @@
"type": "Transitive", "type": "Transitive",
"resolved": "0.16.9", "resolved": "0.16.9",
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ==" "contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
},
"ericlientv1": {
"type": "Project"
} }
}, },
"net8.0/osx-x64": {} "net8.0/osx-arm64": {}
} }
} }
+19 -1
View File
@@ -71,7 +71,25 @@
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
} }
/* Fixed for MudBlazor, tooltips inside of navmenu */ /* Fix for MudBlazor, tooltips inside of navmenu */
.mud-navmenu .mud-tooltip-root { .mud-navmenu .mud-tooltip-root {
display: initial !important; display: initial !important;
} }
/* Context div for inner scrolling component */
.inner-scrolling-context {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Fixed MudSplitter inside context div for inner scrolling component */
.inner-scrolling-context > .mud-splitter {
flex-grow: 1;
overflow: hidden;
}
/* Fixed the slider part of MudSplitter inside context div for inner scrolling component */
.inner-scrolling-context > .mud-splitter > .mud-slider > .mud-slider-container {
padding-bottom: 12px;
}
@@ -0,0 +1,9 @@
# v0.9.27, build 202 (2025-01-21 18:24 UTC)
- Improved the inner content scrolling to use the entire space available.
- Improved message process debugging. This helps to identify issues related to the message handling.
- Fixed the hostname validation message for ERI v1 data sources.
- Fixed a memory leak in the chat component.
- Fixed an issue with the workspace title not being updated when a chat was moved to another workspace.
- Fixed an issue with the chat component not loading the current workspace name when the component was refreshed.
- Fixed an issue with tooltips at the bottom of the app not being displayed as expected.
- Removed the "send to" button from the ERI server assistant, since it is not supported.
@@ -0,0 +1,9 @@
# v0.9.28, build 203 (2025-02-09 16:33 UTC)
- Added an information view to all data sources to the data source configuration page. The data source configuration is a preview feature behind the RAG feature flag.
- Added a ERI ((E)xternal (R)etrieval (I)nterface) client for communication with any ERI server.
- Added the option to select the default app provider in the assistant settings under provider selection. Thank you, Peer (`peerschuett`), for reporting the issue.
- Added `donework` as financial supporter. Thank you, Dominic!
- Added content contributors to the supporter page. We thank Luc (`DevNullx64`) for his build script contribution as well as Kerstin (`KeSah001`) for creating the Wiki pages.
- Improved the resource handling when loading models.
- Improved the build script to support Arch Linux and other Linux distributions, along with Rust nightly environments. Thanks `SolsticeSpectrum` for the contribution.
- Upgraded the dependencies Rust, .NET, OpenSSL, MudBlazor, `rand`, `rand_chacha`, and others to the latest usable versions.
+8 -8
View File
@@ -1,9 +1,9 @@
0.9.26 0.9.28
2025-01-13 19:11:57 UTC 2025-02-09 16:33:40 UTC
201 203
8.0.111 (commit f3835a7fb4) 8.0.112 (commit f7cd007b9d)
8.0.11 (commit 9cb3b725e3) 8.0.12 (commit 89ef51c5d8)
1.84.0 (commit 9fc6b4312) 1.84.1 (commit e71f9a9a9)
7.15.0 7.16.0
1.8.1 1.8.1
ae72c50236f, release 6da4cf1425f, release
+236 -56
View File
@@ -325,9 +325,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -1310,6 +1310,18 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.29.0" version = "0.29.0"
@@ -1706,6 +1718,23 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.22",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
"tower-service",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@@ -1737,9 +1766,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.6" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -1750,7 +1779,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
"tower",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
@@ -2319,7 +2347,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]] [[package]]
name = "mindwork-ai-studio" name = "mindwork-ai-studio"
version = "0.9.26" version = "0.9.28"
dependencies = [ dependencies = [
"aes", "aes",
"arboard", "arboard",
@@ -2331,11 +2359,12 @@ dependencies = [
"keyring", "keyring",
"log", "log",
"once_cell", "once_cell",
"openssl",
"pbkdf2", "pbkdf2",
"rand 0.8.5", "rand 0.9.0",
"rand_chacha 0.3.1", "rand_chacha 0.9.0",
"rcgen", "rcgen",
"reqwest 0.12.4", "reqwest 0.12.12",
"rocket", "rocket",
"serde", "serde",
"serde_json", "serde_json",
@@ -2745,9 +2774,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.66" version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if", "cfg-if",
@@ -2786,9 +2815,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.103" version = "0.9.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -3050,26 +3079,6 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@@ -3237,6 +3246,17 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.2.2" version = "0.2.2"
@@ -3257,6 +3277,16 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.5.1" version = "0.5.1"
@@ -3275,6 +3305,16 @@ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.15",
] ]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy",
]
[[package]] [[package]]
name = "rand_hc" name = "rand_hc"
version = "0.2.0" version = "0.2.0"
@@ -3433,8 +3473,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper 0.1.2",
"system-configuration", "system-configuration 0.5.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-util", "tokio-util",
@@ -3449,9 +3489,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.4" version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -3463,6 +3503,7 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.4.1", "hyper 1.4.1",
"hyper-rustls",
"hyper-tls 0.6.0", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
@@ -3477,16 +3518,17 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper 1.0.2",
"system-configuration", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tower",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winreg 0.52.0", "windows-registry",
] ]
[[package]] [[package]]
@@ -3601,7 +3643,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"ref-cast", "ref-cast",
"rustls", "rustls 0.21.12",
"rustls-pemfile 1.0.4", "rustls-pemfile 1.0.4",
"serde", "serde",
"smallvec", "smallvec",
@@ -3609,7 +3651,7 @@ dependencies = [
"state 0.6.0", "state 0.6.0",
"time", "time",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls 0.24.1",
"uncased", "uncased",
] ]
@@ -3649,10 +3691,23 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [ dependencies = [
"log", "log",
"ring", "ring",
"rustls-webpki", "rustls-webpki 0.101.7",
"sct", "sct",
] ]
[[package]]
name = "rustls"
version = "0.23.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.4" version = "1.0.4"
@@ -3674,9 +3729,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.7.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@@ -3688,6 +3743,17 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.17" version = "1.0.17"
@@ -4109,9 +4175,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.1" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
@@ -4141,6 +4207,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.13.1" version = "0.13.1"
@@ -4160,7 +4235,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"system-configuration-sys", "system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"system-configuration-sys 0.6.0",
] ]
[[package]] [[package]]
@@ -4173,6 +4259,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "system-deps" name = "system-deps"
version = "5.0.0" version = "5.0.0"
@@ -4665,7 +4761,17 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [ dependencies = [
"rustls", "rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.22",
"tokio", "tokio",
] ]
@@ -4763,14 +4869,14 @@ dependencies = [
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.4.13" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"pin-project",
"pin-project-lite", "pin-project-lite",
"sync_wrapper 1.0.2",
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@@ -4778,15 +4884,15 @@ dependencies = [
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]] [[package]]
name = "tracing" name = "tracing"
@@ -5024,6 +5130,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.92" version = "0.2.92"
@@ -5306,6 +5421,36 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.42.0" version = "0.42.0"
@@ -5624,6 +5769,15 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.6.0",
]
[[package]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"
@@ -5765,6 +5919,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zerocopy"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.5" version = "0.1.5"
@@ -5786,6 +5960,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.10.4" version = "0.10.4"
+7 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mindwork-ai-studio" name = "mindwork-ai-studio"
version = "0.9.26" version = "0.9.28"
edition = "2021" edition = "2021"
description = "MindWork AI Studio" description = "MindWork AI Studio"
authors = ["Thorsten Sommer"] authors = ["Thorsten Sommer"]
@@ -20,8 +20,8 @@ flexi_logger = "0.29.8"
log = { version = "0.4", features = ["kv"] } log = { version = "0.4", features = ["kv"] }
once_cell = "1.20" once_cell = "1.20"
rocket = { version = "0.5.1", features = ["json", "tls"] } rocket = { version = "0.5.1", features = ["json", "tls"] }
rand = "0.8" rand = "0.9"
rand_chacha = "0.3.1" rand_chacha = "0.9"
base64 = "0.22.1" base64 = "0.22.1"
cipher = { version = "0.4.4", features = ["std"] } cipher = { version = "0.4.4", features = ["std"] }
aes = "0.8.4" aes = "0.8.4"
@@ -36,7 +36,10 @@ url = "2.5"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
# See issue https://github.com/tauri-apps/tauri/issues/4470 # See issue https://github.com/tauri-apps/tauri/issues/4470
reqwest = { version = "0.12", features = ["native-tls-vendored"] } reqwest = { version = "0.12.12", features = ["native-tls-vendored"] }
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
openssl = "0.10.70"
[features] [features]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
+1 -1
View File
@@ -8,7 +8,7 @@ use rocket::request::FromRequest;
/// The API token used to authenticate requests. /// The API token used to authenticate requests.
pub static API_TOKEN: Lazy<APIToken> = Lazy::new(|| { pub static API_TOKEN: Lazy<APIToken> = Lazy::new(|| {
let mut token = [0u8; 32]; let mut token = [0u8; 32];
let mut rng = rand_chacha::ChaChaRng::from_entropy(); let mut rng = rand_chacha::ChaChaRng::from_os_rng();
rng.fill_bytes(&mut token); rng.fill_bytes(&mut token);
let token = APIToken::from_bytes(token.to_vec()); let token = APIToken::from_bytes(token.to_vec());
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more