mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-14 03:33:48 +00:00
Merge branch 'main' into chunk-data
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
This commit is contained in:
640 files changed
+45059
-4952
No files matched your search
@@ -12,42 +12,26 @@
|
||||
|
||||
<InnerScrolling>
|
||||
|
||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("General",
|
||||
(Components.TEXT_SUMMARIZER_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.TRANSLATION_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.GRAMMAR_SPELLING_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.REWRITE_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.PROMPT_OPTIMIZER_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.SYNONYMS_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.META_ASSISTANT, PreviewFeatures.PRE_META_ASSISTANT_V1)
|
||||
))
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3">
|
||||
@T("General")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogTextSummarizer" Component="Components.TEXT_SUMMARIZER_ASSISTANT" Name="@T("Text Summarizer")" Description="@T("Use an LLM to summarize a given text.")" Icon="@Icons.Material.Filled.TextSnippet" Link="@Routes.ASSISTANT_SUMMARIZER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogTranslation" Component="Components.TRANSLATION_ASSISTANT" Name="@T("Translation")" Description="@T("Translate text into another language.")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_TRANSLATION"/>
|
||||
<AssistantBlock TSettings="SettingsDialogGrammarSpelling" Component="Components.GRAMMAR_SPELLING_ASSISTANT" Name="@T("Grammar & Spelling")" Description="@T("Check grammar and spelling of a given text.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_GRAMMAR_SPELLING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogRewrite" Component="Components.REWRITE_ASSISTANT" Name="@T("Rewrite & Improve")" Description="@T("Rewrite and improve a given text for a chosen style.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/>
|
||||
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
||||
</MudStack>
|
||||
}
|
||||
<AssistantCategoryBlock Title="@T("General")" HeaderClass="mb-2 mr-3">
|
||||
<AssistantBlock TSettings="SettingsDialogTextSummarizer" Component="Components.TEXT_SUMMARIZER_ASSISTANT" Name="@T("Text Summarizer")" Description="@T("Use an LLM to summarize a given text.")" Icon="@Icons.Material.Filled.TextSnippet" Link="@Routes.ASSISTANT_SUMMARIZER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogTranslation" Component="Components.TRANSLATION_ASSISTANT" Name="@T("Translation")" Description="@T("Translate text into another language.")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_TRANSLATION"/>
|
||||
<AssistantBlock TSettings="SettingsDialogGrammarSpelling" Component="Components.GRAMMAR_SPELLING_ASSISTANT" Name="@T("Grammar & Spelling")" Description="@T("Check grammar and spelling of a given text.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_GRAMMAR_SPELLING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogRewrite" Component="Components.REWRITE_ASSISTANT" Name="@T("Rewrite & Improve")" Description="@T("Rewrite and improve a given text for a chosen style.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/>
|
||||
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
@if (this.AssistantPlugins.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||
@T("Installed Assistants")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantCategoryBlock Title="@T("Installed Assistants")">
|
||||
@foreach (var assistantPlugin in this.AssistantPlugins)
|
||||
{
|
||||
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, assistantPlugin);
|
||||
var launchLink = assistantPlugin.StartsChatDirectly ? string.Empty : $"{Routes.ASSISTANT_DYNAMIC}?assistantId={assistantPlugin.Id}";
|
||||
var availablePlugin = PluginFactory.AvailablePlugins.OfType<IAvailablePlugin>().FirstOrDefault(plugin => plugin.Id == assistantPlugin.Id);
|
||||
<AssistantBlock TSettings="NoSettingsPanel"
|
||||
Component="Components.DYNAMIC_ASSISTANT"
|
||||
Name="@T(assistantPlugin.AssistantTitle)"
|
||||
Description="@T(assistantPlugin.Description)"
|
||||
Icon="@Icons.Material.Filled.FindInPage"
|
||||
@@ -56,9 +40,10 @@
|
||||
Link="@launchLink"
|
||||
OnClick="@(() => this.StartAssistantPluginAsync(assistantPlugin))">
|
||||
<AdditionalActions>
|
||||
<DirectChatLauncherSettingsAction Plugin="@assistantPlugin" />
|
||||
@if (availablePlugin is not null)
|
||||
{
|
||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
||||
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||
}
|
||||
</AdditionalActions>
|
||||
<SecurityBadge>
|
||||
@@ -66,76 +51,35 @@
|
||||
</SecurityBadge>
|
||||
</AssistantBlock>
|
||||
}
|
||||
</MudStack>
|
||||
</AssistantCategoryBlock>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business",
|
||||
(Components.EMAIL_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.MY_TASKS_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.AGENDA_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.LEGAL_CHECK_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.SLIDE_BUILDER_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.VISUAL_BRIEFING_ASSISTANT, Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature())
|
||||
))
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||
@T("Business")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogWritingEMails" Component="Components.EMAIL_ASSISTANT" Name="@T("E-Mail")" Description="@T("Generate an e-mail for a given context.")" Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Name="@T("Document Analysis")" Description="@T("Analyze a document regarding defined rules and extract key information.")" Icon="@Icons.Material.Filled.DocumentScanner" Link="@Routes.ASSISTANT_DOCUMENT_ANALYSIS"/>
|
||||
<AssistantBlock TSettings="SettingsDialogMyTasks" Component="Components.MY_TASKS_ASSISTANT" Name="@T("My Tasks")" Description="@T("Analyze a text or an email for tasks you need to complete.")" Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
|
||||
<AssistantBlock TSettings="SettingsDialogAgenda" Component="Components.AGENDA_ASSISTANT" Name="@T("Agenda Planner")" Description="@T("Generate an agenda for a given meeting, seminar, etc.")" Icon="@Icons.Material.Filled.CalendarToday" Link="@Routes.ASSISTANT_AGENDA"/>
|
||||
<AssistantBlock TSettings="SettingsDialogJobPostings" Component="Components.JOB_POSTING_ASSISTANT" Name="@T("Job Posting")" Description="@T("Generate a job posting for a given job description.")" Icon="@Icons.Material.Filled.Work" Link="@Routes.ASSISTANT_JOB_POSTING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogLegalCheck" Component="Components.LEGAL_CHECK_ASSISTANT" Name="@T("Legal Check")" Description="@T("Ask a question about a legal document.")" Icon="@Icons.Material.Filled.Gavel" Link="@Routes.ASSISTANT_LEGAL_CHECK"/>
|
||||
<AssistantBlock TSettings="SettingsDialogIconFinder" Component="Components.ICON_FINDER_ASSISTANT" Name="@T("Icon Finder")" Description="@T("Use an LLM to find an icon for a given context.")" Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogSlideBuilder" Component="Components.SLIDE_BUILDER_ASSISTANT" Name="@T("Slide Planner Assistant")" Description="@T("Develop slide content based on a given topic and content.")" Icon="@Icons.Material.Filled.Slideshow" Link="@Routes.ASSISTANT_SLIDE_BUILDER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogVisualBriefing" Component="Components.VISUAL_BRIEFING_ASSISTANT" RequiredPreviewFeature="Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature()" Name="@T("Visual Briefing Assistant")" Description="@T("Turn documents, data, images, audio, and video into an audience-ready interactive briefing.")" Icon="@Icons.Material.Filled.DashboardCustomize" Link="@Routes.ASSISTANT_VISUAL_BRIEFING" />
|
||||
</MudStack>
|
||||
}
|
||||
<AssistantCategoryBlock Title="@T("Business")">
|
||||
<AssistantBlock TSettings="SettingsDialogWritingEMails" Component="Components.EMAIL_ASSISTANT" Name="@T("E-Mail")" Description="@T("Generate an e-mail for a given context.")" Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Name="@T("Document Analysis")" Description="@T("Analyze a document regarding defined rules and extract key information.")" Icon="@Icons.Material.Filled.DocumentScanner" Link="@Routes.ASSISTANT_DOCUMENT_ANALYSIS"/>
|
||||
<AssistantBlock TSettings="SettingsDialogBatchProcessing" Component="Components.BATCH_PROCESSING_ASSISTANT" Name="@T("Batch Processing")" Description="@T("Process all documents of a folder in one batch run and collect the results.")" Icon="@Icons.Material.Filled.DynamicFeed" Link="@Routes.ASSISTANT_BATCH_PROCESSING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogMyTasks" Component="Components.MY_TASKS_ASSISTANT" Name="@T("My Tasks")" Description="@T("Analyze a text or an email for tasks you need to complete.")" Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
|
||||
<AssistantBlock TSettings="SettingsDialogAgenda" Component="Components.AGENDA_ASSISTANT" Name="@T("Agenda Planner")" Description="@T("Generate an agenda for a given meeting, seminar, etc.")" Icon="@Icons.Material.Filled.CalendarToday" Link="@Routes.ASSISTANT_AGENDA"/>
|
||||
<AssistantBlock TSettings="SettingsDialogJobPostings" Component="Components.JOB_POSTING_ASSISTANT" Name="@T("Job Posting")" Description="@T("Generate a job posting for a given job description.")" Icon="@Icons.Material.Filled.Work" Link="@Routes.ASSISTANT_JOB_POSTING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogLegalCheck" Component="Components.LEGAL_CHECK_ASSISTANT" Name="@T("Legal Check")" Description="@T("Ask a question about a legal document.")" Icon="@Icons.Material.Filled.Gavel" Link="@Routes.ASSISTANT_LEGAL_CHECK"/>
|
||||
<AssistantBlock TSettings="SettingsDialogIconFinder" Component="Components.ICON_FINDER_ASSISTANT" Name="@T("Icon Finder")" Description="@T("Use an LLM to find an icon for a given context.")" Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogSlideBuilder" Component="Components.SLIDE_BUILDER_ASSISTANT" Name="@T("Slide Planner Assistant")" Description="@T("Develop slide content based on a given topic and content.")" Icon="@Icons.Material.Filled.Slideshow" Link="@Routes.ASSISTANT_SLIDE_BUILDER"/>
|
||||
<AssistantBlock TSettings="SettingsDialogVisualBriefing" Component="Components.VISUAL_BRIEFING_ASSISTANT" RequiredPreviewFeature="Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature()" Name="@T("Visual Briefing Assistant")" Description="@T("Turn documents, data, images, audio, and video into an audience-ready interactive briefing.")" Icon="@Icons.Material.Filled.DashboardCustomize" Link="@Routes.ASSISTANT_VISUAL_BRIEFING" />
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Learning",
|
||||
(Components.BIAS_DAY_ASSISTANT, PreviewFeatures.NONE)
|
||||
))
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||
@T("Learning")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogAssistantBias" Component="Components.BIAS_DAY_ASSISTANT" Name="@T("Bias of the Day")" Description="@T("Learn about one cognitive bias every day.")" Icon="@Icons.Material.Filled.Psychology" Link="@Routes.ASSISTANT_BIAS"/>
|
||||
</MudStack>
|
||||
}
|
||||
<AssistantCategoryBlock Title="@T("Learning")">
|
||||
<AssistantBlock TSettings="SettingsDialogAssistantBias" Component="Components.BIAS_DAY_ASSISTANT" Name="@T("Bias of the Day")" Description="@T("Learn about one cognitive bias every day.")" Icon="@Icons.Material.Filled.Psychology" Link="@Routes.ASSISTANT_BIAS"/>
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Software Engineering",
|
||||
(Components.CODING_ASSISTANT, PreviewFeatures.NONE),
|
||||
(Components.ERI_ASSISTANT, PreviewFeatures.PRE_RAG_2024),
|
||||
(Components.LOG_VIEWER_ASSISTANT, PreviewFeatures.NONE)
|
||||
))
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||
@T("Software Engineering")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
||||
</MudStack>
|
||||
}
|
||||
<AssistantCategoryBlock Title="@T("Software Engineering")">
|
||||
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("AI Studio Development",
|
||||
(Components.I18N_ASSISTANT, PreviewFeatures.NONE)
|
||||
))
|
||||
{
|
||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||
@T("AI Studio Development")
|
||||
</MudText>
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogI18N" Component="Components.I18N_ASSISTANT" Name="@T("Localization")" Description="@T("Translate AI Studio text content into other languages")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_AI_STUDIO_I18N"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
||||
</MudStack>
|
||||
}
|
||||
<AssistantCategoryBlock Title="@T("AI Studio Development")">
|
||||
<AssistantBlock TSettings="SettingsDialogI18N" Component="Components.I18N_ASSISTANT" Name="@T("Localization")" Description="@T("Translate AI Studio text content into other languages")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_AI_STUDIO_I18N"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
||||
</AssistantCategoryBlock>
|
||||
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
@@ -18,7 +18,7 @@ public partial class Assistants : MSGComponentBase
|
||||
private NavigationManager NavigationManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Assistants> Logger { get; init; } = null!;
|
||||
private DirectChatService DirectChatService { get; init; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -100,36 +100,15 @@ public partial class Assistants : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
var chatThread = await this.TryCreateDirectChatThreadAsync(assistantPlugin);
|
||||
if (chatThread is null)
|
||||
return;
|
||||
|
||||
MessageBus.INSTANCE.DeferMessage(this, Event.SEND_TO_CHAT, chatThread);
|
||||
this.NavigationManager.NavigateTo(Routes.CHAT);
|
||||
}
|
||||
|
||||
private async Task<ChatThread?> TryCreateDirectChatThreadAsync(PluginAssistants assistantPlugin)
|
||||
{
|
||||
var workspaceId = await WorkspaceBehaviour.ResolveOrCreateWorkspaceIdByNameAsync(assistantPlugin.LaunchWorkspaceName);
|
||||
if (workspaceId == Guid.Empty)
|
||||
var result = await this.DirectChatService.TryCreateAssistantChatAsync(assistantPlugin);
|
||||
if (result.Request is null)
|
||||
{
|
||||
this.Logger.LogWarning("Assistant plugin '{PluginName}' could not resolve or create workspace '{WorkspaceName}'.", assistantPlugin.Name, assistantPlugin.LaunchWorkspaceName);
|
||||
return null;
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, result.ErrorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
return new ChatThread
|
||||
{
|
||||
IncludeDateTime = true,
|
||||
SelectedProvider = string.Empty,
|
||||
SelectedProfile = string.Empty,
|
||||
SelectedChatTemplate = string.Empty,
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = assistantPlugin.AssistantTitle,
|
||||
DataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(),
|
||||
Blocks = [],
|
||||
};
|
||||
MessageBus.INSTANCE.DeferMessage(this, Event.SEND_TO_CHAT, result.Request);
|
||||
this.NavigationManager.NavigateTo(Routes.CHAT);
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
@@ -39,10 +39,10 @@ public partial class Chat : MSGComponentBase
|
||||
|
||||
this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
|
||||
this.splitterSaveTimer.AutoReset = false;
|
||||
this.splitterSaveTimer.Elapsed += async (_, _) =>
|
||||
this.splitterSaveTimer.Elapsed += (_, _) =>
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.Workspace.SplitterPosition = this.splitterPosition;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
this.SettingsManager.StoreSettings().Observe($"{nameof(Chat)}: storing the splitter position");
|
||||
};
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
@@ -42,7 +42,7 @@ public partial class Home : MSGComponentBase
|
||||
|
||||
// Read the last change content asynchronously
|
||||
// without blocking the UI thread:
|
||||
_ = this.ReadLastChangeAsync();
|
||||
this.ReadLastChangeAsync().Observe($"{nameof(Home)}: reading the last change");
|
||||
}
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -63,7 +63,7 @@ public partial class Home : MSGComponentBase
|
||||
this.itemsAdvantages = [
|
||||
new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")),
|
||||
new(this.T("Democratization of AI"), this.T("We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.")),
|
||||
new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")),
|
||||
new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")),
|
||||
new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")),
|
||||
new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")),
|
||||
new(this.T("Cost-effective"), this.T("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.")),
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.LinuxPackageType"/>
|
||||
}
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.InstallDesktop" Text="@this.InstallationKind"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||
@switch (HasAnyActiveEnvironment)
|
||||
{
|
||||
@@ -158,6 +159,31 @@
|
||||
break;
|
||||
}
|
||||
|
||||
@*
|
||||
A staged test configuration speaks for the organization without anybody
|
||||
having deployed it. We report it without the details having to be expanded:
|
||||
*@
|
||||
@if (this.testConfigPlugins.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2">
|
||||
@T("A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app.")
|
||||
</MudText>
|
||||
@foreach (var testConfigPlugin in this.testConfigPlugins)
|
||||
{
|
||||
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Science"
|
||||
HeaderText="@testConfigPlugin.Name"
|
||||
Items="@this.BuildTestConfigurationItems(testConfigPlugin)"
|
||||
ShowWarning="@true"
|
||||
WarningText="@T("Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio.")"/>
|
||||
}
|
||||
}
|
||||
else if (PluginFactory.RemovedTestConfigurationsAtStartup > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2">
|
||||
@string.Format(T("AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running."), PluginFactory.RemovedTestConfigurationsAtStartup)
|
||||
</MudText>
|
||||
}
|
||||
|
||||
@if (this.HasEnterpriseConfigurationDetails)
|
||||
{
|
||||
<MudButton StartIcon="@(this.showEnterpriseConfigDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
@@ -250,21 +276,52 @@
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents.")
|
||||
@T("You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Material.Filled.Article"
|
||||
Href="@Routes.ASSISTANT_LOG_VIEWER"
|
||||
Class="mb-3">
|
||||
@T("Open Log Viewer")
|
||||
</MudButton>
|
||||
|
||||
<MudText Typo="Typo.h4">
|
||||
@T("Startup log file")
|
||||
</MudText>
|
||||
<MudList T="string" Class="mb-3">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogStartupPath" OnClick="@(() => this.CopyStartupLogPath())"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1" Style="width: 100%; min-width: 0;">
|
||||
<MudLink OnClick="@this.CopyStartupLogPath" Style="min-width: 0; overflow-wrap: anywhere;">
|
||||
@this.logPaths.LogStartupPath
|
||||
</MudLink>
|
||||
<MudTooltip Text="@T("Open in folder")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen"
|
||||
Size="Size.Medium"
|
||||
Disabled="@string.IsNullOrWhiteSpace(this.logPaths.LogStartupPath)"
|
||||
OnClick="@(() => this.OpenLogInFileManager(this.logPaths.LogStartupPath))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
|
||||
<MudText Typo="Typo.h4">
|
||||
@T("Usage log file")
|
||||
</MudText>
|
||||
<MudList T="string" Class="mb-3">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogAppPath" OnClick="@(() => this.CopyAppLogPath())"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1" Style="width: 100%; min-width: 0;">
|
||||
<MudLink OnClick="@this.CopyAppLogPath" Style="min-width: 0; overflow-wrap: anywhere;">
|
||||
@this.logPaths.LogAppPath
|
||||
</MudLink>
|
||||
<MudTooltip Text="@T("Open in folder")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen"
|
||||
Size="Size.Medium"
|
||||
Disabled="@string.IsNullOrWhiteSpace(this.logPaths.LogAppPath)"
|
||||
OnClick="@(() => this.OpenLogInFileManager(this.logPaths.LogAppPath))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
|
||||
@@ -295,6 +352,9 @@
|
||||
|
||||
<ThirdPartyComponent Name="Qdrant Edge" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
|
||||
<ThirdPartyComponent Name="SQLite" Developer="Richard Hipp & Team" LicenseName="None (public domain)" LicenseUrl="https://www.sqlite.org/copyright.html" RepositoryUrl="https://www.sqlite.org/src/" UseCase="@T("SQLite stores local RAG indexing metadata, searchable chunk text, and the file fingerprints used to decide whether local files need to be indexed again, without requiring a separate database server or a system SQLite installation.")"/>
|
||||
<ThirdPartyComponent Name="regex" Developer="Andrew Gallant, Alex Crichton, The Rust Project Developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/regex/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/regex" UseCase="@T("The regex crate detects structural and obfuscated prompt-injection patterns in untrusted document content. Its linear-time matching without backtracking keeps these scans predictable, even for hostile input.")"/>
|
||||
<ThirdPartyComponent Name="aho-corasick" Developer="Alfred V. Aho, Margaret J. Corasick, Andrew Gallant & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/BurntSushi/aho-corasick/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/BurntSushi/aho-corasick" UseCase="@T("The aho-corasick crate searches the fixed catalog of prompt-injection phrases in one pass, allowing AI Studio to scan large documents efficiently. We thank Alfred V. Aho and Margaret J. Corasick for publishing the algorithm in 1975, and Andrew Gallant and the Open Source Community for bringing it to Rust.")"/>
|
||||
<ThirdPartyComponent Name="toml" Developer="Ed Page, Alex Crichton, ordian, Eric Huss & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/toml-rs/toml/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/toml-rs/toml" UseCase="@T("The toml crate parses the embedded prompt-injection phrase catalog when the runtime starts. This keeps the detection rules separate from the Rust implementation and easier to maintain.")"/>
|
||||
<ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/>
|
||||
<ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/>
|
||||
<ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/>
|
||||
@@ -313,8 +373,11 @@
|
||||
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
|
||||
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@T("For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="windows-registry" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.")"/>
|
||||
<ThirdPartyComponent Name="windows-rs" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="objc2" Developer="Steven Sheldon, Mads Marquart, silvanshade, Dzmitry Malyshau, Felix Nemo Kaaman, adamnemecek, Samuel Sleight, Paul Mabileau & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/madsmtm/objc2/blob/main/LICENSE-MIT.txt" RepositoryUrl="https://github.com/madsmtm/objc2" UseCase="@T("The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library identifies files by their content. It is used for document streaming and as the first safety and media classification step before local audio processing.")"/>
|
||||
<ThirdPartyComponent Name="chardetng" Developer="Henri Sivonen & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hsivonen/chardetng/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/hsivonen/chardetng" UseCase="@T("Text files are not always saved in the same encoding: files written on Windows often use a legacy one. chardetng recognizes which encoding a text file uses, so AI Studio can read it instead of rejecting it.")"/>
|
||||
<ThirdPartyComponent Name="encoding_rs" Developer="Henri Sivonen, M. Larsen, kornelski, Manish Goregaokar & Open Source Community" LicenseName="MIT & BSD-3-Clause" LicenseUrl="https://github.com/hsivonen/encoding_rs/blob/main/COPYRIGHT" RepositoryUrl="https://github.com/hsivonen/encoding_rs" UseCase="@T("Once the encoding of a text file is known, encoding_rs turns its content into the text AI Studio works with. Together with chardetng, this lets AI Studio read text, CSV, and similar files no matter which encoding they were saved in.")"/>
|
||||
<ThirdPartyComponent Name="Symphonia" Developer="Philip Deljanov & Open Source Community" LicenseName="MPL-2.0" LicenseUrl="https://github.com/pdeljanov/Symphonia/blob/v0.6.0/LICENSE" RepositoryUrl="https://github.com/pdeljanov/Symphonia" UseCase="@T("Symphonia is used for media container demuxing and audio decoding. The exact MPL-covered source is available from the repository linked and is identified in the offline notices bundled with AI Studio.")"/>
|
||||
<ThirdPartyComponent Name="Ropus" Developer="0x4D44, Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon & Open Source Community" LicenseName="BSD-3-Clause" LicenseUrl="https://github.com/0x4D44/ropus/blob/main/LICENSE" RepositoryUrl="https://github.com/0x4d44/ropus" UseCase="@T("Ropus provides the Opus encoder and decoder used by the media pipeline.")"/>
|
||||
<ThirdPartyComponent Name="Rubato" Developer="Henrik Enquist & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/HEnquist/rubato/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/HEnquist/rubato" UseCase="@T("We use Rubato to resample the decoded audio to 48 kHz before the Opus encoding.")"/>
|
||||
@@ -333,8 +396,20 @@
|
||||
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
|
||||
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="@T("This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.")"/>
|
||||
<ThirdPartyComponent Name="CodeJar" Developer="Anton Medvedev" LicenseName="MIT" LicenseUrl="https://github.com/antonmedv/codejar/blob/master/LICENSE" RepositoryUrl="https://github.com/antonmedv/codejar/" UseCase="@T("CodeJar is a lightweight embeddable code editor for the browser.")"/>
|
||||
<ThirdPartyComponent Name="Simple Icons" Developer="Simple Icons Contributors & Open Source Community" LicenseName="CC0-1.0" LicenseUrl="https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md" RepositoryUrl="https://github.com/simple-icons/simple-icons" UseCase="@T("Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners.")"/>
|
||||
</MudGrid>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Copyright" HeaderText="@T("Trademarks & Brand Assets")">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies.")
|
||||
</MudJustifiedText>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider.")
|
||||
</MudJustifiedText>
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@T("Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides.")
|
||||
</MudJustifiedText>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
|
||||
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</ExpansionPanel>
|
||||
|
||||
@@ -23,6 +23,9 @@ public partial class Information : MSGComponentBase
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Information> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@@ -65,12 +68,26 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string LinuxPackageTypeDisplayName => this.RuntimeInfo.LinuxPackageType switch
|
||||
{
|
||||
"appimage" => "AppImage",
|
||||
"flatpak" => "Flatpak",
|
||||
"unknown" => T("unknown"),
|
||||
Tools.Rust.LinuxPackageType.APP_IMAGE => "AppImage",
|
||||
Tools.Rust.LinuxPackageType.FLATPAK => "Flatpak",
|
||||
Tools.Rust.LinuxPackageType.UNKNOWN => T("unknown"),
|
||||
_ => T("not applicable")
|
||||
};
|
||||
|
||||
private string InstallationKind => $"{T("Installation")}: {this.InstallationKindDisplayName}";
|
||||
|
||||
private string InstallationKindDisplayName => this.RuntimeInfo.LinuxPackageType switch
|
||||
{
|
||||
Tools.Rust.LinuxPackageType.FLATPAK => T("Flatpak installation, updates are handled outside of AI Studio"),
|
||||
_ => this.RuntimeInfo.InstallationKind switch
|
||||
{
|
||||
Tools.Rust.InstallationKind.MANAGED => T("managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates"),
|
||||
Tools.Rust.InstallationKind.UNSUPPORTED_LOCATION => T("current installation location does not support automatic updates"),
|
||||
Tools.Rust.InstallationKind.DEVELOPMENT => T("development build, no support for automatic updates"),
|
||||
_ => T("standard; automatic updates supported")
|
||||
}
|
||||
};
|
||||
|
||||
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
||||
|
||||
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
||||
@@ -107,10 +124,16 @@ public partial class Information : MSGComponentBase
|
||||
private bool showVectorStoreDetails;
|
||||
private bool showExternalHttpCustomRootCertificateDetails;
|
||||
|
||||
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
|
||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||
.OfType<IAvailablePlugin>()
|
||||
.ToList();
|
||||
private List<IAvailablePlugin> configPlugins = [];
|
||||
|
||||
/// <summary>
|
||||
/// The configuration plugins an administrator staged for a test.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// They are kept apart from the other configuration plugins: nobody deployed them, yet they act
|
||||
/// on behalf of the organization while they are loaded. That deserves its own note.
|
||||
/// </remarks>
|
||||
private List<IAvailablePlugin> testConfigPlugins = [];
|
||||
|
||||
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
||||
|
||||
@@ -174,7 +197,7 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
// Determine the Pandoc version may take some time, so we start it here
|
||||
// without waiting for the result:
|
||||
_ = this.DeterminePandocVersion();
|
||||
this.DeterminePandocVersion().Observe($"{nameof(Information)}: determining the Pandoc version");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -201,11 +224,14 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private void RefreshEnterpriseConfigurationState()
|
||||
{
|
||||
this.configPlugins = PluginFactory.AvailablePlugins
|
||||
var availableConfigPlugins = PluginFactory.AvailablePlugins
|
||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||
.OfType<IAvailablePlugin>()
|
||||
.ToList();
|
||||
|
||||
this.testConfigPlugins = availableConfigPlugins.Where(plugin => PluginFactory.IsEnterpriseTestConfigurationPath(plugin.LocalPath)).ToList();
|
||||
this.configPlugins = availableConfigPlugins.Except(this.testConfigPlugins).ToList();
|
||||
|
||||
this.enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
||||
this.mandatoryInfoPanels = PluginFactory.GetMandatoryInfos()
|
||||
.Select(info =>
|
||||
@@ -314,7 +340,7 @@ public partial class Information : MSGComponentBase
|
||||
this.vectorStoreRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = this.vectorStoreRefreshCancellationTokenSource.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
const int MAX_TRIES = 12;
|
||||
for (var attempt = 0; attempt < MAX_TRIES; attempt++)
|
||||
@@ -340,7 +366,7 @@ public partial class Information : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}, cancellationToken).Observe($"{nameof(Information)}: refreshing the vector store info");
|
||||
}
|
||||
|
||||
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||
@@ -404,6 +430,27 @@ public partial class Information : MSGComponentBase
|
||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects what a user needs to find and judge a staged test configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no enterprise environment behind it, so we show what identifies it instead: the plugin
|
||||
/// ID it claims and the directory it was staged in.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<ConfigInfoRowItem> BuildTestConfigurationItems(IAvailablePlugin plugin) =>
|
||||
[
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Configuration plugin ID:")} {plugin.Id}",
|
||||
plugin.Id.ToString(),
|
||||
T("Copies the configuration plugin ID to the clipboard")),
|
||||
|
||||
new(Icons.Material.Filled.ArrowRightAlt,
|
||||
$"{T("Plugin directory:")} {plugin.LocalPath}",
|
||||
plugin.LocalPath,
|
||||
T("Copies the plugin directory to the clipboard"),
|
||||
"margin-top: 4px;"),
|
||||
];
|
||||
|
||||
private string ExternalHttpCustomRootCertificateWarningText
|
||||
{
|
||||
get
|
||||
@@ -492,6 +539,36 @@ public partial class Information : MSGComponentBase
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.logPaths.LogAppPath);
|
||||
}
|
||||
|
||||
private async Task OpenLogInFileManager(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Folder, T("The log file path is not available yet.")));
|
||||
return;
|
||||
}
|
||||
|
||||
OpenPathResponse response;
|
||||
try
|
||||
{
|
||||
response = await this.RustService.TryOpenPathInRuntimeFileManager(path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.Logger.LogWarning(e, "Could not open the log file location in the file manager.");
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Folder, T("Could not open the log file location.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FolderOpen, T("Opened the log file location.")));
|
||||
return;
|
||||
}
|
||||
|
||||
var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue;
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Folder, string.Format(T("Could not open the log file location: {0}"), issue)));
|
||||
}
|
||||
|
||||
private const string LICENSE = """
|
||||
# Functional Source License, Version 1.1, MIT Future License
|
||||
@@ -625,6 +702,21 @@ public partial class Information : MSGComponentBase
|
||||
parameters.Add(x => x.Message, T("AI Studio cannot update itself when installed as a Flatpak. A Flathub listing is planned. Until then, you can find the latest release on GitHub."));
|
||||
parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest");
|
||||
}
|
||||
else if (this.updatePolicyMode is UpdatePolicyMode.MANAGED_INSTALLATION)
|
||||
{
|
||||
// No release link here: the app cannot tell how this installation receives updates,
|
||||
// and installing a second copy from GitHub next to it is exactly what we want to avoid.
|
||||
parameters.Add(x => x.Message, T("This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions."));
|
||||
}
|
||||
else if (this.updatePolicyMode is UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION)
|
||||
{
|
||||
parameters.Add(x => x.Message, T("AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation."));
|
||||
parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest");
|
||||
}
|
||||
else if (this.updatePolicyMode is UpdatePolicyMode.DEVELOPMENT)
|
||||
{
|
||||
parameters.Add(x => x.Message, T("You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead."));
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudText Typo="Typo.h3" Class="mb-2">
|
||||
@T("Plugins")
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h3">
|
||||
@T("Plugins")
|
||||
</MudText>
|
||||
<MudSpacer />
|
||||
<LockableButton Text="@T("Import")"
|
||||
Tooltip="@T("Import plugin from a file")"
|
||||
Icon="@IMPORT_ICON"
|
||||
ButtonVariant="Variant.Outlined"
|
||||
ButtonColor="Color.Default"
|
||||
NoMargin="@true"
|
||||
Class="flex-none"
|
||||
Disabled="@(() => this.isImportingAssistantPlugin)"
|
||||
IsLocked="@(() => !this.AllowPluginImport)"
|
||||
OnClickAsync="@this.ImportAssistantPluginAsync"/>
|
||||
</MudStack>
|
||||
|
||||
<InnerScrolling>
|
||||
|
||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="border-dashed border rounded-lg">
|
||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="@this.PluginTableClass">
|
||||
<ColGroup>
|
||||
<col style="width: 2em;" />
|
||||
<col style="width: 2.1em; "/>
|
||||
@@ -50,7 +63,10 @@
|
||||
<MudTd>
|
||||
<MudAvatar Size="Size.Medium" Style="width: 2em; height: 2em;" Class="align-content-stretch">
|
||||
<div class="plugin-icon-container">
|
||||
@((MarkupString)context.IconSVG)
|
||||
@if (!string.IsNullOrEmpty(context.IconDataUrl))
|
||||
{
|
||||
<img src="@context.IconDataUrl" alt="" aria-hidden="true" draggable="false" />
|
||||
}
|
||||
</div>
|
||||
</MudAvatar>
|
||||
</MudTd>
|
||||
@@ -65,12 +81,13 @@
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center">
|
||||
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd">
|
||||
@if (context.Type is PluginType.ASSISTANT)
|
||||
{
|
||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == context.Id);
|
||||
<AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true"/>
|
||||
}
|
||||
|
||||
@if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
|
||||
{
|
||||
var isEnabled = this.SettingsManager.IsPluginEnabled(context);
|
||||
@@ -80,7 +97,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudButtonGroup Class="ms-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0" Class="ms-3" Style="gap: 4px;">
|
||||
@if (context is { IsInternal: false } && !string.IsNullOrWhiteSpace(context.SourceURL))
|
||||
{
|
||||
var sourceUrl = context.SourceURL;
|
||||
@@ -89,7 +106,7 @@
|
||||
{
|
||||
var isDefaultSupportContact = string.Equals(sourceUrl, AssistantPluginGenerationService.DEFAULT_SUPPORT_CONTACT, StringComparison.Ordinal);
|
||||
<MudTooltip Text="@(isDefaultSupportContact ? string.Empty : T("Send a mail"))">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Email" Href="@sourceUrl" Target="_blank" Size="Size.Medium" Disabled="@isDefaultSupportContact"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Email" Href="@sourceUrl" Target="_blank" Size="Size.Medium" Disabled="@isDefaultSupportContact"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
@@ -107,19 +124,30 @@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" Size="Size.Medium" OnClick="@(() => this.OpenAssistantPluginEditorDialogAsync(editablePlugin))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (context is IAvailablePlugin shareablePlugin && CanSharePlugin(shareablePlugin))
|
||||
{
|
||||
<MudTooltip Text="@(this.AllowPluginSharing ? this.SharePluginTooltip : this.SharePluginLockText)">
|
||||
<MudIconButton Icon="@SharePluginIcon" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(this.isSharingPlugin || !this.AllowPluginSharing)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@* A direct chat launcher has nothing to prompt about: its settings are
|
||||
plain selections, so it gets the mechanical dialog instead of the AI
|
||||
revision. *@
|
||||
@if (context is IAvailablePlugin revisionPlugin && CanReviseAssistantPlugin(revisionPlugin))
|
||||
{
|
||||
<MudTooltip Text="@T("Revise assistant plugin with AI")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AutoMode" Size="Size.Medium" OnClick="@(() => this.OpenAssistantPluginRevisionDialogAsync(revisionPlugin))"/>
|
||||
var isLauncher = IsDirectChatLauncher(revisionPlugin);
|
||||
<MudTooltip Text="@(isLauncher ? T("Change what this tile opens") : T("Revise assistant plugin with AI"))">
|
||||
<MudIconButton Icon="@(isLauncher ? Icons.Material.Filled.Tune : Icons.Material.Filled.AutoMode)" Size="Size.Medium" OnClick="@(() => this.OpenAssistantPluginRevisionDialogAsync(revisionPlugin))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (context is IAvailablePlugin availablePlugin)
|
||||
{
|
||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
||||
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||
}
|
||||
</MudButtonGroup>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
@@ -4,7 +4,8 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
@@ -16,6 +17,7 @@ public partial class Plugins : MSGComponentBase
|
||||
private const string GROUP_DISABLED = "Disabled";
|
||||
private const string GROUP_INTERNAL = "Internal";
|
||||
private bool isAutoAuditing;
|
||||
private bool isImportingAssistantPlugin;
|
||||
|
||||
private DataAssistantPluginAudit AssistantPluginAuditSettings => this.SettingsManager.ConfigurationData.AssistantPluginAudit;
|
||||
|
||||
@@ -27,14 +29,43 @@ public partial class Plugins : MSGComponentBase
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PluginShareService PluginShareService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(Plugins));
|
||||
|
||||
private bool isSharingPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Number of active drop areas above this page. While there is any, another component owns the
|
||||
/// dropped files and this page must not catch them.
|
||||
/// </summary>
|
||||
private uint numDropAreasAboveThis;
|
||||
|
||||
private bool isDraggingOverPage;
|
||||
|
||||
private const string IMPORT_ICON =
|
||||
@"<svg class=""mud-icon-root mud-svg-icon mud-dark-text mud-icon-size-medium"" focusable=""false"" viewBox=""0 0 24 24"" aria-hidden=""true"" role=""img"">
|
||||
<path d=""M0 0h24v24H0V0z"" fill=""none""></path>
|
||||
<path d=""M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4z"" transform=""rotate(180 12 10)""></path>
|
||||
<path d=""M20 10v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z""></path>
|
||||
</svg>";
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED ]);
|
||||
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED, Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
|
||||
// Register the whole page as a drop area, so users can drop a plugin archive anywhere on it:
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||
|
||||
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
||||
{
|
||||
Expandable = true,
|
||||
@@ -59,10 +90,26 @@ public partial class Plugins : MSGComponentBase
|
||||
await this.TryAutoAuditAssistantsAsync();
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
// Release the drop area again, so lower layers can catch dropped files:
|
||||
this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, DropLayers.PAGES).Observe($"{nameof(Plugins)}: releasing the drop area");
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||
{
|
||||
//
|
||||
// The switch is disabled for these, so this cannot be reached through the user interface. We
|
||||
// check anyway: removing the plugin from the enabled list would achieve nothing, because the
|
||||
// activation is decided live, but it would leave the settings in a state which claims the
|
||||
// opposite of what the user sees:
|
||||
//
|
||||
if (PluginFactory.IsAssistantActivationEnforced(pluginMeta.Id))
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.IsPluginEnabled(pluginMeta))
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.EnabledPlugins.Remove(pluginMeta.Id);
|
||||
@@ -137,7 +184,7 @@ public partial class Plugins : MSGComponentBase
|
||||
{
|
||||
x => x.Message,
|
||||
string.Format(
|
||||
this.T("The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \"{2}\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"),
|
||||
this.T("The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"),
|
||||
pluginName,
|
||||
actualLevel.GetName(),
|
||||
this.AssistantPluginAuditSettings.MinimumLevel.GetName())
|
||||
@@ -152,6 +199,10 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
private bool IsActivationSwitchDisabled(IPluginMetadata pluginMeta, bool isEnabled)
|
||||
{
|
||||
// An assistant plugin your organization requires to stay enabled has no switch to offer:
|
||||
if (PluginFactory.IsAssistantActivationEnforced(pluginMeta.Id))
|
||||
return true;
|
||||
|
||||
if (isEnabled || pluginMeta.Type is not PluginType.ASSISTANT)
|
||||
return false;
|
||||
|
||||
@@ -165,6 +216,9 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
private string GetActivationTooltip(IPluginMetadata pluginMeta, bool isEnabled)
|
||||
{
|
||||
if (PluginFactory.IsAssistantActivationEnforced(pluginMeta.Id))
|
||||
return this.T("Your organization requires this assistant to stay enabled");
|
||||
|
||||
if (isEnabled)
|
||||
return this.T("Disable plugin");
|
||||
|
||||
@@ -184,16 +238,68 @@ public partial class Plugins : MSGComponentBase
|
||||
: this.T("Enable plugin");
|
||||
}
|
||||
|
||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
//
|
||||
// These methods decide whether an action exists for a plugin at all. They must not depend on
|
||||
// transient state like an ongoing share: they gate the markup, so a transient value would make
|
||||
// the action buttons disappear and reappear. Transient state belongs into the buttons' Disabled.
|
||||
//
|
||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this plugin is a direct chat launcher whose settings can be changed without AI.
|
||||
/// </summary>
|
||||
private static bool IsDirectChatLauncher(IAvailablePlugin plugin)
|
||||
{
|
||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == plugin.Id);
|
||||
return assistantPlugin is not null && DirectChatLauncherLuaWriter.CanRewrite(assistantPlugin);
|
||||
}
|
||||
|
||||
private static bool CanReviseAssistantPlugin(IAvailablePlugin plugin)
|
||||
{
|
||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == plugin.Id);
|
||||
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } &&
|
||||
!string.IsNullOrWhiteSpace(plugin.LocalPath) &&
|
||||
assistantPlugin?.IsManagedByConfigServer is false;
|
||||
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath) && assistantPlugin?.IsManagedByConfigServer is false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugin types users may share. This list has to match what the import accepts, otherwise
|
||||
/// users would create archives nobody can install.
|
||||
/// </summary>
|
||||
private static readonly PluginType[] SHAREABLE_PLUGIN_TYPES = [PluginType.ASSISTANT, PluginType.CONFIGURATION, PluginType.LANGUAGE];
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a plugin may be shared or exported as an archive. Plugins shipped with
|
||||
/// AI Studio and plugins deployed by an organization stay with their owner.
|
||||
/// </summary>
|
||||
private static bool CanSharePlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false } && SHAREABLE_PLUGIN_TYPES.Contains(plugin.Type) && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the plugin table while the user drags a file over the page, so it is visible
|
||||
/// where the file would land.
|
||||
/// </summary>
|
||||
private string PluginTableClass => this.isDraggingOverPage
|
||||
? "border-dashed border rounded-lg mud-border-primary border-4"
|
||||
: "border-dashed border rounded-lg";
|
||||
|
||||
/// <summary>
|
||||
/// Organizations may disable importing plugin archives by using a configuration plugin.
|
||||
/// </summary>
|
||||
private bool AllowPluginImport => this.SettingsManager.ConfigurationData.App.AllowUserToImportPlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Organizations may disable sharing and exporting plugins by using a configuration plugin.
|
||||
/// </summary>
|
||||
private bool AllowPluginSharing => this.SettingsManager.ConfigurationData.App.AllowUserToSharePlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Linux has no native share sheet, hence the plugin archive is exported to a location of the
|
||||
/// user's choice there. The action must be labeled accordingly.
|
||||
/// </summary>
|
||||
private static string SharePluginIcon => OperatingSystem.IsLinux() ? Icons.Material.Filled.FileDownload : Icons.Material.Filled.IosShare;
|
||||
|
||||
private string SharePluginTooltip => OperatingSystem.IsLinux() ? this.T("Export plugin archive") : this.T("Share plugin archive");
|
||||
|
||||
private string SharePluginLockText => OperatingSystem.IsLinux() ? this.T("Your organization has disabled exporting plugins.") : this.T("Your organization has disabled sharing plugins.");
|
||||
|
||||
private async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
||||
@@ -209,12 +315,25 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Save, string.Format(this.T("The assistant plugin '{0}' has been successfully saved."), result.PluginName)));
|
||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully updated.");
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
||||
|
||||
// Saving the plugin ran LoadAll, which already sent PLUGINS_RELOADED. Editing the plugin
|
||||
// code changes no settings, so there is nothing else to announce:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OpenAssistantPluginRevisionDialogAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
//
|
||||
// Changing a launcher means picking a different workspace, provider, profile, chat template,
|
||||
// or set of data sources. Prompting a model for that would be a detour, so launchers go to
|
||||
// the mechanical dialog instead:
|
||||
//
|
||||
if (IsDirectChatLauncher(plugin))
|
||||
{
|
||||
await this.OpenDirectChatLauncherSettingsDialogAsync(plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<AssistantPluginRevisionDialog>
|
||||
{
|
||||
{ x => x.PluginId, plugin.Id },
|
||||
@@ -228,11 +347,161 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.AutoFixHigh, string.Format(this.T("The assistant plugin '{0}' has been successfully revised."), result.PluginName)));
|
||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully revised.");
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
||||
|
||||
// Saving the revision ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
|
||||
// configuration change: with automatic audits enabled, the dialog stored an audit result:
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OpenDirectChatLauncherSettingsDialogAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
var parameters = new DialogParameters<DirectChatLauncherSettingsDialog>
|
||||
{
|
||||
{ x => x.PluginId, plugin.Id },
|
||||
{ x => x.PluginLocalPath, plugin.LocalPath },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DirectChatLauncherSettingsDialog>(this.T("Tile Settings"), parameters, DialogOptions.BLOCKING_FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DirectChatLauncherSettingsDialogResult result)
|
||||
return;
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Save, string.Format(this.T("The tile '{0}' has been updated."), result.PluginName)));
|
||||
LOG.LogInformation($"The chat launcher '{result.PluginName}' ({result.PluginId}) has been successfully updated.");
|
||||
|
||||
// Saving ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
|
||||
// configuration change: with automatic audits enabled, the dialog stored an audit result:
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task SharePluginAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
if (this.isSharingPlugin)
|
||||
return;
|
||||
|
||||
this.isSharingPlugin = true;
|
||||
// invoke a state change right away to guard action buttons
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var shareResult = await this.PluginShareService.ShareAsync(plugin, CancellationToken.None);
|
||||
if (shareResult.Cancelled)
|
||||
return;
|
||||
|
||||
if (!shareResult.Success)
|
||||
{
|
||||
LOG.LogError($"Sharing the plugin '{shareResult.PluginName}' from archive '{shareResult.ArchivePath}' failed with Issue: '{shareResult.Issue}'.");
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, OperatingSystem.IsLinux() ? T("An error occurred while exporting the plugin.") : T("An error occurred while sharing the plugin.")));
|
||||
return;
|
||||
}
|
||||
|
||||
// On Linux, the user chose the target location, so we confirm where the archive was stored:
|
||||
if (OperatingSystem.IsLinux())
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileDownload, string.Format(T("The plugin archive was exported to '{0}'."), shareResult.ArchivePath)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isSharingPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImportAssistantPluginAsync()
|
||||
{
|
||||
if (this.isImportingAssistantPlugin)
|
||||
return;
|
||||
|
||||
if (!this.AllowPluginImport)
|
||||
return;
|
||||
|
||||
var selection = await this.RustService.SelectFile(this.T("Import plugin"), [FileTypes.PLUGIN_ARCHIVE]);
|
||||
if (selection.UserCancelled)
|
||||
return;
|
||||
|
||||
await this.ImportPluginArchiveAsync(selection.SelectedFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin archive, no matter whether the user picked it through the import button or
|
||||
/// dropped it onto the page.
|
||||
/// </summary>
|
||||
/// <param name="archivePath">The local plugin archive to install.</param>
|
||||
private async Task ImportPluginArchiveAsync(string archivePath)
|
||||
{
|
||||
if (this.isImportingAssistantPlugin)
|
||||
return;
|
||||
|
||||
if (!this.AllowPluginImport)
|
||||
return;
|
||||
|
||||
this.isImportingAssistantPlugin = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await this.PluginInstallService.InstallArchiveAsync(archivePath, this.ConfirmPluginImportAsync, CancellationToken.None);
|
||||
if (result.Cancelled)
|
||||
return;
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
LOG.LogError("Failed to import assistant plugin archive '{ArchivePath}': {Issue}", archivePath, result.Issue);
|
||||
|
||||
// The user actively started this import, so we report the reason in a dialog
|
||||
// instead of a snackbar. Refused imports must not be missed:
|
||||
await this.ShowImportRefusedDialogAsync(result.Issue);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = result.ReplacedExisting
|
||||
? this.T("Plugin updated.")
|
||||
: this.T("Plugin installed.");
|
||||
|
||||
// We do not announce the reload ourselves: a successful installation ran LoadAll, which
|
||||
// already sent PLUGINS_RELOADED. The import changes no settings either, so there is
|
||||
// nothing to report as a configuration change:
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Extension, message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isImportingAssistantPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the metadata of a validated plugin archive and asks whether it may be installed.
|
||||
/// </summary>
|
||||
/// <param name="preview">The metadata the archive declares about itself.</param>
|
||||
/// <returns>True when the user confirmed the installation.</returns>
|
||||
private async Task<bool> ConfirmPluginImportAsync(PluginImportPreview preview)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<PluginImportDialog>
|
||||
{
|
||||
{ x => x.Preview, preview },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<PluginImportDialog>(this.T("Install Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
return dialogResult is { Canceled: false };
|
||||
}
|
||||
|
||||
private async Task ShowImportRefusedDialogAsync(string issue)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<InformationDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(this.T("The plugin could not be imported: {0}"), issue) },
|
||||
{ x => x.Icon, Icons.Material.Filled.ReportProblem },
|
||||
{ x => x.IconColor, Color.Error },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<InformationDialog>(this.T("Import not possible"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
}
|
||||
|
||||
private static bool IsSendingMail(string sourceUrl) => sourceUrl.TrimStart().StartsWith("mailto:", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private PluginAssistants? TryGetAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == pluginId);
|
||||
@@ -302,8 +571,71 @@ public partial class Plugins : MSGComponentBase
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int registeredLayer && registeredLayer > DropLayers.PAGES)
|
||||
this.numDropAreasAboveThis++;
|
||||
|
||||
break;
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int unregisteredLayer && unregisteredLayer > DropLayers.PAGES && this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if (!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
this.isDraggingOverPage = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var droppedPaths }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
if (!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
await this.ImportDroppedPluginArchiveAsync(droppedPaths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether this page may process dropped files: only when no drop area above it is
|
||||
/// active and when the organization allows importing plugins at all.
|
||||
/// </summary>
|
||||
private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && this.AllowPluginImport && !this.isImportingAssistantPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Imports a plugin archive the user dropped onto the page. Anything that is not exactly one
|
||||
/// plugin archive is reported instead of guessing what the user meant.
|
||||
/// </summary>
|
||||
/// <param name="droppedPaths">The paths of the dropped files.</param>
|
||||
private async Task ImportDroppedPluginArchiveAsync(IReadOnlyList<string> droppedPaths)
|
||||
{
|
||||
var archivePaths = droppedPaths.Where(path => FileTypes.IsAllowedPath(path, FileTypes.PLUGIN_ARCHIVE)).ToList();
|
||||
switch (archivePaths.Count)
|
||||
{
|
||||
case 0:
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, string.Format(this.T("Please drop a plugin archive with the extension {0} or .zip."), PluginArchive.PLUGIN_FILE_EXTENSION)));
|
||||
return;
|
||||
|
||||
case > 1:
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, this.T("Please drop only one plugin archive at a time.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ImportPluginArchiveAsync(archivePaths[0]);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders"/>
|
||||
}
|
||||
|
||||
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<SettingsPanelTranscription AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)" @bind-AvailableTranscriptionProviders="@this.availableTranscriptionProviders"/>
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
<SettingsPanelApp AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
|
||||
|
||||
<SettingsPanelTools />
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<SettingsPanelAgentDataSourceSelection AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class Writer : MSGComponentBase
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
this.typeTimer.Elapsed += async (_, _) => await this.InvokeAsync(this.GetSuggestions);
|
||||
this.typeTimer.Elapsed += (_, _) => this.InvokeAsync(this.GetSuggestions).Observe($"{nameof(Writer)}: getting writing suggestions");
|
||||
this.typeTimer.AutoReset = false;
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
Reference in new issue
Block a user