mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-21 11:58:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03cd299a86 | ||
|
|
2144cfe059 | ||
|
|
c10c084da4 | ||
|
|
f8c60d87ae | ||
|
|
c6e45c26d6 | ||
|
|
09020415db | ||
|
|
b153b9c4e9 | ||
|
|
c715b38f41 | ||
|
|
3fc15d9789 | ||
|
|
171ed37c27 | ||
|
|
80c58ea749 | ||
|
|
1ff27fe21f | ||
|
|
712ed2938f | ||
|
|
56e99ffaba | ||
|
|
693fe5e161 |
No files matched your search
@@ -160,6 +160,13 @@ jobs:
|
||||
# Format the app version:
|
||||
formatted_app_version="v${app_version}"
|
||||
|
||||
# Set the architecture:
|
||||
if sed --version 2>/dev/null | grep -q GNU; then
|
||||
sed -i "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
|
||||
else
|
||||
sed -i '' "10s/.*/${{ matrix.dotnet_runtime }}/" metadata.txt
|
||||
fi
|
||||
|
||||
# Write the metadata to the environment:
|
||||
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
|
||||
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
|
||||
@@ -170,6 +177,7 @@ jobs:
|
||||
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV
|
||||
echo "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $GITHUB_ENV
|
||||
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
|
||||
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
|
||||
|
||||
# Log the metadata:
|
||||
echo "App version: '${formatted_app_version}'"
|
||||
@@ -180,6 +188,7 @@ jobs:
|
||||
echo "Rust version: '${rust_version}'"
|
||||
echo "MudBlazor version: '${mud_blazor_version}'"
|
||||
echo "Tauri version: '${tauri_version}'"
|
||||
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||
|
||||
- name: Read and format metadata (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
@@ -212,6 +221,12 @@ jobs:
|
||||
# Format the app version:
|
||||
$formatted_app_version = "v${app_version}"
|
||||
|
||||
# Set the architecture:
|
||||
$metadata[9] = "${{ matrix.dotnet_runtime }}"
|
||||
|
||||
# Write the changed metadata back to the file:
|
||||
Set-Content -Path metadata.txt -Value $metadata
|
||||
|
||||
# Write the metadata to the environment:
|
||||
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV
|
||||
@@ -221,6 +236,7 @@ jobs:
|
||||
Write-Output "DOTNET_RUNTIME_VERSION=${dotnet_runtime_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "RUST_VERSION=${rust_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
|
||||
Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
|
||||
|
||||
# Log the metadata:
|
||||
Write-Output "App version: '${formatted_app_version}'"
|
||||
@@ -231,6 +247,7 @@ jobs:
|
||||
Write-Output "Rust version: '${rust_version}'"
|
||||
Write-Output "MudBlazor version: '${mud_blazor_version}'"
|
||||
Write-Output "Tauri version: '${tauri_version}'"
|
||||
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
|
||||
@@ -15,6 +15,7 @@ Things we are currently working on:
|
||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [ ] App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393))
|
||||
- [ ] App: Implement external embedding providers
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||
@@ -24,26 +25,37 @@ Things we are currently working on:
|
||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||
|
||||
|
||||
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
|
||||
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[PR #167](https://github.com/MindWorkAI/AI-Studio/pull/167), [PR #226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~, [PR #376](https://github.com/MindWorkAI/AI-Studio/pull/376).
|
||||
|
||||
- Since March 2025: We have started developing the plugin system. There will be language plugins to offer AI Studio in other languages, configuration plugins to centrally manage certain providers and rules within an organization, and assistant plugins that allow anyone to develop their own assistants. We are using Lua as the plugin language:
|
||||
- [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~
|
||||
- [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~
|
||||
- [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))~~
|
||||
- [ ] 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))
|
||||
- [ ] Add an I18N assistant to translate all AI Studio texts to a certain language & culture
|
||||
- [ ] Provide MindWork AI Studio in German ([#31](https://github.com/MindWorkAI/Planning/issues/31))
|
||||
- [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations
|
||||
- [ ] 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
|
||||
|
||||
Other News:
|
||||
|
||||
- October 2024: We've found the first two financial supporters. Huge thanks to `richard-stanton` and `peerschuett` for backing the project. Thanks for having the courage to be the first to support us.
|
||||
- April 2025: We have two active financial supporters: Peer `peerschuett` and Dominic `donework`. Thank you very much for your support. MindWork AI reinvests these donations by passing them on to our AI Studio dependencies ([see here](https://github.com/orgs/MindWorkAI/sponsoring)). In the event that we receive large donations, we will first sign the app ([#56](https://github.com/MindWorkAI/Planning/issues/56)). In case we receive more donations, we will look for and pay staff to develop features for AI Studio.
|
||||
|
||||
- October 2024: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of one project and will also contribute to its further development. This is great news.
|
||||
- April 2025: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of three projects and will also contribute to its further development. This is great news.
|
||||
|
||||
|
||||
Features we have recently released:
|
||||
|
||||
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
|
||||
- v0.9.39: Added the plugin system as a preview feature.
|
||||
- v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well.
|
||||
- v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process)
|
||||
- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature.
|
||||
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
|
||||
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
|
||||
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
|
||||
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
|
||||
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
|
||||
- v0.9.16: Added workspace display options & improved the layout of the app window.
|
||||
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
|
||||
- v0.9.13: You can use `ollama` providers secured with API keys.
|
||||
|
||||
## What is AI Studio?
|
||||
|
||||
@@ -56,7 +68,19 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
||||
|
||||
**Key advantages:**
|
||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) 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.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support:
|
||||
- [OpenAI](https://openai.com/) (GPT4o, GPT4.1, o1, o3, o4, etc.)
|
||||
- [Mistral](https://mistral.ai/)
|
||||
- [Anthropic](https://www.anthropic.com/) (Claude)
|
||||
- [Google Gemini](https://gemini.google.com)
|
||||
- [xAI](https://x.ai/) (Grok)
|
||||
- [DeepSeek](https://www.deepseek.com/en)
|
||||
- [Alibaba Cloud](https://www.alibabacloud.com) (Qwen)
|
||||
- [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face
|
||||
- Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/)
|
||||
- [Groq](https://groq.com/)
|
||||
- [Fireworks](https://fireworks.ai/)
|
||||
- For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) 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.
|
||||
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
||||
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Build</RootNamespace>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>build</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedType.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
namespace Build.Commands;
|
||||
|
||||
public sealed class CheckRidsCommand
|
||||
{
|
||||
[Command("check-rids", Description = "Check the RIDs for the current OS")]
|
||||
public void GetRids()
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
var rids = Environment.GetRidsForCurrentOS();
|
||||
Console.WriteLine("The following RIDs are available for the current OS:");
|
||||
foreach (var rid in rids)
|
||||
{
|
||||
Console.WriteLine($"- {rid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedType.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public sealed partial class CollectI18NKeysCommand
|
||||
{
|
||||
[Command("collect-i18n", Description = "Collect I18N keys")]
|
||||
public async Task CollectI18NKeys()
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
Console.WriteLine("=========================");
|
||||
Console.Write("- Collecting I18N keys ...");
|
||||
|
||||
var cwd = Environment.GetAIStudioDirectory();
|
||||
var binPath = Path.Join(cwd, "bin");
|
||||
var objPath = Path.Join(cwd, "obj");
|
||||
var wwwrootPath = Path.Join(cwd, "wwwroot");
|
||||
var allFiles = Directory.EnumerateFiles(cwd, "*", SearchOption.AllDirectories);
|
||||
var counter = 0;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var filePath in allFiles)
|
||||
{
|
||||
counter++;
|
||||
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if(filePath.StartsWith(objPath, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if(filePath.StartsWith(wwwrootPath, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
|
||||
var matches = this.FindAllTextTags(content);
|
||||
if (matches.Count == 0)
|
||||
continue;
|
||||
|
||||
var ns = this.DetermineNamespace(filePath);
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty);
|
||||
var langNamespace = $"{ns}::{name}".ToUpperInvariant().Replace(".", "::");
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var key = $"root::{langNamespace}::T{match.ToFNV32()}";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($" {counter:###,###} files processed.");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
private List<string> FindAllTextTags(ReadOnlySpan<char> fileContent)
|
||||
{
|
||||
const string START_TAG = """
|
||||
T("
|
||||
""";
|
||||
|
||||
const string END_TAG = """
|
||||
")
|
||||
""";
|
||||
|
||||
var matches = new List<string>();
|
||||
var startIdx = fileContent.IndexOf(START_TAG);
|
||||
var content = fileContent;
|
||||
while (startIdx > -1)
|
||||
{
|
||||
content = content[(startIdx + START_TAG.Length)..];
|
||||
var endIdx = content.IndexOf(END_TAG);
|
||||
if (endIdx == -1)
|
||||
break;
|
||||
|
||||
var match = content[..endIdx];
|
||||
matches.Add(match.ToString());
|
||||
|
||||
startIdx = content.IndexOf(START_TAG);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private string? DetermineNamespace(string filePath)
|
||||
{
|
||||
// Is it a C# file? Then we can read the namespace from it:
|
||||
if (filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
||||
return this.ReadNamespaceFromCSharp(filePath);
|
||||
|
||||
// Is it a Razor file? Then, it depends:
|
||||
if (filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Check if the file contains a namespace declaration:
|
||||
var blazorNamespace = this.ReadNamespaceFromRazor(filePath);
|
||||
if (blazorNamespace != null)
|
||||
return blazorNamespace;
|
||||
|
||||
// Alright, no namespace declaration. Let's check the corresponding C# file:
|
||||
var csFilePath = $"{filePath}.cs";
|
||||
if (File.Exists(csFilePath))
|
||||
{
|
||||
var csNamespace = this.ReadNamespaceFromCSharp(csFilePath);
|
||||
if (csNamespace != null)
|
||||
return csNamespace;
|
||||
|
||||
Console.WriteLine($"- Error: Neither the blazor file '{filePath}' nor the corresponding C# file '{csFilePath}' contain a namespace declaration.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Error: The blazor file '{filePath}' does not contain a namespace declaration and the corresponding C# file '{csFilePath}' does not exist.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Not a C# or Razor file. We can't determine the namespace:
|
||||
Console.WriteLine($"- Error: The file '{filePath}' is neither a C# nor a Razor file. We can't determine the namespace.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ReadNamespaceFromCSharp(string filePath)
|
||||
{
|
||||
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||
var matches = CSharpNamespaceRegex().Matches(content);
|
||||
|
||||
if (matches.Count == 0)
|
||||
return null;
|
||||
|
||||
if (matches.Count > 1)
|
||||
{
|
||||
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var match = matches[0];
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
private string? ReadNamespaceFromRazor(string filePath)
|
||||
{
|
||||
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||
var matches = BlazorNamespaceRegex().Matches(content);
|
||||
|
||||
if (matches.Count == 0)
|
||||
return null;
|
||||
|
||||
if (matches.Count > 1)
|
||||
{
|
||||
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var match = matches[0];
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||
private static partial Regex BlazorNamespaceRegex();
|
||||
|
||||
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
|
||||
private static partial Regex CSharpNamespaceRegex();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Build.Commands;
|
||||
|
||||
public enum PrepareAction
|
||||
{
|
||||
NONE,
|
||||
|
||||
PATCH,
|
||||
MINOR,
|
||||
MAJOR,
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedType.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
public sealed partial class UpdateMetadataCommands
|
||||
{
|
||||
[Command("release", Description = "Prepare & build the next release")]
|
||||
public async Task Release(PrepareAction action)
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
// Prepare the metadata for the next release:
|
||||
await this.Prepare(action);
|
||||
|
||||
// Build once to allow the Rust compiler to read the changed metadata
|
||||
// and to update all .NET artifacts:
|
||||
await this.Build();
|
||||
|
||||
// Now, we update the web assets (which may were updated by the first build):
|
||||
new UpdateWebAssetsCommand().UpdateWebAssets();
|
||||
|
||||
// Collect the I18N keys from the source code. This step yields a I18N file
|
||||
// that must be part of the final release:
|
||||
await new CollectI18NKeysCommand().CollectI18NKeys();
|
||||
|
||||
// Build the final release, where Rust knows the updated metadata, the .NET
|
||||
// artifacts are already in place, and .NET knows the updated web assets, etc.:
|
||||
await this.Build();
|
||||
}
|
||||
|
||||
[Command("update-versions", Description = "The command will update the package versions in the metadata file")]
|
||||
public async Task UpdateVersions()
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
Console.WriteLine("==============================");
|
||||
Console.WriteLine("- Update the main package versions ...");
|
||||
|
||||
await this.UpdateDotnetVersion();
|
||||
await this.UpdateRustVersion();
|
||||
await this.UpdateMudBlazorVersion();
|
||||
await this.UpdateTauriVersion();
|
||||
}
|
||||
|
||||
[Command("prepare", Description = "Prepare the metadata for the next release")]
|
||||
public async Task Prepare(PrepareAction action)
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
Console.WriteLine("==============================");
|
||||
Console.WriteLine("- Prepare the metadata for the next release ...");
|
||||
|
||||
var appVersion = await this.UpdateAppVersion(action);
|
||||
if (!string.IsNullOrWhiteSpace(appVersion))
|
||||
{
|
||||
var buildNumber = await this.IncreaseBuildNumber();
|
||||
var buildTime = await this.UpdateBuildTime();
|
||||
await this.UpdateChangelog(buildNumber, appVersion, buildTime);
|
||||
await this.UpdateDotnetVersion();
|
||||
await this.UpdateRustVersion();
|
||||
await this.UpdateMudBlazorVersion();
|
||||
await this.UpdateTauriVersion();
|
||||
await this.UpdateProjectCommitHash();
|
||||
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
|
||||
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "About.razor.cs")));
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
[Command("build", Description = "Build MindWork AI Studio")]
|
||||
public async Task Build()
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
//
|
||||
// Build the .NET project:
|
||||
//
|
||||
var pathApp = Environment.GetAIStudioDirectory();
|
||||
var rids = Environment.GetRidsForCurrentOS();
|
||||
foreach (var rid in rids)
|
||||
{
|
||||
Console.WriteLine("==============================");
|
||||
await this.UpdateArchitecture(rid);
|
||||
|
||||
Console.Write($"- Start .NET build for '{rid.AsMicrosoftRid()}' ...");
|
||||
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
|
||||
var dotnetBuildOutput = await this.ReadCommandOutput(pathApp, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --disable-build-servers --force");
|
||||
var dotnetBuildOutputLines = dotnetBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
var foundIssue = false;
|
||||
foreach (var buildOutputLine in dotnetBuildOutputLines)
|
||||
{
|
||||
if(buildOutputLine.Contains(" error ") || buildOutputLine.Contains("#warning"))
|
||||
{
|
||||
if(!foundIssue)
|
||||
{
|
||||
foundIssue = true;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("- Build has issues:");
|
||||
}
|
||||
|
||||
Console.Write(" - ");
|
||||
Console.WriteLine(buildOutputLine);
|
||||
}
|
||||
}
|
||||
|
||||
if(foundIssue)
|
||||
Console.WriteLine();
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" completed successfully.");
|
||||
}
|
||||
|
||||
//
|
||||
// Prepare the .NET artifact to be used by Tauri as sidecar:
|
||||
//
|
||||
var os = Environment.GetOS();
|
||||
var tauriSidecarArtifactName = rid switch
|
||||
{
|
||||
RID.WIN_X64 => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
|
||||
RID.WIN_ARM64 => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
|
||||
|
||||
RID.LINUX_X64 => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
|
||||
RID.LINUX_ARM64 => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
|
||||
|
||||
RID.OSX_ARM64 => "mindworkAIStudioServer-aarch64-apple-darwin",
|
||||
RID.OSX_X64 => "mindworkAIStudioServer-x86_64-apple-darwin",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
|
||||
{
|
||||
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
|
||||
if(!Directory.Exists(dotnetArtifactPath))
|
||||
Directory.CreateDirectory(dotnetArtifactPath);
|
||||
|
||||
var dotnetArtifactFilename = os switch
|
||||
{
|
||||
"windows" => "mindworkAIStudio.exe",
|
||||
_ => "mindworkAIStudio",
|
||||
};
|
||||
|
||||
var dotnetPublishedPath = Path.Combine(pathApp, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", dotnetArtifactFilename);
|
||||
var finalDestination = Path.Combine(dotnetArtifactPath, tauriSidecarArtifactName);
|
||||
|
||||
if(File.Exists(dotnetPublishedPath))
|
||||
Console.WriteLine("- Published .NET artifact found.");
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
|
||||
try
|
||||
{
|
||||
File.Move(dotnetPublishedPath, finalDestination, true);
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($" - Error: {e.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
//
|
||||
// Build the Rust project / runtime:
|
||||
//
|
||||
|
||||
Console.WriteLine("==============================");
|
||||
Console.WriteLine("- Start building the Rust runtime ...");
|
||||
|
||||
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
|
||||
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
var foundRustIssue = false;
|
||||
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||
{
|
||||
if(buildOutputLine.Contains("error", StringComparison.OrdinalIgnoreCase) || buildOutputLine.Contains("warning"))
|
||||
{
|
||||
if(!foundRustIssue)
|
||||
{
|
||||
foundRustIssue = true;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("- Build has issues:");
|
||||
}
|
||||
|
||||
Console.Write(" - ");
|
||||
Console.WriteLine(buildOutputLine);
|
||||
}
|
||||
}
|
||||
|
||||
if(foundRustIssue)
|
||||
Console.WriteLine();
|
||||
else
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("- Compilation completed successfully.");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateChangelog(int buildNumber, string appVersion, string buildTime)
|
||||
{
|
||||
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
|
||||
var expectedLogFilename = $"v{appVersion}.md";
|
||||
var expectedLogFilePath = Path.Combine(pathChangelogs, expectedLogFilename);
|
||||
|
||||
if(!File.Exists(expectedLogFilePath))
|
||||
{
|
||||
Console.WriteLine($"- Error: The changelog file '{expectedLogFilename}' does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Right now, the build time is formatted as "yyyy-MM-dd HH:mm:ss UTC", but must remove the seconds:
|
||||
buildTime = buildTime[..^7] + " UTC";
|
||||
|
||||
const string CODE_START =
|
||||
"""
|
||||
LOGS =
|
||||
[
|
||||
""";
|
||||
|
||||
var changelogCodePath = Path.Join(Environment.GetAIStudioDirectory(), "Components", "Changelog.Logs.cs");
|
||||
var changelogCode = await File.ReadAllTextAsync(changelogCodePath, Encoding.UTF8);
|
||||
var updatedCode =
|
||||
$"""
|
||||
{CODE_START}
|
||||
new ({buildNumber}, "v{appVersion}, build {buildNumber} ({buildTime})", "{expectedLogFilename}"),
|
||||
""";
|
||||
|
||||
changelogCode = changelogCode.Replace(CODE_START, updatedCode);
|
||||
await File.WriteAllTextAsync(changelogCodePath, changelogCode, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task UpdateArchitecture(RID rid)
|
||||
{
|
||||
const int ARCHITECTURE_INDEX = 9;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
Console.Write("- Updating architecture ...");
|
||||
lines[ARCHITECTURE_INDEX] = rid.AsMicrosoftRid();
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
|
||||
private async Task UpdateProjectCommitHash()
|
||||
{
|
||||
const int COMMIT_HASH_INDEX = 8;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentCommitHash = lines[COMMIT_HASH_INDEX].Trim();
|
||||
var headCommitHash = await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD");
|
||||
var first10Chars = headCommitHash[..11];
|
||||
var updatedCommitHash = $"{first10Chars}, release";
|
||||
|
||||
Console.WriteLine($"- Updating commit hash from '{currentCommitHash}' to '{updatedCommitHash}'.");
|
||||
lines[COMMIT_HASH_INDEX] = updatedCommitHash;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task<string> UpdateAppVersion(PrepareAction action)
|
||||
{
|
||||
const int APP_VERSION_INDEX = 0;
|
||||
|
||||
if (action == PrepareAction.NONE)
|
||||
{
|
||||
Console.WriteLine("- No action specified. Skipping app version update.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim();
|
||||
var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
|
||||
var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
|
||||
var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
|
||||
var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value);
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case PrepareAction.PATCH:
|
||||
currentPatch++;
|
||||
break;
|
||||
|
||||
case PrepareAction.MINOR:
|
||||
currentPatch = 0;
|
||||
currentMinor++;
|
||||
break;
|
||||
|
||||
case PrepareAction.MAJOR:
|
||||
currentPatch = 0;
|
||||
currentMinor = 0;
|
||||
currentMajor++;
|
||||
break;
|
||||
}
|
||||
|
||||
var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}";
|
||||
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
|
||||
|
||||
lines[APP_VERSION_INDEX] = updatedAppVersion;
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
return updatedAppVersion;
|
||||
}
|
||||
|
||||
private async Task UpdateLicenceYear(string licenceFilePath)
|
||||
{
|
||||
var currentYear = DateTime.UtcNow.Year.ToString();
|
||||
var lines = await File.ReadAllLinesAsync(licenceFilePath, Encoding.UTF8);
|
||||
|
||||
var found = false;
|
||||
var copyrightYear = string.Empty;
|
||||
var updatedLines = new List<string>(lines.Length);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = FindCopyrightRegex().Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
copyrightYear = match.Groups["year"].Value;
|
||||
|
||||
if(!found && copyrightYear != currentYear)
|
||||
Console.WriteLine($"- Updating the licence's year in '{Path.GetFileName(licenceFilePath)}' from '{copyrightYear}' to '{currentYear}'.");
|
||||
|
||||
updatedLines.Add(ReplaceCopyrightYearRegex().Replace(line, currentYear));
|
||||
found = true;
|
||||
}
|
||||
else
|
||||
updatedLines.Add(line);
|
||||
}
|
||||
|
||||
await File.WriteAllLinesAsync(licenceFilePath, updatedLines, Environment.UTF8_NO_BOM);
|
||||
if (!found)
|
||||
Console.WriteLine($"- Error: No copyright year found in '{Path.GetFileName(licenceFilePath)}'.");
|
||||
else if (copyrightYear == currentYear)
|
||||
Console.WriteLine($"- The copyright year in '{Path.GetFileName(licenceFilePath)}' is already up to date.");
|
||||
}
|
||||
|
||||
private async Task UpdateTauriVersion()
|
||||
{
|
||||
const int TAURI_VERSION_INDEX = 7;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentTauriVersion = lines[TAURI_VERSION_INDEX].Trim();
|
||||
|
||||
var matches = await this.DetermineVersion("Tauri", Environment.GetRustRuntimeDirectory(), TauriVersionRegex(), "cargo", "tree --depth 1");
|
||||
if (matches.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedTauriVersion = matches[0].Groups["version"].Value;
|
||||
if(currentTauriVersion == updatedTauriVersion)
|
||||
{
|
||||
Console.WriteLine("- The Tauri version is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Updated Tauri version from {currentTauriVersion} to {updatedTauriVersion}.");
|
||||
lines[TAURI_VERSION_INDEX] = updatedTauriVersion;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task UpdateMudBlazorVersion()
|
||||
{
|
||||
const int MUD_BLAZOR_VERSION_INDEX = 6;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentMudBlazorVersion = lines[MUD_BLAZOR_VERSION_INDEX].Trim();
|
||||
|
||||
var matches = await this.DetermineVersion("MudBlazor", Environment.GetAIStudioDirectory(), MudBlazorVersionRegex(), "dotnet", "list package");
|
||||
if (matches.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedMudBlazorVersion = matches[0].Groups["version"].Value;
|
||||
if(currentMudBlazorVersion == updatedMudBlazorVersion)
|
||||
{
|
||||
Console.WriteLine("- The MudBlazor version is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Updated MudBlazor version from {currentMudBlazorVersion} to {updatedMudBlazorVersion}.");
|
||||
lines[MUD_BLAZOR_VERSION_INDEX] = updatedMudBlazorVersion;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task UpdateRustVersion()
|
||||
{
|
||||
const int RUST_VERSION_INDEX = 5;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentRustVersion = lines[RUST_VERSION_INDEX].Trim();
|
||||
var matches = await this.DetermineVersion("Rust", Environment.GetRustRuntimeDirectory(), RustVersionRegex(), "rustc", "-Vv");
|
||||
if (matches.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedRustVersion = matches[0].Groups["version"].Value + " (commit " + matches[0].Groups["commit"].Value + ")";
|
||||
if(currentRustVersion == updatedRustVersion)
|
||||
{
|
||||
Console.WriteLine("- Rust version is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Updated Rust version from {currentRustVersion} to {updatedRustVersion}.");
|
||||
lines[RUST_VERSION_INDEX] = updatedRustVersion;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task UpdateDotnetVersion()
|
||||
{
|
||||
const int DOTNET_VERSION_INDEX = 4;
|
||||
const int DOTNET_SDK_VERSION_INDEX = 3;
|
||||
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var currentDotnetVersion = lines[DOTNET_VERSION_INDEX].Trim();
|
||||
var currentDotnetSdkVersion = lines[DOTNET_SDK_VERSION_INDEX].Trim();
|
||||
|
||||
var matches = await this.DetermineVersion(".NET", Environment.GetAIStudioDirectory(), DotnetVersionRegex(), "dotnet", "--info");
|
||||
if (matches.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedDotnetVersion = matches[0].Groups["hostVersion"].Value + " (commit " + matches[0].Groups["hostCommit"].Value + ")";
|
||||
var updatedDotnetSdkVersion = matches[0].Groups["sdkVersion"].Value + " (commit " + matches[0].Groups["sdkCommit"].Value + ")";
|
||||
if(currentDotnetVersion == updatedDotnetVersion && currentDotnetSdkVersion == updatedDotnetSdkVersion)
|
||||
{
|
||||
Console.WriteLine("- .NET version is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"- Updated .NET SDK version from {currentDotnetSdkVersion} to {updatedDotnetSdkVersion}.");
|
||||
Console.WriteLine($"- Updated .NET version from {currentDotnetVersion} to {updatedDotnetVersion}.");
|
||||
|
||||
lines[DOTNET_VERSION_INDEX] = updatedDotnetVersion;
|
||||
lines[DOTNET_SDK_VERSION_INDEX] = updatedDotnetSdkVersion;
|
||||
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
|
||||
private async Task<IList<Match>> DetermineVersion(string name, string workingDirectory, Regex regex, string program, string command)
|
||||
{
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
FileName = program,
|
||||
Arguments = command,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = new Process();
|
||||
process.StartInfo = processInfo;
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var matches = regex.Matches(output);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"- Error: Was not able to determine the {name} version.");
|
||||
return [];
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private async Task<string> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
|
||||
{
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
FileName = program,
|
||||
Arguments = command,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
var sb = new StringBuilder();
|
||||
using var process = new Process();
|
||||
process.StartInfo = processInfo;
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
process.OutputDataReceived += (_, args) =>
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(args.Data))
|
||||
{
|
||||
if(showLiveOutput)
|
||||
Console.WriteLine(args.Data);
|
||||
sb.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (_, args) =>
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(args.Data))
|
||||
{
|
||||
if(showLiveOutput)
|
||||
Console.WriteLine(args.Data);
|
||||
sb.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private async Task<int> IncreaseBuildNumber()
|
||||
{
|
||||
const int BUILD_NUMBER_INDEX = 2;
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var buildNumber = int.Parse(lines[BUILD_NUMBER_INDEX]) + 1;
|
||||
|
||||
Console.WriteLine($"- Updating build number from '{lines[BUILD_NUMBER_INDEX]}' to '{buildNumber}'.");
|
||||
|
||||
lines[BUILD_NUMBER_INDEX] = buildNumber.ToString();
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
private async Task<string> UpdateBuildTime()
|
||||
{
|
||||
const int BUILD_TIME_INDEX = 1;
|
||||
var pathMetadata = Environment.GetMetadataPath();
|
||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||
var buildTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC";
|
||||
|
||||
Console.WriteLine($"- Updating build time from '{lines[BUILD_TIME_INDEX]}' to '{buildTime}'.");
|
||||
|
||||
lines[BUILD_TIME_INDEX] = buildTime;
|
||||
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
|
||||
return buildTime;
|
||||
}
|
||||
|
||||
[GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?<sdkVersion>[0-9.]+).+Commit:\s+(?<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?<hostVersion>[0-9.]+).+Commit:\s+(?<hostCommit>[a-zA-Z0-9]+)""")]
|
||||
private static partial Regex DotnetVersionRegex();
|
||||
|
||||
[GeneratedRegex("""rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)""")]
|
||||
private static partial Regex RustVersionRegex();
|
||||
|
||||
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
|
||||
private static partial Regex MudBlazorVersionRegex();
|
||||
|
||||
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
|
||||
private static partial Regex TauriVersionRegex();
|
||||
|
||||
[GeneratedRegex("""^\s*Copyright\s+(?<year>[0-9]{4})""")]
|
||||
private static partial Regex FindCopyrightRegex();
|
||||
|
||||
[GeneratedRegex("""([0-9]{4})""")]
|
||||
private static partial Regex ReplaceCopyrightYearRegex();
|
||||
|
||||
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
|
||||
private static partial Regex AppVersionRegex();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedType.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
using Build.Tools;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public sealed class UpdateWebAssetsCommand
|
||||
{
|
||||
[Command("update-web", Description = "Update web assets")]
|
||||
public void UpdateWebAssets()
|
||||
{
|
||||
if(!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
Console.WriteLine("=========================");
|
||||
Console.Write("- Updating web assets ...");
|
||||
|
||||
var rid = Environment.GetRidsForCurrentOS().First();
|
||||
var cwd = Environment.GetAIStudioDirectory();
|
||||
var contentPath = Path.Join(cwd, "bin", "release", Environment.DOTNET_VERSION, rid.AsMicrosoftRid(), "publish", "wwwroot", "_content");
|
||||
var isMudBlazorDirectoryPresent = Directory.Exists(Path.Join(contentPath, "MudBlazor"));
|
||||
if (!isMudBlazorDirectoryPresent)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"- Error: No web assets found for RID '{rid}'. Please publish the project first.");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.Join(cwd, "wwwroot", "system"));
|
||||
var sourcePaths = Directory.EnumerateFiles(contentPath, "*", SearchOption.AllDirectories);
|
||||
var counter = 0;
|
||||
foreach(var sourcePath in sourcePaths)
|
||||
{
|
||||
counter++;
|
||||
var relativePath = Path.GetRelativePath(cwd, sourcePath);
|
||||
var targetPath = Path.Join(cwd, "wwwroot", relativePath);
|
||||
var targetDirectory = Path.GetDirectoryName(targetPath);
|
||||
if (targetDirectory != null)
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
|
||||
File.Copy(sourcePath, targetPath, true);
|
||||
}
|
||||
|
||||
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Global using directives
|
||||
|
||||
global using System.Text;
|
||||
|
||||
global using Cocona;
|
||||
|
||||
global using Environment = Build.Tools.Environment;
|
||||
@@ -0,0 +1,9 @@
|
||||
using Build.Commands;
|
||||
|
||||
var builder = CoconaApp.CreateBuilder();
|
||||
var app = builder.Build();
|
||||
app.AddCommands<CheckRidsCommand>();
|
||||
app.AddCommands<UpdateMetadataCommands>();
|
||||
app.AddCommands<UpdateWebAssetsCommand>();
|
||||
app.AddCommands<CollectI18NKeysCommand>();
|
||||
app.Run();
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Tools;
|
||||
|
||||
public static class Environment
|
||||
{
|
||||
public const string DOTNET_VERSION = "net9.0";
|
||||
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
|
||||
|
||||
private static readonly Dictionary<RID, string> ALL_RIDS = Enum.GetValues<RID>().Select(rid => new KeyValuePair<RID, string>(rid, rid.AsMicrosoftRid())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
public static bool IsWorkingDirectoryValid()
|
||||
{
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
var mainFile = Path.Combine(currentDirectory, "Program.cs");
|
||||
var projectFile = Path.Combine(currentDirectory, "Build Script.csproj");
|
||||
|
||||
if (!currentDirectory.EndsWith("Build", StringComparison.Ordinal) || !File.Exists(mainFile) || !File.Exists(projectFile))
|
||||
{
|
||||
Console.WriteLine("The current directory is not a valid working directory for the build script. Go to the /app/Build directory within the git repository.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string GetAIStudioDirectory()
|
||||
{
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
var directory = Path.Combine(currentDirectory, "..", "MindWork AI Studio");
|
||||
return Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
public static string GetRustRuntimeDirectory()
|
||||
{
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
var directory = Path.Combine(currentDirectory, "..", "..", "runtime");
|
||||
return Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
public static string GetMetadataPath()
|
||||
{
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
var directory = Path.Combine(currentDirectory, "..", "..", "metadata.txt");
|
||||
return Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
public static string? GetOS()
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return "windows";
|
||||
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return "linux";
|
||||
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return "darwin";
|
||||
|
||||
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<RID> GetRidsForCurrentOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return ALL_RIDS.Where(rid => rid.Value.StartsWith("win-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return ALL_RIDS.Where(rid => rid.Value.StartsWith("osx-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return ALL_RIDS.Where(rid => rid.Value.StartsWith("linux-", StringComparison.Ordinal)).Select(n => n.Key);
|
||||
|
||||
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindW
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build Script.csproj", "{447A5590-68E1-4EF8-9451-A41AF5FBE571}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -18,6 +22,14 @@ Global
|
||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
EndGlobalSection
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RID/@EntryIndexedValue">RID</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=huggingface/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -13,6 +13,7 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (215, "v0.9.40, build 215 (2025-04-20 13:30 UTC)", "v0.9.40.md"),
|
||||
new (214, "v0.9.39, build 214 (2025-04-07 17:39 UTC)", "v0.9.39.md"),
|
||||
new (213, "v0.9.38, build 213 (2025-03-17 18:18 UTC)", "v0.9.38.md"),
|
||||
new (212, "v0.9.37, build 212 (2025-03-16 20:32 UTC)", "v0.9.37.md"),
|
||||
|
||||
@@ -840,8 +840,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(ChatComponent);
|
||||
|
||||
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@@ -860,7 +860,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
protected override Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
|
||||
@@ -46,8 +46,8 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(InnerScrolling);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@@ -59,11 +59,6 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
|
||||
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver, ILang
|
||||
{
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||
@@ -12,12 +15,37 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<PluginLanguage> Logger { get; init; } = null!;
|
||||
|
||||
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
base.OnInitialized();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ILang
|
||||
|
||||
/// <inheritdoc />
|
||||
public string T(string fallbackEN)
|
||||
{
|
||||
var type = this.GetType();
|
||||
var ns = $"{type.Namespace!}::{type.Name}".ToUpperInvariant().Replace(".", "::");
|
||||
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
|
||||
|
||||
if(this.Lang.TryGetText(key, out var text, logWarning: false))
|
||||
return text;
|
||||
|
||||
this.Logger.LogWarning($"Missing translation key '{key}' for content '{fallbackEN}'.");
|
||||
return fallbackEN;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -26,7 +54,7 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
|
||||
public abstract string ComponentName { get; }
|
||||
|
||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@@ -34,19 +62,34 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
default:
|
||||
return this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public abstract Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
|
||||
|
||||
public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
|
||||
|
||||
public async Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return await this.ProcessIncomingMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected virtual Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
@@ -71,7 +114,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
// Append the color theme changed event to the list of events:
|
||||
var eventsList = new List<Event>(events)
|
||||
{
|
||||
Event.COLOR_THEME_CHANGED
|
||||
Event.COLOR_THEME_CHANGED,
|
||||
Event.PLUGINS_RELOADED,
|
||||
};
|
||||
|
||||
this.MessageBus.ApplyFilters(this, components, eventsList.ToArray());
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="App Options">
|
||||
|
||||
@if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="Language behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.LanguageBehavior)" Data="@ConfigurationSelectDataFactory.GetLangBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.LanguageBehavior = selectedValue)" OptionHelp="Select the language behavior for the app. The default is to use the system language. You might want to choose a language manually?"/>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.LanguageBehavior is LangBehavior.MANUAL)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="Language" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.LanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.LanguagePluginId = selectedValue)" OptionHelp="Select the language for the app."/>
|
||||
}
|
||||
}
|
||||
|
||||
<ConfigurationSelect OptionDescription="Color theme" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="Choose the color theme that best suits for you."/>
|
||||
<ConfigurationOption OptionDescription="Save energy?" LabelOn="Energy saving is enabled" LabelOff="Energy saving is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available."/>
|
||||
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." />
|
||||
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections."/>
|
||||
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
|
||||
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
|
||||
<ConfigurationSelect OptionDescription="Preview feature visibility" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="Do you want to show preview features in the app?"/>
|
||||
|
||||
@if(this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||
{
|
||||
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
||||
if (availablePreviewFeatures.Count > 0)
|
||||
@@ -18,7 +29,7 @@
|
||||
<ConfigurationMultiSelect OptionDescription="Select preview features" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures)" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="Which preview features would you like to enable?"/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
|
||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
|
||||
</ExpansionPanel>
|
||||
@@ -41,7 +41,7 @@
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.UsedLLMProvider</MudTd>
|
||||
<MudTd>@context.UsedLLMProvider.ToName()</MudTd>
|
||||
<MudTd>@this.GetEmbeddingProviderModelName(context)</MudTd>
|
||||
|
||||
<MudTd>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
<MudTd>@context.InstanceName</MudTd>
|
||||
<MudTd>@context.UsedLLMProvider</MudTd>
|
||||
<MudTd>@context.UsedLLMProvider.ToName()</MudTd>
|
||||
<MudTd>
|
||||
@if (context.UsedLLMProvider is not LLMProviders.SELF_HOSTED)
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
||||
{ x => x.IsSelfHosted, provider.IsSelfHosted },
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataHost, provider.Host },
|
||||
{ x => x.HFInferenceProviderId, provider.HFInferenceProvider },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit LLM Provider", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
|
||||
@@ -11,49 +11,58 @@
|
||||
{
|
||||
if (provider.ProvideEmbeddings())
|
||||
{
|
||||
<MudSelectItem Value="@provider">@provider</MudSelectItem>
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">Create account</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataAPIKey"
|
||||
Label="@this.APIKeyText"
|
||||
Disabled="@(!this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
||||
AdornmentColor="Color.Info"
|
||||
InputType="InputType.Password"
|
||||
Validation="@this.providerValidation.ValidatingAPIKey"
|
||||
/>
|
||||
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="Hostname"
|
||||
Disabled="@(!this.DataLLMProvider.IsHostnameNeeded())"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataAPIKey"
|
||||
Label="@this.APIKeyText"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
||||
AdornmentColor="Color.Info"
|
||||
InputType="InputType.Password"
|
||||
Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
|
||||
<MudSelect Disabled="@(!this.DataLLMProvider.IsHostNeeded())" @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.AreEmbeddingsSupported())
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="Hostname"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
<MudSelectItem Value="@host">@host.Name()</MudSelectItem>
|
||||
if (host.AreEmbeddingsSupported())
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Provider.HuggingFace
|
||||
@using AIStudio.Provider.SelfHosted
|
||||
|
||||
<MudDialog>
|
||||
@@ -9,57 +10,80 @@
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="Provider" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
<MudSelectItem Value="@provider">@provider</MudSelectItem>
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.ShowRegisterButton())" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetCreationURL()" Target="_blank">Create account</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataAPIKey"
|
||||
Label="@this.APIKeyText"
|
||||
Disabled="@(!this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
||||
AdornmentColor="Color.Info"
|
||||
InputType="InputType.Password"
|
||||
Validation="@this.providerValidation.ValidatingAPIKey"
|
||||
/>
|
||||
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="Hostname"
|
||||
Disabled="@(!this.DataLLMProvider.IsHostnameNeeded())"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataAPIKey"
|
||||
Label="@this.APIKeyText"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
||||
AdornmentColor="Color.Info"
|
||||
InputType="InputType.Password"
|
||||
Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
}
|
||||
|
||||
<MudSelect Disabled="@(!this.DataLLMProvider.IsHostNeeded())" @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
<MudSelectItem Value="@host">@host.Name()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.DataLLMProvider.IsHostnameNeeded())
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.DataHostname"
|
||||
Label="Hostname"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
<MudSelectItem Value="@host">
|
||||
@host.Name()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect Disabled="@(!this.DataLLMProvider.IsHFInstanceProviderNeeded())" @bind-Value="@this.HFInferenceProviderId" Label="HF Inference Provider" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudJustifiedText Class="mb-3"> Please double-check if your model name matches the curl specifications provided by the inference provider. If it doesn't, you might get a <b>Not Found</b> error when trying to use the model. Here's a <MudLink Href="https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct?inference_api=true&inference_provider=novita&language=sh" Target="_blank">curl example</MudLink>.</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@if (this.DataLLMProvider.IsLLMModelProvidedManually())
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetModelsOverviewURL()" Target="_blank">Show available models</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetModelsOverviewURL(this.HFInferenceProviderId)" Target="_blank">Show available models</MudButton>
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataManuallyModel"
|
||||
Label="Model"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Validation;
|
||||
@@ -47,6 +48,12 @@ public partial class ProviderDialog : ComponentBase, ISecretId
|
||||
[Parameter]
|
||||
public Host DataHost { get; set; } = Host.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The HFInstanceProvider to use, e.g., CEREBRAS.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public HFInferenceProvider HFInferenceProviderId { get; set; } = HFInferenceProvider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Is this provider self-hosted?
|
||||
/// </summary>
|
||||
@@ -122,10 +129,16 @@ public partial class ProviderDialog : ComponentBase, ISecretId
|
||||
Id = this.DataId,
|
||||
InstanceName = this.DataInstanceName,
|
||||
UsedLLMProvider = this.DataLLMProvider,
|
||||
Model = this.DataLLMProvider is LLMProviders.FIREWORKS ? new Model(this.dataManuallyModel, null) : this.DataModel,
|
||||
Model = this.DataLLMProvider switch
|
||||
{
|
||||
LLMProviders.FIREWORKS => new Model(this.dataManuallyModel, null),
|
||||
LLMProviders.HUGGINGFACE => new Model(this.dataManuallyModel, null),
|
||||
_ => this.DataModel
|
||||
},
|
||||
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
Host = this.DataHost,
|
||||
HFInferenceProvider = this.HFInferenceProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,8 +159,8 @@ public partial class ProviderDialog : ComponentBase, ISecretId
|
||||
{
|
||||
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
|
||||
|
||||
// When using Fireworks, we must copy the model name:
|
||||
if (this.DataLLMProvider is LLMProviders.FIREWORKS)
|
||||
// When using Fireworks or Hugging Face, we must copy the model name:
|
||||
if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE)
|
||||
this.dataManuallyModel = this.DataModel.Id;
|
||||
|
||||
//
|
||||
@@ -230,7 +243,7 @@ public partial class ProviderDialog : ComponentBase, ISecretId
|
||||
|
||||
private string? ValidateManuallyModel(string manuallyModel)
|
||||
{
|
||||
if (this.DataLLMProvider is LLMProviders.FIREWORKS && string.IsNullOrWhiteSpace(manuallyModel))
|
||||
if ((this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) && string.IsNullOrWhiteSpace(manuallyModel))
|
||||
return "Please enter a model name.";
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools.Metadata;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -83,26 +83,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
|
||||
//
|
||||
// We cannot process the plugins before the settings are loaded,
|
||||
// and we know our data directory.
|
||||
//
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
// 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));
|
||||
_ = PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
}
|
||||
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR ]);
|
||||
this.MessageBus.ApplyFilters(this, [],
|
||||
[
|
||||
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
||||
Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED
|
||||
]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
UpdateService.SetBlazorDependencies(this.Snackbar);
|
||||
@@ -115,6 +102,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
// Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
|
||||
MudGlobal.TooltipDefaults.Duration = TimeSpan.Zero;
|
||||
|
||||
// Send a message to start the plugin system:
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_PLUGIN_SYSTEM);
|
||||
|
||||
await this.themeProvider.WatchSystemPreference(this.SystemeThemeChanged);
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
@@ -179,6 +169,32 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.STARTUP_PLUGIN_SYSTEM:
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Set up the plugin system:
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace AIStudio;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class MetaDataAttribute(
|
||||
string version,
|
||||
string buildTime,
|
||||
uint buildNum,
|
||||
string dotnetSdkVersion,
|
||||
string dotnetVersion,
|
||||
string rustVersion,
|
||||
string mudBlazorVersion,
|
||||
string tauriVersion,
|
||||
string appCommitHash
|
||||
) : Attribute
|
||||
{
|
||||
public string BuildTime { get; } = buildTime;
|
||||
|
||||
public string Version { get; } = version;
|
||||
|
||||
public uint BuildNum { get; } = buildNum;
|
||||
|
||||
public string DotnetVersion { get; } = dotnetVersion;
|
||||
|
||||
public string DotnetSdkVersion { get; } = dotnetSdkVersion;
|
||||
|
||||
public string RustVersion { get; } = rustVersion;
|
||||
|
||||
public string MudBlazorVersion { get; } = mudBlazorVersion;
|
||||
|
||||
public string TauriVersion { get; } = tauriVersion;
|
||||
|
||||
public string AppCommitHash { get; } = appCommitHash;
|
||||
}
|
||||
@@ -47,15 +47,16 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.0.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.3" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
|
||||
<PackageReference Include="MudBlazor" Version="8.5.1" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.0.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.5.1" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
|
||||
<ProjectReference Include="..\SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -77,6 +78,7 @@
|
||||
<MetaMudBlazorVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 6 ])</MetaMudBlazorVersion>
|
||||
<MetaTauriVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 7 ])</MetaTauriVersion>
|
||||
<MetaAppCommitHash>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ])</MetaAppCommitHash>
|
||||
<MetaArchitecture>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ])</MetaArchitecture>
|
||||
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
|
||||
@@ -86,7 +88,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="AIStudio.MetaDataAttribute">
|
||||
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaData">
|
||||
<_Parameter1>$(MetaVersion)</_Parameter1>
|
||||
<_Parameter2>$(MetaBuildTime)</_Parameter2>
|
||||
<_Parameter3>$(MetaBuild)</_Parameter3>
|
||||
@@ -98,6 +100,9 @@
|
||||
<_Parameter8>$(MetaTauriVersion)</_Parameter8>
|
||||
<_Parameter9>$(MetaAppCommitHash)</_Parameter9>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataArchitecture">
|
||||
<_Parameter1>$(MetaArchitecture)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Target>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools.Metadata;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class About : ComponentBase
|
||||
@@ -20,6 +23,7 @@ public partial class About : ComponentBase
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
private static readonly MetaDataArchitecture META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitecture>()!;
|
||||
|
||||
private static string VersionDotnetRuntime => $"Used .NET runtime: v{META_DATA.DotnetVersion}";
|
||||
|
||||
@@ -27,7 +31,7 @@ public partial class About : ComponentBase
|
||||
|
||||
private static string VersionRust => $"Used Rust compiler: v{META_DATA.RustVersion}";
|
||||
|
||||
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum})";
|
||||
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})";
|
||||
|
||||
private static string BuildTime => $"Build time: {META_DATA.BuildTime}";
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
@if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
|
||||
{
|
||||
@($"Chat in Workspace \"{this.currentWorkspaceName}\"")
|
||||
@(T("Chat in Workspace") + $" \"{this.currentWorkspaceName}\"")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Temporary Chat")
|
||||
@(T("Short-Term Chat"))
|
||||
}
|
||||
</MudText>
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ public partial class Chat : MSGComponentBase
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Chat);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@@ -96,10 +96,5 @@ public partial class Chat : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
@attribute [Route(Routes.HOME)]
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudImage Src="svg/banner.svg" Style="max-height: 16em; width: 100%; object-fit: cover;" />
|
||||
<MudText Typo="Typo.h3" Class="mt-2 mb-2">Let's get started</MudText>
|
||||
<MudText Typo="Typo.h3" Class="mt-2 mb-2">
|
||||
@T("Let's get started")
|
||||
</MudText>
|
||||
|
||||
<InnerScrolling>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
|
||||
@@ -6,10 +6,10 @@ using Changelog = AIStudio.Components.Changelog;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Home : ComponentBase
|
||||
public partial class Home : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; set; } = null!;
|
||||
private HttpClient HttpClient { get; init; } = null!;
|
||||
|
||||
private string LastChangeContent { get; set; } = string.Empty;
|
||||
|
||||
@@ -22,7 +22,13 @@ public partial class Home : ComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Home);
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ReadLastChangeAsync()
|
||||
{
|
||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
||||
@@ -33,7 +39,7 @@ public partial class Home : ComponentBase
|
||||
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
||||
[
|
||||
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
|
||||
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using 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("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using 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("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
|
||||
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
|
||||
new("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using AIStudio.Tools.PluginSystem
|
||||
@inherits MSGComponentBase
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
public partial class Plugins : MSGComponentBase
|
||||
{
|
||||
private const string GROUP_ENABLED = "Enabled";
|
||||
private const string GROUP_DISABLED = "Disabled";
|
||||
private const string GROUP_INTERNAL = "Internal";
|
||||
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
private TableGroupDefinition<IPluginMetadata> groupConfig = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
@@ -45,7 +37,13 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Plugins);
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||
{
|
||||
if (this.SettingsManager.IsPluginEnabled(pluginMeta))
|
||||
@@ -56,27 +54,4 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public string ComponentName => nameof(Plugins);
|
||||
|
||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
<MudList T="string">
|
||||
<Supporter Name="DevNullx64" Type="SupporterType.INDIVIDUAL" URL="https://github.com/DevNullx64" Acknowledgment="Thanks Luc for your build script contribution."/>
|
||||
<Supporter Name="SolsticeSpectrum" Type="SupporterType.INDIVIDUAL" URL="https://github.com/SolsticeSpectrum" Acknowledgment="Thanks for your build script contribution."/>
|
||||
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thanks for your contributions to the project."/>
|
||||
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thank you, Peer, for familiarizing yourself with C#, providing excellent contributions like the Alibaba and Hugging Face providers, and revising the settings management."/>
|
||||
<Supporter Name="nilskruthoff" Type="SupporterType.INDIVIDUAL" URL="https://github.com/nilskruthoff" Acknowledgment="Thanks, Nils, for taking the time to learn Rust and build the foundation for local retrieval."/>
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
|
||||
@@ -24,7 +24,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
||||
private string userInput = string.Empty;
|
||||
private string userDirection = string.Empty;
|
||||
private string suggestion = string.Empty;
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -43,16 +43,6 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
public override string ComponentName => nameof(Writer);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
+14
@@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = ""
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "de-DE"
|
||||
|
||||
-- The language name in the user's language:
|
||||
LANG_NAME = "Deutsch (Deutschland)"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
AISTUDIO = {
|
||||
PAGES = {
|
||||
HOME = {
|
||||
T2331588413 = "Lass uns anfangen",
|
||||
},
|
||||
|
||||
CHAT = {
|
||||
T3718856736 = "Vorläufiger Chat",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
+14
@@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = ""
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "en-US"
|
||||
|
||||
-- The language name in the user's language:
|
||||
LANG_NAME = "English (United States)"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
AISTUDIO = {
|
||||
PAGES = {
|
||||
HOME = {
|
||||
T2331588413 = "Let's get started",
|
||||
},
|
||||
|
||||
CHAT = {
|
||||
T3718856736 = "Short-Term Chat",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.AlibabaCloud;
|
||||
|
||||
public sealed class ProviderAlibabaCloud(ILogger logger) : BaseProvider("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/", logger)
|
||||
{
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.ALIBABA_CLOUD.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "AlibabaCloud";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the AlibabaCloud HTTP chat request:
|
||||
var alibabaCloudChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(alibabaCloudChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("AlibabaCloud", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var additionalModels = new[]
|
||||
{
|
||||
new Model("qwq-plus", "QwQ plus"), // reasoning model
|
||||
new Model("qwen-max-latest", "Qwen-Max (Latest)"),
|
||||
new Model("qwen-plus-latest", "Qwen-Plus (Latest)"),
|
||||
new Model("qwen-turbo-latest", "Qwen-Turbo (Latest)"),
|
||||
new Model("qvq-max", "QVQ Max"), // visual reasoning model
|
||||
new Model("qvq-max-latest", "QVQ Max (Latest)"), // visual reasoning model
|
||||
new Model("qwen-vl-max", "Qwen-VL Max"), // text generation model that can understand and process images
|
||||
new Model("qwen-vl-plus", "Qwen-VL Plus"), // text generation model that can understand and process images
|
||||
new Model("qwen-mt-plus", "Qwen-MT Plus"), // machine translation
|
||||
new Model("qwen-mt-turbo", "Qwen-MT Turbo"), // machine translation
|
||||
|
||||
//Open source
|
||||
new Model("qwen2.5-14b-instruct-1m", "Qwen2.5 14b 1m context"),
|
||||
new Model("qwen2.5-7b-instruct-1m", "Qwen2.5 7b 1m context"),
|
||||
new Model("qwen2.5-72b-instruct", "Qwen2.5 72b"),
|
||||
new Model("qwen2.5-32b-instruct", "Qwen2.5 32b"),
|
||||
new Model("qwen2.5-14b-instruct", "Qwen2.5 14b"),
|
||||
new Model("qwen2.5-7b-instruct", "Qwen2.5 7b"),
|
||||
new Model("qwen2.5-omni-7b", "Qwen2.5-Omni 7b"), // omni-modal understanding and generation model
|
||||
new Model("qwen2.5-vl-72b-instruct", "Qwen2.5-VL 72b"),
|
||||
new Model("qwen2.5-vl-32b-instruct", "Qwen2.5-VL 32b"),
|
||||
new Model("qwen2.5-vl-7b-instruct", "Qwen2.5-VL 7b"),
|
||||
new Model("qwen2.5-vl-3b-instruct", "Qwen2.5-VL 3b"),
|
||||
};
|
||||
|
||||
return this.LoadModels(["q"],token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
|
||||
var additionalModels = new[]
|
||||
{
|
||||
new Model("text-embedding-v3", "text-embedding-v3"),
|
||||
};
|
||||
|
||||
return this.LoadModels(["text-embedding-"], token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,10 +35,10 @@ public sealed record Confidence
|
||||
""",
|
||||
};
|
||||
|
||||
public static readonly Confidence USA_NOT_TRUSTED = new()
|
||||
public static readonly Confidence USA_HUB = new()
|
||||
{
|
||||
Level = ConfidenceLevel.UNTRUSTED,
|
||||
Description = "The provider operates its service from the USA and is subject to **U.S. jurisdiction**. In case of suspicion, authorities in the USA can access your data. The provider's terms of service state that **all your data can be used by the provider at will.**",
|
||||
Level = ConfidenceLevel.UNKNOWN,
|
||||
Description = "The provider operates its service from the USA and is subject to **U.S. jurisdiction**. In case of suspicion, authorities in the USA can access your data. Please inform yourself about the use of your data. We do not know if your data is safe.",
|
||||
};
|
||||
|
||||
public static readonly Confidence UNKNOWN = new()
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Helmholtz", RequestBuilder, token))
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("DeepSeek", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace AIStudio.Provider.HuggingFace;
|
||||
|
||||
/// <summary>
|
||||
/// Enum for inference providers that Hugging Face supports.
|
||||
/// </summary>
|
||||
public enum HFInferenceProvider
|
||||
{
|
||||
NONE,
|
||||
|
||||
CEREBRAS,
|
||||
NEBIUS_AI_STUDIO,
|
||||
SAMBANOVA,
|
||||
NOVITA,
|
||||
HYPERBOLIC,
|
||||
TOGETHER_AI,
|
||||
FIREWORKS,
|
||||
HF_INFERENCE_API,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace AIStudio.Provider.HuggingFace;
|
||||
|
||||
public static class HFInferenceProviderExtensions
|
||||
{
|
||||
public static string Endpoints(this HFInferenceProvider provider, Model model) => provider switch
|
||||
{
|
||||
HFInferenceProvider.CEREBRAS => "cerebras/v1/",
|
||||
HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius/v1/",
|
||||
HFInferenceProvider.SAMBANOVA => "sambanova/v1/",
|
||||
HFInferenceProvider.NOVITA => "novita/v3/openai/",
|
||||
HFInferenceProvider.HYPERBOLIC => "hyperbolic/v1/",
|
||||
HFInferenceProvider.TOGETHER_AI => "together/v1/",
|
||||
HFInferenceProvider.FIREWORKS => "fireworks-ai/inference/v1/",
|
||||
HFInferenceProvider.HF_INFERENCE_API => $"hf-inference/models/{model.ToString()}/v1/",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string EndpointsId(this HFInferenceProvider provider) => provider switch
|
||||
{
|
||||
HFInferenceProvider.CEREBRAS => "cerebras",
|
||||
HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius",
|
||||
HFInferenceProvider.SAMBANOVA => "sambanova",
|
||||
HFInferenceProvider.NOVITA => "novita",
|
||||
HFInferenceProvider.HYPERBOLIC => "hyperbolic",
|
||||
HFInferenceProvider.TOGETHER_AI => "together",
|
||||
HFInferenceProvider.FIREWORKS => "fireworks",
|
||||
HFInferenceProvider.HF_INFERENCE_API => "hf-inference",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string ToName(this HFInferenceProvider provider) => provider switch
|
||||
{
|
||||
HFInferenceProvider.CEREBRAS => "Cerebras",
|
||||
HFInferenceProvider.NEBIUS_AI_STUDIO => "Nebius AI Studio",
|
||||
HFInferenceProvider.SAMBANOVA => "Sambanova",
|
||||
HFInferenceProvider.NOVITA => "Novita",
|
||||
HFInferenceProvider.HYPERBOLIC => "Hyperbolic",
|
||||
HFInferenceProvider.TOGETHER_AI => "Together AI",
|
||||
HFInferenceProvider.FIREWORKS => "Fireworks AI",
|
||||
HFInferenceProvider.HF_INFERENCE_API => "Hugging Face Inference API",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.HuggingFace;
|
||||
|
||||
public sealed class ProviderHuggingFace : BaseProvider
|
||||
{
|
||||
public ProviderHuggingFace(ILogger logger, HFInferenceProvider hfProvider, Model model) : base($"https://router.huggingface.co/{hfProvider.Endpoints(model)}", logger)
|
||||
{
|
||||
logger.LogInformation($"We use the inferende provider '{hfProvider}'. Thus we use the base URL 'https://router.huggingface.co/{hfProvider.Endpoints(model)}'.");
|
||||
}
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.HUGGINGFACE.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "HuggingFace";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the HuggingFace HTTP chat request:
|
||||
var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(huggingfaceChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("HuggingFace", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -13,9 +13,11 @@ public enum LLMProviders
|
||||
GOOGLE = 7,
|
||||
X = 8,
|
||||
DEEP_SEEK = 11,
|
||||
ALIBABA_CLOUD = 12,
|
||||
|
||||
FIREWORKS = 5,
|
||||
GROQ = 6,
|
||||
HUGGINGFACE = 13,
|
||||
|
||||
SELF_HOSTED = 4,
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using AIStudio.Provider.AlibabaCloud;
|
||||
using AIStudio.Provider.Anthropic;
|
||||
using AIStudio.Provider.DeepSeek;
|
||||
using AIStudio.Provider.Fireworks;
|
||||
@@ -5,6 +6,7 @@ using AIStudio.Provider.Google;
|
||||
using AIStudio.Provider.Groq;
|
||||
using AIStudio.Provider.GWDG;
|
||||
using AIStudio.Provider.Helmholtz;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Provider.Mistral;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
@@ -32,9 +34,11 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => "Google",
|
||||
LLMProviders.X => "xAI",
|
||||
LLMProviders.DEEP_SEEK => "DeepSeek",
|
||||
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
|
||||
|
||||
LLMProviders.GROQ => "Groq",
|
||||
LLMProviders.FIREWORKS => "Fireworks.ai",
|
||||
LLMProviders.HUGGINGFACE => "Hugging Face",
|
||||
|
||||
LLMProviders.SELF_HOSTED => "Self-hosted",
|
||||
|
||||
@@ -54,7 +58,10 @@ public static class LLMProvidersExtensions
|
||||
{
|
||||
LLMProviders.NONE => Confidence.NONE,
|
||||
|
||||
LLMProviders.FIREWORKS => Confidence.USA_NOT_TRUSTED.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
LLMProviders.FIREWORKS => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
// Not trusted, because huggingface only routes you to a third-party-provider and we can't make sure they do not use your data
|
||||
LLMProviders.HUGGINGFACE => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://huggingface.co/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources(
|
||||
"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint",
|
||||
@@ -75,6 +82,8 @@ public static class LLMProvidersExtensions
|
||||
|
||||
LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
@@ -97,6 +106,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
LLMProviders.ALIBABA_CLOUD => true,
|
||||
|
||||
//
|
||||
// Providers that do not support embeddings:
|
||||
@@ -107,6 +117,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.X => false,
|
||||
LLMProviders.GWDG => false,
|
||||
LLMProviders.DEEP_SEEK => false,
|
||||
LLMProviders.HUGGINGFACE => false,
|
||||
|
||||
//
|
||||
// Self-hosted providers are treated as a special case anyway.
|
||||
@@ -124,7 +135,7 @@ public static class LLMProvidersExtensions
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings, ILogger logger)
|
||||
{
|
||||
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, logger);
|
||||
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider ,logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,10 +146,10 @@ public static class LLMProvidersExtensions
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings, ILogger logger)
|
||||
{
|
||||
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, logger);
|
||||
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE,logger);
|
||||
}
|
||||
|
||||
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, ILogger logger)
|
||||
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider , ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -150,9 +161,11 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
|
||||
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
|
||||
LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName },
|
||||
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud(logger) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
|
||||
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
|
||||
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(logger, inferenceProvider, model) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName },
|
||||
|
||||
@@ -177,9 +190,11 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
|
||||
LLMProviders.X => "https://accounts.x.ai/sign-up",
|
||||
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up",
|
||||
LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm",
|
||||
|
||||
LLMProviders.GROQ => "https://console.groq.com/",
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
|
||||
LLMProviders.HUGGINGFACE => "https://huggingface.co/login",
|
||||
|
||||
LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab",
|
||||
LLMProviders.GWDG => "https://docs.hpc.gwdg.de/services/saia/index.html#api-request",
|
||||
@@ -197,6 +212,8 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
|
||||
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage",
|
||||
LLMProviders.ALIBABA_CLOUD => "https://usercenter2-intl.aliyun.com/billing",
|
||||
LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
@@ -211,19 +228,23 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
LLMProviders.ALIBABA_CLOUD => true,
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public static string GetModelsOverviewURL(this LLMProviders provider) => provider switch
|
||||
public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch
|
||||
{
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless",
|
||||
LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.EndpointsId()}",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch
|
||||
{
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -253,11 +274,13 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
LLMProviders.ALIBABA_CLOUD => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
LLMProviders.GWDG => true,
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
|
||||
LLMProviders.SELF_HOSTED => host is Host.OLLAMA,
|
||||
|
||||
@@ -272,11 +295,13 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
LLMProviders.ALIBABA_CLOUD => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
LLMProviders.GWDG => true,
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
@@ -306,4 +331,10 @@ public static class LLMProvidersExtensions
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsHFInstanceProviderNeeded(this LLMProviders provider) => provider switch
|
||||
{
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(["gpt-", "o1-"], token, apiKeyProvisional);
|
||||
return this.LoadModels(["gpt-", "o1-", "o3-", "o4-"], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -6,6 +6,7 @@ using AIStudio.Assistants.TextSummarizer;
|
||||
using AIStudio.Assistants.EMail;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles;
|
||||
using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles;
|
||||
@@ -25,6 +26,21 @@ public readonly record struct ConfigurationSelectData<T>(string Name, T Value);
|
||||
/// </summary>
|
||||
public static class ConfigurationSelectDataFactory
|
||||
{
|
||||
public static IEnumerable<ConfigurationSelectData<LangBehavior>> GetLangBehaviorData()
|
||||
{
|
||||
foreach (var behavior in Enum.GetValues<LangBehavior>())
|
||||
yield return new(behavior.Name(), behavior);
|
||||
}
|
||||
|
||||
public static IEnumerable<ConfigurationSelectData<Guid>> GetLanguagesData()
|
||||
{
|
||||
foreach (var runningPlugin in PluginFactory.RunningPlugins)
|
||||
{
|
||||
if(runningPlugin is ILanguagePlugin languagePlugin)
|
||||
yield return new(languagePlugin.LangName, runningPlugin.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ConfigurationSelectData<LoadingChatProviderBehavior>> GetLoadingChatProviderBehavior()
|
||||
{
|
||||
yield return new("When possible, use the LLM provider which was used for each chat in the first place", LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE);
|
||||
|
||||
@@ -2,6 +2,16 @@ namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public sealed class DataApp
|
||||
{
|
||||
/// <summary>
|
||||
/// The language behavior.
|
||||
/// </summary>
|
||||
public LangBehavior LanguageBehavior { get; set; } = LangBehavior.AUTO;
|
||||
|
||||
/// <summary>
|
||||
/// The language plugin ID to use.
|
||||
/// </summary>
|
||||
public Guid LanguagePluginId { get; set; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The preferred theme to use.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public enum LangBehavior
|
||||
{
|
||||
AUTO,
|
||||
MANUAL,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public static class LangBehaviorExtensions
|
||||
{
|
||||
public static string Name(this LangBehavior langBehavior) => langBehavior switch
|
||||
{
|
||||
LangBehavior.AUTO => "Choose the language automatically, based on your system language.",
|
||||
LangBehavior.MANUAL => "Choose the language manually.",
|
||||
|
||||
_ => "Unknown option"
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
@@ -24,7 +24,8 @@ public readonly record struct Provider(
|
||||
Model Model,
|
||||
bool IsSelfHosted = false,
|
||||
string Hostname = "http://localhost:1234",
|
||||
Host Host = Host.NONE) : ISecretId
|
||||
Host Host = Host.NONE,
|
||||
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId
|
||||
{
|
||||
#region Overrides of ValueType
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Json.Serialization;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||
|
||||
@@ -13,17 +14,18 @@ namespace AIStudio.Settings;
|
||||
/// <summary>
|
||||
/// The settings manager.
|
||||
/// </summary>
|
||||
public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
public sealed class SettingsManager(ILogger<SettingsManager> logger, RustService rustService)
|
||||
{
|
||||
private const string SETTINGS_FILENAME = "settings.json";
|
||||
|
||||
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
Converters = { new TolerantEnumConverter() },
|
||||
};
|
||||
|
||||
private readonly ILogger<SettingsManager> logger = logger;
|
||||
private readonly RustService rustService = rustService;
|
||||
|
||||
/// <summary>
|
||||
/// The directory where the configuration files are stored.
|
||||
@@ -143,8 +145,53 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return minimumLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given plugin is enabled.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin to check.</param>
|
||||
/// <returns>True, when the plugin is enabled, false otherwise.</returns>
|
||||
public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the active language plugin.
|
||||
/// </summary>
|
||||
/// <returns>The active language plugin.</returns>
|
||||
public async Task<ILanguagePlugin> GetActiveLanguagePlugin()
|
||||
{
|
||||
switch (this.ConfigurationData.App.LanguageBehavior)
|
||||
{
|
||||
case LangBehavior.AUTO:
|
||||
var languageCode = await this.rustService.ReadUserLanguage();
|
||||
var languagePlugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x is ILanguagePlugin langPlug && langPlug.IETFTag == languageCode);
|
||||
if (languagePlugin is null)
|
||||
return PluginFactory.BaseLanguage;
|
||||
|
||||
if (languagePlugin is ILanguagePlugin langPlugin)
|
||||
return langPlugin;
|
||||
|
||||
this.logger.LogError("The language plugin is not a language plugin.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
|
||||
case LangBehavior.MANUAL:
|
||||
var pluginId = this.ConfigurationData.App.LanguagePluginId;
|
||||
var plugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x.Id == pluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
this.logger.LogWarning($"The chosen language plugin (id='{pluginId}') is not available.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
if (plugin is ILanguagePlugin chosenLangPlugin)
|
||||
return chosenLangPlugin;
|
||||
|
||||
this.logger.LogError("The chosen language plugin is not a language plugin.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
this.logger.LogError("The language behavior is unknown.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert a JSON string to an enum value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the target enum value does not exist, the value will be the default value.
|
||||
/// This converter handles enum values as property names and values.
|
||||
/// </remarks>
|
||||
public sealed class TolerantEnumConverter : JsonConverter<object>
|
||||
{
|
||||
private static readonly ILogger<TolerantEnumConverter> LOG = Program.LOGGER_FACTORY.CreateLogger<TolerantEnumConverter>();
|
||||
|
||||
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
|
||||
|
||||
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// Is this token a string?
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
// Try to use that string as the name of the enum value:
|
||||
if (Enum.TryParse(typeToConvert, reader.GetString(), out var result))
|
||||
return result;
|
||||
|
||||
// In any other case, we will return the default enum value:
|
||||
LOG.LogWarning($"Cannot read '{reader.GetString()}' as '{typeToConvert.Name}' enum; token type: {reader.TokenType}");
|
||||
return Activator.CreateInstance(typeToConvert);
|
||||
}
|
||||
|
||||
public override object ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// Is this token a property name?
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
// Try to use that property name as the name of the enum value:
|
||||
if (Enum.TryParse(typeToConvert, reader.GetString(), out var result))
|
||||
return result;
|
||||
|
||||
// In any other case, we will return the default enum value:
|
||||
LOG.LogWarning($"Cannot read '{reader.GetString()}' as '{typeToConvert.Name}' enum; token type: {reader.TokenType}");
|
||||
return Activator.CreateInstance(typeToConvert)!;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
|
||||
public override void WriteAsPropertyName(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WritePropertyName(value.ToString()!);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public enum Event
|
||||
STATE_HAS_CHANGED,
|
||||
CONFIGURATION_CHANGED,
|
||||
COLOR_THEME_CHANGED,
|
||||
STARTUP_PLUGIN_SYSTEM,
|
||||
PLUGINS_RELOADED,
|
||||
SHOW_ERROR,
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
namespace AIStudio.Tools.Metadata;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class MetaDataArchitecture(string architecture) : Attribute
|
||||
{
|
||||
public string Architecture => architecture;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
namespace AIStudio.Tools.Metadata;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class MetaDataAttribute(
|
||||
string version,
|
||||
string buildTime,
|
||||
uint buildNum,
|
||||
string dotnetSdkVersion,
|
||||
string dotnetVersion,
|
||||
string rustVersion,
|
||||
string mudBlazorVersion,
|
||||
string tauriVersion,
|
||||
string appCommitHash
|
||||
) : Attribute
|
||||
{
|
||||
public string BuildTime => buildTime;
|
||||
|
||||
public string Version => version;
|
||||
|
||||
public uint BuildNum => buildNum;
|
||||
|
||||
public string DotnetVersion => dotnetVersion;
|
||||
|
||||
public string DotnetSdkVersion => dotnetSdkVersion;
|
||||
|
||||
public string RustVersion => rustVersion;
|
||||
|
||||
public string MudBlazorVersion => mudBlazorVersion;
|
||||
|
||||
public string TauriVersion => tauriVersion;
|
||||
|
||||
public string AppCommitHash => appCommitHash;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public interface IAvailablePlugin : IPluginMetadata
|
||||
{
|
||||
public string LocalPath { get; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a contract to access text from a language plugin.
|
||||
/// </summary>
|
||||
public interface ILang
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The given fallback text is used to determine the key for
|
||||
/// the language plugin. Base for the key is the namespace of
|
||||
/// the using component and the fallback text in English (US).
|
||||
/// The given text getting hashed. When the key does not exist,
|
||||
/// the fallback text will be returned.
|
||||
/// </remarks>
|
||||
/// <param name="fallbackEN">The fallback text in English (US).</param>
|
||||
/// <returns>The text from the language plugin or the fallback text.</returns>
|
||||
public string T(string fallbackEN);
|
||||
}
|
||||
@@ -16,6 +16,17 @@ public interface ILanguagePlugin
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <param name="logWarning">When true, a warning will be logged if the key does not exist.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value);
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IETF tag of the language plugin.
|
||||
/// </summary>
|
||||
public string IETFTag { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the language.
|
||||
/// </summary>
|
||||
public string LangName { get; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class NoPluginLanguage : PluginBase, ILanguagePlugin
|
||||
{
|
||||
public static readonly NoPluginLanguage INSTANCE = new();
|
||||
|
||||
private NoPluginLanguage() : base(true, LuaState.Create(), PluginType.LANGUAGE, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
#region Implementation of ILanguagePlugin
|
||||
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false)
|
||||
{
|
||||
value = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public string IETFTag => string.Empty;
|
||||
|
||||
public string LangName => string.Empty;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -4,6 +4,12 @@ public static partial class PluginFactory
|
||||
{
|
||||
public static void SetUpHotReloading()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'.");
|
||||
try
|
||||
{
|
||||
|
||||
@@ -10,6 +10,12 @@ public static partial class PluginFactory
|
||||
{
|
||||
public static async Task EnsureInternalPlugins()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation("Start ensuring internal plugins.");
|
||||
foreach (var plugin in Enum.GetValues<InternalPlugin>())
|
||||
{
|
||||
@@ -40,15 +46,15 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
// Ensure that the additional resources exist:
|
||||
foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
|
||||
foreach (var contentFilePath in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
|
||||
{
|
||||
if(content.IsDirectory)
|
||||
if(contentFilePath.IsDirectory)
|
||||
{
|
||||
LOG.LogError("The plugin contains a directory. This is not allowed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
await CopyInternalPluginFile(content, metaData);
|
||||
await CopyInternalPluginFile(contentFilePath, metaData);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -57,9 +63,9 @@ public static partial class PluginFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
|
||||
private static async Task CopyInternalPluginFile(IFileInfo resourceFilePath, InternalPluginData metaData)
|
||||
{
|
||||
await using var inputStream = resourceInfo.CreateReadStream();
|
||||
await using var inputStream = resourceFilePath.CreateReadStream();
|
||||
|
||||
var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory());
|
||||
|
||||
@@ -73,7 +79,7 @@ public static partial class PluginFactory
|
||||
if (!Directory.Exists(pluginPath))
|
||||
Directory.CreateDirectory(pluginPath);
|
||||
|
||||
var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
|
||||
var pluginFilePath = Path.Join(pluginPath, resourceFilePath.Name);
|
||||
|
||||
await using var outputStream = File.Create(pluginFilePath);
|
||||
await inputStream.CopyToAsync(outputStream);
|
||||
|
||||
@@ -9,29 +9,45 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory");
|
||||
|
||||
private static readonly string DATA_DIR = SettingsManager.DataDirectory!;
|
||||
|
||||
private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
|
||||
private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory));
|
||||
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
private static readonly List<IAvailablePlugin> AVAILABLE_PLUGINS = [];
|
||||
private static readonly List<PluginBase> RUNNING_PLUGINS = [];
|
||||
|
||||
private static readonly FileSystemWatcher HOT_RELOAD_WATCHER;
|
||||
|
||||
private static readonly List<IPluginMetadata> AVAILABLE_PLUGINS = [];
|
||||
private static bool IS_INITIALIZED;
|
||||
private static string DATA_DIR = string.Empty;
|
||||
private static string PLUGINS_ROOT = string.Empty;
|
||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all available plugins.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<IPluginMetadata> AvailablePlugins => AVAILABLE_PLUGINS;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all running plugins.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<PluginBase> RunningPlugins => RUNNING_PLUGINS;
|
||||
|
||||
static PluginFactory()
|
||||
public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN;
|
||||
|
||||
/// <summary>
|
||||
/// Set up the plugin factory. We will read the data directory from the settings manager.
|
||||
/// Afterward, we will create the plugins directory and the internal plugin directory.
|
||||
/// </summary>
|
||||
public static void Setup()
|
||||
{
|
||||
DATA_DIR = SettingsManager.DataDirectory!;
|
||||
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||
|
||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||
IS_INITIALIZED = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,6 +64,12 @@ public static partial class PluginFactory
|
||||
/// </remarks>
|
||||
public static async Task LoadAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation("Start loading plugins.");
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
{
|
||||
@@ -96,8 +118,11 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin));
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath));
|
||||
}
|
||||
|
||||
// Start or restart all plugins:
|
||||
await RestartAllPlugins(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Load(string pluginPath, string code, CancellationToken cancellationToken = default)
|
||||
@@ -148,9 +173,100 @@ public static partial class PluginFactory
|
||||
_ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task RestartAllPlugins(CancellationToken cancellationToken = default)
|
||||
{
|
||||
LOG.LogInformation("Try to start or restart all plugins.");
|
||||
RUNNING_PLUGINS.Clear();
|
||||
|
||||
//
|
||||
// Get the base language plugin. This is the plugin that will be used to fill in missing keys.
|
||||
//
|
||||
var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id;
|
||||
var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId);
|
||||
if (baseLanguagePluginMetaData is null)
|
||||
{
|
||||
LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation.");
|
||||
return;
|
||||
}
|
||||
|
||||
var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken);
|
||||
if (startedBasePlugin is NoPlugin noPlugin)
|
||||
{
|
||||
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startedBasePlugin is PluginLanguage languagePlugin)
|
||||
{
|
||||
BASE_LANGUAGE_PLUGIN = languagePlugin;
|
||||
RUNNING_PLUGINS.Add(languagePlugin);
|
||||
LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Iterate over all available plugins and try to start them.
|
||||
//
|
||||
foreach (var availablePlugin in AVAILABLE_PLUGINS)
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (availablePlugin.Id == baseLanguagePluginId)
|
||||
continue;
|
||||
|
||||
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin))
|
||||
if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin)
|
||||
RUNNING_PLUGINS.Add(plugin);
|
||||
|
||||
// Inform all components that the plugins have been reloaded or started:
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua");
|
||||
if(!File.Exists(pluginMainFile))
|
||||
{
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: The plugin file does not exist.");
|
||||
return new NoPlugin($"The plugin file does not exist: {pluginMainFile}");
|
||||
}
|
||||
|
||||
var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
|
||||
var plugin = await Load(meta.LocalPath, code, cancellationToken);
|
||||
if (plugin is NoPlugin noPlugin)
|
||||
{
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {noPlugin.Issues.First()}");
|
||||
return noPlugin;
|
||||
}
|
||||
|
||||
if (plugin.IsValid)
|
||||
{
|
||||
//
|
||||
// When this is a language plugin, we need to set the base language plugin.
|
||||
//
|
||||
if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE)
|
||||
languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN);
|
||||
|
||||
LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'");
|
||||
return plugin;
|
||||
}
|
||||
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}");
|
||||
return new NoPlugin($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}");
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
if(!IS_INITIALIZED)
|
||||
return;
|
||||
|
||||
HOT_RELOAD_WATCHER.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,12 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
{
|
||||
private static readonly ILogger<PluginLanguage> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginLanguage>();
|
||||
|
||||
private readonly Dictionary<string, string> content = [];
|
||||
private readonly List<ILanguagePlugin> otherLanguagePlugins = [];
|
||||
private readonly string langCultureTag;
|
||||
private readonly string langName;
|
||||
|
||||
private ILanguagePlugin? baseLanguage;
|
||||
|
||||
@@ -15,6 +18,9 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
if(!this.TryInitIETFTag(out var issue, out this.langCultureTag))
|
||||
this.pluginIssues.Add(issue);
|
||||
|
||||
if(!this.TryInitLangName(out issue, out this.langName))
|
||||
this.pluginIssues.Add(issue);
|
||||
|
||||
if (this.TryInitUITextContent(out issue, out var readContent))
|
||||
this.content = readContent;
|
||||
else
|
||||
@@ -36,39 +42,6 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
/// </remarks>
|
||||
/// <param name="languagePlugin">The language plugin to add.</param>
|
||||
public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key neither in the base language nor in this language exist,
|
||||
/// the value will be an empty string. Please note that the key is case-sensitive.
|
||||
/// Furthermore, the keys are in the format "root::key". That means that
|
||||
/// the keys are hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value)
|
||||
{
|
||||
// First, we check if the key is part of the main language pack:
|
||||
if (this.content.TryGetValue(key, out value!))
|
||||
return true;
|
||||
|
||||
// Second, we check if the key is part of the other language packs, such as the assistant plugins:
|
||||
foreach (var otherLanguagePlugin in this.otherLanguagePlugins)
|
||||
if(otherLanguagePlugin.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
// Finally, we check if the key is part of the base language pack. This is the case,
|
||||
// when a language plugin does not cover all keys. In this case, the base language plugin
|
||||
// will be used to fill in the missing keys:
|
||||
if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the IETF tag.
|
||||
@@ -127,4 +100,71 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryInitLangName(out string message, out string readLangName)
|
||||
{
|
||||
if (!this.state.Environment["LANG_NAME"].TryRead(out readLangName))
|
||||
{
|
||||
message = "The field LANG_NAME does not exist or is not a valid string.";
|
||||
readLangName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(readLangName))
|
||||
{
|
||||
message = "The field LANG_NAME is empty. Use a valid language name.";
|
||||
readLangName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Implementation of ILanguagePlugin
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key neither in the base language nor in this language exist,
|
||||
/// the value will be an empty string. Please note that the key is case-sensitive.
|
||||
/// Furthermore, the keys are in the format "root::key". That means that
|
||||
/// the keys are hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <param name="logWarning">When true, a warning will be logged if the key does not exist.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false)
|
||||
{
|
||||
// First, we check if the key is part of the main language pack:
|
||||
if (this.content.TryGetValue(key, out value!))
|
||||
return true;
|
||||
|
||||
// Second, we check if the key is part of the other language packs, such as the assistant plugins:
|
||||
foreach (var otherLanguagePlugin in this.otherLanguagePlugins)
|
||||
if(otherLanguagePlugin.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
// Finally, we check if the key is part of the base language pack. This is the case,
|
||||
// when a language plugin does not cover all keys. In this case, the base language plugin
|
||||
// will be used to fill in the missing keys:
|
||||
if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
if(logWarning)
|
||||
LOGGER.LogWarning($"Missing translation key '{key}'.");
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IETFTag => this.langCultureTag;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string LangName => this.langName;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
|
||||
public sealed class PluginMetadata(PluginBase plugin, string localPath) : IAvailablePlugin
|
||||
{
|
||||
#region Implementation of IPluginMetadata
|
||||
|
||||
@@ -47,4 +47,10 @@ public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
|
||||
public bool IsInternal { get; } = plugin.IsInternal;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IAvailablePlugin
|
||||
|
||||
public string LocalPath { get; } = localPath;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using AIStudio.Provider;
|
||||
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
|
||||
namespace AIStudio.Tools.Validation;
|
||||
@@ -93,4 +93,15 @@ public sealed class ProviderValidation
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingHFInstanceProvider(HFInferenceProvider inferenceProvider)
|
||||
{
|
||||
if(this.GetProvider() is not LLMProviders.HUGGINGFACE)
|
||||
return null;
|
||||
|
||||
if (inferenceProvider is HFInferenceProvider.NONE)
|
||||
return "Please select an Hugging Face inference provider.";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
#!/usr/bin/env nu
|
||||
|
||||
def main [] {}
|
||||
|
||||
def are_assets_exist [rid: string] {
|
||||
$"bin/release/net9.0/($rid)/publish/wwwroot/_content/MudBlazor/MudBlazor.min.css" | path exists
|
||||
}
|
||||
|
||||
def "main help" [] {
|
||||
print "Usage: nu build.nu [action]"
|
||||
print ""
|
||||
print "Optional Actions:"
|
||||
print "-----------------"
|
||||
print " fix_web_assets Prepare the web assets; run this once for each release on one platform; changes will be committed."
|
||||
print ""
|
||||
print " metadata Update the metadata file; run this on every platform right before the release; changes will be"
|
||||
print " committed once; there should be no differences between the platforms."
|
||||
print ""
|
||||
print "Actions:"
|
||||
print "---------"
|
||||
print " prepare [action] Prepare the project for a release; increases the version & build numbers, updates the build time,"
|
||||
print " and runs fix_web_assets; run this once for each release on one platform; changes will be committed."
|
||||
print " The action can be 'major', 'minor', or 'patch'. The version will be updated accordingly."
|
||||
print ""
|
||||
print " publish Publish the project for all supported RIDs; run this on every platform."
|
||||
print ""
|
||||
}
|
||||
|
||||
def "main prepare" [action: string] {
|
||||
if (update_app_version $action) {
|
||||
main fix_web_assets
|
||||
inc_build_number
|
||||
update_build_time
|
||||
update_changelog
|
||||
main metadata
|
||||
}
|
||||
}
|
||||
|
||||
def "main metadata" [] {
|
||||
update_dotnet_version
|
||||
update_rust_version
|
||||
update_mudblazor_version
|
||||
update_tauri_version
|
||||
update_project_commit_hash
|
||||
update_license_year "../../LICENSE.md"
|
||||
update_license_year "Pages/About.razor.cs"
|
||||
}
|
||||
|
||||
def "main fix_web_assets" [] {
|
||||
|
||||
# Get the matching RIDs for the current OS:
|
||||
let rids = get_rids
|
||||
|
||||
# We chose the first RID to copy the assets from:
|
||||
let rid = $rids.0
|
||||
|
||||
if (are_assets_exist $rid) == false {
|
||||
print $"Web assets do not exist for ($rid). Please build the project first."
|
||||
return
|
||||
}
|
||||
|
||||
# Ensure, that the dist directory exists:
|
||||
mkdir wwwroot/system
|
||||
|
||||
# Copy the web assets from the first RID to the source project:
|
||||
let source_paths = glob --depth 99 bin/release/net9.0/($rid)/publish/wwwroot/_content/*
|
||||
|
||||
for source_path in $source_paths {
|
||||
cp --recursive --force --update $source_path wwwroot/system/
|
||||
}
|
||||
}
|
||||
|
||||
def "main publish" [] {
|
||||
|
||||
main metadata
|
||||
|
||||
# Ensure, that the dist directory exists:
|
||||
mkdir bin/dist
|
||||
|
||||
# Get the matching RIDs for the current OS:
|
||||
let rids = get_rids
|
||||
|
||||
if ($rids | length) == 0 {
|
||||
print "No RIDs to build for."
|
||||
return
|
||||
}
|
||||
|
||||
let current_os = get_os
|
||||
let published_filename_dotnet = match $current_os {
|
||||
"windows" => "mindworkAIStudio.exe",
|
||||
_ => "mindworkAIStudio"
|
||||
}
|
||||
|
||||
# Build for each RID:
|
||||
for rid in $rids {
|
||||
print "=============================="
|
||||
print $"Start building for ($rid)..."
|
||||
|
||||
^dotnet publish --configuration release --runtime $rid --disable-build-servers --force
|
||||
|
||||
let final_filename = match $rid {
|
||||
"win-x64" => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
|
||||
"win-arm64" => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
|
||||
"linux-x64" => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
|
||||
"linux-arm64" => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
|
||||
"osx-arm64" => "mindworkAIStudioServer-aarch64-apple-darwin",
|
||||
"osx-x64" => "mindworkAIStudioServer-x86_64-apple-darwin",
|
||||
|
||||
_ => {
|
||||
print $"Unsupported RID for final filename: ($rid)"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let published_path = $"bin/release/net9.0/($rid)/publish/($published_filename_dotnet)"
|
||||
let final_path = $"bin/dist/($final_filename)"
|
||||
|
||||
if ($published_path | path exists) {
|
||||
print $"Published file ($published_path) exists."
|
||||
} else {
|
||||
print $"Published file ($published_path) does not exist. Compiling might failed?"
|
||||
return
|
||||
}
|
||||
|
||||
print $"Moving ($published_path) to ($final_path)..."
|
||||
mv --force $published_path $final_path
|
||||
}
|
||||
|
||||
print "=============================="
|
||||
print "Start building runtime..."
|
||||
|
||||
cd ../../runtime
|
||||
try {
|
||||
cargo tauri build --bundles none
|
||||
};
|
||||
|
||||
cd "../app/MindWork AI Studio"
|
||||
print "=============================="
|
||||
print "Building done."
|
||||
}
|
||||
|
||||
def get_rids [] {
|
||||
# Define the list of RIDs to build for, cf. https://learn.microsoft.com/en-us/dotnet/core/rid-catalog:
|
||||
let rids = ["win-x64", "win-arm64", "linux-x64", "linux-arm64", "osx-arm64", "osx-x64"]
|
||||
|
||||
# Get the current OS:
|
||||
let current_os = get_os
|
||||
let current_os_dotnet = match $current_os {
|
||||
"windows" => "win-",
|
||||
"linux" => "linux-",
|
||||
"darwin" => "osx-",
|
||||
|
||||
_ => {
|
||||
print $"Unsupported OS: ($current_os)"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Filter the RIDs to build for the current OS:
|
||||
let rids = $rids | where $it =~ $current_os_dotnet
|
||||
|
||||
# Return the list of RIDs to build for:
|
||||
$rids
|
||||
}
|
||||
|
||||
def get_os [] {
|
||||
let os = (sys host).name | str downcase
|
||||
if $os =~ "linux" {
|
||||
return "linux"
|
||||
}
|
||||
$os
|
||||
}
|
||||
|
||||
def update_build_time [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut build_time = $meta_lines.1
|
||||
|
||||
let updated_build_time = (date now | date to-timezone UTC | format date "%Y-%m-%d %H:%M:%S")
|
||||
print $"Updated build time from ($build_time) to ($updated_build_time) UTC."
|
||||
|
||||
$build_time = $"($updated_build_time) UTC"
|
||||
$meta_lines.1 = $build_time
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def inc_build_number [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut build_number = $meta_lines.2 | into int
|
||||
|
||||
let updated_build_number = ([$build_number, 1] | math sum)
|
||||
print $"Incremented build number from ($build_number) to ($updated_build_number)."
|
||||
|
||||
$build_number = $updated_build_number
|
||||
$meta_lines.2 = ($build_number | into string)
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_dotnet_version [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut dotnet_sdk_version = $meta_lines.3
|
||||
mut dotnet_version = $meta_lines.4
|
||||
|
||||
let dotnet_data = (^dotnet --info) | collect | parse --regex '(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?P<sdkVersion>[0-9.]+).+Commit:\s+(?P<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?P<hostVersion>[0-9.]+).+Commit:\s+(?P<hostCommit>[a-zA-Z0-9]+)'
|
||||
let sdk_version = $dotnet_data.sdkVersion.0
|
||||
let host_version = $dotnet_data.hostVersion.0
|
||||
let sdkCommit = $dotnet_data.sdkCommit.0
|
||||
let hostCommit = $dotnet_data.hostCommit.0
|
||||
|
||||
print $"Updated .NET SDK version from ($dotnet_sdk_version) to ($sdk_version) \(commit ($sdkCommit)\)."
|
||||
$meta_lines.3 = $"($sdk_version) \(commit ($sdkCommit)\)"
|
||||
|
||||
print $"Updated .NET version from ($dotnet_version) to ($host_version) \(commit ($hostCommit)\)."
|
||||
$meta_lines.4 = $"($host_version) \(commit ($hostCommit)\)"
|
||||
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_rust_version [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut rust_version = $meta_lines.5
|
||||
|
||||
let rust_data = (^rustc -Vv) | parse --regex 'rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)'
|
||||
let version = $rust_data.version.0
|
||||
let commit = $rust_data.commit.0
|
||||
|
||||
print $"Updated Rust version from ($rust_version) to ($version) \(commit ($commit)\)."
|
||||
$meta_lines.5 = $"($version) \(commit ($commit)\)"
|
||||
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_mudblazor_version [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut mudblazor_version = $meta_lines.6
|
||||
|
||||
let mudblazor_data = (^dotnet list package) | parse --regex 'MudBlazor\s+(?<version>[0-9.]+)'
|
||||
let version = $mudblazor_data.version.0
|
||||
|
||||
print $"Updated MudBlazor version from ($mudblazor_version) to ($version)."
|
||||
$meta_lines.6 = $version
|
||||
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_tauri_version [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut tauri_version = $meta_lines.7
|
||||
|
||||
cd ../../runtime
|
||||
let tauri_data = (^cargo tree --depth 1) | parse --regex 'tauri\s+v(?<version>[0-9.]+)'
|
||||
let version = $tauri_data.version.0
|
||||
cd "../app/MindWork AI Studio"
|
||||
|
||||
print $"Updated Tauri version from ($tauri_version) to ($version)."
|
||||
$meta_lines.7 = $version
|
||||
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_license_year [licence_file: string] {
|
||||
let current_year = (date now | date to-timezone UTC | format date "%Y")
|
||||
let license_text = open --raw $licence_file | lines
|
||||
print $"Updating the license's year in ($licence_file) to ($current_year)."
|
||||
|
||||
# Target line looks like `Copyright 2024 Thorsten Sommer`.
|
||||
# Perhaps, there are whitespaces at the beginning. Using
|
||||
# a regex to match the year.
|
||||
let updated_license_text = $license_text | each { |it|
|
||||
if $it =~ '^\s*Copyright\s+[0-9]{4}' {
|
||||
$it | str replace --regex '([0-9]{4})' $"($current_year)"
|
||||
} else {
|
||||
$it
|
||||
}
|
||||
}
|
||||
|
||||
$updated_license_text | save --raw --force $licence_file
|
||||
}
|
||||
|
||||
def update_app_version [action: string] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut app_version = $meta_lines.0
|
||||
|
||||
let version_data = $app_version | parse --regex '(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)'
|
||||
|
||||
if $action == "major" {
|
||||
|
||||
mut major = $version_data.major | into int
|
||||
$major = ([$major.0, 1] | math sum)
|
||||
|
||||
let updated_version = [$major, 0, 0] | str join "."
|
||||
print $"Updated app version from ($app_version) to ($updated_version)."
|
||||
$meta_lines.0 = $updated_version
|
||||
|
||||
} else if $action == "minor" {
|
||||
|
||||
let major = $version_data.major | into int
|
||||
mut minor = $version_data.minor | into int
|
||||
$minor = ([$minor.0, 1] | math sum)
|
||||
|
||||
let updated_version = [$major.0, $minor, 0] | str join "."
|
||||
print $"Updated app version from ($app_version) to ($updated_version)."
|
||||
$meta_lines.0 = $updated_version
|
||||
|
||||
} else if $action == "patch" {
|
||||
|
||||
let major = $version_data.major | into int
|
||||
let minor = $version_data.minor | into int
|
||||
mut patch = $version_data.patch | into int
|
||||
$patch = ([$patch.0, 1] | math sum)
|
||||
|
||||
let updated_version = [$major.0, $minor.0, $patch] | str join "."
|
||||
print $"Updated app version from ($app_version) to ($updated_version)."
|
||||
$meta_lines.0 = $updated_version
|
||||
|
||||
} else {
|
||||
print $"Invalid action '($action)'. Please use 'major', 'minor', or 'patch'."
|
||||
return false
|
||||
}
|
||||
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
return true
|
||||
}
|
||||
|
||||
def update_project_commit_hash [] {
|
||||
mut meta_lines = open --raw ../../metadata.txt | lines
|
||||
mut commit_hash = $meta_lines.8
|
||||
|
||||
# Check, if the work directory is clean. We allow, that the metadata file is dirty:
|
||||
let git_status = (^git status --porcelain) | lines
|
||||
let dirty_files = $git_status | length
|
||||
let first_is_metadata = ($dirty_files > 0 and $git_status.0 =~ '^\sM\s+metadata.txt$')
|
||||
let git_tag_response = ^git describe --tags --exact-match | complete
|
||||
let state = {
|
||||
num_dirty: $dirty_files,
|
||||
first_is_metadata: $first_is_metadata,
|
||||
git_tag_present: ($git_tag_response.exit_code == 0),
|
||||
git_tag: $git_tag_response.stdout
|
||||
}
|
||||
|
||||
let commit_postfix = match $state {
|
||||
{ num_dirty: $num_dirty, first_is_metadata: _, git_tag_present: _, git_tag: _ } if $num_dirty > 1 => ", dev debug",
|
||||
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: _, git_tag: _ } if $num_dirty == 1 => ", dev debug",
|
||||
{ num_dirty: $num_dirty, first_is_metadata: true, git_tag_present: false, git_tag: _ } if $num_dirty == 1 => ", dev testing",
|
||||
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: false, git_tag: _ } if $num_dirty == 0 => ", dev testing",
|
||||
{ num_dirty: $num_dirty, first_is_metadata: true, git_tag_present: true, git_tag: $tag } if $num_dirty == 1 => $", release $tag",
|
||||
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: true, git_tag: $tag } if $num_dirty == 0 => $", release $tag",
|
||||
|
||||
_ => "-dev unknown"
|
||||
}
|
||||
|
||||
# Use the first ten characters of the commit hash:
|
||||
let updated_commit_hash = (^git rev-parse HEAD) | str substring 0..10 | append $commit_postfix | str join
|
||||
print $"Updated commit hash from ($commit_hash) to ($updated_commit_hash)."
|
||||
|
||||
$meta_lines.8 = $updated_commit_hash
|
||||
$meta_lines | save --raw --force ../../metadata.txt
|
||||
}
|
||||
|
||||
def update_changelog [] {
|
||||
# Get all changelog files:
|
||||
let all_changelog_files = glob wwwroot/changelog/*.md
|
||||
|
||||
# Create a table with the build numbers and the corresponding file names:
|
||||
let table = $all_changelog_files | reduce --fold [] { |it, acc|
|
||||
let header_line = open --raw $it | lines | first
|
||||
let file_name = $it | path basename
|
||||
let header_data = $header_line | parse --regex '#\s+(?P<header>(?P<version>v[0-9.]+)[,\s]+build\s+(?P<build_num>[0-9]+)[,\s]+\((?P<build_time>[0-9-?\s:UTC]+)\))'
|
||||
$acc ++ [[build_num, file_name, header]; [$header_data.build_num.0, $file_name, $header_data.header.0]]
|
||||
} | sort-by build_num --natural --reverse
|
||||
|
||||
# Now, we build the necessary C# code:
|
||||
const tab = " "; # using 4 spaces as one tab
|
||||
let code_rows = $table | reduce --fold "" { |it, acc|
|
||||
$acc ++ $"($tab)($tab)new \(($it.build_num), \"($it.header)\", \"($it.file_name)\"\),\n"
|
||||
}
|
||||
|
||||
let code = ($"LOGS = \n($tab)[\n($code_rows)\n($tab)];")
|
||||
|
||||
# Next, update the Changelog.Logs.cs file:
|
||||
let changelog_logs_source_file = open --raw "Components/Changelog.Logs.cs"
|
||||
let result = $changelog_logs_source_file | str replace --regex '(?ms)LOGS =\s+\[[\w\s".,-:()?]+\];' $code
|
||||
|
||||
# Save the updated file:
|
||||
$result | save --raw --force "Components/Changelog.Logs.cs"
|
||||
|
||||
let number_change_logs = $table | length
|
||||
print $"Updated Changelog.Logs.cs with ($number_change_logs) change logs."
|
||||
}
|
||||
@@ -18,9 +18,9 @@
|
||||
},
|
||||
"HtmlAgilityPack": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.12.0, )",
|
||||
"resolved": "1.12.0",
|
||||
"contentHash": "VHtVZmfoYhQyA/POvZRLuTpCz1zhzIDrdYRJIRV73e9wKAzjW71biYNOHOWx8MxEX3TE4TWVfx1QDRoZcj2AWw=="
|
||||
"requested": "[1.12.1, )",
|
||||
"resolved": "1.12.1",
|
||||
"contentHash": "SP6/2Y26CXtxjXn0Wwsom9Ek35SNWKHEu/IWhNEFejBSSVWWXPRSlpqpBSYWv1SQhYFnwMO01xVbEdK3iRR4hg=="
|
||||
},
|
||||
"LuaCSharp": {
|
||||
"type": "Direct",
|
||||
@@ -30,18 +30,18 @@
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Embedded": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "UKfKGlZ7jKfe6v4rLsjnH/mGbD3e4YD9EK+Uobu+KIxwfhZuLLCtXm4CWTOf2s1t+ItmMs0QqbSJAXaMXCxLOw==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "fictUnSF95D+M9iH4X6TYBjud2gbB2r6bcIi0sQknXFc2bHbNucoaK+SzfLCzb47tHSR9a5pm0F1Ioj0PgmFeQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3"
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA=="
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "xUdlUxiFwXhTYhB4VxKg/IA0+jlZXJPo70LYuMryWbJHdonIpZjw+7DO2B0pWwpXIOs6MlH5WVXPEtfrGEcVZA=="
|
||||
},
|
||||
"MudBlazor": {
|
||||
"type": "Direct",
|
||||
@@ -56,12 +56,13 @@
|
||||
},
|
||||
"MudBlazor.Markdown": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0DcXQFEIgKJsaMCDva0Ck3gempoctyc7s8GLK5VagozlZdXql6W4SKX/imM/NfyfV7SxLrUTRJyLJX0Te+02sQ==",
|
||||
"requested": "[8.5.1, )",
|
||||
"resolved": "8.5.1",
|
||||
"contentHash": "UrNr948Nn70CuDoTWaN/HgtaIKO4oKfj7W6Mw8Ei9OG7MCzCwkhUYJLe/CRTKCkt3wINquB/2AAN7ezgEfmbww==",
|
||||
"dependencies": {
|
||||
"Markdig": "0.40.0",
|
||||
"MudBlazor": "8.0.0"
|
||||
"Markdig": "0.41.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.4",
|
||||
"MudBlazor": "8.5.1"
|
||||
}
|
||||
},
|
||||
"ReverseMarkdown": {
|
||||
@@ -85,8 +86,8 @@
|
||||
},
|
||||
"Markdig": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.40.0",
|
||||
"contentHash": "4ve14zs+gt1irldTQE3y5FLAHuzmhW7T99lAAvVipe/q2LWT/nUCO0iICb9TXGvMX6n7Z1OZroFXkdSy91rO8w=="
|
||||
"resolved": "0.41.0",
|
||||
"contentHash": "nEGSjfQ2i+MzJjvCZqoIBqW2x0iBALhhVogud48oPA/39a0n0jOhghdTYdm4xaDFBXmc4MxsVJAP5gtdvADvWQ=="
|
||||
},
|
||||
"Microsoft.AspNetCore.Authorization": {
|
||||
"type": "Transitive",
|
||||
@@ -137,6 +138,26 @@
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "EZnHifamF7IFEIyjAKMtJM3I/94OIe72i3P09v5oL0twmsmfQwal6Ni3m8lbB5mge3jWFhMozeW+rUdRSqnXRQ=="
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.4",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.4",
|
||||
"Microsoft.Extensions.Options": "9.0.4",
|
||||
"Microsoft.Extensions.Primitives": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.1",
|
||||
@@ -147,15 +168,15 @@
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA=="
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "umczZ3+QPpzlrW/lkvy+IB0p52+qZ5w++aqx2lTCMOaPKzwcbVdrJgiQ3ajw5QWBp7gChLUiCYkSlWUpfjv24g==",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.3"
|
||||
"Microsoft.Extensions.Primitives": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Localization": {
|
||||
@@ -176,25 +197,25 @@
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "nggoNKnWcsBIAaOWHA+53XZWrslC7aGeok+aR+epDPRy7HI7GwMnGZE8yEsL2Onw7kMOHVHwKcsDls1INkNUJQ==",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Primitives": "9.0.1"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
|
||||
"Microsoft.Extensions.Primitives": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "yCCJHvBcRyqapMSNzP+kTc57Eaavq2cr5Tmuil6/XVnipQf5xmskxakSQ1enU6S4+fNg3sJ27WcInV64q24JsA=="
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
|
||||
},
|
||||
"Microsoft.JSInterop": {
|
||||
"type": "Transitive",
|
||||
@@ -205,6 +226,9 @@
|
||||
"type": "Transitive",
|
||||
"resolved": "0.16.9",
|
||||
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
|
||||
},
|
||||
"sharedtools": {
|
||||
"type": "Project"
|
||||
}
|
||||
},
|
||||
"net9.0/osx-arm64": {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# v0.9.40, build 215 (2025-04-20 13:30 UTC)
|
||||
- Added support for the announced OpenAI `o4` models.
|
||||
- Added Alibaba Cloud as a new provider. Thanks Peer `peerschuett` for the contribution.
|
||||
- Added the Hugging Face inference provider as an LLM provider to AI Studio. Thanks Peer `peerschuett` for the contribution.
|
||||
- Added the current CPU architecture to the about page. This information helps us identify issues.
|
||||
- Improved the LLM & embedding provider dialogs by hiding not relevant options.
|
||||
- Improved the provider selection by showing the name of the provider in the provider selection instead of its identifier.
|
||||
- Improved the developer experience by adding a tolerant enum converter for better configuration handling.
|
||||
- Fixed an issue where OpenAI `o3` models were not shown in the model selection.
|
||||
- Upgraded to .NET 9.0.4.
|
||||
- Upgraded .NET & Rust dependencies.
|
||||
@@ -0,0 +1 @@
|
||||
# v0.9.41, build 216 (2025-0x-xx xx:xx UTC)
|
||||
@@ -0,0 +1,62 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
namespace SharedTools;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes.
|
||||
/// </summary>
|
||||
public static class FNVHash
|
||||
{
|
||||
private const uint FNV_OFFSET_BASIS_32_BIT = 2_166_136_261;
|
||||
private const ulong FNV_OFFSET_BASIS_64_BIT = 14_695_981_039_346_656_037;
|
||||
|
||||
private const uint FNV_PRIME_32_BIT = 16_777_619;
|
||||
private const ulong FNV_PRIME_64_BIT = 1_099_511_628_211;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 32bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 32bit FNV-1a hash of the string.</returns>
|
||||
public static uint ToFNV32(this string text) => ToFNV32(text.AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 32bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 32bit FNV-1a hash of the string.</returns>
|
||||
public static uint ToFNV32(this ReadOnlySpan<char> text)
|
||||
{
|
||||
var hash = FNV_OFFSET_BASIS_32_BIT;
|
||||
foreach (var c in text)
|
||||
{
|
||||
hash ^= c;
|
||||
hash *= FNV_PRIME_32_BIT;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 64bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 64bit FNV-1a hash of the string.</returns>
|
||||
public static ulong ToFNV64(this string text) => ToFNV64(text.AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 64bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 64bit FNV-1a hash of the string.</returns>
|
||||
public static ulong ToFNV64(this ReadOnlySpan<char> text)
|
||||
{
|
||||
var hash = FNV_OFFSET_BASIS_64_BIT;
|
||||
foreach (var c in text)
|
||||
{
|
||||
hash ^= c;
|
||||
hash *= FNV_PRIME_64_BIT;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SharedTools;
|
||||
|
||||
public enum RID
|
||||
{
|
||||
NONE,
|
||||
|
||||
WIN_X64,
|
||||
WIN_ARM64,
|
||||
|
||||
LINUX_X64,
|
||||
LINUX_ARM64,
|
||||
|
||||
OSX_X64,
|
||||
OSX_ARM64,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace SharedTools;
|
||||
|
||||
public static class RIDExtensions
|
||||
{
|
||||
public static string AsMicrosoftRid(this RID rid) => rid switch
|
||||
{
|
||||
RID.WIN_X64 => "win-x64",
|
||||
RID.WIN_ARM64 => "win-arm64",
|
||||
|
||||
RID.LINUX_X64 => "linux-x64",
|
||||
RID.LINUX_ARM64 => "linux-arm64",
|
||||
|
||||
RID.OSX_X64 => "osx-x64",
|
||||
RID.OSX_ARM64 => "osx-arm64",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string ToUserFriendlyName(this RID rid) => rid switch
|
||||
{
|
||||
RID.WIN_X64 => "Windows x64",
|
||||
RID.WIN_ARM64 => "Windows ARM64",
|
||||
|
||||
RID.LINUX_X64 => "Linux x64",
|
||||
RID.LINUX_ARM64 => "Linux ARM64",
|
||||
|
||||
RID.OSX_X64 => "macOS x64",
|
||||
RID.OSX_ARM64 => "macOS ARM64",
|
||||
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
public static RID ToRID(this string rid) => rid switch
|
||||
{
|
||||
"win-x64" => RID.WIN_X64,
|
||||
"win-arm64" => RID.WIN_ARM64,
|
||||
|
||||
"linux-x64" => RID.LINUX_X64,
|
||||
"linux-arm64" => RID.LINUX_ARM64,
|
||||
|
||||
"osx-x64" => RID.OSX_X64,
|
||||
"osx-arm64" => RID.OSX_ARM64,
|
||||
|
||||
_ => RID.NONE,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
+25
-22
@@ -5,43 +5,46 @@ You just want to use the app? Then simply [download the appropriate setup for yo
|
||||
1. Install the [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0).
|
||||
2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest version.
|
||||
3. Met the prerequisites for building [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/). Node.js is **not** required, though.
|
||||
4. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`.
|
||||
5. [Install NuShell](https://www.nushell.sh/). NuShell works on all operating systems and is required because the build script is written in NuShell.
|
||||
6. Clone the repository.
|
||||
4. Clone the repository.
|
||||
|
||||
## One-time mandatory steps
|
||||
Regardless of whether you want to build the app locally for yourself (not trusting the pre-built binaries) or test your changes before creating a PR, you have to run the following commands at least once:
|
||||
|
||||
1. Open a terminal using NuShell.
|
||||
2. Navigate to the `/app/MindWork AI Studio` directory within the repository.
|
||||
3. Run `dotnet restore` to bring up the .NET dependencies.
|
||||
4. Run `nu build.nu publish` to build the entire app.
|
||||
1. Open a terminal.
|
||||
2. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`.
|
||||
3. Navigate to the `/app/Build` directory within the repository.
|
||||
4. Run `dotnet run build` to build the entire app.
|
||||
|
||||
This is necessary because the build script and the Tauri framework assume that the .NET app is available as a so-called "sidecar." Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development as well.
|
||||
|
||||
## Build AI Studio from source
|
||||
In order to build MindWork AI Studio from source instead of using the pre-built binaries, follow these steps:
|
||||
1. Ensure you have met all the prerequisites.
|
||||
2. Open a terminal with NuShell.
|
||||
3. Navigate to the `/app/MindWork AI Studio` directory within the repository.
|
||||
4. To build the current version, run `nu build.nu publish` to build the entire app.
|
||||
2. Open a terminal.
|
||||
3. Navigate to the `/app/Build` directory within the repository.
|
||||
4. To build the current version, run `dotnet run build` to build the entire app.
|
||||
- This will build the app for the current operating system, for both x64 (Intel, AMD) and ARM64 (e.g., Apple Silicon, Raspberry Pi).
|
||||
- The final setup program will be located in `runtime/target/release/bundle` afterward.
|
||||
5. In order to create a new release:
|
||||
1. Before finishing the PR, make sure to create a changelog file in the `/app/MindWork AI Studio/wwwroot/changelog` directory. The file should be named `vX.Y.Z.md` and contain the changes made in the release (your changes and any other changes that are part of the release).
|
||||
2. To prepare a new release, run `nu build.nu prepare <ACTION>`, where `<ACTION>` is either `patch`, `minor`, or `major`.
|
||||
3. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes.
|
||||
4. Your proposed changes will be reviewed and merged.
|
||||
5. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`.
|
||||
6. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest).
|
||||
7. Building the release including virus scanning takes some time. Please be patient.
|
||||
- The final setup program will be located in `runtime/target/release` afterward.
|
||||
|
||||
## Run the app locally with all your changes
|
||||
Do you want to test your changes before creating a PR? Follow these steps:
|
||||
1. Ensure you have met all the prerequisites.
|
||||
2. At least once, you have to run the `nu build.nu publish` command (see above, "Build instructions"). This is necessary because the Tauri framework checks whether the .NET app as so-called "sidecar" is available. Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development.
|
||||
3. Open a terminal (in this case, it doesn't have to be NuShell).
|
||||
2. At least once, you have to run the `dotnet run build` command (see above, "Build instructions"). This is necessary because the Tauri framework checks whether the .NET app as so-called "sidecar" is available. Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development.
|
||||
3. Open a terminal.
|
||||
4. Navigate to the `runtime` directory within the repository, e.g. `cd repos/mindwork-ai-studio/runtime`.
|
||||
5. Run `cargo tauri dev --no-watch`.
|
||||
|
||||
Cargo will compile the Rust code and start the runtime. The runtime will then start the .NET compiler. When the .NET source code is compiled, the app will start. You can now test your changes.
|
||||
Cargo will compile the Rust code and start the runtime. The runtime will then start the .NET compiler. When the .NET source code is compiled, the app will start. You can now test your changes.
|
||||
|
||||
## Create a release
|
||||
In order to create a release:
|
||||
1. To create a new release, you need to be a maintainer of the repository—see step 8.
|
||||
2. Make sure there's a changelog file for the version you want to create in the `/app/MindWork AI Studio/wwwroot/changelog` directory. Name the file `vX.Y.Z.md` and include all release changes—your updates and any others included in this version.
|
||||
3. After you have created the changelog file, you must commit the changes to the repository.
|
||||
4. To prepare a new release, open a terminal, go to `/app/Build` and run `dotnet run release --action <ACTION>`, where `<ACTION>` is either `patch` (creating a patch version), `minor` (creating a minor version), or `major` (creating a major version).
|
||||
5. Now wait until all process steps have been completed. Among other things, the version number will be incremented, the new changelog registered, and the version numbers of central dependencies updated, etc.
|
||||
6. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes.
|
||||
7. Your proposed changes will be reviewed and merged.
|
||||
8. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`.
|
||||
9. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest).
|
||||
10. Building the release including virus scanning takes some time. Please be patient.
|
||||
@@ -14,7 +14,7 @@ AI Studio is only available for modern 64-bit Windows systems. When you have an
|
||||
|
||||
- **Intel/AMD:** In almost all other cases, you have an Intel/AMD system. [Download the x64 version](https://github.com/MindWorkAI/AI-Studio/releases/latest/download/MindWork.AI.Studio_x64-setup.exe) of AI Studio.
|
||||
|
||||
When you try to install the app, you get a message regarding protection of your PC (see screenshots below). For Windows to trust our app, we need to purchase a certificate that costs around $1000 per year. Would you like to help us with this? [Please consider supporting us](https://github.com/sponsors/MindWorkAI). You might want to [visit our release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). There, we provide VirusTotal scan results for each release. If you are unsure about the safety of the app, you can check the results there. Ensure that the majority of scanners have a green checkmark.
|
||||
When you try to install the app, you get a message regarding protection of your PC (see screenshots below). For Windows to trust our app, we need to purchase a certificate that [costs around $1000 per year](https://github.com/MindWorkAI/Planning/issues/56). Would you like to help us with this? [Please consider supporting us](https://github.com/sponsors/MindWorkAI). You might want to [visit our release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). There, we provide VirusTotal scan results for each release. If you are unsure about the safety of the app, you can check the results there. Ensure that the majority of scanners have a green checkmark.
|
||||
|
||||
When you are confident in the app's safety, click on "More info" and then "Run anyway" to proceed with the installation:
|
||||
|
||||
@@ -43,7 +43,7 @@ When you try to open the app, you get a message that the app is damaged:
|
||||
|
||||

|
||||
|
||||
This is because we don't have an Apple Developer account, which costs around $100 per year. Would you like to help us with this? [Please consider supporting us](https://github.com/sponsors/MindWorkAI). You might want to [visit our release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). There, we provide VirusTotal scan results for each release. If you are unsure about the safety of the app, you can check the results there. Ensure that the majority of scanners have a green checkmark.
|
||||
This is because we don't have an Apple Developer account, [which costs around $100 per year](https://github.com/MindWorkAI/Planning/issues/56). Would you like to help us with this? [Please consider supporting us](https://github.com/sponsors/MindWorkAI). You might want to [visit our release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). There, we provide VirusTotal scan results for each release. If you are unsure about the safety of the app, you can check the results there. Ensure that the majority of scanners have a green checkmark.
|
||||
|
||||
When you are confident in the app's safety, follow these steps:
|
||||
|
||||
|
||||
+7
-6
@@ -1,9 +1,10 @@
|
||||
0.9.39
|
||||
2025-04-07 17:39:09 UTC
|
||||
214
|
||||
9.0.104 (commit 2750432faa)
|
||||
9.0.3 (commit 831d23e561)
|
||||
0.9.40
|
||||
2025-04-20 13:30:03 UTC
|
||||
215
|
||||
9.0.105 (commit 35890ecb87)
|
||||
9.0.4 (commit f57e6dc747)
|
||||
1.86.0 (commit 05f9846f8)
|
||||
8.5.1
|
||||
1.8.1
|
||||
19935769035, release
|
||||
2144cfe0590, release
|
||||
osx-arm64
|
||||
Generated
+9
-9
@@ -711,9 +711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -2610,7 +2610,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.39"
|
||||
version = "0.9.40"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arboard",
|
||||
@@ -2619,6 +2619,7 @@ dependencies = [
|
||||
"calamine",
|
||||
"cbc",
|
||||
"cipher",
|
||||
"crossbeam-channel",
|
||||
"file-format",
|
||||
"flexi_logger",
|
||||
"futures",
|
||||
@@ -2629,7 +2630,7 @@ dependencies = [
|
||||
"openssl",
|
||||
"pbkdf2",
|
||||
"pdfium-render",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"rand_chacha 0.9.0",
|
||||
"rcgen",
|
||||
"reqwest 0.12.15",
|
||||
@@ -3199,9 +3200,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfium-render"
|
||||
version = "0.8.29"
|
||||
version = "0.8.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5cbb29c282cfbd0a8142ccd3bb0ce8da53e59141ce02a023b980bc72b6c0eec"
|
||||
checksum = "2773a939ec2c736640f5f2e62a325c2e1a997d694961c50f17cadfb4c8682e84"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
@@ -3615,13 +3616,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.0",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.39"
|
||||
version = "0.9.40"
|
||||
edition = "2021"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
@@ -23,7 +23,7 @@ flexi_logger = "0.30.1"
|
||||
log = { version = "0.4.27", features = ["kv"] }
|
||||
once_cell = "1.21.3"
|
||||
rocket = { version = "0.5.1", features = ["json", "tls"] }
|
||||
rand = "0.9"
|
||||
rand = "0.9.1"
|
||||
rand_chacha = "0.9"
|
||||
base64 = "0.22.1"
|
||||
cipher = { version = "0.4.4", features = ["std"] }
|
||||
@@ -35,12 +35,13 @@ sha2 = "0.10.8"
|
||||
rcgen = { version = "0.13.2", features = ["pem"] }
|
||||
file-format = "0.26.0"
|
||||
calamine = "0.26.1"
|
||||
pdfium-render = "0.8.29"
|
||||
pdfium-render = "0.8.30"
|
||||
sys-locale = "0.3.2"
|
||||
|
||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
||||
url = "2.5"
|
||||
ring = "0.17.14"
|
||||
crossbeam-channel = "0.5.15"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
||||
|
||||
+38
-7
@@ -2,17 +2,48 @@ use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
|
||||
// Tells Cargo to re-run this script only, when the version.txt file was changed:
|
||||
println!("cargo:rerun-if-changed=../metadata.txt");
|
||||
|
||||
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
println!("cargo:rustc-env=OUT_DIR={}", out_dir.to_str().unwrap());
|
||||
|
||||
//
|
||||
// When we are in debug mode, we want to set the current RID
|
||||
// to the current architecture. This is necessary, so that
|
||||
// the developers get the right behavior.
|
||||
//
|
||||
// We read the current OS and architecture and overwrite the
|
||||
// current RID in the metadata file (line #10).
|
||||
//
|
||||
// We have to take care of different naming conventions. The
|
||||
// following RIDs are supported: win-x64, win-arm64, linux-x64,
|
||||
// linux-arm64, osx-x64, osx-arm64.
|
||||
//
|
||||
let current_os = std::env::consts::OS;
|
||||
let current_arch = std::env::consts::ARCH;
|
||||
let rid = match (current_os, current_arch) {
|
||||
("windows", "x86_64") => "win-x64",
|
||||
("windows", "aarch64") => "win-arm64",
|
||||
|
||||
("linux", "x86_64") => "linux-x64",
|
||||
("linux", "aarch64") => "linux-arm64",
|
||||
|
||||
("macos", "x86_64") => "osx-x64",
|
||||
("macos", "aarch64") => "osx-arm64",
|
||||
|
||||
_ => panic!("Unsupported OS or architecture: {current_os} {current_arch}"),
|
||||
};
|
||||
|
||||
let metadata = include_str!("../metadata.txt");
|
||||
let mut metadata_lines = metadata.lines();
|
||||
let version = metadata_lines.next().unwrap();
|
||||
|
||||
let mut metadata_lines = metadata.lines().collect::<Vec<_>>();
|
||||
metadata_lines[9] = rid;
|
||||
let new_metadata = metadata_lines.join("\n");
|
||||
std::fs::write("../metadata.txt", new_metadata).unwrap();
|
||||
|
||||
//
|
||||
// Read the current version and update the
|
||||
// Rust and Tauri configuration files:
|
||||
//
|
||||
let version = metadata_lines[0];
|
||||
update_cargo_toml("Cargo.toml", version);
|
||||
update_tauri_conf("tauri.conf.json", version);
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,7 +14,6 @@ use mindwork_ai_studio::runtime_api::start_runtime_api;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
||||
let metadata = include_str!("../../metadata.txt");
|
||||
let mut metadata_lines = metadata.lines();
|
||||
let app_version = metadata_lines.next().unwrap();
|
||||
@@ -26,6 +25,7 @@ async fn main() {
|
||||
let mud_blazor_version = metadata_lines.next().unwrap();
|
||||
let tauri_version = metadata_lines.next().unwrap();
|
||||
let app_commit_hash = metadata_lines.next().unwrap();
|
||||
let architecture = metadata_lines.next().unwrap();
|
||||
|
||||
init_logging();
|
||||
info!("Starting MindWork AI Studio:");
|
||||
@@ -33,7 +33,7 @@ async fn main() {
|
||||
let working_directory = std::env::current_dir().unwrap();
|
||||
info!(".. The working directory is: '{working_directory:?}'");
|
||||
|
||||
info!(".. Version: v{app_version} (commit {app_commit_hash}, build {build_number})");
|
||||
info!(".. Version: v{app_version} (commit {app_commit_hash}, build {build_number}, {architecture})");
|
||||
info!(".. Build time: {build_time}");
|
||||
info!(".. .NET SDK: v{dotnet_sdk_version}");
|
||||
info!(".. .NET: v{dotnet_version}");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "MindWork AI Studio",
|
||||
"version": "0.9.39"
|
||||
"version": "0.9.40"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in new issue
Block a user