mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-20 06:18:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aeb51c9f1 | ||
|
|
7c4ddf1164 | ||
|
|
793a9cae0a | ||
|
|
368fc9ce39 | ||
|
|
2ef563e9ab | ||
|
|
32e1ab977d | ||
|
|
7a498d524e | ||
|
|
ce915292dc | ||
|
|
6979a73e5c |
No files matched your search
@@ -33,8 +33,8 @@ Things we are currently working on:
|
||||
- [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~
|
||||
- [x] ~~Add support for other languages (I18N) to AI Studio ([PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~
|
||||
- [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486)~~)
|
||||
- [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations (~~[PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491)~~)
|
||||
- [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~
|
||||
- [x] ~~Add configuration plugins, which allow pre-defining some LLM providers in organizations ([PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491), [PR #493](https://github.com/MindWorkAI/AI-Studio/pull/493), [PR #494](https://github.com/MindWorkAI/AI-Studio/pull/494), [PR #497](https://github.com/MindWorkAI/AI-Studio/pull/497))~~
|
||||
- [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform.
|
||||
- [ ] Add assistant plugins
|
||||
|
||||
@@ -123,6 +123,9 @@ Stay tuned for more updates and enhancements to make MindWork AI Studio even mor
|
||||
## Building
|
||||
You want to know how to build MindWork AI Studio from source? [Check out the instructions here](documentation/Build.md).
|
||||
|
||||
## Enterprise IT
|
||||
Do you want to manage AI Studio centrally from your IT department? Yes, that’s possible. [Here’s how it works.](documentation/Enterprise%20IT.md)
|
||||
|
||||
## License
|
||||
MindWork AI Studio is licensed under the `FSL-1.1-MIT` license (functional source license). Here’s a simple rundown of what that means for you:
|
||||
- **Permitted Use**: Feel free to use, copy, modify, and share the software for your own projects, educational purposes, research, or even in professional services. The key is to use it in a way that doesn't compete with our offerings.
|
||||
|
||||
@@ -67,12 +67,12 @@
|
||||
@if (this.Content.IsStreaming)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Style="white-space: pre-wrap;">
|
||||
@textContent.Text
|
||||
@textContent.Text.RemoveThinkTags()
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
|
||||
<MudMarkdown Value="@textContent.Text.RemoveThinkTags().Trim()" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ public sealed class ContentText : IContent
|
||||
this.IsStreaming = false;
|
||||
}, token);
|
||||
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
return chatThread;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string RemoveThinkTags(this string input)
|
||||
{
|
||||
const string OPEN_TAG = "<think>";
|
||||
const string CLOSE_TAG = "</think>";
|
||||
if (string.IsNullOrWhiteSpace(input) || !input.StartsWith(OPEN_TAG, StringComparison.Ordinal))
|
||||
return input;
|
||||
|
||||
var endIndex = input.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
|
||||
if (endIndex == -1)
|
||||
return string.Empty;
|
||||
|
||||
return input[(endIndex + CLOSE_TAG.Length)..];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (223, "v0.9.48, build 223 (2025-06-10 13:15 UTC)", "v0.9.48.md"),
|
||||
new (222, "v0.9.47, build 222 (2025-06-02 18:25 UTC)", "v0.9.47.md"),
|
||||
new (221, "v0.9.46, build 221 (2025-06-01 19:19 UTC)", "v0.9.46.md"),
|
||||
new (220, "v0.9.45, build 220 (2025-05-25 13:56 UTC)", "v0.9.45.md"),
|
||||
new (219, "v0.9.44, build 219 (2025-05-18 19:33 UTC)", "v0.9.44.md"),
|
||||
|
||||
@@ -13,10 +13,6 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
private ILogger<MSGComponentBase> Logger { get; init; } = null!;
|
||||
|
||||
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
@@ -45,19 +41,22 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
await this.InvokeAsync(async () =>
|
||||
{
|
||||
case Event.COLOR_THEME_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.COLOR_THEME_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@this.Message
|
||||
</MudText>
|
||||
</MudJustifiedText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
using EnterpriseEnvironment = AIStudio.Tools.EnterpriseEnvironment;
|
||||
|
||||
namespace AIStudio.Layout;
|
||||
|
||||
@@ -138,90 +139,100 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
|
||||
public async Task ProcessMessage<TMessage>(ComponentBase? sendingComponent, Event triggeredEvent, TMessage? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
await this.InvokeAsync(async () =>
|
||||
{
|
||||
case Event.UPDATE_AVAILABLE:
|
||||
if (data is UpdateResponse updateResponse)
|
||||
{
|
||||
this.currentUpdateResponse = updateResponse;
|
||||
var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
|
||||
this.Snackbar.Add(message, Severity.Info, config =>
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.UPDATE_AVAILABLE:
|
||||
if (data is UpdateResponse updateResponse)
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Update;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 32_000;
|
||||
config.OnClick = async _ =>
|
||||
this.currentUpdateResponse = updateResponse;
|
||||
var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
|
||||
this.Snackbar.Add(message, Severity.Info, config =>
|
||||
{
|
||||
await this.ShowUpdateDialog();
|
||||
};
|
||||
config.Action = T("Show details");
|
||||
config.ActionVariant = Variant.Filled;
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||
this.navBarOpen = true;
|
||||
else
|
||||
this.navBarOpen = false;
|
||||
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.COLOR_THEME_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_SUCCESS:
|
||||
if (data is DataSuccessMessage success)
|
||||
success.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_ERROR:
|
||||
if (data is DataErrorMessage error)
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_WARNING:
|
||||
if (data is DataWarningMessage warning)
|
||||
warning.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.STARTUP_PLUGIN_SYSTEM:
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Set up the plugin system:
|
||||
if (PluginFactory.Setup())
|
||||
{
|
||||
// Ensure that all internal plugins are present:
|
||||
await PluginFactory.EnsureInternalPlugins();
|
||||
|
||||
// Load (but not start) all plugins, without waiting for them:
|
||||
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
config.Icon = Icons.Material.Filled.Update;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 32_000;
|
||||
config.OnClick = async _ =>
|
||||
{
|
||||
await this.ShowUpdateDialog();
|
||||
};
|
||||
config.Action = T("Show details");
|
||||
config.ActionVariant = Variant.Filled;
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
I18N.Init(this.Lang);
|
||||
this.LoadNavItems();
|
||||
break;
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||
this.navBarOpen = true;
|
||||
else
|
||||
this.navBarOpen = false;
|
||||
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.COLOR_THEME_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_SUCCESS:
|
||||
if (data is DataSuccessMessage success)
|
||||
success.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_ERROR:
|
||||
if (data is DataErrorMessage error)
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_WARNING:
|
||||
if (data is DataWarningMessage warning)
|
||||
warning.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.STARTUP_PLUGIN_SYSTEM:
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Set up the plugin system:
|
||||
if (PluginFactory.Setup())
|
||||
{
|
||||
// Ensure that all internal plugins are present:
|
||||
await PluginFactory.EnsureInternalPlugins();
|
||||
|
||||
//
|
||||
// Check if there is an enterprise configuration plugin to download:
|
||||
//
|
||||
var enterpriseEnvironment = this.MessageBus.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT).FirstOrDefault();
|
||||
if (enterpriseEnvironment != default)
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl);
|
||||
|
||||
// Load (but not start) all plugins without waiting for them:
|
||||
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
I18N.Init(this.Lang);
|
||||
this.LoadNavItems();
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.2.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.6" />
|
||||
<PackageReference Include="MudBlazor" Version="8.6.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.6.0" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||
|
||||
+9
-9
@@ -1129,7 +1129,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T3032662264"] = "Benutzerdefinierte Sprache"
|
||||
|
||||
-- Your input to improve
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T3037449423"] = "Ihr Vorschlag zur Verbesserung"
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T3037449423"] = "Ihr Text, der verbessert werden soll"
|
||||
|
||||
-- Writing style
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T3754048862"] = "Schreibstil"
|
||||
@@ -4213,7 +4213,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "Diese Bibliothek wird
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "Für die sichere Kommunikation zwischen der Benutzeroberfläche und der Laufzeit müssen wir Zertifikate erstellen. Diese Rust-Bibliothek eignet sich hervorragend dafür."
|
||||
|
||||
-- AI Studio runs without an enterprise configuration.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio läuft ohne eine Konfiguration ihrer Organisation."
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "Dies ist eine private AI Studio-Installation. Es wird keine Konfiguration einer Organisation verwendet."
|
||||
|
||||
-- OK
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK"
|
||||
@@ -5014,7 +5014,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMMONLANGUAGEEXTENSIONS::T4187368718"] = "Rus
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMMONLANGUAGEEXTENSIONS::T658789330"] = "Sprache nicht ändern"
|
||||
|
||||
-- Coding Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1082499335"] = "Programmierassistent"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1082499335"] = "Programmier-Assistent"
|
||||
|
||||
-- E-Mail Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1185802704"] = "E-Mail-Assistent"
|
||||
@@ -5023,25 +5023,25 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1185802704"] = "E-Mail-
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "Meine Aufgaben-Assistent"
|
||||
|
||||
-- Grammar & Spelling Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammatik- & Rechtschreibassistent"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammatik- & Rechtschreib-Assistent"
|
||||
|
||||
-- Legal Check Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Assistent für rechtliche Prüfungen"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Rechtlichen Prüfungs-Assistent"
|
||||
|
||||
-- Job Posting Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2212811874"] = "Assistent für Stellenanzeigen"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2212811874"] = "Stellenanzeigen-Assistent"
|
||||
|
||||
-- Icon Finder Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2457005512"] = "Icon Finder-Assistent"
|
||||
|
||||
-- Text Summarizer Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2684676843"] = "Assistent zur Textzusammenfassung"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2684676843"] = "Texte zusammenfassen-Assistent"
|
||||
|
||||
-- Synonym Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2921123194"] = "Synonym-Assistent"
|
||||
|
||||
-- Translation Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T3887962308"] = "Übersetzungsassistent"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T3887962308"] = "Übersetzungs-Assistent"
|
||||
|
||||
-- Agenda Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4034795997"] = "Agenda-Assistent"
|
||||
@@ -5050,7 +5050,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4034795997"] = "Agenda-
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4204533420"] = "ERI-Server"
|
||||
|
||||
-- Rewrite Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4262148639"] = "Assistent zum Umformulieren & Verbessern"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4262148639"] = "Umformulieren & Verbessern-Assistent"
|
||||
|
||||
-- Localization Assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Lokalisierungs-Assistent"
|
||||
|
||||
+1
-1
@@ -4213,7 +4213,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "This library is used t
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose."
|
||||
|
||||
-- AI Studio runs without an enterprise configuration.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio runs without an enterprise configuration."
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
|
||||
|
||||
-- OK
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK"
|
||||
|
||||
@@ -160,6 +160,7 @@ internal sealed class Program
|
||||
|
||||
// Get the logging factory for e.g., static classes:
|
||||
LOGGER_FACTORY = app.Services.GetRequiredService<ILoggerFactory>();
|
||||
MessageBus.INSTANCE.Initialize(LOGGER_FACTORY.CreateLogger<MessageBus>());
|
||||
|
||||
// Get a program logger:
|
||||
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId)
|
||||
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, EntityTagHeaderValue? ETag)
|
||||
{
|
||||
public bool IsActive => !string.IsNullOrEmpty(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public enum Event
|
||||
CONFIGURATION_CHANGED,
|
||||
COLOR_THEME_CHANGED,
|
||||
STARTUP_PLUGIN_SYSTEM,
|
||||
STARTUP_ENTERPRISE_ENVIRONMENT,
|
||||
PLUGINS_RELOADED,
|
||||
SHOW_ERROR,
|
||||
SHOW_WARNING,
|
||||
|
||||
@@ -15,10 +15,18 @@ public sealed class MessageBus
|
||||
private readonly ConcurrentQueue<Message> messageQueue = new();
|
||||
private readonly SemaphoreSlim sendingSemaphore = new(1, 1);
|
||||
|
||||
private static ILogger<MessageBus>? LOG;
|
||||
|
||||
private MessageBus()
|
||||
{
|
||||
}
|
||||
|
||||
public void Initialize(ILogger<MessageBus> logger)
|
||||
{
|
||||
LOG = logger;
|
||||
LOG.LogInformation("Message bus initialized.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define for which components and events you want to receive messages.
|
||||
/// </summary>
|
||||
@@ -61,11 +69,16 @@ public sealed class MessageBus
|
||||
|
||||
var eventFilter = this.componentEvents[receiver];
|
||||
if (eventFilter.Length == 0 || eventFilter.Contains(triggeredEvent))
|
||||
|
||||
// We don't await the task here because we don't want to block the message bus:
|
||||
_ = receiver.ProcessMessage(message.SendingComponent, message.TriggeredEvent, message.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG?.LogError(e, "Error while sending message.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.sendingSemaphore.Release();
|
||||
|
||||
@@ -1,33 +1,65 @@
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
public static async Task<EntityTagHeaderValue?> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if(configPlugId == Guid.Empty || string.IsNullOrWhiteSpace(configServerUrl))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
|
||||
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
|
||||
|
||||
using var http = new HttpClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
|
||||
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
return response.Headers.ETag;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
if(!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Downloading configuration plugin with ID: {configPlugId} from server: {configServerUrl}");
|
||||
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
|
||||
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
|
||||
|
||||
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
|
||||
var tempDownloadFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await LockHotReloadAsync();
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.GetAsync($"{configServerUrl}/{configPlugId}.zip", cancellationToken);
|
||||
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await using var tempFileStream = File.Create(tempDownloadFile);
|
||||
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
|
||||
await using(var tempFileStream = File.Create(tempDownloadFile))
|
||||
{
|
||||
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
|
||||
}
|
||||
|
||||
var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||
if(Directory.Exists(pluginDirectory))
|
||||
Directory.Delete(pluginDirectory, true);
|
||||
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||
if(Directory.Exists(configDirectory))
|
||||
Directory.Delete(configDirectory, true);
|
||||
|
||||
Directory.CreateDirectory(pluginDirectory);
|
||||
ZipFile.ExtractToDirectory(tempDownloadFile, pluginDirectory);
|
||||
Directory.CreateDirectory(configDirectory);
|
||||
ZipFile.ExtractToDirectory(tempDownloadFile, configDirectory);
|
||||
|
||||
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{pluginDirectory}'.");
|
||||
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
|
||||
}
|
||||
else
|
||||
LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}");
|
||||
@@ -49,6 +81,8 @@ public static partial class PluginFactory
|
||||
LOG.LogError(e, "Failed to delete the temporary download file.");
|
||||
}
|
||||
}
|
||||
|
||||
UnlockHotReload();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -46,9 +46,41 @@ public static partial class PluginFactory
|
||||
try
|
||||
{
|
||||
LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins...");
|
||||
if (File.Exists(HOT_RELOAD_LOCK_FILE))
|
||||
{
|
||||
LOG.LogInformation("Hot reload lock file exists. Waiting for it to be released before proceeding with the reload.");
|
||||
|
||||
var lockFileCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var token = lockFileCancellationTokenSource.Token;
|
||||
var waitTime = TimeSpan.FromSeconds(1);
|
||||
while (File.Exists(HOT_RELOAD_LOCK_FILE) && !token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
LOG.LogDebug("Waiting for hot reload lock to be released...");
|
||||
await Task.Delay(waitTime, token);
|
||||
waitTime = TimeSpan.FromSeconds(Math.Min(waitTime.TotalSeconds * 2, 120)); // Exponential backoff with a cap
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Case: The cancellation token was triggered, meaning the lock file is still present.
|
||||
// We expect that something goes wrong. So, we try to delete the lock file:
|
||||
LOG.LogWarning("Hot reload lock file still exists after 30 seconds. Attempting to delete it...");
|
||||
UnlockHotReload();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG.LogInformation("Hot reload lock file released. Proceeding with plugin reload.");
|
||||
}
|
||||
|
||||
await LoadAll();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.LogError(e, $"Error while reloading plugins after change in file '{args.FullPath}' with change type '{changeType}'.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
HOT_RELOAD_SEMAPHORE.Release();
|
||||
|
||||
@@ -13,6 +13,7 @@ public static partial class PluginFactory
|
||||
private static string PLUGINS_ROOT = string.Empty;
|
||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||
private static string CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
|
||||
|
||||
@@ -27,8 +28,10 @@ public static partial class PluginFactory
|
||||
if(IS_INITIALIZED)
|
||||
return false;
|
||||
|
||||
LOG.LogInformation("Initializing plugin factory...");
|
||||
DATA_DIR = SettingsManager.DataDirectory!;
|
||||
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock");
|
||||
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
|
||||
|
||||
@@ -37,10 +40,55 @@ public static partial class PluginFactory
|
||||
|
||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||
IS_INITIALIZED = true;
|
||||
|
||||
LOG.LogInformation("Plugin factory initialized successfully.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task LockHotReloadAsync()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(HOT_RELOAD_LOCK_FILE))
|
||||
{
|
||||
LOG.LogWarning("Hot reload lock file already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(HOT_RELOAD_LOCK_FILE, DateTime.UtcNow.ToString("o"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError(e, "An error occurred while trying to lock hot reloading.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnlockHotReload()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(File.Exists(HOT_RELOAD_LOCK_FILE))
|
||||
File.Delete(HOT_RELOAD_LOCK_FILE);
|
||||
else
|
||||
LOG.LogWarning("Hot reload lock file does not exist. Nothing to unlock.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError(e, "An error occurred while trying to unlock hot reloading.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
if(!IS_INITIALIZED)
|
||||
|
||||
@@ -2,11 +2,15 @@ using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> logger, RustService rustService) : BackgroundService
|
||||
public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentService> logger, RustService rustService) : BackgroundService
|
||||
{
|
||||
public static EnterpriseEnvironment CURRENT_ENVIRONMENT;
|
||||
|
||||
#if DEBUG
|
||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
||||
#else
|
||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(16);
|
||||
#endif
|
||||
|
||||
#region Overrides of BackgroundService
|
||||
|
||||
@@ -14,7 +18,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> l
|
||||
{
|
||||
logger.LogInformation("The enterprise environment service was initialized.");
|
||||
|
||||
await this.StartUpdating();
|
||||
await this.StartUpdating(isFirstRun: true);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(CHECK_INTERVAL, stoppingToken);
|
||||
@@ -24,7 +28,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> l
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task StartUpdating()
|
||||
private async Task StartUpdating(bool isFirstRun = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -40,7 +44,8 @@ public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> l
|
||||
|
||||
var enterpriseConfigServerUrl = await rustService.EnterpriseEnvConfigServerUrl();
|
||||
var enterpriseConfigId = await rustService.EnterpriseEnvConfigId();
|
||||
var nextEnterpriseEnvironment = new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId);
|
||||
var etag = await PluginFactory.DetermineConfigPluginETagAsync(enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
var nextEnterpriseEnvironment = new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId, etag);
|
||||
if (CURRENT_ENVIRONMENT != nextEnterpriseEnvironment)
|
||||
{
|
||||
logger.LogInformation("The enterprise environment has changed. Updating the current environment.");
|
||||
@@ -49,6 +54,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> l
|
||||
switch (enterpriseConfigServerUrl)
|
||||
{
|
||||
case null when enterpriseConfigId == Guid.Empty:
|
||||
case not null when string.IsNullOrWhiteSpace(enterpriseConfigServerUrl) && enterpriseConfigId == Guid.Empty:
|
||||
logger.LogInformation("AI Studio runs without an enterprise configuration.");
|
||||
break;
|
||||
|
||||
@@ -56,16 +62,22 @@ public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> l
|
||||
logger.LogWarning($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}'), but the configuration server URL is not set.");
|
||||
break;
|
||||
|
||||
case not null when enterpriseConfigId == Guid.Empty:
|
||||
case not null when !string.IsNullOrWhiteSpace(enterpriseConfigServerUrl) && enterpriseConfigId == Guid.Empty:
|
||||
logger.LogWarning($"AI Studio runs with an enterprise configuration server URL ('{enterpriseConfigServerUrl}'), but the configuration ID is not set.");
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogInformation($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}') and configuration server URL ('{enterpriseConfigServerUrl}').");
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
|
||||
if(isFirstRun)
|
||||
MessageBus.INSTANCE.DeferMessage(null, Event.STARTUP_ENTERPRISE_ENVIRONMENT, new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId, etag));
|
||||
else
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
logger.LogInformation("The enterprise environment has not changed. No update required.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -30,18 +30,18 @@
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Embedded": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.5, )",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "tgYXJtPa72hYrQuw+pqJvvhUOOQtZuk5jhRZINxIgR0cXwe4bLCQhCGffN+Ad4+AIQOlz4YyOc+GX+unsHc9Kg==",
|
||||
"requested": "[9.0.6, )",
|
||||
"resolved": "9.0.6",
|
||||
"contentHash": "KyXroxnOI6ZpCth3pW718Fn4hwtvgjjLCKRJg43UFR/5Wfo0ZKVGUJGnQT3+fTkoyLCElJUKwIHezY6IjOXz/g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.5"
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.6"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.5, )",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "4L7wbb3Y4BJgIeSnmAf/C6S/qCs9rT1b0dX72uj3qo0qvUH7D0U1tU5ZlC7A3Wb8E/dXK4caOi4JkdLTVJaXhw=="
|
||||
"requested": "[9.0.6, )",
|
||||
"resolved": "9.0.6",
|
||||
"contentHash": "TXy3SbJzGXQbxxIxCjdrp8bwEyTDImyYNpTpd6v7P3JL2Y7dno8EYG7dPezfYTa5SoWKdhbH9cbnwHHs3BR5gA=="
|
||||
},
|
||||
"MudBlazor": {
|
||||
"type": "Direct",
|
||||
@@ -173,10 +173,10 @@
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "LLm+e8lvD+jOI+blHRSxPqywPaohOTNcVzQv548R1UpkEiNB2D+zf3RrqxBdB1LDPicRMTnfiaKJovxF8oX1bQ==",
|
||||
"resolved": "9.0.6",
|
||||
"contentHash": "q9FPkSGVA9ipI255p3PBAvWNXas5Tzjyp/DwYSwT+46mIFw9fWZahsF6vHpoxLt5/vtANotH2sAm7HunuFIx9g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.5"
|
||||
"Microsoft.Extensions.Primitives": "9.0.6"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Localization": {
|
||||
@@ -214,8 +214,8 @@
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "b4OAv1qE1C9aM+ShWJu3rlo/WjDwa/I30aIPXqDWSKXTtKl1Wwh6BZn+glH5HndGVVn3C6ZAPQj5nv7/7HJNBQ=="
|
||||
"resolved": "9.0.6",
|
||||
"contentHash": "BHniU24QV67qp1pJknqYSofAPYGmijGI8D+ci9yfw33iuFdyOeB9lWTg78ThyYLyQwZw3s0vZ36VMb0MqbUuLw=="
|
||||
},
|
||||
"Microsoft.JSInterop": {
|
||||
"type": "Transitive",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# v0.9.47, build 222 (2025-06-xx xx:xx UTC)
|
||||
# v0.9.47, build 222 (2025-06-02 18:25 UTC)
|
||||
- We improved AI Studio for managed enterprise environments by refactoring the remote config server handling, the config plugin loading mechanism, and we improved the overall resilience.
|
||||
@@ -0,0 +1,6 @@
|
||||
# v0.9.48, build 223 (2025-06-11 08:14 UTC)
|
||||
- Improved German translation.
|
||||
- Improved the confirmation dialog for longer texts.
|
||||
- Improved how configurations are applied in organizations and made the process more robust.
|
||||
- Removed `<think>` tags from streaming and final text output because of reasoning models.
|
||||
- Upgraded to .NET 9.0.6.
|
||||
@@ -0,0 +1 @@
|
||||
# v0.9.49, build 224 (2025-06-xx xx:xx UTC)
|
||||
@@ -0,0 +1,95 @@
|
||||
# Enterprise IT
|
||||
|
||||
## Overview
|
||||
Do you want to manage MindWork AI Studio in a corporate environment or within an organization? This documentation explains what you need to do and how it works. First, here's an overview of the entire process:
|
||||
|
||||
- You can distribute MindWork AI Studio to employees' devices using tools like Microsoft System Center Configuration Manager (SCCM).
|
||||
- Employees can get updates through the built-in update feature. If you want, you can disable automatic updates and control which version gets distributed.
|
||||
- AI Studio checks about every 16 minutes to see where and which configuration it should load. This information is loaded from the local system. On Windows, you might use the registry, for example.
|
||||
- If it finds the necessary metadata, AI Studio downloads the configuration as a ZIP file from the specified server.
|
||||
- The configuration is an AI Studio plugin written in Lua.
|
||||
- Any changes to the configuration apply live while the software is running, so employees don’t need to restart it.
|
||||
|
||||
AI Studio checks about every 16 minutes to see if the configuration ID, the server for the configuration, or the configuration itself has changed. If it finds any changes, it loads the updated configuration from the server and applies it right away.
|
||||
|
||||
## Configure the devices
|
||||
So that MindWork AI Studio knows where to load which configuration, this information must be provided as metadata on employees’ devices. Currently, the following options are available:
|
||||
|
||||
- **Registry** (only available for Microsoft Windows): On Windows devices, AI Studio first tries to read the information from the registry. The registry information can be managed and distributed centrally as a so-called Group Policy Object (GPO).
|
||||
|
||||
- **Environment variables**: On all operating systems (on Windows as a fallback after the registry), AI Studio tries to read the configuration metadata from environment variables.
|
||||
|
||||
The following keys and values (registry) and variables are checked and read:
|
||||
|
||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID`: This must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). It uniquely identifies the configuration. You can use an ID per department, institute, or even per person.
|
||||
|
||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `delete_config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID`: This is a configuration ID that should be removed. This is helpful if an employee moves to a different department or leaves the organization.
|
||||
|
||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_server_url` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL`: An HTTP or HTTPS address using an IP address or DNS name. This is the web server from which AI Studio attempts to load the specified configuration as a ZIP file.
|
||||
|
||||
Let's assume as example that `https://intranet.my-company.com:30100/ai-studio/configuration` is the server address and `9072b77d-ca81-40da-be6a-861da525ef7b` is the configuration ID. AI Studio will derive the following address from this information: `https://intranet.my-company.com:30100/ai-studio/configuration/9072b77d-ca81-40da-be6a-861da525ef7b.zip`. Important: The configuration ID will always be written in lowercase, even if it is configured in uppercase. If `9072B77D-CA81-40DA-BE6A-861DA525EF7B` is configured, the same address will be derived. Your web server must be configured accordingly.
|
||||
|
||||
Finally, AI Studio will send a GET request and download the ZIP file. The ZIP file only contains the files necessary for the configuration. It's normal to include a file for an icon along with the actual configuration plugin.
|
||||
|
||||
Approximately every 16 minutes, AI Studio checks the metadata of the ZIP file by reading the [ETag](https://en.wikipedia.org/wiki/HTTP_ETag). When the ETag was not changed, no download will be performed. Make sure that your web server supports this.
|
||||
|
||||
## Configure the configuration web server
|
||||
|
||||
In principle, you can use any web server that can serve ZIP files from a folder. However, keep in mind that AI Studio queries the file's metadata using [ETag](https://en.wikipedia.org/wiki/HTTP_ETag). Your web server must support this feature. For security reasons, you should also make sure that users cannot list the contents of the directory. This is important because the different configurations may contain confidential information such as API keys. Each user should only know their own configuration ID. Otherwise, a user might try to use someone else’s ID to gain access to exclusive resources.
|
||||
|
||||
The ZIP file names for the configurations must be in lowercase on the server, or your web server needs to ignore the spelling in requests. Also, make sure the web server is only accessible within your organization’s intranet. You don’t want the server open to everyone worldwide.
|
||||
|
||||
You can use the open source web server [Caddy](https://caddyserver.com/). The project is openly developed on [GitHub](https://github.com/caddyserver/caddy). Below you’ll find an example configuration, a so-called `Caddyfile`, for serving configurations from the folder `/localdata1/ai-studio/config` to AI Studio. The TLS certificates are loaded from the folder `/localdata1/tls-certificate`.
|
||||
|
||||
```
|
||||
{
|
||||
# Disable logging:
|
||||
log {
|
||||
output discard
|
||||
}
|
||||
|
||||
# Disable automatic HTTPS redirection:
|
||||
auto_https off
|
||||
}
|
||||
|
||||
intranet.my-company.com:30100 {
|
||||
# Load TLS certificates:
|
||||
tls /localdata1/tls-certificate/cert_webserver.pem /localdata1/tls-certificate/key_webserver.pem
|
||||
|
||||
# Serve the configuration files:
|
||||
handle_path /ai-studio/configuration/* {
|
||||
file_server {
|
||||
root /localdata1/ai-studio/config
|
||||
|
||||
# Disable directory browsing:
|
||||
browse false
|
||||
}
|
||||
}
|
||||
|
||||
# All other requests will receive a 404 Not Found response:
|
||||
handle {
|
||||
respond "Not Found" 404
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example AI Studio configuration
|
||||
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files:
|
||||
|
||||
- [The icon](../app/MindWork%20AI%20Studio/Plugins/configuration/icon.lua)
|
||||
- [The configuration with explanations](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua)
|
||||
|
||||
Please note that the icon must be an SVG vector graphic. Raster graphics like PNGs, GIFs, and others aren’t supported. You can use the sample icon, which looks like a gear.
|
||||
|
||||
Currently, you can configure the following things:
|
||||
- Any number of self-hosted LLM providers (a combination of server and model), but currently only without API keys
|
||||
- The update behavior of AI Studio
|
||||
|
||||
All other settings can be made by the user themselves. If you need additional settings, feel free to create an issue in our planning repository: https://github.com/MindWorkAI/Planning/issues
|
||||
|
||||
In the coming months, we will allow more settings, such as:
|
||||
- Using API keys for providers
|
||||
- Configuration of embedding providers for RAG
|
||||
- Configuration of data sources for RAG
|
||||
- Configuration of chat templates
|
||||
- Configuration of assistant plugins (for example, your own assistants for your company or specific departments)
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
0.9.46
|
||||
2025-06-01 19:19:04 UTC
|
||||
221
|
||||
9.0.106 (commit 8cdf6f0897)
|
||||
9.0.5 (commit e36e4d1a8f)
|
||||
0.9.48
|
||||
2025-06-11 08:14:42 UTC
|
||||
223
|
||||
9.0.107 (commit ef50dc9d27)
|
||||
9.0.6 (commit 3875b54e7b)
|
||||
1.87.0 (commit 17067e9ac)
|
||||
8.6.0
|
||||
1.8.1
|
||||
e8cfce05fea, release
|
||||
2616101607a, release
|
||||
osx-arm64
|
||||
137.0.7123.0
|
||||
Generated
+1
-1
@@ -2624,7 +2624,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.46"
|
||||
version = "0.9.48"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.46"
|
||||
version = "0.9.48"
|
||||
edition = "2021"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
|
||||
+14
-12
@@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
use std::sync::OnceLock;
|
||||
use log::info;
|
||||
use log::{debug, warn};
|
||||
use rocket::{delete, get};
|
||||
use sys_locale::get_locale;
|
||||
use crate::api_token::APIToken;
|
||||
@@ -42,7 +42,7 @@ pub fn is_prod() -> bool {
|
||||
#[get("/system/language")]
|
||||
pub fn read_user_language(_token: APIToken) -> String {
|
||||
get_locale().unwrap_or_else(|| {
|
||||
log::warn!("Could not determine the system language. Use default 'en-US'.");
|
||||
warn!("Could not determine the system language. Use default 'en-US'.");
|
||||
String::from("en-US")
|
||||
})
|
||||
}
|
||||
@@ -64,6 +64,7 @@ pub fn read_enterprise_env_config_id(_token: APIToken) -> String {
|
||||
// The environment variable is:
|
||||
// MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID
|
||||
//
|
||||
debug!("Trying to read the enterprise environment for some config ID.");
|
||||
get_enterprise_configuration(
|
||||
"config_id",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID",
|
||||
@@ -87,6 +88,7 @@ pub fn delete_enterprise_env_config_id(_token: APIToken) -> String {
|
||||
// The environment variable is:
|
||||
// MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID
|
||||
//
|
||||
debug!("Trying to read the enterprise environment for some config ID, which should be deleted.");
|
||||
get_enterprise_configuration(
|
||||
"delete_config_id",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID",
|
||||
@@ -110,6 +112,7 @@ pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String {
|
||||
// The environment variable is:
|
||||
// MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL
|
||||
//
|
||||
debug!("Trying to read the enterprise environment for the config server URL.");
|
||||
get_enterprise_configuration(
|
||||
"config_server_url",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL",
|
||||
@@ -117,23 +120,22 @@ pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String {
|
||||
}
|
||||
|
||||
fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
info!("Trying to read the enterprise environment for some predefined configuration.");
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "windows")] {
|
||||
info!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or environment variables.");
|
||||
debug!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or environment variables.");
|
||||
use windows_registry::*;
|
||||
let key_path = r"Software\github\MindWork AI Studio\Enterprise IT";
|
||||
let key = match CURRENT_USER.open(key_path) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
info!(r"Could not read the registry key HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT. Falling back to environment variables.");
|
||||
debug!(r"Could not read the registry key HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT. Falling back to environment variables.");
|
||||
return match env::var(env_name) {
|
||||
Ok(val) => {
|
||||
info!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
debug!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
val
|
||||
},
|
||||
Err(_) => {
|
||||
info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name);
|
||||
debug!("Falling back to the environment variable '{}' was not successful.", env_name);
|
||||
"".to_string()
|
||||
},
|
||||
}
|
||||
@@ -143,14 +145,14 @@ fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
match key.get_string(_reg_value) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
info!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", _reg_value);
|
||||
debug!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", _reg_value);
|
||||
match env::var(env_name) {
|
||||
Ok(val) => {
|
||||
info!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
debug!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
val
|
||||
},
|
||||
Err(_) => {
|
||||
info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name);
|
||||
debug!("Falling back to the environment variable '{}' was not successful.", env_name);
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
@@ -158,11 +160,11 @@ fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
}
|
||||
} else {
|
||||
// In the case of macOS or Linux, we just read the environment variable:
|
||||
info!(r"Detected a Unix machine, trying to read the environment variable '{}'.", env_name);
|
||||
debug!(r"Detected a Unix machine, trying to read the environment variable '{}'.", env_name);
|
||||
match env::var(env_name) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
info!("The environment variable '{}' was not found. It appears that this is not an enterprise environment.", env_name);
|
||||
debug!("The environment variable '{}' was not found.", env_name);
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "MindWork AI Studio",
|
||||
"version": "0.9.46"
|
||||
"version": "0.9.48"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in new issue
Block a user