Merge branch 'main' into chunk-data

This commit is contained in:
Paul Koudelka 2026-06-10 14:11:44 +02:00
commit b7b18aa45c
109 changed files with 6469 additions and 1691 deletions

View File

@ -329,8 +329,8 @@ jobs:
pdfium_version=$(sed -n '11p' metadata.txt)
pdfium_version=$(echo $pdfium_version | cut -d'.' -f3)
# Next line is the Qdrant version:
qdrant_version="v$(sed -n '12p' metadata.txt)"
# Next line is the vector store version:
vector_store_version="$(sed -n '12p' metadata.txt)"
# Write the metadata to the environment:
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
@ -344,7 +344,7 @@ jobs:
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV
echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV
echo "VECTOR_STORE_VERSION=${vector_store_version}" >> $GITHUB_ENV
# Log the metadata:
echo "App version: '${formatted_app_version}'"
@ -357,7 +357,7 @@ jobs:
echo "Tauri version: '${tauri_version}'"
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
echo "PDFium version: '${pdfium_version}'"
echo "Qdrant version: '${qdrant_version}'"
echo "Vector store version: '${vector_store_version}'"
- name: Read and format metadata (Windows)
if: matrix.platform == 'windows-latest'
@ -402,8 +402,8 @@ jobs:
$pdfium_version = $metadata[10]
$pdfium_version = $pdfium_version.Split('.')[2]
# Next line is the necessary Qdrant version:
$qdrant_version = "v$($metadata[11])"
# Next line is the vector store version:
$vector_store_version = $metadata[11]
# Write the metadata to the environment:
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
@ -416,7 +416,7 @@ jobs:
Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV
Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV
Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV
Write-Output "QDRANT_VERSION=${qdrant_version}" >> $env:GITHUB_ENV
Write-Output "VECTOR_STORE_VERSION=${vector_store_version}" >> $env:GITHUB_ENV
# Log the metadata:
Write-Output "App version: '${formatted_app_version}'"
@ -429,7 +429,7 @@ jobs:
Write-Output "Tauri version: '${tauri_version}'"
Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'"
Write-Output "PDFium version: '${pdfium_version}'"
Write-Output "Qdrant version: '${qdrant_version}'"
Write-Output "Vector store version: '${vector_store_version}'"
- name: Setup .NET
uses: actions/setup-dotnet@v4
@ -558,129 +558,6 @@ jobs:
} catch {
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
}
- name: Deploy Qdrant (Unix)
if: matrix.platform != 'windows-latest'
env:
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
RUST_TARGET: ${{ matrix.rust_target }}
run: |
set -e
# Target directory:
TDB_DIR="runtime/target/databases/qdrant"
mkdir -p "$TDB_DIR"
case "${DOTNET_RUNTIME}" in
linux-x64)
QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
linux-arm64)
QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
osx-x64)
QDRANT_FILE="x86_64-apple-darwin.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
osx-arm64)
QDRANT_FILE="aarch64-apple-darwin.tar.gz"
DB_SOURCE="qdrant"
DB_TARGET="qdrant-${RUST_TARGET}"
;;
*)
echo "Unknown platform: ${DOTNET_RUNTIME}"
exit 1
;;
esac
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-${QDRANT_FILE}"
echo "Download Qdrant $QDRANT_URL ..."
TMP=$(mktemp -d)
ARCHIVE="${TMP}/qdrant.tgz"
curl -fsSL -o "$ARCHIVE" "$QDRANT_URL"
echo "Extracting Qdrant ..."
tar xzf "$ARCHIVE" -C "$TMP"
SRC="${TMP}/${DB_SOURCE}"
if [ ! -f "$SRC" ]; then
echo "Was not able to find Qdrant source: $SRC"
exit 1
fi
echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/"
cp -f "$SRC" "$TDB_DIR/$DB_TARGET"
echo "Cleaning up ..."
rm -fr "$TMP"
- name: Deploy Qdrant (Windows)
if: matrix.platform == 'windows-latest'
env:
QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
RUST_TARGET: ${{ matrix.rust_target }}
run: |
$TDB_DIR = "runtime\target\databases\qdrant"
New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null
switch ($env:DOTNET_RUNTIME) {
"win-x64" {
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
$DB_SOURCE = "qdrant.exe"
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
}
"win-arm64" {
$QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
$DB_SOURCE = "qdrant.exe"
$DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
}
default {
Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)"
exit 1
}
}
$QDRANT_URL = "https://github.com/qdrant/qdrant/releases/download/$($env:QDRANT_VERSION)/qdrant-$QDRANT_FILE"
Write-Host "Download $QDRANT_URL ..."
# Create a unique temporary directory (not just a file)
$TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $TMP -Force | Out-Null
$ARCHIVE = Join-Path $TMP "qdrant.tgz"
Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE
Write-Host "Extracting Qdrant ..."
tar -xzf $ARCHIVE -C $TMP
$SRC = Join-Path $TMP $DB_SOURCE
if (!(Test-Path $SRC)) {
Write-Error "Cannot find Qdrant source: $SRC"
exit 1
}
$DEST = Join-Path $TDB_DIR $DB_TARGET
Copy-Item -Path $SRC -Destination $DEST -Force
Write-Host "Cleaning up ..."
Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue
# Try to remove the temporary directory, but ignore errors if files are still in use
try {
Remove-Item $TMP -Recurse -Force -ErrorAction Stop
Write-Host "Successfully cleaned up temporary directory: $TMP"
} catch {
Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)"
}
- name: Build .NET project
run: |
cd "app/MindWork AI Studio"

View File

@ -1,120 +0,0 @@
using System.Formats.Tar;
using System.IO.Compression;
using SharedTools;
namespace Build.Commands;
public static class Qdrant
{
public static async Task InstallAsync(RID rid, string version)
{
Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ...");
var cwd = Environment.GetRustRuntimeDirectory();
var qdrantTmpDownloadPath = Path.GetTempFileName();
var qdrantTmpExtractPath = Directory.CreateTempSubdirectory();
var qdrantUrl = GetQdrantDownloadUrl(rid, version);
//
// Download the file:
//
Console.Write(" downloading ...");
using (var client = new HttpClient())
{
var response = await client.GetAsync(qdrantUrl);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}");
return;
}
await using var fileStream = File.Create(qdrantTmpDownloadPath);
await response.Content.CopyToAsync(fileStream);
}
//
// Extract the downloaded file:
//
Console.Write(" extracting ...");
await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
if (rid == RID.WIN_X64)
{
using var archive = new ZipArchive(zStream, ZipArchiveMode.Read);
archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true);
}
else
{
await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress);
await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true);
}
}
//
// Copy the database to the target directory:
//
Console.Write(" deploying ...");
var database = GetDatabasePath(rid);
if (string.IsNullOrWhiteSpace(database.Path))
{
Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}");
return;
}
var qdrantDbSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path);
var qdrantDbTargetPath = Path.Join(cwd, "target", "databases", "qdrant",database.Filename);
if (!File.Exists(qdrantDbSourcePath))
{
Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'");
return;
}
Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant"));
if (File.Exists(qdrantDbTargetPath))
File.Delete(qdrantDbTargetPath);
File.Copy(qdrantDbSourcePath, qdrantDbTargetPath);
//
// Cleanup:
//
Console.Write(" cleaning up ...");
File.Delete(qdrantTmpDownloadPath);
Directory.Delete(qdrantTmpExtractPath.FullName, true);
Console.WriteLine(" done.");
}
private static Database GetDatabasePath(RID rid) => rid switch
{
RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"),
RID.WIN_ARM64 => new("qdrant.exe", "qdrant-aarch64-pc-windows-msvc.exe"),
_ => new(string.Empty, string.Empty),
};
private static string GetQdrantDownloadUrl(RID rid, string version)
{
var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-";
return rid switch
{
RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz",
RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz",
RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz",
RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz",
RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
_ => string.Empty,
};
}
}

View File

@ -69,6 +69,7 @@ public sealed partial class UpdateMetadataCommands
await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
}
[Command("prepare", Description = "Prepare the metadata for the next release")]
@ -126,6 +127,7 @@ public sealed partial class UpdateMetadataCommands
await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
await this.UpdateProjectCommitHash();
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs")));
@ -147,12 +149,11 @@ public sealed partial class UpdateMetadataCommands
Console.WriteLine("==============================");
await this.UpdateArchitecture(rid);
await this.UpdateTauriVersion();
await this.UpdateVectorStoreVersion();
var pdfiumVersion = await this.ReadPdfiumVersion();
await Pdfium.InstallAsync(rid, pdfiumVersion);
var qdrantVersion = await this.ReadQdrantVersion();
await Qdrant.InstallAsync(rid, qdrantVersion);
Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ...");
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
@ -367,16 +368,6 @@ public sealed partial class UpdateMetadataCommands
return shortVersion;
}
private async Task<string> ReadQdrantVersion()
{
const int QDRANT_VERSION_INDEX = 11;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim();
return currentQdrantVersion;
}
private async Task UpdateArchitecture(RID rid)
{
const int ARCHITECTURE_INDEX = 9;
@ -529,7 +520,32 @@ public sealed partial class UpdateMetadataCommands
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateVectorStoreVersion()
{
const int VECTOR_STORE_VERSION_INDEX = 11;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentVectorStoreVersion = lines[VECTOR_STORE_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Qdrant Edge", Environment.GetRustRuntimeDirectory(), QdrantEdgeVersionRegex(), "cargo", "tree --depth 1");
if (matches.Count == 0)
return;
var updatedVectorStoreVersion = matches[0].Groups["version"].Value;
if(currentVectorStoreVersion == updatedVectorStoreVersion)
{
Console.WriteLine("- The vector store version is already up to date.");
return;
}
Console.WriteLine($"- Updated vector store version from {currentVectorStoreVersion} to {updatedVectorStoreVersion}.");
lines[VECTOR_STORE_VERSION_INDEX] = updatedVectorStoreVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateMudBlazorVersion()
{
const int MUD_BLAZOR_VERSION_INDEX = 6;
@ -720,6 +736,9 @@ public sealed partial class UpdateMetadataCommands
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
private static partial Regex MudBlazorVersionRegex();
[GeneratedRegex("""qdrant-edge\s+v(?<version>[0-9.]+)""")]
private static partial Regex QdrantEdgeVersionRegex();
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
private static partial Regex TauriVersionRegex();

View File

@ -2170,6 +2170,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Shows and
-- This feature is managed by your organization and has therefore been disabled.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "This feature is managed by your organization and has therefore been disabled."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T4285779702"] = "Choose File"
-- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."
@ -2632,12 +2635,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"]
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"
-- Additional root certificates are enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1380446131"] = "Additional root certificates are enabled"
-- Select preview features
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Select preview features"
-- Your organization provided a default start page, but you can still change it.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Your organization provided a default start page, but you can still change it."
-- Root certificate bundle path
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path"
-- Select the desired behavior for the navigation bar.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar."
@ -2692,12 +2701,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"]
-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio."
-- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox."
-- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates."
-- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
-- Spellchecking is enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
-- External HTTPS certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "External HTTPS certificates"
-- Allowed hosts for additional root certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3562495752"] = "Allowed hosts for additional root certificates"
-- Request timeout
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
@ -2716,9 +2737,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"]
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
-- When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3798070907"] = "When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak."
-- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?"
-- Additional root certificates are disabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Additional root certificates are disabled"
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Preselect one of your profiles?"
@ -2731,6 +2758,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"]
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
-- Use additional root certificates for external HTTPS requests?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?"
-- Select a root certificate bundle
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle"
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
@ -3169,15 +3202,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete"
-- Rename Workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Rename Workspace"
-- Clear search
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Clear search"
-- Rename Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Rename Chat"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Add workspace"
-- Search chats
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Search chats"
-- Start a new chat in workspace '{0}'
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Start a new chat in workspace '{0}'"
-- Add chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Add chat"
-- No chats found
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "No chats found"
-- Create Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Create Chat"
@ -3214,6 +3259,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to
-- Please enter a new or edit the name for your workspace '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter a new or edit the name for your workspace '{0}':"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is already a workspace with this name. Please choose a different name."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name."
@ -3223,6 +3271,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename"
-- Please enter a new or edit the name for your chat '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Please enter a new or edit the name for your chat '{0}':"
-- Search chat contents
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Search chat contents"
-- Load Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Load Chat"
@ -5797,6 +5848,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Update from v{0
-- Install later
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Install later"
-- Create new workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1541251414"] = "Create new workspace"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Add workspace"
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Workspace name"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "There is already a workspace with this name. Please choose a different name."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Please enter a workspace name."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel"
@ -5992,6 +6058,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co
-- Localization
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
-- Hide search
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Hide search"
-- Reload your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces"
@ -6004,6 +6073,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Open Chat Options"
-- Disappearing Chat
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Disappearing Chat"
-- Search your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Search your workspaces"
-- Configure your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Configure your workspaces"
@ -6130,6 +6202,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T91074375"] = "The app is free to use, b
-- Startup log file
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startup log file"
-- The configured root certificates could not be used.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "The configured root certificates could not be used."
-- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions."
@ -6138,6 +6213,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1124039623"] = "Vector store ver
-- The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1132433749"] = "The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer."
-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the plugin ID differs from the enterprise configuration ID."
@ -6145,15 +6222,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
-- Copies the allowed host pattern to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Copies the allowed host pattern to the clipboard"
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the configuration plugin..."
@ -6163,9 +6249,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secre
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active."
-- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant."
-- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library."
@ -6178,6 +6261,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create
-- Consent:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
-- Copies the executable path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the executable path to the clipboard"
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."
@ -6202,12 +6288,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
-- Copies the number of loaded root certificates to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
-- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
-- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file."
@ -6241,6 +6333,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@ -6250,8 +6348,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant."
-- unknown
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unknown"
-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems."
@ -6295,9 +6393,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
-- Copies the root certificate fingerprint to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Copies the root certificate fingerprint to the clipboard"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
-- External HTTPS custom root certificates are configured but not active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
-- Vector store
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store"
@ -6313,6 +6420,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
-- Hide Details
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
-- Linux package
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3196139293"] = "Linux package"
-- External HTTPS custom root certificates are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208455732"] = "External HTTPS custom root certificates are active."
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface."
@ -6325,9 +6438,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
-- Discover MindWork AI's mission and vision on our official homepage.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3294830584"] = "Discover MindWork AI's mission and vision on our official homepage."
-- External HTTPS custom root certificates
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3315279770"] = "External HTTPS custom root certificates"
-- User-language provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3334355246"] = "User-language provided by the OS"
-- Status:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3396815215"] = "Status:"
-- The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3405978777"] = "The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:"
@ -6346,18 +6465,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to
-- AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service."
-- Copies the certificate bundle path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3550115021"] = "Copies the certificate bundle path to the clipboard"
-- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
-- active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
-- Allowed host:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Allowed host:"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
@ -6367,6 +6498,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
-- not applicable
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T396609403"] = "not applicable"
-- Copies the allowed host configuration to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3970230163"] = "Copies the allowed host configuration to the clipboard"
-- Installed Pandoc version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3983971016"] = "Installed Pandoc version"
@ -6376,6 +6513,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins
-- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
-- Allowed hosts: none configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured"
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication."
@ -6385,12 +6525,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is
-- Community & Code
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code"
-- Executable path
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
-- Copies the working directory to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4194302113"] = "Copies the working directory to the clipboard"
-- Certificate bundle:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4197142390"] = "Certificate bundle:"
-- When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4229014037"] = "When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project."
-- Copies the status to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Copies the status to the clipboard"
-- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow."
@ -6403,6 +6555,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "We use the DeepSe
-- starting
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
-- Root certificate fingerprint:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T615041128"] = "Root certificate fingerprint:"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
@ -6412,6 +6567,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bu
-- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T64689067"] = "This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."
-- not active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T70364248"] = "not active"
-- Loaded root certificates:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T709525418"] = "Loaded root certificates:"
-- Working directory
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T768480635"] = "Working directory"
-- Copies the config ID to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Copies the config ID to the clipboard"
@ -7102,6 +7266,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Sta
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Reason"
-- Starting
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starting"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T3662391977"] = "Unavailable"
@ -7109,19 +7276,19 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
-- Number of vector stores
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Number of vector stores"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
-- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available."
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."
@ -7234,6 +7401,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
-- No certificate bundle path is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1033171304"] = "No certificate bundle path is configured."
-- app settings
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1736441001"] = "app settings"
-- environment variables
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T317663851"] = "environment variables"
-- configuration plugin
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3427095600"] = "configuration plugin"
-- The configured certificate bundle file does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "The configured certificate bundle file does not exist."
-- The configured certificate bundle does not contain usable root CA certificates.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates."
-- AI Studio couldn't install Pandoc because the archive was not found.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
@ -7753,6 +7938,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Custom"
-- Media
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Media"
-- Certificate bundle
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate bundle"
-- Source like prefix
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix"

View File

@ -89,7 +89,7 @@ public static class IImageSourceExtensions
case ContentImageSource.URL:
{
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient();
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
var timeoutToken = timeoutTokenSource.Token;
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);

View File

@ -107,7 +107,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
protected override async Task OnInitializedAsync()
{
// Apply the filters for the message bus:
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED ]);
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
@ -383,6 +383,29 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.WorkspaceName(this.currentWorkspaceName);
}
private async Task RefreshRenamedWorkspaceHeaderAsync(Guid workspaceId)
{
var currentChatThread = this.ChatThread;
if (currentChatThread is null || currentChatThread.WorkspaceId != workspaceId)
return;
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
var chatThreadId = currentChatThread.ChatId;
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
if (syncVersion != this.workspaceHeaderSyncVersion)
return;
if (this.ChatThread is null
|| this.ChatThread.ChatId != chatThreadId
|| this.ChatThread.WorkspaceId != workspaceId)
return;
this.currentChatThreadId = chatThreadId;
this.currentWorkspaceId = workspaceId;
this.PublishWorkspaceNameIfChanged(loadedWorkspaceName);
}
private async Task SyncForegroundChatAsync()
{
var nextForegroundChatId = this.ChatThread?.ChatId ?? Guid.Empty;
@ -874,7 +897,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{ x => x.ConfirmText, T("Move chat") },
};
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
@ -1085,6 +1108,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.autoSaveEnabled)
await this.SaveThread();
break;
case Event.WORKSPACE_RENAMED:
if (data is Guid workspaceId)
await this.RefreshRenamedWorkspaceHeaderAsync(workspaceId);
break;
case Event.AI_JOB_CHANGED:
case Event.AI_JOB_FINISHED:

View File

@ -56,10 +56,12 @@ public abstract partial class ConfigurationBase : MSGComponentBase
protected bool IsDisabled => this.Disabled() || this.IsLocked();
private string Classes => $"{this.GetClassForBase} {MARGIN_CLASS}";
private string Classes => $"{this.GetClassForBase} {JUSTIFIED_HELP_CLASS} {MARGIN_CLASS}";
private protected virtual RenderFragment? Body => null;
private const string JUSTIFIED_HELP_CLASS = "configuration-help-justified";
private const string MARGIN_CLASS = "mb-6";
protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();

View File

@ -0,0 +1,27 @@
@inherits ConfigurationBaseCore
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudTextField
T="string"
Text="@this.Text()"
TextChanged="@this.InternalUpdate"
Disabled="@this.IsDisabled"
Adornment="Adornment.Start"
AdornmentIcon="@this.Icon"
AdornmentColor="@this.IconColor"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Immediate="@true"
Underline="false"
Class="flex-grow-1"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
Disabled="@this.IsDisabled"
Class="mb-1"
OnClick="@this.OpenFileDialog">
@T("Choose File")
</MudButton>
</MudStack>

View File

@ -0,0 +1,127 @@
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Timer = System.Timers.Timer;
namespace AIStudio.Components;
public partial class ConfigurationFile : ConfigurationBaseCore
{
/// <summary>
/// The text used for the textfield.
/// </summary>
[Parameter]
public Func<string> Text { get; set; } = () => string.Empty;
/// <summary>
/// An action which is called when the text was changed.
/// </summary>
[Parameter]
public Action<string> TextUpdate { get; set; } = _ => { };
/// <summary>
/// The icon to display next to the textfield.
/// </summary>
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.AttachFile;
/// <summary>
/// The color of the icon to use.
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Default;
/// <summary>
/// The title of the file selection dialog.
/// </summary>
[Parameter]
public string FileDialogTitle { get; set; } = "Select File";
/// <summary>
/// The optional file type filter for the file selection dialog.
/// </summary>
[Parameter]
public FileTypeFilter[]? Filter { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
private string internalText = string.Empty;
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
{
AutoReset = false
};
#region Overrides of ConfigurationBase
/// <inheritdoc />
protected override bool Stretch => true;
protected override Variant Variant => Variant.Outlined;
protected override string Label => this.OptionDescription;
#endregion
#region Overrides of ConfigurationBase
protected override async Task OnInitializedAsync()
{
this.timer.Elapsed += async (_, _) => await this.InvokeAsync(async () => await this.OptionChanged(this.internalText));
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
this.internalText = this.Text();
await base.OnParametersSetAsync();
}
#endregion
private void InternalUpdate(string text)
{
this.timer.Stop();
this.internalText = text;
this.timer.Start();
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
if (response.UserCancelled)
return;
this.timer.Stop();
this.internalText = response.SelectedFilePath;
await this.OptionChanged(response.SelectedFilePath);
}
private async Task OptionChanged(string updatedText)
{
this.TextUpdate(updatedText);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
this.timer.Stop();
this.timer.Dispose();
}
catch
{
// ignore
}
base.DisposeResources();
}
#endregion
}

View File

@ -3,7 +3,7 @@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@this.Icon" Color="@this.IconColor"/>
<MudText Typo="Typo.body1" Class="flex-grow-1">
@if (string.IsNullOrWhiteSpace(this.Shortcut()))
@if (string.IsNullOrWhiteSpace(this.Data.Value()))
{
@T("No shortcut configured")
}

View File

@ -1,5 +1,4 @@
using AIStudio.Dialogs;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
@ -19,22 +18,10 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
private RustService RustService { get; init; } = null!;
/// <summary>
/// The current shortcut value.
/// The shortcut binding data.
/// </summary>
[Parameter]
public Func<string> Shortcut { get; set; } = () => string.Empty;
/// <summary>
/// An action which is called when the shortcut was changed.
/// </summary>
[Parameter]
public Action<string> ShortcutUpdate { get; set; } = _ => { };
/// <summary>
/// The name/identifier of the shortcut (used for conflict detection and registration).
/// </summary>
[Parameter]
public Shortcut ShortcutId { get; init; }
public ConfigurationShortcutData Data { get; set; } = ConfigurationShortcutData.Empty;
/// <summary>
/// The icon to display.
@ -60,10 +47,18 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
private string GetDisplayShortcut()
{
var shortcut = this.Shortcut();
var shortcut = this.Data.Value();
if (string.IsNullOrWhiteSpace(shortcut))
return string.Empty;
var shortcutDisplayName = this.Data.DisplayName();
var shortcutDisplaySource = this.Data.DisplaySource();
if (!string.IsNullOrWhiteSpace(shortcutDisplayName)
&& string.Equals(shortcutDisplaySource, shortcut, StringComparison.Ordinal))
{
return shortcutDisplayName;
}
// Convert internal format to display format:
return shortcut
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
@ -80,8 +75,8 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
{
var dialogParameters = new DialogParameters<ShortcutDialog>
{
{ x => x.InitialShortcut, this.Shortcut() },
{ x => x.ShortcutId, this.ShortcutId },
{ x => x.InitialShortcut, this.Data.Value() },
{ x => x.ShortcutId, this.Data.Id },
};
var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>(
@ -93,9 +88,17 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
if (dialogResult is null || dialogResult.Canceled)
return;
if (dialogResult.Data is string newShortcut)
if (dialogResult.Data is ShortcutDialogResult shortcutResult)
{
this.ShortcutUpdate(newShortcut);
this.Data.ValueUpdate(shortcutResult.Shortcut);
this.Data.DisplayUpdate(shortcutResult.DisplayName, shortcutResult.DisplaySource);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
else if (dialogResult.Data is string newShortcut)
{
this.Data.ValueUpdate(newShortcut);
this.Data.DisplayUpdate(string.Empty, string.Empty);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}

View File

@ -0,0 +1,44 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Components;
/// <summary>
/// UI binding data for a configurable keyboard shortcut.
/// </summary>
public sealed class ConfigurationShortcutData
{
/// <summary>
/// Empty shortcut binding.
/// </summary>
public static ConfigurationShortcutData Empty { get; } = new();
/// <summary>
/// The name/identifier of the shortcut, used for conflict detection and registration.
/// </summary>
public Shortcut Id { get; init; } = Shortcut.NONE;
/// <summary>
/// The current shortcut value.
/// </summary>
public Func<string> Value { get; init; } = () => string.Empty;
/// <summary>
/// An action that is called when the shortcut was changed.
/// </summary>
public Action<string> ValueUpdate { get; init; } = _ => { };
/// <summary>
/// The optional user-facing shortcut label.
/// </summary>
public Func<string> DisplayName { get; init; } = () => string.Empty;
/// <summary>
/// The canonical shortcut value the optional user-facing label belongs to.
/// </summary>
public Func<string> DisplaySource { get; init; } = () => string.Empty;
/// <summary>
/// An action that is called when the user-facing shortcut label was changed.
/// </summary>
public Action<string, string> DisplayUpdate { get; init; } = (_, _) => { };
}

View File

@ -3,9 +3,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Policy" HeaderText="@T("Agent: Security Audit for external Assistants")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("This Agent audits newly installed or updated external Plugin-Assistant for security risks before they are activated and stores the latest audit card until the plugin manifest changes.")
</MudText>
</MudJustifiedText>
<MudField Label="@T("Require a security audit before activating external Assistants?")" Variant="Variant.Outlined" Underline="false" Class="mb-6" InnerPadding="false">
<MudSwitch T="bool" Value="@this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation" ValueChanged="@this.RequireAuditBeforeActivationChanged" Color="Color.Primary">
@(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit"))

View File

@ -2,9 +2,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextFields" HeaderText="@T("Agent: Text Content Cleaner Options")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things, and attempts to convert relative links into absolute links so that they can be used.")
</MudText>
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Preselect text content cleaner options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider = selectedValue)"/>
</MudPaper>

View File

@ -2,9 +2,9 @@
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="@T("Agent: Data Source Selection Options")">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudText Typo="Typo.body1" Class="mb-3">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to select the appropriate data sources for the current prompt.")
</MudText>
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Preselect data source selection options?")" LabelOn="@T("Options are preselected")" LabelOff="@T("No options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some agent options. This is might be useful when you prefer an LLM.")"/>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/>
</MudPaper>

View File

@ -1,9 +1,9 @@
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Assessment" HeaderText="@T("Agent: Retrieval Context Validation Options")">
<MudText Typo="Typo.body1" Class="mb-3">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.")
</MudText>
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Enable the retrieval context validation agent?")" LabelOn="@T("The validation agent is enabled")" LabelOff="@T("No validation is performed")" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation = updatedState)" OptionHelp="@T("When enabled, the retrieval context validation agent will check each retrieval context of any retrieval process, whether a context makes sense for the given prompt.")"/>
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
{

View File

@ -37,7 +37,7 @@
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
<ConfigurationShortcut ShortcutId="Shortcut.VOICE_RECORDING_TOGGLE" OptionDescription="@T("Voice recording shortcut")" Shortcut="@(() => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording)" ShortcutUpdate="@(shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut)" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
<ConfigurationShortcut Data="@this.VoiceRecordingShortcut" OptionDescription="@T("Voice recording shortcut")" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
}
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
@ -46,12 +46,12 @@
@T("Enterprise Administration")
</MudText>
<MudText Typo="Typo.body2" Class="mb-3">
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("Generate a 256-bit encryption secret for encrypting API keys in configuration plugins. Deploy this secret to client machines via Group Policy (Windows Registry) or environment variables. Providers can then be exported with encrypted API keys using the export buttons in the provider settings.")
<MudLink Href="https://github.com/MindWorkAI/AI-Studio/blob/main/documentation/Enterprise%20IT.md" Target="_blank">
@T("Read the Enterprise IT documentation for details.")
</MudLink>
</MudText>
</MudJustifiedText>
<MudButton StartIcon="@Icons.Material.Filled.Key"
Variant="Variant.Filled"
@ -59,5 +59,13 @@
OnClick="@this.GenerateEncryptionSecret">
@T("Generate an encryption secret and copy it to the clipboard")
</MudButton>
<MudText Typo="Typo.h6" Class="mt-6 mb-3">
@T("External HTTPS certificates")
</MudText>
<ConfigurationOption OptionDescription="@T("Use additional root certificates for external HTTPS requests?")" LabelOn="@T("Additional root certificates are enabled")" LabelOff="@T("Additional root certificates are disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled = updatedState)" OptionHelp="@T("When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var meta) && meta.IsLocked"/>
<ConfigurationFile OptionDescription="@T("Root certificate bundle path")" Icon="@Icons.Material.Filled.Folder" Text="@(() => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath = updatedText)" FileDialogTitle="@T("Select a root certificate bundle")" Filter="@([FileTypes.CERTIFICATE_BUNDLE])" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var meta) && meta.IsLocked"/>
<ConfigurationText OptionDescription="@T("Allowed hosts for additional root certificates")" Icon="@Icons.Material.Filled.Dns" NumLines="3" Text="@this.GetExternalHttpCustomRootCertificateAllowedHostsText" TextUpdate="@this.UpdateExternalHttpCustomRootCertificateAllowedHosts" Disabled="@this.AreExternalHttpCustomRootCertificateDetailsDisabled" OptionHelp="@T("Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var meta) && meta.IsLocked"/>
}
</ExpansionPanel>

View File

@ -1,11 +1,22 @@
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Rust;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelApp : SettingsPanelBase
{
private ConfigurationShortcutData VoiceRecordingShortcut => new()
{
Id = Shortcut.VOICE_RECORDING_TOGGLE,
Value = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording,
ValueUpdate = shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut,
DisplayName = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName,
DisplaySource = () => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource,
DisplayUpdate = this.UpdateShortcutVoiceRecordingDisplay,
};
private async Task GenerateEncryptionSecret()
{
var secret = EnterpriseEncryption.GenerateSecret();
@ -67,12 +78,38 @@ public partial class SettingsPanelApp : SettingsPanelBase
return enabled;
}
private string GetExternalHttpCustomRootCertificateAllowedHostsText()
{
return string.Join(Environment.NewLine, this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts.Order(StringComparer.OrdinalIgnoreCase));
}
private bool AreExternalHttpCustomRootCertificateDetailsDisabled()
{
return !this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled;
}
private void UpdateExternalHttpCustomRootCertificateAllowedHosts(string updatedText)
{
var patterns = updatedText
.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts = patterns;
}
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
{
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
}
private void UpdateShortcutVoiceRecordingDisplay(string displayName, string displaySource)
{
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplayName = displayName;
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecordingDisplaySource = displaySource;
}
private async Task UpdateLangBehaviour(LangBehavior behavior)
{
this.SettingsManager.ConfigurationData.App.LanguageBehavior = behavior;

View File

@ -11,6 +11,36 @@
}
else
{
@if (this.SearchVisible)
{
<MudStack Class="mx-3 mt-2 mb-1" Spacing="1" Style="position: sticky; top: 0; z-index: 2; background-color: var(--mud-palette-background);">
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
<MudTextField T="string"
Text="@this.searchText"
TextChanged="@this.OnSearchTextChanged"
Placeholder="@T("Search chats")"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Immediate="@true"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"/>
<MudTooltip Text="@T("Clear search")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Clear" Size="Size.Medium" Color="Color.Inherit" Disabled="@(string.IsNullOrWhiteSpace(this.searchText))" OnClick="@this.ClearSearchAsync"/>
</MudTooltip>
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="1">
<MudSwitch T="bool" Value="@this.includeThreadContents" ValueChanged="@this.IncludeThreadContentsChanged" Color="Color.Primary">
@T("Search chat contents")
</MudSwitch>
@if (this.isSearchRunning)
{
<MudProgressCircular Size="Size.Small" Indeterminate="@true"/>
}
</MudStack>
</MudStack>
}
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
<ItemTemplate Context="item">
@switch (item.Value)
@ -35,7 +65,7 @@ else
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudText Style="justify-self: start;">
<MudText Style="@this.GetChatTreeItemTextStyle(treeItem)">
@if (string.IsNullOrWhiteSpace(treeItem.Text))
{
@T("Empty chat")
@ -71,15 +101,22 @@ else
<MudText Style="justify-self: start;">
@treeItem.Text
</MudText>
<div style="justify-self: end;">
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
</MudTooltip>
@if (!this.HasSearchQuery)
{
<div style="justify-self: end;">
<MudTooltip Text="@this.GetAddChatToWorkspaceTooltip(treeItem.Text)" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.AddChatAsync(treeItem.Path))"/>
</MudTooltip>
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
</MudTooltip>
</div>
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
</MudTooltip>
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
</MudTooltip>
</div>
}
</div>
</BodyContent>
</MudTreeViewItem>

View File

@ -3,7 +3,6 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Tools.AIJobs;
using Microsoft.AspNetCore.Components;
@ -32,21 +31,32 @@ public partial class Workspaces : MSGComponentBase
[Parameter]
public bool ExpandRootNodes { get; set; } = true;
[Parameter]
public bool SearchVisible { get; set; }
[Parameter]
public EventCallback<bool> SearchVisibleChanged { get; set; }
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1);
private readonly List<TreeItemData<ITreeItem>> treeItems = [];
private readonly HashSet<Guid> loadingWorkspaceChatLists = [];
private CancellationTokenSource? prefetchCancellationTokenSource;
private CancellationTokenSource? searchCancellationTokenSource;
private bool isInitialLoading = true;
private bool isDisposed;
private bool includeThreadContents;
private bool isSearchRunning;
private string searchText = string.Empty;
private long searchRevision;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_CREATED ]);
_ = this.LoadTreeItemsAsync(startPrefetch: true);
}
@ -54,6 +64,7 @@ public partial class Workspaces : MSGComponentBase
private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false)
{
var shouldRunSearch = false;
await this.treeLoadingSemaphore.WaitAsync();
try
{
@ -64,7 +75,11 @@ public partial class Workspaces : MSGComponentBase
await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync();
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
this.BuildTreeItems(snapshot);
if (this.HasSearchQuery)
shouldRunSearch = true;
else
this.BuildTreeItems(snapshot);
this.isInitialLoading = false;
}
finally
@ -72,12 +87,40 @@ public partial class Workspaces : MSGComponentBase
this.treeLoadingSemaphore.Release();
}
await this.SafeStateHasChanged();
if (shouldRunSearch)
await this.SearchWorkspaceItemsAsync();
else
await this.SafeStateHasChanged();
if (startPrefetch)
await this.StartPrefetchAsync();
}
private bool HasSearchQuery => this.SearchVisible && !string.IsNullOrWhiteSpace(this.searchText);
private string GetAddChatToWorkspaceTooltip(string workspaceName) => string.Format(T("Start a new chat in workspace '{0}'"), workspaceName);
private async Task<Func<string?, string?>> CreateWorkspaceNameValidationAsync(Guid excludedWorkspaceId = default, string? originalWorkspaceName = null)
{
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
return workspaceName =>
{
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return null;
if (!string.IsNullOrWhiteSpace(originalWorkspaceName) &&
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(originalWorkspaceName), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
return null;
var nameExists = snapshot.Workspaces.Any(workspace =>
workspace.WorkspaceId != excludedWorkspaceId &&
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
return nameExists ? T("There is already a workspace with this name. Please choose a different name.") : null;
};
}
private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot)
{
this.treeItems.Clear();
@ -219,6 +262,109 @@ public partial class Workspaces : MSGComponentBase
};
}
private void BuildSearchTreeItems(WorkspaceSearchSnapshot snapshot)
{
this.treeItems.Clear();
if (snapshot.Workspaces.Count == 0 && snapshot.TemporaryChats.Count == 0)
{
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.NONE,
Text = T("No chats found"),
Icon = Icons.Material.Filled.Search,
Expandable = false,
Path = "search_empty",
},
});
return;
}
if (snapshot.Workspaces.Count > 0)
{
var workspaceChildren = new List<TreeItemData<ITreeItem>>();
foreach (var workspace in snapshot.Workspaces)
workspaceChildren.Add(this.CreateSearchWorkspaceTreeItem(workspace));
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.WORKSPACES,
Text = T("Workspaces"),
Icon = Icons.Material.Filled.Folder,
Expandable = true,
Path = "search_workspaces",
Children = workspaceChildren,
},
});
}
if (snapshot.Workspaces.Count > 0 && snapshot.TemporaryChats.Count > 0)
{
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expandable = false,
Value = new TreeDivider(),
});
}
if (snapshot.TemporaryChats.Count > 0)
{
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
foreach (var temporaryChat in snapshot.TemporaryChats)
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat.Chat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
this.treeItems.Add(new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Depth = 0,
Branch = WorkspaceBranch.TEMPORARY_CHATS,
Text = T("Disappearing Chats"),
Icon = Icons.Material.Filled.Timer,
Expandable = true,
Path = "search_temp",
Children = temporaryChatsChildren,
},
});
}
}
private TreeItemData<ITreeItem> CreateSearchWorkspaceTreeItem(WorkspaceSearchWorkspace workspace)
{
var children = new List<TreeItemData<ITreeItem>>();
foreach (var chat in workspace.Chats)
children.Add(this.CreateChatTreeItem(chat.Chat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
return new TreeItemData<ITreeItem>
{
Expanded = true,
Expandable = true,
Value = new TreeItemData
{
Type = TreeItemType.WORKSPACE,
Depth = 1,
Branch = WorkspaceBranch.WORKSPACES,
Text = workspace.Name,
Icon = Icons.Material.Filled.Description,
Expandable = true,
Path = workspace.WorkspacePath,
Children = children,
},
};
}
private string GetTreeItemIcon(TreeItemData treeItem)
{
if (treeItem.Type is not TreeItemType.CHAT)
@ -233,6 +379,19 @@ public partial class Workspaces : MSGComponentBase
return treeItem.Type is TreeItemType.CHAT && this.AIJobService.IsChatGenerationActive(treeItem.ChatId);
}
private string GetChatTreeItemTextStyle(TreeItemData treeItem)
{
return this.IsCurrentChatTreeItem(treeItem) ? "justify-self: start; font-weight: 700;" : "justify-self: start;";
}
private bool IsCurrentChatTreeItem(TreeItemData treeItem)
{
return treeItem.Type is TreeItemType.CHAT
&& this.CurrentChatThread is not null
&& treeItem.ChatId == this.CurrentChatThread.ChatId
&& treeItem.WorkspaceId == this.CurrentChatThread.WorkspaceId;
}
private string GetChatTreeIcon(Guid chatId, string defaultIcon)
{
var snapshot = this.AIJobService.TryGetChatSnapshot(chatId);
@ -289,6 +448,106 @@ public partial class Workspaces : MSGComponentBase
}
}
public async Task ToggleSearchAsync()
{
var searchVisible = !this.SearchVisible;
this.SearchVisible = searchVisible;
await this.SearchVisibleChanged.InvokeAsync(searchVisible);
if (this.SearchVisible)
{
await this.SafeStateHasChanged();
return;
}
await this.CancelSearchAsync();
this.searchText = string.Empty;
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
}
private async Task CancelSearchAsync()
{
this.searchRevision++;
if (this.searchCancellationTokenSource is not null)
{
await this.searchCancellationTokenSource.CancelAsync();
this.searchCancellationTokenSource.Dispose();
this.searchCancellationTokenSource = null;
}
}
private async Task OnSearchTextChanged(string value)
{
this.searchText = value;
if (string.IsNullOrWhiteSpace(this.searchText))
{
await this.CancelSearchAsync();
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
return;
}
await this.SearchWorkspaceItemsAsync();
}
private async Task IncludeThreadContentsChanged(bool value)
{
this.includeThreadContents = value;
if (this.HasSearchQuery)
await this.SearchWorkspaceItemsAsync();
}
private async Task ClearSearchAsync()
{
this.searchText = string.Empty;
await this.CancelSearchAsync();
this.isSearchRunning = false;
await this.LoadTreeItemsAsync(startPrefetch: false);
}
private async Task SearchWorkspaceItemsAsync()
{
await this.CancelSearchAsync();
var text = this.searchText;
if (string.IsNullOrWhiteSpace(text))
return;
this.searchCancellationTokenSource = new CancellationTokenSource();
var token = this.searchCancellationTokenSource.Token;
var revision = ++this.searchRevision;
this.isSearchRunning = true;
await this.SafeStateHasChanged();
try
{
var snapshot = await WorkspaceBehaviour.SearchWorkspaceChatsAsync(text, this.includeThreadContents, token);
if (this.isDisposed || token.IsCancellationRequested || revision != this.searchRevision)
return;
this.BuildSearchTreeItems(snapshot);
}
catch (OperationCanceledException)
{
// Expected when the user keeps typing or hides the search row.
}
catch (Exception ex)
{
this.Logger.LogWarning(ex, "Failed while searching workspace chats.");
this.BuildSearchTreeItems(new([], []));
}
finally
{
if (revision == this.searchRevision)
{
this.isSearchRunning = false;
await this.SafeStateHasChanged();
}
}
}
private async Task OnWorkspaceClicked(TreeItemData treeItem)
{
if (treeItem.Type is not TreeItemType.WORKSPACE)
@ -494,6 +753,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmColor, Color.Info },
{ x => x.AllowEmptyInput, false },
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync(workspaceId, workspaceName) },
};
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Rename Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
@ -502,9 +762,10 @@ public partial class Workspaces : MSGComponentBase
return;
var alteredWorkspaceName = (dialogResult.Data as string)!;
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8);
await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName);
if (!await WorkspaceBehaviour.RenameWorkspaceAsync(workspaceId, alteredWorkspaceName))
return;
await this.SendMessage(Event.WORKSPACE_RENAMED, workspaceId);
await this.LoadTreeItemsAsync(startPrefetch: false);
}
@ -519,6 +780,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmColor, Color.Info },
{ x => x.AllowEmptyInput, false },
{ x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") },
{ x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync() },
};
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Add Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
@ -526,14 +788,10 @@ public partial class Workspaces : MSGComponentBase
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = Guid.NewGuid();
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
Directory.CreateDirectory(workspacePath);
var workspaceName = (dialogResult.Data as string)!;
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName);
var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(workspaceName);
if (!result.Success)
return;
await this.LoadTreeItemsAsync(startPrefetch: false);
}
@ -578,7 +836,7 @@ public partial class Workspaces : MSGComponentBase
{ x => x.ConfirmText, T("Move chat") },
};
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN_MANUAL_ESCAPE);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
@ -642,6 +900,10 @@ public partial class Workspaces : MSGComponentBase
await this.ForceRefreshFromDiskAsync();
break;
case Event.WORKSPACE_CREATED:
await this.LoadTreeItemsAsync(startPrefetch: false);
break;
case Event.AI_JOB_CHANGED:
case Event.AI_JOB_FINISHED:
case Event.CHAT_GENERATION_CHANGED:
@ -656,9 +918,12 @@ public partial class Workspaces : MSGComponentBase
this.prefetchCancellationTokenSource?.Cancel();
this.prefetchCancellationTokenSource?.Dispose();
this.prefetchCancellationTokenSource = null;
this.searchCancellationTokenSource?.Cancel();
this.searchCancellationTokenSource?.Dispose();
this.searchCancellationTokenSource = null;
base.DisposeResources();
}
#endregion
}
}

View File

@ -7,6 +7,12 @@ public static class DialogOptions
CloseOnEscapeKey = true,
FullWidth = true, MaxWidth = MaxWidth.Medium,
};
public static readonly MudBlazor.DialogOptions FULLSCREEN_MANUAL_ESCAPE = new()
{
CloseOnEscapeKey = false,
FullWidth = true, MaxWidth = MaxWidth.Medium,
};
public static readonly MudBlazor.DialogOptions FULLSCREEN_NO_HEADER = new()
{

View File

@ -34,6 +34,7 @@ public partial class ShortcutDialog : MSGComponentBase
private string currentShortcut = string.Empty;
private string originalShortcut = string.Empty;
private string currentDisplayName = string.Empty;
private string validationMessage = string.Empty;
private Severity validationSeverity = Severity.Info;
private bool hasValidationError;
@ -115,6 +116,7 @@ public partial class ShortcutDialog : MSGComponentBase
{
this.UpdateModifiers(e);
this.currentKey = null;
this.currentDisplayName = string.Empty;
this.UpdateShortcutString();
return;
}
@ -123,10 +125,12 @@ public partial class ShortcutDialog : MSGComponentBase
// Get the key:
this.currentKey = TranslateKeyCode(e.Code);
this.currentDisplayName = this.BuildDisplayShortcut(e.Key);
// Validate: must have at least one modifier + a key
if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta)
{
this.currentDisplayName = string.Empty;
this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).");
this.validationSeverity = Severity.Warning;
this.hasValidationError = true;
@ -216,6 +220,9 @@ public partial class ShortcutDialog : MSGComponentBase
private string GetDisplayShortcut()
{
if (!string.IsNullOrWhiteSpace(this.currentDisplayName))
return this.currentDisplayName;
// Convert internal format to display format:
return this.currentShortcut
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
@ -225,6 +232,7 @@ public partial class ShortcutDialog : MSGComponentBase
private void ClearShortcut()
{
this.currentShortcut = string.Empty;
this.currentDisplayName = string.Empty;
this.currentKey = null;
this.hasCtrl = false;
this.hasShift = false;
@ -237,7 +245,17 @@ public partial class ShortcutDialog : MSGComponentBase
private void Cancel() => this.MudDialog.Cancel();
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut));
private void Confirm()
{
var displaySource = string.IsNullOrWhiteSpace(this.currentDisplayName)
? string.Empty
: this.currentShortcut;
this.MudDialog.Close(DialogResult.Ok(new ShortcutDialogResult(
this.currentShortcut,
this.currentDisplayName,
displaySource)));
}
/// <summary>
/// Checks if the key code represents a modifier key.
@ -377,6 +395,36 @@ public partial class ShortcutDialog : MSGComponentBase
_ => code,
};
private string BuildDisplayShortcut(string? key)
{
var displayKey = GetDisplayKey(key);
if (string.IsNullOrWhiteSpace(displayKey))
return string.Empty;
var parts = new List<string>();
if (this.hasCtrl)
parts.Add(OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
if (this.hasShift)
parts.Add("Shift");
if (this.hasAlt)
parts.Add("Alt");
parts.Add(displayKey);
return string.Join("+", parts);
}
private static string GetDisplayKey(string? key) => key switch
{
null or "" => string.Empty,
" " => "Space",
"Control" or "Shift" or "Alt" or "Meta" => string.Empty,
_ when key.Length == 1 && key[0] >= 'a' && key[0] <= 'z' => key.ToUpperInvariant(),
_ => key,
};
private void HandleBlur()
{
// Re-focus the input field to keep capturing keys:

View File

@ -0,0 +1,3 @@
namespace AIStudio.Dialogs;
public readonly record struct ShortcutDialogResult(string Shortcut, string DisplayName, string DisplaySource);

View File

@ -31,6 +31,9 @@ public partial class SingleInputDialog : MSGComponentBase
[Parameter]
public string EmptyInputErrorMessage { get; set; } = string.Empty;
[Parameter]
public Func<string?, string?>? AdditionalValidation { get; set; }
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private MudForm form = null!;
@ -52,8 +55,8 @@ public partial class SingleInputDialog : MSGComponentBase
{
if (!this.AllowEmptyInput && string.IsNullOrWhiteSpace(value))
return string.IsNullOrWhiteSpace(this.EmptyInputErrorMessage) ? T("Please enter a value.") : this.EmptyInputErrorMessage;
return null;
return this.AdditionalValidation?.Invoke(value);
}
private void Cancel() => this.MudDialog.Cancel();

View File

@ -5,18 +5,57 @@
@this.Message
</MudText>
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
@foreach (var (workspaceName, workspaceId) in this.workspaces)
@foreach (var workspace in this.workspaces)
{
<MudListItem Text="@workspaceName" Icon="@Icons.Material.Filled.Description" Value="@workspaceId" />
<MudListItem Text="@workspace.Name" Icon="@Icons.Material.Filled.Description" Value="@workspace.WorkspaceId" />
}
</MudList>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info">
@this.ConfirmText
</MudButton>
<MudStack Style="width: 100%;" Spacing="2">
<MudDivider/>
@if (this.showCreateWorkspaceForm)
{
<MudForm @ref="this.createWorkspaceForm">
<MudTextField T="string"
@ref="@this.newWorkspaceNameField"
@bind-Text="@this.newWorkspaceName"
Variant="Variant.Outlined"
AutoGrow="@false"
Lines="1"
Label="@T("Workspace name")"
AutoFocus="@true"
Immediate="@true"
Disabled="@this.isCreatingWorkspace"
OnKeyDown="@this.HandleNewWorkspaceNameKeyDown"
Validation="@this.ValidateNewWorkspaceName" />
</MudForm>
}
else
{
<MudButton StartIcon="@Icons.Material.Filled.LibraryAdd" Variant="Variant.Filled" OnClick="@this.ShowCreateWorkspaceForm">
@T("Create new workspace")
</MudButton>
}
<MudStack Row="@true" Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="2">
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (this.showCreateWorkspaceForm)
{
<MudButton OnClick="@this.CreateWorkspaceAsync" Variant="Variant.Filled" Color="Color.Info" Disabled="@this.isCreatingWorkspace">
@T("Add workspace")
</MudButton>
}
else
{
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info" Disabled="@(this.selectedWorkspace == Guid.Empty)">
@this.ConfirmText
</MudButton>
}
</MudStack>
</MudStack>
</DialogActions>
</MudDialog>

View File

@ -1,14 +1,20 @@
using AIStudio.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace AIStudio.Dialogs;
public partial class WorkspaceSelectionDialog : MSGComponentBase
{
private readonly record struct WorkspaceSelectionItem(Guid WorkspaceId, string Name);
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
@ -18,8 +24,18 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
[Parameter]
public string ConfirmText { get; set; } = "OK";
private readonly Dictionary<string, Guid> workspaces = new();
private readonly List<WorkspaceSelectionItem> workspaces = [];
private readonly string escapeHandlerId = $"workspace-selection-dialog-{Guid.NewGuid():N}";
private MudForm? createWorkspaceForm;
private MudTextField<string>? newWorkspaceNameField;
private DotNetObjectReference<WorkspaceSelectionDialog>? dotNetReference;
private Guid selectedWorkspace;
private string newWorkspaceName = string.Empty;
private bool isCreatingWorkspace;
private bool showCreateWorkspaceForm;
private bool shouldFocusNewWorkspaceName;
private string? createWorkspaceError;
private string? createWorkspaceErrorName;
#region Overrides of ComponentBase
@ -29,15 +45,156 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
foreach (var workspace in snapshot.Workspaces)
this.workspaces[workspace.Name] = workspace.WorkspaceId;
this.workspaces.Add(new(workspace.WorkspaceId, workspace.Name));
this.StateHasChanged();
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
this.dotNetReference = DotNetObjectReference.Create(this);
await this.JsRuntime.InvokeVoidAsync("registerEscapeHandler", this.escapeHandlerId, this.dotNetReference);
}
if (this.shouldFocusNewWorkspaceName && this.newWorkspaceNameField is not null)
{
this.shouldFocusNewWorkspaceName = false;
await this.newWorkspaceNameField.FocusAsync();
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private void Cancel() => this.MudDialog.Cancel();
private string? ValidateNewWorkspaceName(string? workspaceName)
{
var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return T("Please enter a workspace name.");
if (this.IsWorkspaceNameExisting(normalizedWorkspaceName))
return T("There is already a workspace with this name. Please choose a different name.");
if (this.createWorkspaceError is not null && string.Equals(this.createWorkspaceErrorName, normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
return this.createWorkspaceError;
return null;
}
private bool IsWorkspaceNameExisting(string normalizedWorkspaceName)
{
return this.workspaces.Any(workspace =>
string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase));
}
private async Task HandleNewWorkspaceNameKeyDown(KeyboardEventArgs keyEvent)
{
var key = keyEvent.Key.ToLowerInvariant();
var code = keyEvent.Code.ToLowerInvariant();
if (key is not "enter" && code is not "enter" and not "numpadenter")
return;
if (keyEvent is { AltKey: true } or { CtrlKey: true } or { MetaKey: true })
return;
await this.CreateWorkspaceAsync();
}
private void ShowCreateWorkspaceForm()
{
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty;
this.showCreateWorkspaceForm = true;
this.shouldFocusNewWorkspaceName = true;
}
private async Task CreateWorkspaceAsync()
{
if (this.createWorkspaceForm is null)
return;
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
await this.createWorkspaceForm.Validate();
if (!this.createWorkspaceForm.IsValid)
return;
this.isCreatingWorkspace = true;
try
{
var result = await WorkspaceBehaviour.TryCreateWorkspaceAsync(this.newWorkspaceName);
if (!result.Success)
{
this.createWorkspaceError = T("There is already a workspace with this name. Please choose a different name.");
this.createWorkspaceErrorName = WorkspaceBehaviour.NormalizeWorkspaceName(this.newWorkspaceName);
await this.createWorkspaceForm.Validate();
return;
}
this.workspaces.Add(new(result.Workspace.WorkspaceId, result.Workspace.Name));
this.selectedWorkspace = result.Workspace.WorkspaceId;
this.newWorkspaceName = string.Empty;
this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false;
await this.SendMessage(Event.WORKSPACE_CREATED, result.Workspace.WorkspaceId);
}
finally
{
this.isCreatingWorkspace = false;
}
}
private void Cancel()
{
if (!this.showCreateWorkspaceForm)
{
this.MudDialog.Cancel();
return;
}
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty;
this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false;
this.shouldFocusNewWorkspaceName = false;
}
[JSInvokable]
public async Task HandleEscapeKeyAsync()
{
await this.InvokeAsync(() =>
{
this.Cancel();
this.StateHasChanged();
});
}
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace));
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
_ = this.JsRuntime.InvokeVoidAsync("unregisterEscapeHandler", this.escapeHandlerId).AsTask();
}
catch
{
// Ignore JS cleanup errors while the dialog is being disposed.
}
this.dotNetReference?.Dispose();
this.dotNetReference = null;
base.DisposeResources();
}
#endregion
}

View File

@ -53,7 +53,6 @@
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.16" />
<PackageReference Include="MudBlazor" Version="8.15.0" />
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
<PackageReference Include="Qdrant.Client" Version="1.18.1" />
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
<PackageReference Include="LuaCSharp" Version="0.5.5" />
</ItemGroup>

View File

@ -51,13 +51,16 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
<MudTooltip Text="@T("Hide your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Size="Size.Medium" Icon="@this.WorkspaceSidebarToggleIcon" Class="me-1" OnClick="@(() => this.ToggleWorkspaceSidebar())"/>
</MudTooltip>
</MudStack>
</HeaderContent>
<ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</ChildContent>
</InnerScrolling>
}
@ -77,10 +80,13 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
</MudStack>
</HeaderContent>
<ChildContent>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</ChildContent>
</InnerScrolling>
}
@ -149,11 +155,14 @@
<MudTooltip Text="@T("Reload your workspaces")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Medium" OnClick="@this.RefreshWorkspaces"/>
</MudTooltip>
<MudTooltip Text="@this.WorkspaceSearchTooltip" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@this.WorkspaceSearchIcon" Size="Size.Medium" OnClick="@this.ToggleWorkspaceSearch"/>
</MudTooltip>
<MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Error" Size="Size.Medium" OnClick="@(() => this.ToggleWorkspacesOverlay())"/>
</MudStack>
</MudDrawerHeader>
<MudDrawerContainer Class="ml-6">
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread"/>
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" @bind-SearchVisible="@this.workspaceSearchVisible"/>
</MudDrawerContainer>
</MudDrawer>
}

View File

@ -23,6 +23,7 @@ public partial class Chat : MSGComponentBase
private ChatThread? chatThread;
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
private bool workspaceOverlayVisible;
private bool workspaceSearchVisible;
private string currentWorkspaceName = string.Empty;
private Workspaces? workspaces;
private double splitterPosition = 30;
@ -51,6 +52,10 @@ public partial class Chat : MSGComponentBase
private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
private string WorkspaceSearchIcon => this.workspaceSearchVisible ? Icons.Material.Filled.SearchOff : Icons.Material.Filled.Search;
private string WorkspaceSearchTooltip => this.workspaceSearchVisible ? T("Hide search") : T("Search your workspaces");
private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|| this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
@ -107,6 +112,14 @@ public partial class Chat : MSGComponentBase
await this.workspaces.ForceRefreshFromDiskAsync();
}
private async Task ToggleWorkspaceSearch()
{
if (this.workspaces is null)
return;
await this.workspaces.ToggleSearchAsync();
}
#region Overrides of MSGComponentBase
protected override void DisposeResources()

View File

@ -34,9 +34,12 @@
<Vision/>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
@if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide)
{
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
}
</MudExpansionPanels>
</InnerScrolling>

View File

@ -71,6 +71,10 @@ public partial class Home : MSGComponentBase
this.InitializeAdvantagesItems();
await this.InvokeAsync(this.StateHasChanged);
break;
case Event.CONFIGURATION_CHANGED:
await this.InvokeAsync(this.StateHasChanged);
break;
}
}

View File

@ -48,6 +48,26 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.AccountCircle" Text="@this.OSUserName"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder">
<div style="display: flex; align-items: center; gap: 8px;">
<MudText Typo="Typo.body1">
@this.WorkingDirectory
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the working directory to the clipboard"))" StringContent="@this.runtimeInfo.WorkingDirectory"/>
</div>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.InsertDriveFile">
<div style="display: flex; align-items: center; gap: 8px;">
<MudText Typo="Typo.body1">
@this.ExecutablePath
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the executable path to the clipboard"))" StringContent="@this.runtimeInfo.ExecutablePath"/>
</div>
</MudListItem>
@if (OperatingSystem.IsLinux())
{
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.LinuxPackageType"/>
}
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
@switch (HasAnyActiveEnvironment)
{
@ -89,18 +109,7 @@
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
}
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
@ -130,41 +139,13 @@
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
continue;
}
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
HeaderText="@matchingPlugin.Name"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
matchingPlugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;")
])"
Items="@this.BuildEnterpriseConfigurationItems(env, matchingPlugin)"
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
}
@ -186,9 +167,32 @@
</MudButton>
}
</MudListItem>
@if (ExternalHttpClientTimeout.CustomRootCertificateState.IsEnabled)
{
<MudListItem T="string" Icon="@Icons.Material.Outlined.Security">
<MudText Typo="Typo.body1">
@(ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable
? T("External HTTPS custom root certificates are active.")
: T("External HTTPS custom root certificates are configured but not active."))
</MudText>
<MudCollapse Expanded="@this.showExternalHttpCustomRootCertificateDetails">
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Security"
HeaderText="@T("External HTTPS custom root certificates")"
Items="@this.BuildExternalHttpCustomRootCertificateItems()"
ShowWarning="@(!ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable)"
WarningText="@this.ExternalHttpCustomRootCertificateWarningText"/>
</MudCollapse>
<MudButton StartIcon="@(this.showExternalHttpCustomRootCertificateDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
Variant="Variant.Text"
OnClick="@this.ToggleExternalHttpCustomRootCertificateDetails">
@(this.showExternalHttpCustomRootCertificateDetails ? T("Hide Details") : T("Show Details"))
</MudButton>
</MudListItem>
}
</MudList>
<MudStack Row="true">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@(() => this.CheckForUpdate())">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@this.CheckForUpdate">
@T("Check for updates")
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" StartIcon="@Icons.Material.Filled.Download" OnClick="@(async () => await this.ShowPandocDialog())">
@ -285,7 +289,7 @@
<ThirdPartyComponent Name="GStreamer" Developer="GStreamer contributors & Open Source Community" LicenseName="LGPL-2.1" LicenseUrl="https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html" RepositoryUrl="https://gitlab.freedesktop.org/gstreamer/gstreamer" UseCase="@T("Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.")"/>
}
<ThirdPartyComponent Name="Qdrant" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
<ThirdPartyComponent Name="Qdrant Edge" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/>
<ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/>
<ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/>
<ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/>
@ -310,7 +314,7 @@
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/>
<ThirdPartyComponent Name="whoami" Developer="Ardaku Systems, Jeryn Aldaron Lau, Chase Johnson & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ardaku/whoami/blob/stable/LICENSE_MIT" RepositoryUrl="https://github.com/ardaku/whoami" UseCase="@T("This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.")"/>
<ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/>
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.")"/>
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders in runtime tests and supporting filesystem operations.")"/>
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" />
<ThirdPartyComponent Name="DeepSeek-V3.2 Tokenizer" Developer="DeepSeek-AI" LicenseName="MIT" LicenseUrl="https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md" RepositoryUrl="https://huggingface.co/deepseek-ai/DeepSeek-V3.2/tree/main" UseCase="@T("We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate.")" />
<ThirdPartyComponent Name="Tokenizer" Developer="Anthony Moi, Nicolas Patry, Pierric Cistac, Arthur Zucker & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/huggingface/tokenizers/blob/main/LICENSE" RepositoryUrl="https://github.com/huggingface/tokenizers" UseCase="@T("The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer.")" />

View File

@ -4,6 +4,7 @@ using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.VectorStore;
using AIStudio.Tools.Metadata;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
@ -41,6 +42,7 @@ public partial class Information : MSGComponentBase
private string osLanguage = string.Empty;
private string osUserName = string.Empty;
private RuntimeInfoResponse runtimeInfo;
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()})";
@ -52,6 +54,20 @@ public partial class Information : MSGComponentBase
private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'";
private string WorkingDirectory => $"{T("Working directory")}: {this.runtimeInfo.WorkingDirectory}";
private string ExecutablePath => $"{T("Executable path")}: {this.runtimeInfo.ExecutablePath}";
private string LinuxPackageType => $"{T("Linux package")}: {this.LinuxPackageTypeDisplayName}";
private string LinuxPackageTypeDisplayName => this.runtimeInfo.LinuxPackageType switch
{
"appimage" => "AppImage",
"flatpak" => "Flatpak",
"unknown" => T("unknown"),
_ => T("not applicable")
};
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
@ -86,6 +102,7 @@ public partial class Information : MSGComponentBase
private bool showEnterpriseConfigDetails;
private bool showVectorStoreDetails;
private bool showExternalHttpCustomRootCertificateDetails;
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
.Where(x => x.Type is PluginType.CONFIGURATION)
@ -145,6 +162,7 @@ public partial class Information : MSGComponentBase
this.osLanguage = await this.RustService.ReadUserLanguage();
this.osUserName = await this.RustService.ReadUserName();
this.runtimeInfo = await this.RustService.GetRuntimeInfo();
this.logPaths = await this.RustService.GetLogPaths();
await this.RefreshVectorStoreInfo(CancellationToken.None);
@ -247,6 +265,11 @@ public partial class Information : MSGComponentBase
{
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
}
private void ToggleExternalHttpCustomRootCertificateDetails()
{
this.showExternalHttpCustomRootCertificateDetails = !this.showExternalHttpCustomRootCertificateDetails;
}
private void ToggleVectorStoreDetails()
{
@ -272,7 +295,7 @@ public partial class Information : MSGComponentBase
}
catch (Exception e)
{
this.vectorStore = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
this.vectorStore = new NoVectorStoreClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
await foreach (var (label, value) in this.vectorStore.GetDisplayInfo().WithCancellation(cancellationToken))
{
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
@ -323,11 +346,132 @@ public partial class Information : MSGComponentBase
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
}
private IReadOnlyList<ConfigInfoRowItem> BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null)
{
var items = new List<ConfigInfoRowItem>
{
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {environment.ConfigurationId}",
environment.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {environment.ConfigurationServerUrl}",
environment.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration source:")} {environment.Source}",
environment.Source,
T("Copies the configuration source to the clipboard"),
"margin-top: 4px;"),
};
if (!string.IsNullOrWhiteSpace(environment.SourceDetail))
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration origin:")} {environment.SourceDetail}",
environment.SourceDetail,
T("Copies the configuration origin to the clipboard"),
"margin-top: 4px;"));
}
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration slot:")} {environment.Slot}",
environment.Slot,
T("Copies the configuration slot to the clipboard"),
"margin-top: 4px;"));
if (plugin is not null)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {plugin.Id}",
plugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;"));
}
return items;
}
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
{
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
}
private string ExternalHttpCustomRootCertificateWarningText
{
get
{
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
return string.IsNullOrWhiteSpace(state.Issue)
? T("The configured root certificates could not be used.")
: state.Issue;
}
}
private IReadOnlyList<ConfigInfoRowItem> BuildExternalHttpCustomRootCertificateItems()
{
var state = ExternalHttpClientTimeout.CustomRootCertificateState;
var items = new List<ConfigInfoRowItem>
{
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Status:")} {(state.IsUsable ? T("active") : T("not active"))}",
state.IsUsable ? T("active") : T("not active"),
T("Copies the status to the clipboard")),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration source:")} {state.Source}",
state.Source,
T("Copies the configuration source to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Certificate bundle:")} {state.BundlePath}",
state.BundlePath,
T("Copies the certificate bundle path to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Loaded root certificates:")} {state.CertificateCount}",
state.CertificateCount.ToString(),
T("Copies the number of loaded root certificates to the clipboard"),
"margin-top: 4px;")
};
if (state.AllowedHostPatterns.Count == 0)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
T("Allowed hosts: none configured"),
string.Empty,
T("Copies the allowed host configuration to the clipboard"),
"margin-top: 4px;"));
}
else
{
foreach (var allowedHostPattern in state.AllowedHostPatterns)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Dns,
$"{T("Allowed host:")} {allowedHostPattern}",
allowedHostPattern,
T("Copies the allowed host pattern to the clipboard"),
"margin-top: 4px;"));
}
}
foreach (var fingerprint in state.CertificateFingerprints)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.Fingerprint,
$"{T("Root certificate fingerprint:")} {fingerprint}",
fingerprint,
T("Copies the root certificate fingerprint to the clipboard"),
"margin-top: 4px;"));
}
return items;
}
protected override void DisposeResources()
{
this.vectorStoreRefreshCancellationTokenSource?.Cancel();

View File

@ -210,12 +210,13 @@ CONFIG["SETTINGS"] = {}
-- but users can still choose another start page in the app settings.
-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true
-- Configure whether the quick start guide is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false
-- Configure the user permission to add providers:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
-- Configure whether administration settings are visible in the UI:
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
-- Configure the visibility of preview features:
@ -270,6 +271,25 @@ CONFIG["SETTINGS"] = {}
-- The default is 3600 (1 hour).
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
-- Configure additional root certificates for external HTTPS requests.
--
-- This is intended for managed Linux/Flatpak deployments where organization-internal
-- HTTPS certificates chain to a private root CA that is not visible inside the sandbox.
-- The file must be a PEM bundle with one or more root CA certificates and must be
-- readable by AI Studio.
--
-- IMPORTANT: A configuration plugin cannot fix the very first download of that same
-- configuration plugin. For bootstrapping enterprise configuration downloads, deploy
-- the equivalent environment variables before AI Studio starts:
--
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;data.example.org
--
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
-- Example chat templates for this configuration:
CONFIG["CHAT_TEMPLATES"] = {}

View File

@ -2172,6 +2172,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Zeigt ode
-- This feature is managed by your organization and has therefore been disabled.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "Diese Funktion wird von Ihrer Organisation verwaltet und wurde daher deaktiviert."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T4285779702"] = "Datei auswählen"
-- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Wählen Sie das minimale Vertrauensniveau, das alle LLM-Anbieter erfüllen müssen. So stellen Sie sicher, dass nur vertrauenswürdige Anbieter verwendet werden. Anbieter, die dieses Niveau unterschreiten, können nicht verwendet werden."
@ -2634,12 +2637,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"]
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "Wie oft sollen wir nach App-Updates suchen?"
-- Additional root certificates are enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1380446131"] = "Zusätzliche Stammzertifikate sind aktiviert"
-- Select preview features
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Vorschaufunktionen auswählen"
-- Your organization provided a default start page, but you can still change it.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Ihre Organisation hat eine Standard-Startseite festgelegt, die Sie jedoch ändern können."
-- Root certificate bundle path
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Pfad zum Stammzertifikatsbundle"
-- Select the desired behavior for the navigation bar.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus."
@ -2694,12 +2703,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"]
-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Wählen Sie aus, welche Seite AI Studio beim Start der App zuerst öffnen soll. Änderungen werden beim nächsten Start von AI Studio wirksam."
-- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Pfad zu einer PEM-Datei mit einem oder mehreren Root-CA-Zertifikaten. Bei Flatpak-Bereitstellungen muss diese Datei an einem Ort abgelegt werden, der innerhalb der Sandbox lesbar ist."
-- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Geben Sie pro Zeile ein Hostmuster ein. Exakte Hosts wie data.intra.example.org sowie Wildcards mit einem Label wie *.intra.example.org werden unterstützt. In AI Studio integrierte Endpunkte von Cloud-Anbietern wie OpenAI, Google usw. verwenden diese zusätzlichen Stammzertifikate nicht."
-- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?"
-- Spellchecking is enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
-- External HTTPS certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "Externe HTTPS-Zertifikate"
-- Allowed hosts for additional root certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3562495752"] = "Zugelassene Hosts für zusätzliche Stammzertifikate"
-- Request timeout
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Zeitüberschreitung bei der Anfrage"
@ -2718,9 +2739,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"]
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Lesen Sie die Enterprise-IT-Dokumentation für die Details."
-- When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3798070907"] = "Wenn diese Option aktiviert ist, kann AI Studio Stammzertifikate aus einem konfigurierten PEM-Bundle für externe HTTPS-Anfragen vertrauen, zum Beispiel für selbst gehostete KI-Anbieter, Embeddings, Transkription, ERI-Datenquellen und das Herunterladen von Unternehmenskonfigurationen. Die üblichen Prüfungen von Hostnamen und Zertifikatsgültigkeit gelten weiterhin. Integrierte Cloud-Anbieter wie OpenAI, Google und andere verwenden diese zusätzlichen Zertifikate niemals. Bitte beachten Sie, dass Sie diese Einstellung unter macOS oder Windows in der Regel nicht benötigen. Wenn Sie Linux mit der AppImage-Version von MindWork AI Studio verwenden, benötigen Sie diese Option ebenfalls nicht. Ein gültiger Anwendungsfall ist eine Linux-Umgebung, in der AI Studio aus einem Flatpak heraus ausgeführt wird."
-- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Rechtschreibprüfung aktivieren?"
-- Additional root certificates are disabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Zusätzliche Stammzertifikate sind deaktiviert"
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?"
@ -2733,6 +2760,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"]
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "Wie lange AI Studio auf externe HTTP-Anfragen wartet, z. B. an KI-Anbieter, Einbettungen, Transkription, ERI-Datenquellen und Downloads von Enterprise-Konfigurationen."
-- Use additional root certificates for external HTTPS requests?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Zusätzliche Stammzertifikate für externe HTTPS-Anfragen verwenden?"
-- Select a root certificate bundle
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Wählen Sie ein Stammzertifikat-Bundle aus"
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste"
@ -3168,15 +3201,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Löschen"
-- Rename Workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Arbeitsbereich umbenennen"
-- Clear search
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Suche zurücksetzen"
-- Rename Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Chat umbenennen"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Arbeitsbereich hinzufügen"
-- Search chats
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Chats durchsuchen"
-- Start a new chat in workspace '{0}'
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Neuen Chat im Arbeitsbereich „{0}“ starten"
-- Add chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Chat hinzufügen"
-- No chats found
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "Keine Chats gefunden"
-- Create Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Chat erstellen"
@ -3213,6 +3258,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Chat in den
-- Please enter a new or edit the name for your workspace '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Bitte geben Sie einen neuen Namen für ihren Arbeitsbereich „{0}“ ein oder bearbeiten Sie ihn:"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "Es gibt bereits einen Arbeitsbereich mit diesem Namen. Bitte wählen Sie einen anderen Namen."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein."
@ -3222,6 +3270,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Umbenennen"
-- Please enter a new or edit the name for your chat '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Bitte geben Sie einen neuen Namen für ihren Chat „{0}“ ein oder bearbeiten Sie ihn:"
-- Search chat contents
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Chat-Inhalte durchsuchen"
-- Load Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Chat laden"
@ -5751,6 +5802,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Aktualisieren v
-- Install later
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Später installieren"
-- Create new workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1541251414"] = "Neuen Arbeitsbereich erstellen"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Arbeitsbereich hinzufügen"
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Name des Arbeitsbereichs"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "Es gibt bereits einen Arbeitsbereich mit diesem Namen. Bitte wählen Sie einen anderen Namen."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Abbrechen"
@ -5928,6 +5994,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag ei
-- Localization
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung"
-- Hide search
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Suche ausblenden"
-- Reload your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Arbeitsbereiche neu laden"
@ -5940,6 +6009,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Chat-Optionen öffnen"
-- Disappearing Chat
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Selbstlöschender Chat"
-- Search your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Arbeitsbereiche durchsuchen"
-- Configure your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Konfigurieren Sie ihre Arbeitsbereiche"
@ -6042,11 +6114,19 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T91074375"] = "Die App ist sowohl für p
-- Startup log file
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startprotokolldatei"
-- The configured root certificates could not be used.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "Die konfigurierten Root-Zertifikate konnten nicht verwendet werden."
-- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an wir freuen uns über ihre Beiträge."
-- The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1132433749"] = "Die TokenizerBibliothek dient als BasisFramework für die Integration des DeepSeekTokenizers."
-- Vector store version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1124039623"] = "Vektordatenbankversion"
-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge ist eine eingebettete Vektordatenbank und ein Vektoraehnlichkeitssuchmaschine. Wir nutzen sie, um lokal RAG retrieval-augmented generation innerhalb von AI Studio zu realisieren. Vielen Dank für die Anstrengungen und die großartige Arbeit, die in Qdrant investiert wurde und weiterhin investiert wird."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die Plugin-ID stimmt nicht mit der ID der Unternehmenskonfiguration überein."
@ -6054,18 +6134,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Kopiert den Ursprung der Konfiguration in die Zwischenablage"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Kopiert den Slot der Konfiguration in die Zwischenablage"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Datenbankversion"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind."
-- Copies the allowed host pattern to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Kopiert das zulässige Hostmuster in die Zwischenablage"
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Warten auf das Konfigurations-Plugin …"
@ -6075,9 +6161,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Geheimnis für d
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind aktiv."
-- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant ist eine Vektordatenbank und Suchmaschine für Vektoren. Wir nutzen Qdrant, um lokales RAG (Retrieval-Augmented Generation) innerhalb von AI Studio zu realisieren. Vielen Dank für den Einsatz und die großartige Arbeit, die in Qdrant gesteckt wurde und weiterhin gesteckt wird."
-- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek."
@ -6090,6 +6173,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstel
-- Consent:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:"
-- Copies the executable path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Kopiert den Pfad der ausführbaren Datei in die Zwischenablage"
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig."
@ -6114,12 +6200,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
-- Copies the number of loaded root certificates to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Kopiert die Anzahl der geladenen Stammzertifikate in die Zwischenablage"
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage"
-- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage"
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "Diese Bibliothek wird verwendet, um temporäre Ordner bei Laufzeittests zu erstellen und Dateisystemoperationen zu unterstützen."
-- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "Diese Bibliothek wird verwendet, um den Dateityp einer Datei zu bestimmen. Das ist zum Beispiel notwendig, wenn wir eine Datei streamen möchten."
@ -6153,6 +6245,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "Installation vom
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pandoc-Version: Pandoc ist nicht installiert oder nicht verfügbar."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "Diese Bibliothek wird verwendet, um die Sprache des Betriebssystems zu erkennen. Dies ist notwendig, um die Sprache der Benutzeroberfläche einzustellen."
@ -6162,8 +6260,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Verwendete Open-
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "Diese Bibliothek wird verwendet, um temporäre Ordner zu erstellen, in denen das Zertifikat und der private Schlüssel für die Kommunikation mit Qdrant gespeichert werden."
-- unknown
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unbekannt"
-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "Dieses Crate stellt Derive-Makros für Rust-Enums bereit, die wir verwenden, um Boilerplate zu reduzieren, wenn wir String-Konvertierungen und Metadaten für Laufzeittypen implementieren. Das ist hilfreich für die Kommunikation zwischen unseren Rust- und .NET-Systemen."
@ -6207,9 +6305,21 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind noch nicht verfügbar."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Kopiert die Quelle der Konfiguration in die Zwischenablage"
-- Copies the root certificate fingerprint to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Kopiert den Fingerabdruck des Stammzertifikats in die Zwischenablage"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
-- External HTTPS custom root certificates are configured but not active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "Externe benutzerdefinierte Stammzertifikate sind konfiguriert, aber nicht aktiv."
-- Vector store
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank"
-- Enterprise configuration ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Unternehmenskonfigurations-ID:"
@ -6222,6 +6332,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen
-- Hide Details
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
-- Linux package
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3196139293"] = "Linux-Paket"
-- External HTTPS custom root certificates are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208455732"] = "Externe Stammzertifikate sind aktiv."
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Der Axum-Server führt den internen Axum-Dienst über eine sichere lokale Verbindung aus. Dadurch kann AI Studio die Kommunikation zwischen der Rust-Laufzeitumgebung und der Benutzeroberfläche schützen."
@ -6234,9 +6350,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Pandoc aktualisi
-- Discover MindWork AI's mission and vision on our official homepage.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3294830584"] = "Entdecken Sie die Mission und Vision von MindWork AI auf unserer offiziellen Homepage."
-- External HTTPS custom root certificates
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3315279770"] = "Externe HTTPS-Stammzertifikate für benutzerdefinierte Zertifizierungsstellen"
-- User-language provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3334355246"] = "Vom Betriebssystem bereitgestellte Sprache"
-- Status:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3396815215"] = "Status:"
-- The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3405978777"] = "Die folgende Liste zeigt die Versionen von MindWork AI Studio und des verwendeten Compilers, den Build-Zeitpunkt und weitere Informationen:"
@ -6255,18 +6377,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwe
-- AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio speichert vertrauliche Daten wie API-Schlüssel im sicheren Speicher Ihres Betriebssystems. Die Bibliothek keyring-core übernimmt dies, indem sie eine Verbindung zum macOS-Schlüsselbund, zur Windows-Anmeldeinformationsverwaltung und zum Linux Secret Service herstellt."
-- Copies the certificate bundle path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3550115021"] = "Kopiert den Pfad des Zertifikat-Bundles in die Zwischenablage"
-- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar"
-- active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "aktiv"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername"
-- Allowed host:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Zulässiger Host:"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Quelle der Konfiguration:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
@ -6276,6 +6410,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "Diese Bibliothek
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Jetzt haben wir mehrere Systeme, einige entwickelt in .NET und andere in Rust. Das Datenformat JSON ist dafür zuständig, Daten zwischen beiden Welten zu übersetzen (dies nennt man Serialisierung und Deserialisierung von Daten). In der Rust-Welt übernimmt Serde diese Aufgabe. Das Pendant in der .NET-Welt ist ein fester Bestandteil von .NET und findet sich in System.Text.Json."
-- not applicable
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T396609403"] = "nicht zutreffend"
-- Copies the allowed host configuration to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3970230163"] = "Kopiert die zulässige Host-Konfiguration in die Zwischenablage"
-- Installed Pandoc version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3983971016"] = "Installierte Pandoc-Version"
@ -6285,8 +6425,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat
-- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank"
-- Allowed hosts: none configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Zulässige Hosts: keine konfiguriert"
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "Diese Bibliothek wird von der Rust-Laufzeitumgebung verwendet, um den Benutzernamen des aktuellen Benutzers auszulesen, z. B. wenn ein von einer Organisation verwalteter ERI-Server den OS-Benutzernamen für die Authentifizierung verwendet."
@ -6297,12 +6437,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek
-- Community & Code
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code"
-- Executable path
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Pfad der ausführbaren Datei"
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "Wir verwenden das HtmlAgilityPack, um Inhalte aus dem Internet zu extrahieren. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
-- Copies the working directory to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4194302113"] = "Kopiert das Arbeitsverzeichnis in die Zwischenablage"
-- Certificate bundle:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4197142390"] = "Zertifikatsbündel:"
-- When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4229014037"] = "Beim Übertragen sensibler Daten zwischen der Rust-Laufzeitumgebung und der .NET-Anwendung verschlüsseln wir die Daten. Dafür verwenden wir einige Bibliotheken aus dem Rust Crypto-Projekt: cipher, aes, cbc, pbkdf2, hmac und sha2. Wir sind dankbar für die großartige Arbeit des Rust Crypto-Projekts."
-- Copies the status to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Kopiert den Status in die Zwischenablage"
-- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bibliothek, die die Grundlagen für asynchrones Programmieren in Rust bereitstellt. Sie enthält zentrale Trait-Definitionen wie Stream sowie Hilfsfunktionen wie join!, select! und verschiedene Methoden zur Kombination von Futures, die einen ausdrucksstarken asynchronen Kontrollfluss ermöglichen."
@ -6314,6 +6466,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "Wir verwenden den
-- starting
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "wird gestartet"
-- Root certificate fingerprint:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T615041128"] = "Fingerabdruck des Stammzertifikats:"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden."
@ -6323,6 +6478,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Haben Sie einen F
-- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T64689067"] = "Diese Rust-Bibliothek wird verwendet, um die Nachrichten der App im Terminal auszugeben. Das ist während der Entwicklung und Fehlersuche hilfreich. Diese Funktion ist zunächst unsichtbar; werden App über das Terminal gestartet, werden die Nachrichten sichtbar."
-- not active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T70364248"] = "nicht aktiv"
-- Loaded root certificates:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T709525418"] = "Geladene Stammzertifikate:"
-- Working directory
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T768480635"] = "Arbeitsverzeichnis"
-- Copies the config ID to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Kopiert die Konfigurations-ID in die Zwischenablage"
@ -7013,20 +7177,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Grund"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP-Port"
-- Starting
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starten"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T3662391977"] = "Nicht verfügbar"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße"
-- Number of vector stores
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Anzahl der Vektordatenbanken"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Gemeldete Version"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Gemeldete Version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC-Port"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Anzahl der Collections"
-- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge ist nicht verfügbar."
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "Die zugehörigen Daten dürfen an keinen LLM-Anbieter gesendet werden. Das bedeutet, dass diese Datenquelle momentan nicht verwendet werden kann."
@ -7139,6 +7315,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Fehler
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben."
-- No certificate bundle path is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1033171304"] = "Es ist kein Pfad für das Zertifikats-Bundle konfiguriert."
-- app settings
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1736441001"] = "App-Einstellungen"
-- environment variables
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T317663851"] = "Umgebungsvariablen"
-- configuration plugin
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3427095600"] = "Konfigurations-Plugin"
-- The configured certificate bundle file does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Die konfigurierte Zertifikats-Bundle-Datei existiert nicht."
-- The configured certificate bundle does not contain usable root CA certificates.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das konfigurierte Zertifikats-Bundle enthält keine verwendbaren Root-CA-Zertifikate."
-- AI Studio couldn't install Pandoc because the archive was not found.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde."
@ -7658,6 +7852,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Benutzerdefi
-- Media
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Medien"
-- Certificate bundle
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Zertifikatsbündel"
-- Source like prefix
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code ähnlicher Prefix"

View File

@ -2172,6 +2172,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Shows and
-- This feature is managed by your organization and has therefore been disabled.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "This feature is managed by your organization and has therefore been disabled."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONFILE::T4285779702"] = "Choose File"
-- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level."
@ -2634,12 +2637,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"]
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"
-- Additional root certificates are enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1380446131"] = "Additional root certificates are enabled"
-- Select preview features
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Select preview features"
-- Your organization provided a default start page, but you can still change it.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Your organization provided a default start page, but you can still change it."
-- Root certificate bundle path
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path"
-- Select the desired behavior for the navigation bar.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar."
@ -2694,12 +2703,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"]
-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio."
-- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox."
-- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates."
-- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
-- Spellchecking is enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
-- External HTTPS certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "External HTTPS certificates"
-- Allowed hosts for additional root certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3562495752"] = "Allowed hosts for additional root certificates"
-- Request timeout
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
@ -2718,9 +2739,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"]
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
-- When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3798070907"] = "When enabled, AI Studio can trust root certificates from a configured PEM bundle for external HTTPS requests, such as self-hosted AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads. Normal hostname and certificate validity checks still apply. Integrated cloud providers, such as OpenAI, Google, and others, will never use these additional certificates. Please note that you usually do not need this setting on macOS or Windows. If you use Linux with the AppImage version of MindWork AI Studio, you also do not need this option. A valid use case is a Linux environment where AI Studio runs from a Flatpak."
-- Enable spellchecking?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"] = "Enable spellchecking?"
-- Additional root certificates are disabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Additional root certificates are disabled"
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Preselect one of your profiles?"
@ -2733,6 +2760,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"]
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
-- Use additional root certificates for external HTTPS requests?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?"
-- Select a root certificate bundle
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle"
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
@ -3168,15 +3201,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete"
-- Rename Workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Rename Workspace"
-- Clear search
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Clear search"
-- Rename Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Rename Chat"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Add workspace"
-- Search chats
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Search chats"
-- Start a new chat in workspace '{0}'
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Start a new chat in workspace '{0}'"
-- Add chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Add chat"
-- No chats found
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "No chats found"
-- Create Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Create Chat"
@ -3213,6 +3258,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to
-- Please enter a new or edit the name for your workspace '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter a new or edit the name for your workspace '{0}':"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is already a workspace with this name. Please choose a different name."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name."
@ -3222,6 +3270,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename"
-- Please enter a new or edit the name for your chat '{0}':
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Please enter a new or edit the name for your chat '{0}':"
-- Search chat contents
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Search chat contents"
-- Load Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Load Chat"
@ -5751,6 +5802,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Update from v{0
-- Install later
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Install later"
-- Create new workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1541251414"] = "Create new workspace"
-- Add workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Add workspace"
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Workspace name"
-- There is already a workspace with this name. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "There is already a workspace with this name. Please choose a different name."
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Please enter a workspace name."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel"
@ -5928,6 +5994,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co
-- Localization
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
-- Hide search
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Hide search"
-- Reload your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces"
@ -5940,6 +6009,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Open Chat Options"
-- Disappearing Chat
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Disappearing Chat"
-- Search your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Search your workspaces"
-- Configure your workspaces
UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Configure your workspaces"
@ -6042,11 +6114,19 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T91074375"] = "The app is free to use, b
-- Startup log file
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startup log file"
-- The configured root certificates could not be used.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "The configured root certificates could not be used."
-- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions."
-- The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1132433749"] = "The Tokenizer library serves as the base framework for integrating the DeepSeek tokenizer."
-- Vector store version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1124039623"] = "Vector store version"
-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant."
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the plugin ID differs from the enterprise configuration ID."
@ -6054,18 +6134,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Database version"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
-- Copies the allowed host pattern to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Copies the allowed host pattern to the clipboard"
-- Waiting for the configuration plugin...
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the configuration plugin..."
@ -6075,9 +6161,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secre
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active."
-- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant."
-- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library."
@ -6090,6 +6173,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create
-- Consent:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
-- Copies the executable path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the executable path to the clipboard"
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."
@ -6114,12 +6200,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is
-- Encryption secret: is configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
-- Copies the number of loaded root certificates to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard"
-- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
-- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file."
@ -6153,6 +6245,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@ -6162,8 +6260,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
-- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant."
-- unknown
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unknown"
-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems."
@ -6207,9 +6305,21 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
-- Copies the root certificate fingerprint to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Copies the root certificate fingerprint to the clipboard"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
-- External HTTPS custom root certificates are configured but not active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
-- Vector store
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store"
-- Enterprise configuration ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:"
@ -6222,6 +6332,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
-- Hide Details
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
-- Linux package
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3196139293"] = "Linux package"
-- External HTTPS custom root certificates are active.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208455732"] = "External HTTPS custom root certificates are active."
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface."
@ -6234,9 +6350,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
-- Discover MindWork AI's mission and vision on our official homepage.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3294830584"] = "Discover MindWork AI's mission and vision on our official homepage."
-- External HTTPS custom root certificates
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3315279770"] = "External HTTPS custom root certificates"
-- User-language provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3334355246"] = "User-language provided by the OS"
-- Status:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3396815215"] = "Status:"
-- The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3405978777"] = "The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:"
@ -6255,18 +6377,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to
-- AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating systems secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service."
-- Copies the certificate bundle path to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3550115021"] = "Copies the certificate bundle path to the clipboard"
-- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
-- active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
-- Allowed host:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Allowed host:"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
@ -6276,6 +6410,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
-- not applicable
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T396609403"] = "not applicable"
-- Copies the allowed host configuration to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3970230163"] = "Copies the allowed host configuration to the clipboard"
-- Installed Pandoc version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3983971016"] = "Installed Pandoc version"
@ -6285,8 +6425,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins
-- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
-- Allowed hosts: none configured
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured"
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication."
@ -6297,12 +6437,24 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is
-- Community & Code
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code"
-- Executable path
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
-- Copies the working directory to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4194302113"] = "Copies the working directory to the clipboard"
-- Certificate bundle:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4197142390"] = "Certificate bundle:"
-- When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4229014037"] = "When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project."
-- Copies the status to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Copies the status to the clipboard"
-- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow."
@ -6314,6 +6466,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "We use the DeepSe
-- starting
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
-- Root certificate fingerprint:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T615041128"] = "Root certificate fingerprint:"
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
@ -6323,6 +6478,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bu
-- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T64689067"] = "This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."
-- not active
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T70364248"] = "not active"
-- Loaded root certificates:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T709525418"] = "Loaded root certificates:"
-- Working directory
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T768480635"] = "Working directory"
-- Copies the config ID to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Copies the config ID to the clipboard"
@ -7013,20 +7177,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Reason"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
-- Starting
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starting"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T3662391977"] = "Unavailable"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- Number of vector stores
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Number of vector stores"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
-- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available."
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."
@ -7139,6 +7315,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
-- No certificate bundle path is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1033171304"] = "No certificate bundle path is configured."
-- app settings
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T1736441001"] = "app settings"
-- environment variables
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T317663851"] = "environment variables"
-- configuration plugin
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3427095600"] = "configuration plugin"
-- The configured certificate bundle file does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "The configured certificate bundle file does not exist."
-- The configured certificate bundle does not contain usable root CA certificates.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates."
-- AI Studio couldn't install Pandoc because the archive was not found.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
@ -7658,6 +7852,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Custom"
-- Media
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Media"
-- Certificate bundle
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate bundle"
-- Source like prefix
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix"

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.AlibabaCloud;
public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/", LOGGER)
public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_CLOUD, new Uri("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderAlibabaCloud> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAlibabaCloud>();

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic;
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "https://api.anthropic.com/v1/", LOGGER)
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, new Uri("https://api.anthropic.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderAnthropic> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAnthropic>();

View File

@ -29,7 +29,7 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <summary>
/// The HTTP client to use it for all requests.
/// </summary>
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient();
protected readonly HttpClient HttpClient;
/// <summary>
/// The logger to use.
@ -65,21 +65,26 @@ public abstract class BaseProvider : IProvider, ISecretId
/// Constructor for the base provider.
/// </summary>
/// <param name="provider">The provider enum value.</param>
/// <param name="url">The base URL for the provider.</param>
/// <param name="baseUri">The base URI for the provider.</param>
/// <param name="trustPolicy">The trust policy for external HTTPS requests to this provider.</param>
/// <param name="logger">The logger to use.</param>
protected BaseProvider(LLMProviders provider, string url, ILogger logger)
protected BaseProvider(LLMProviders provider, Uri baseUri, ExternalHttpTrustPolicy trustPolicy, ILogger logger)
{
this.logger = logger;
this.Provider = provider;
// Set the base URL:
this.HttpClient.BaseAddress = new(url);
this.BaseUri = baseUri;
this.HttpClient = ExternalHttpClientTimeout.CreateHttpClient(baseUri, trustPolicy);
}
#region Handling of IProvider, which all providers must implement
/// <inheritdoc />
public LLMProviders Provider { get; }
/// <summary>
/// The base URI for all relative provider requests.
/// </summary>
public Uri BaseUri { get; }
/// <inheritdoc />
public abstract string Id { get; }

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.DeepSeek;
public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "https://api.deepseek.com/", LOGGER)
public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, new Uri("https://api.deepseek.com/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderDeepSeek> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderDeepSeek>();

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks;
public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https://api.fireworks.ai/inference/v1/", LOGGER)
public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri("https://api.fireworks.ai/inference/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderFireworks> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderFireworks>();

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.GWDG;
public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://chat-ai.academiccloud.de/v1/", LOGGER)
public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("https://chat-ai.academiccloud.de/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderGWDG> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderGWDG>();

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Google;
public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://generativelanguage.googleapis.com/v1beta/openai/", LOGGER)
public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https://generativelanguage.googleapis.com/v1beta/openai/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderGoogle> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderGoogle>();

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Groq;
public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.com/openai/v1/", LOGGER)
public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://api.groq.com/openai/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderGroq> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderGroq>();

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Helmholtz;
public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "https://api.helmholtz-blablador.fz-juelich.de/v1/", LOGGER)
public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, new Uri("https://api.helmholtz-blablador.fz-juelich.de/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderHelmholtz> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderHelmholtz>();

View File

@ -10,7 +10,7 @@ public sealed class ProviderHuggingFace : BaseProvider
{
private static readonly ILogger<ProviderHuggingFace> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderHuggingFace>();
public ProviderHuggingFace(HFInferenceProvider hfProvider, Model model) : base(LLMProviders.HUGGINGFACE, $"https://router.huggingface.co/{hfProvider.Endpoints(model)}", LOGGER)
public ProviderHuggingFace(HFInferenceProvider hfProvider, Model model) : base(LLMProviders.HUGGINGFACE, new Uri($"https://router.huggingface.co/{hfProvider.Endpoints(model)}"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus we use the base URL 'https://router.huggingface.co/{hfProvider.Endpoints(model)}'.");
}

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Mistral;
public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "https://api.mistral.ai/v1/", LOGGER)
public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new Uri("https://api.mistral.ai/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderMistral> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderMistral>();

View File

@ -13,7 +13,7 @@ namespace AIStudio.Provider.OpenAI;
/// <summary>
/// The OpenAI provider.
/// </summary>
public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https://api.openai.com/v1/", LOGGER)
public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Uri("https://api.openai.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>();

View File

@ -7,7 +7,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.OpenRouter;
public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER, "https://openrouter.ai/api/v1/", LOGGER)
public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER, new Uri("https://openrouter.ai/api/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private const string PROJECT_WEBSITE = "https://github.com/MindWorkAI/AI-Studio";
private const string PROJECT_NAME = "MindWork AI Studio";

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Perplexity;
public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, "https://api.perplexity.ai/", LOGGER)
public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, new Uri("https://api.perplexity.ai/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderPerplexity> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderPerplexity>();

View File

@ -8,7 +8,7 @@ using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider.SelfHosted;
public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvider(LLMProviders.SELF_HOSTED, $"{hostname}{host.BaseURL()}", LOGGER)
public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvider(LLMProviders.SELF_HOSTED, new Uri($"{hostname}{host.BaseURL()}"), ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED, LOGGER)
{
private static readonly ILogger<ProviderSelfHosted> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderSelfHosted>();

View File

@ -6,7 +6,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.X;
public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai/v1/", LOGGER)
public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://api.x.ai/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderX> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderX>();

View File

@ -57,6 +57,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME);
/// <summary>
/// Should the quick start guide be visible on the home page?
/// </summary>
public bool ShowQuickStartGuide { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowQuickStartGuide, true);
/// <summary>
/// The visibility setting for previews features.
/// </summary>
@ -94,11 +99,36 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
/// <summary>
/// The user-facing label for the voice recording shortcut, based on the user's keyboard layout.
/// </summary>
public string ShortcutVoiceRecordingDisplayName { get; set; } = string.Empty;
/// <summary>
/// The canonical voice recording shortcut value this display label belongs to.
/// </summary>
public string ShortcutVoiceRecordingDisplaySource { get; set; } = string.Empty;
/// <summary>
/// The HTTP timeout in seconds for external HTTP clients.
/// </summary>
public int HttpClientTimeoutSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HttpClientTimeoutSeconds, ExternalHttpClientTimeout.DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS);
/// <summary>
/// Should external HTTP clients trust additional root certificates from a configured PEM bundle?
/// </summary>
public bool ExternalHttpCustomRootCertificatesEnabled { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificatesEnabled, false);
/// <summary>
/// Path to a PEM bundle containing additional root certificates for external HTTP clients.
/// </summary>
public string ExternalHttpCustomRootCertificateBundlePath { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificateBundlePath, string.Empty);
/// <summary>
/// Hostnames for which external HTTP clients may use the additional root certificates.
/// </summary>
public HashSet<string> ExternalHttpCustomRootCertificateAllowedHosts { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificateAllowedHosts, []);
/// <summary>
/// Should the user be allowed to add providers?
/// </summary>

View File

@ -91,17 +91,10 @@ public sealed partial class DatabaseClientProvider(RustService rustService, ILog
private async Task<DatabaseClient> CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch
{
DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken),
DatabaseRole.VECTOR_STORE => await QdrantEdgeClientImplementation.CreateAsync(rustService, this.logger, this.databaseClientLogger, cancellationToken),
_ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.")
};
private NoDatabaseClient CreateNoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status)
{
var client = new NoDatabaseClient(name, unavailableReason, status);
client.SetLogger(this.databaseClientLogger);
return client;
}
private static bool IsSameClient(DatabaseClient left, DatabaseClient right) =>
left.IsAvailable
&& right.IsAvailable

View File

@ -2,14 +2,6 @@
public interface IVectorStoreClient
{
string Name { get; }
DatabaseClientStatus Status { get; }
bool IsAvailable { get; }
IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo();
Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token);
Task InsertEmbedding(string storeName, IReadOnlyList<VectorStoragePoint> points, CancellationToken token);

View File

@ -2,19 +2,19 @@ using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases.VectorStore;
public sealed class NoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : IVectorStoreClient
public sealed class NoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : DatabaseClient(name, string.Empty), IVectorStoreClient
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoVectorStoreClient).Namespace, nameof(NoVectorStoreClient));
public string Name => name;
public override DatabaseClientStatus Status => status;
public DatabaseClientStatus Status => status;
public bool IsAvailable => false;
public async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("Status"), TB("Unavailable"));
yield return (TB("Status"), status switch
{
DatabaseClientStatus.STARTING => TB("Starting"),
_ => TB("Unavailable")
});
if (!string.IsNullOrWhiteSpace(unavailableReason))
yield return (TB("Reason"), unavailableReason);
@ -36,4 +36,8 @@ public sealed class NoVectorStoreClient(string name, string? unavailableReason,
private InvalidOperationException CreateUnavailableException() =>
new(unavailableReason ?? "The vector store is not available.");
public override void Dispose()
{
}
}

View File

@ -0,0 +1,115 @@
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.Databases.VectorStore;
public sealed class QdrantEdgeClientImplementation(
string name,
string path,
string version,
int storesCount,
RustService rustService) : DatabaseClient(name, path), IVectorStoreClient
{
private const string DATABASE_NAME = "Qdrant Edge";
private const string INFO_PATH = "/system/qdrant-edge/info";
private const string ENSURE_PATH = "/system/qdrant-edge/ensure";
private const string INSERT_PATH = "/system/qdrant-edge/insert";
private const string DELETE_FILE_PATH = "/system/qdrant-edge/delete-file";
private const string DELETE_STORE_PATH = "/system/qdrant-edge/delete-store";
private readonly string path = path;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(QdrantEdgeClientImplementation).Namespace, nameof(QdrantEdgeClientImplementation));
public override string CacheKey => $"{this.Name}:{this.path}:{version}";
public static async Task<DatabaseClient> CreateAsync(
RustService rustService,
ILogger logger,
ILogger<DatabaseClient> databaseClientLogger,
CancellationToken cancellationToken)
{
var qdrantEdgeInfo = await rustService.GetDatabaseInfo(
DATABASE_NAME,
INFO_PATH,
QdrantEdgeInfo.Unavailable,
cancellationToken);
if (qdrantEdgeInfo.Status is QdrantEdgeStatus.STARTING)
{
return CreateNoVectorStoreClient(
DATABASE_NAME,
$"{DATABASE_NAME} is starting. Details will appear shortly.",
DatabaseClientStatus.STARTING,
databaseClientLogger);
}
if (!qdrantEdgeInfo.IsAvailable || qdrantEdgeInfo.Status is QdrantEdgeStatus.UNAVAILABLE)
{
var reason = qdrantEdgeInfo.UnavailableReason ?? "unknown";
// ReSharper disable DuplicateItemInLoggerTemplate
logger.LogWarning("{VectorStoreName} is not available. Starting without {VectorStoreName} vector store. Reason: '{Reason}'.", DATABASE_NAME, DATABASE_NAME, reason);
// ReSharper restore DuplicateItemInLoggerTemplate
return CreateNoVectorStoreClient(DATABASE_NAME, qdrantEdgeInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE, databaseClientLogger);
}
if (qdrantEdgeInfo.Path == string.Empty)
return CreateNoVectorStoreClient(DATABASE_NAME, $"Failed to get the {DATABASE_NAME} path from Rust.", DatabaseClientStatus.UNAVAILABLE, databaseClientLogger);
var name = string.IsNullOrWhiteSpace(qdrantEdgeInfo.Name) ? DATABASE_NAME : qdrantEdgeInfo.Name;
var client = new QdrantEdgeClientImplementation(name, qdrantEdgeInfo.Path, qdrantEdgeInfo.Version, qdrantEdgeInfo.StoresCount, rustService);
client.SetLogger(databaseClientLogger);
return client;
}
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
var currentInfo = await rustService.GetDatabaseInfo(
DATABASE_NAME,
INFO_PATH,
QdrantEdgeInfo.Unavailable);
var displayVersion = currentInfo.IsAvailable && !string.IsNullOrWhiteSpace(currentInfo.Version) ? currentInfo.Version : version;
var displayStoresCount = currentInfo.IsAvailable ? currentInfo.StoresCount : storesCount;
if (!currentInfo.IsAvailable)
yield return (TB("Status"), currentInfo.UnavailableReason ?? TB("Qdrant Edge is not available."));
yield return (TB("Reported version"), displayVersion);
yield return (TB("Storage size"), $"{this.GetStorageSize()}");
yield return (TB("Number of vector stores"), displayStoresCount.ToString());
}
public Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, ENSURE_PATH, new EnsureVectorStoreRequest(storeName, vectorSize), token);
public Task InsertEmbedding(string storeName, IReadOnlyList<VectorStoragePoint> points, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, INSERT_PATH, new InsertEmbeddingRequest(storeName, points), token);
public Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_FILE_PATH, new DeleteEmbeddingByFileRequest(storeName, filePath), token);
public Task DeleteVectorStore(string storeName, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_STORE_PATH, new DeleteVectorStoreRequest(storeName), token);
public override void Dispose()
{
}
private static NoVectorStoreClient CreateNoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status, ILogger<DatabaseClient> databaseClientLogger)
{
var client = new NoVectorStoreClient(name, unavailableReason, status);
client.SetLogger(databaseClientLogger);
return client;
}
// ReSharper disable NotAccessedPositionalProperty.Local
private sealed record EnsureVectorStoreRequest(string StoreName, int VectorSize);
private sealed record InsertEmbeddingRequest(string StoreName, IReadOnlyList<VectorStoragePoint> Points);
private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath);
private sealed record DeleteVectorStoreRequest(string StoreName);
// ReSharper restore NotAccessedPositionalProperty.Local
}

View File

@ -0,0 +1,16 @@
namespace AIStudio.Tools.Databases.VectorStore;
public sealed record VectorStoragePoint(
string PointId,
IReadOnlyList<float> Vector,
string DataSourceId,
string DataSourceName,
string DataSourceType,
string FilePath,
string FileName,
string RelativePath,
int ChunkIndex,
string Text,
string Fingerprint,
DateTime LastWriteUtc,
DateTime EmbeddedAtUtc);

View File

@ -23,7 +23,7 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
}
};
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(new Uri($"{dataSource.Hostname}:{dataSource.Port}"));
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(new Uri($"{dataSource.Hostname}:{dataSource.Port}"), ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
protected string SecurityToken = string.Empty;

View File

@ -2,7 +2,7 @@ using System.Net.Http.Headers;
namespace AIStudio.Tools;
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, EntityTagHeaderValue? ETag)
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, string Source, string SourceDetail, string Slot, EntityTagHeaderValue? ETag)
{
public bool IsActive => !string.IsNullOrWhiteSpace(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty;
}

View File

@ -155,6 +155,16 @@ public enum Event
/// Requests the chat workspace overlay to be toggled.
/// </summary>
WORKSPACE_TOGGLE_OVERLAY,
/// <summary>
/// Notifies receivers that a workspace was renamed.
/// </summary>
WORKSPACE_RENAMED,
/// <summary>
/// Notifies receivers that a workspace was created.
/// </summary>
WORKSPACE_CREATED,

View File

@ -1,3 +1,7 @@
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using AIStudio.Settings;
namespace AIStudio.Tools;
@ -12,15 +16,38 @@ public static class ExternalHttpClientTimeout
public const int MAX_HTTP_CLIENT_TIMEOUT_SECONDS = 3600;
public const int DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS = 3600;
private static readonly Lazy<SettingsManager> SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>());
private const string ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED";
private const string ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH";
private const string ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS";
public static HttpClient CreateHttpClient(Uri? baseAddress = null)
// id-kp-serverAuth: Extended Key Usage for TLS server authentication.
// See RFC 5280, section 4.2.1.12: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12
private const string TLS_SERVER_AUTHENTICATION_EKU_OID = "1.3.6.1.5.5.7.3.1";
private static string TB(string fallbackEN) => PluginSystem.I18N.I.T(fallbackEN, typeof(ExternalHttpClientTimeout).Namespace, nameof(ExternalHttpClientTimeout));
private static readonly Lazy<ILogger> LOGGER = new(() => Program.LOGGER_FACTORY.CreateLogger(nameof(ExternalHttpClientTimeout)));
private static readonly Lazy<SettingsManager> SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>());
private static readonly Lock CUSTOM_ROOT_CERTIFICATE_LOCK = new();
private static CustomRootCertificateCache? CUSTOM_ROOT_CERTIFICATE_CACHE;
public static HttpClient CreateHttpClient(ExternalHttpTrustPolicy trustPolicy) => CreateHttpClient(null, trustPolicy);
public static HttpClient CreateHttpClient(Uri? baseAddress, ExternalHttpTrustPolicy trustPolicy)
{
var httpClient = new HttpClient();
var customRootCertificateCache = GetCustomRootCertificateCache();
var httpClient = customRootCertificateCache.State.IsUsable
? new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (request, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificateWithCustomRootCertificates(request, certificate, chain, sslPolicyErrors, customRootCertificateCache, trustPolicy)
})
: new HttpClient();
Configure(httpClient, baseAddress);
return httpClient;
}
public static ExternalHttpCustomRootCertificateState CustomRootCertificateState => GetCustomRootCertificateCache().State;
public static string GetTimeoutDescription()
{
var timeout = GetTimeout();
@ -78,4 +105,364 @@ public static class ExternalHttpClientTimeout
if (baseAddress is not null)
httpClient.BaseAddress = baseAddress;
}
private static CustomRootCertificateCache GetCustomRootCertificateCache()
{
var configuration = ReadCustomRootCertificateConfiguration();
var cacheKey = $"{configuration.Enabled}|{configuration.BundlePath}|{string.Join(";", configuration.AllowedHostPatterns)}|{ReadCertificateBundleFileSignature(configuration.BundlePath)}";
lock (CUSTOM_ROOT_CERTIFICATE_LOCK)
{
if (CUSTOM_ROOT_CERTIFICATE_CACHE is not null && CUSTOM_ROOT_CERTIFICATE_CACHE.CacheKey == cacheKey)
return CUSTOM_ROOT_CERTIFICATE_CACHE;
CUSTOM_ROOT_CERTIFICATE_CACHE = LoadCustomRootCertificateCache(cacheKey, configuration);
LogCustomRootCertificateState(CUSTOM_ROOT_CERTIFICATE_CACHE.State);
return CUSTOM_ROOT_CERTIFICATE_CACHE;
}
}
private static CustomRootCertificateConfiguration ReadCustomRootCertificateConfiguration()
{
var envEnabled = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED);
var envBundlePath = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH);
var envAllowedHosts = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS);
var enabled = TryParseBooleanEnvironmentValue(envEnabled, out var parsedEnvEnabled)
? parsedEnvEnabled
: SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled;
var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath)
? envBundlePath.Trim()
: SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim();
var allowedHostPatterns = ReadAllowedHostPatterns(envAllowedHosts);
var source = ReadCustomRootCertificateConfigurationSource(envEnabled, envBundlePath, envAllowedHosts);
return new(enabled, bundlePath, allowedHostPatterns, source);
}
private static string ReadCustomRootCertificateConfigurationSource(string? envEnabled, string? envBundlePath, string? envAllowedHosts)
{
if (!string.IsNullOrWhiteSpace(envEnabled) || !string.IsNullOrWhiteSpace(envBundlePath) || !string.IsNullOrWhiteSpace(envAllowedHosts))
return TB("environment variables");
var enabledIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var enabledMeta) && enabledMeta.IsLocked;
var bundlePathIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var bundlePathMeta) && bundlePathMeta.IsLocked;
var allowedHostsIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var allowedHostsMeta) && allowedHostsMeta.IsLocked;
return enabledIsManaged || bundlePathIsManaged || allowedHostsIsManaged
? TB("configuration plugin")
: TB("app settings");
}
private static IReadOnlyList<string> ReadAllowedHostPatterns(string? envAllowedHosts)
{
IEnumerable<string> rawPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts)
? envAllowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
: SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts;
var patterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var rawPattern in rawPatterns)
{
if (TryNormalizeAllowedHostPattern(rawPattern, out var pattern))
patterns.Add(pattern);
else
LOGGER.Value.LogWarning($"Ignoring invalid external HTTP custom root certificate host pattern: '{rawPattern}'.");
}
return patterns.Order(StringComparer.OrdinalIgnoreCase).ToList();
}
private static bool TryNormalizeAllowedHostPattern(string? rawPattern, out string pattern)
{
pattern = string.Empty;
if (string.IsNullOrWhiteSpace(rawPattern))
return false;
var normalized = rawPattern.Trim().TrimEnd('.').ToLowerInvariant();
if (normalized.Contains("://", StringComparison.Ordinal) || normalized.Contains('/', StringComparison.Ordinal) || normalized.Contains(':', StringComparison.Ordinal))
return false;
if (normalized.StartsWith("*.", StringComparison.Ordinal))
{
var suffix = normalized[2..];
if (!IsValidDnsHost(suffix))
return false;
pattern = $"*.{suffix}";
return true;
}
if (normalized.Contains('*', StringComparison.Ordinal))
return false;
if (!IsValidDnsHost(normalized))
return false;
pattern = normalized;
return true;
}
private static bool IsValidDnsHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return false;
if (Uri.CheckHostName(host) is not UriHostNameType.Dns)
return false;
return host.Split('.').All(label => !string.IsNullOrWhiteSpace(label) && !label.StartsWith('-') && !label.EndsWith('-'));
}
private static string ReadCertificateBundleFileSignature(string bundlePath)
{
if (string.IsNullOrWhiteSpace(bundlePath))
return string.Empty;
try
{
var fileInfo = new FileInfo(bundlePath);
return fileInfo.Exists
? $"{fileInfo.Length}|{fileInfo.LastWriteTimeUtc.Ticks}"
: "missing";
}
catch
{
return "unavailable";
}
}
private static bool TryParseBooleanEnvironmentValue(string? value, out bool parsedValue)
{
parsedValue = false;
if (string.IsNullOrWhiteSpace(value))
return false;
var normalized = value.Trim();
if (bool.TryParse(normalized, out parsedValue))
return true;
if (normalized is "1" || normalized.Equals("yes", StringComparison.OrdinalIgnoreCase) || normalized.Equals("on", StringComparison.OrdinalIgnoreCase))
{
parsedValue = true;
return true;
}
if (normalized is "0" || normalized.Equals("no", StringComparison.OrdinalIgnoreCase) || normalized.Equals("off", StringComparison.OrdinalIgnoreCase))
{
parsedValue = false;
return true;
}
return false;
}
private static CustomRootCertificateCache LoadCustomRootCertificateCache(string cacheKey, CustomRootCertificateConfiguration configuration)
{
var certificates = new X509Certificate2Collection();
if (!configuration.Enabled)
{
return new(
cacheKey,
certificates,
new ExternalHttpCustomRootCertificateState(false, configuration.Source, configuration.BundlePath, configuration.AllowedHostPatterns, false, 0, [], string.Empty));
}
if (string.IsNullOrWhiteSpace(configuration.BundlePath))
{
return new(
cacheKey,
certificates,
new ExternalHttpCustomRootCertificateState(true, configuration.Source, configuration.BundlePath, configuration.AllowedHostPatterns, false, 0, [], TB("No certificate bundle path is configured.")));
}
if (!File.Exists(configuration.BundlePath))
{
return new(
cacheKey,
certificates,
new ExternalHttpCustomRootCertificateState(true, configuration.Source, configuration.BundlePath, configuration.AllowedHostPatterns, false, 0, [], TB("The configured certificate bundle file does not exist.")));
}
try
{
var importedCertificates = new X509Certificate2Collection();
importedCertificates.ImportFromPemFile(configuration.BundlePath);
foreach (var certificate in importedCertificates)
{
if (!IsRootCertificateAuthority(certificate))
continue;
certificates.Add(certificate);
}
var fingerprints = certificates
.Select(certificate => certificate.GetCertHashString(HashAlgorithmName.SHA256))
.Order(StringComparer.OrdinalIgnoreCase)
.ToList();
var issue = certificates.Count == 0
? TB("The configured certificate bundle does not contain usable root CA certificates.")
: string.Empty;
return new(
cacheKey,
certificates,
new ExternalHttpCustomRootCertificateState(true, configuration.Source, configuration.BundlePath, configuration.AllowedHostPatterns, certificates.Count > 0, certificates.Count, fingerprints, issue));
}
catch (Exception e)
{
return new(
cacheKey,
certificates,
new ExternalHttpCustomRootCertificateState(true, configuration.Source, configuration.BundlePath, configuration.AllowedHostPatterns, false, 0, [], e.Message));
}
}
private static bool IsRootCertificateAuthority(X509Certificate2 certificate)
{
if (!certificate.SubjectName.RawData.SequenceEqual(certificate.IssuerName.RawData))
return false;
return certificate.Extensions
.OfType<X509BasicConstraintsExtension>()
.Any(extension => extension.CertificateAuthority);
}
private static bool ValidateServerCertificateWithCustomRootCertificates(
HttpRequestMessage request,
X509Certificate? certificate,
X509Chain? originalChain,
SslPolicyErrors sslPolicyErrors,
CustomRootCertificateCache customRootCertificateCache,
ExternalHttpTrustPolicy trustPolicy)
{
if (sslPolicyErrors is SslPolicyErrors.None)
return true;
if (sslPolicyErrors is not SslPolicyErrors.RemoteCertificateChainErrors || certificate is null)
return false;
var host = ReadRequestHost(request);
if (trustPolicy is ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY)
{
LOGGER.Value.LogError($"Rejected external HTTPS certificate for '{HostForLog(host)}' because this request requires system trust only. Configured custom root certificates are not allowed for this request.");
return false;
}
if (!IsAllowedCustomRootCertificateHost(host, customRootCertificateCache.State.AllowedHostPatterns))
{
LOGGER.Value.LogError($"Rejected external HTTPS certificate for '{HostForLog(host)}' because the host is not allowed to use configured custom root certificates.");
return false;
}
var ownsServerCertificate = certificate is not X509Certificate2;
var serverCertificate = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
try
{
using var customChain = new X509Chain();
customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
customChain.ChainPolicy.CustomTrustStore.AddRange(customRootCertificateCache.Certificates);
customChain.ChainPolicy.ApplicationPolicy.Add(new Oid(TLS_SERVER_AUTHENTICATION_EKU_OID));
if (originalChain is not null)
{
foreach (var element in originalChain.ChainElements)
{
if (element.Certificate.Thumbprint == serverCertificate.Thumbprint)
continue;
customChain.ChainPolicy.ExtraStore.Add(element.Certificate);
}
}
var isValid = customChain.Build(serverCertificate);
if (isValid)
LogCustomRootCertificateAccepted(request);
return isValid;
}
finally
{
if (ownsServerCertificate)
serverCertificate.Dispose();
}
}
private static bool IsAllowedCustomRootCertificateHost(string host, IReadOnlyList<string> allowedHostPatterns)
{
if (string.IsNullOrWhiteSpace(host))
return false;
var normalizedHost = host.Trim().TrimEnd('.').ToLowerInvariant();
foreach (var pattern in allowedHostPatterns)
{
if (!pattern.StartsWith("*.", StringComparison.Ordinal))
{
if (normalizedHost.Equals(pattern, StringComparison.OrdinalIgnoreCase))
return true;
continue;
}
var suffix = pattern[2..];
if (!normalizedHost.EndsWith($".{suffix}", StringComparison.OrdinalIgnoreCase))
continue;
var prefix = normalizedHost[..^(suffix.Length + 1)];
if (!prefix.Contains('.', StringComparison.Ordinal))
return true;
}
return false;
}
private static void LogCustomRootCertificateState(ExternalHttpCustomRootCertificateState state)
{
if (!state.IsEnabled)
{
LOGGER.Value.LogInformation("External HTTP custom root certificates are disabled.");
return;
}
if (state.IsUsable)
{
LOGGER.Value.LogWarning($"External HTTP custom root certificates are enabled from {state.Source}. Loaded {state.CertificateCount} root certificate(s) from '{state.BundlePath}'. Allowed hosts: {FormatAllowedHostPatternsForLog(state.AllowedHostPatterns)}. Fingerprints: {string.Join(", ", state.CertificateFingerprints)}");
return;
}
LOGGER.Value.LogWarning($"External HTTP custom root certificates are enabled from {state.Source}, but no additional root certificates are usable. Bundle path: '{state.BundlePath}'. Issue: {state.Issue}");
}
private static void LogCustomRootCertificateAccepted(HttpRequestMessage request)
{
var host = ReadRequestHost(request);
LOGGER.Value.LogWarning($"Accepted an external HTTPS certificate for '{host}' using configured custom root certificates.");
}
private static string ReadRequestHost(HttpRequestMessage request)
{
var host = request.RequestUri?.IdnHost;
if (string.IsNullOrWhiteSpace(host))
host = request.RequestUri?.Host;
return host ?? string.Empty;
}
private static string HostForLog(string host) => string.IsNullOrWhiteSpace(host) ? "unknown host" : host;
private static string FormatAllowedHostPatternsForLog(IReadOnlyList<string> allowedHostPatterns)
{
if (allowedHostPatterns.Count == 0)
return "none";
return string.Join(", ", allowedHostPatterns);
}
private readonly record struct CustomRootCertificateConfiguration(bool Enabled, string BundlePath, IReadOnlyList<string> AllowedHostPatterns, string Source);
private sealed record CustomRootCertificateCache(
string CacheKey,
X509Certificate2Collection Certificates,
ExternalHttpCustomRootCertificateState State);
}

View File

@ -0,0 +1,11 @@
namespace AIStudio.Tools;
public sealed record ExternalHttpCustomRootCertificateState(
bool IsEnabled,
string Source,
string BundlePath,
IReadOnlyList<string> AllowedHostPatterns,
bool IsUsable,
int CertificateCount,
IReadOnlyList<string> CertificateFingerprints,
string Issue);

View File

@ -0,0 +1,7 @@
namespace AIStudio.Tools;
public enum ExternalHttpTrustPolicy
{
SYSTEM_TRUST_ONLY,
ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED
}

View File

@ -5,7 +5,7 @@ public class I18N : ILang
public static readonly I18N I = new();
private static readonly ILogger<I18N> LOG = Program.LOGGER_FACTORY.CreateLogger<I18N>();
private ILanguagePlugin? language = PluginFactory.BaseLanguage;
private ILanguagePlugin? language;
private I18N()
{

View File

@ -85,6 +85,14 @@ public abstract partial class PluginBase : IPluginMetadata
if(!string.IsNullOrWhiteSpace(parseError))
issues.Add(parseError);
if (this is NoPlugin or NoPluginLanguage)
{
this.IsInternal = isInternal;
this.IconSVG = string.Empty;
this.baseIssues = issues;
return;
}
// Notice: when no icon is specified, the default icon will be used.
this.TryInitIconSVG(out _, out var iconSVG);
this.IconSVG = iconSVG;

View File

@ -155,6 +155,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: what should be the start page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun);
// Config: show quick start guide on the home page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun);
// Config: allow the user to add providers?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
@ -176,6 +179,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: timeout for external HTTP requests
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HttpClientTimeoutSeconds, this.Id, settingsTable, dryRun);
// Config: custom root certificates for external HTTP requests
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, this.Id, settingsTable, dryRun);
// Handle configured LLM providers:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);

View File

@ -15,7 +15,7 @@ public static partial class PluginFactory
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
using var http = ExternalHttpClientTimeout.CreateHttpClient();
using var http = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
@ -52,7 +52,7 @@ public static partial class PluginFactory
try
{
await LockHotReloadAsync();
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient();
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
if (!response.IsSuccessStatusCode)
{

View File

@ -213,6 +213,10 @@ public static partial class PluginFactory
// Check for the start page:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the quick start guide visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for users allowed to added providers:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
@ -249,6 +253,16 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HttpClientTimeoutSeconds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for custom root certificates for external HTTP requests:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check if audit is required before it can be activated
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;

View File

@ -1,3 +1,3 @@
namespace AIStudio.Tools.Rust;
public sealed record EnterpriseConfig(string Id, string ServerUrl);
public sealed record EnterpriseConfig(string Id, string ServerUrl, string Source, string SourceDetail, string Slot);

View File

@ -68,6 +68,7 @@ public static class FileTypes
public static readonly FileTypeFilter MEDIA = FileTypeFilter.Parent(TB("Media"), IMAGE, AUDIO, VIDEO);
// Other standalone types
public static readonly FileTypeFilter CERTIFICATE_BUNDLE = FileTypeFilter.Leaf(TB("Certificate bundle"), "pem", "crt", "cer");
public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage");
public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types)

View File

@ -0,0 +1,27 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response of the Qdrant Edge information request.
/// </summary>
public readonly record struct QdrantEdgeInfo
{
public QdrantEdgeStatus Status { get; init; }
public bool IsAvailable { get; init; }
public string? UnavailableReason { get; init; }
public string Name { get; init; }
public string Version { get; init; }
public string Path { get; init; }
public int StoresCount { get; init; }
public static QdrantEdgeInfo Unavailable(string reason) => new()
{
Status = QdrantEdgeStatus.UNAVAILABLE,
UnavailableReason = reason
};
}

View File

@ -1,8 +1,8 @@
namespace AIStudio.Tools.Rust;
public enum QdrantStatus
public enum QdrantEdgeStatus
{
STARTING,
AVAILABLE,
UNAVAILABLE,
}
}

View File

@ -1,23 +0,0 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response of the Qdrant information request.
/// </summary>
public readonly record struct QdrantInfo
{
public QdrantStatus Status { get; init; }
public bool IsAvailable { get; init; }
public string? UnavailableReason { get; init; }
public string Path { get; init; }
public int PortHttp { get; init; }
public int PortGrpc { get; init; }
public string Fingerprint { get; init; }
public string ApiToken { get; init; }
}

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType);

View File

@ -14,4 +14,4 @@ public enum Shortcut
/// Toggles voice recording on/off.
/// </summary>
VOICE_RECORDING_TOGGLE,
}
}

View File

@ -14,7 +14,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
private static EnterpriseSecretSnapshot CURRENT_SECRET_SNAPSHOT;
private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string? ETag);
private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string Source, string SourceDetail, string Slot, string? ETag);
private readonly record struct EnterpriseSecretSnapshot(bool HasSecret, string Fingerprint);
@ -224,6 +224,9 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
.Select(environment => new EnterpriseEnvironmentSnapshot(
environment.ConfigurationId,
NormalizeServerUrl(environment.ConfigurationServerUrl),
environment.Source,
environment.SourceDetail,
environment.Slot,
environment.ETag?.ToString()))
.OrderBy(environment => environment.ConfigurationId)
.ToList();

View File

@ -120,6 +120,12 @@ public sealed partial class RustService
return await response.Content.ReadAsStringAsync();
}
public async Task<RuntimeInfoResponse> GetRuntimeInfo()
{
var response = await this.http.GetFromJsonAsync<RuntimeInfoResponse>("/system/runtime/info", this.jsonRustSerializerOptions);
return response;
}
/// <summary>
/// Requests the Rust runtime to exit the entire desktop application.
/// </summary>

View File

@ -1,44 +1,53 @@
using AIStudio.Tools.Databases;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
public async Task<QdrantInfo> GetQdrantInfo(CancellationToken cancellationToken = default)
public async Task<TDatabaseInfo> GetDatabaseInfo<TDatabaseInfo>(
string databaseName,
string infoPath,
Func<string, TDatabaseInfo> unavailableFactory,
CancellationToken cancellationToken = default)
{
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(45));
return await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token);
var databaseInfo = await this.http.GetFromJsonAsync<TDatabaseInfo>(infoPath, this.jsonRustSerializerOptions, cts.Token);
return databaseInfo ?? unavailableFactory("The database information response was empty.");
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
if(this.logger is not null)
this.logger.LogWarning("Fetching Qdrant info from Rust service was cancelled by caller.");
this.logger.LogWarning("Fetching {DatabaseName} info from Rust service was cancelled by caller.", databaseName);
else
Console.WriteLine("Fetching Qdrant info from Rust service was cancelled by caller.");
return new QdrantInfo
{
Status = QdrantStatus.UNAVAILABLE,
UnavailableReason = "Operation cancelled by caller."
};
Console.WriteLine($"Fetching {databaseName} info from Rust service was cancelled by caller.");
return unavailableFactory("Operation cancelled by caller.");
}
catch (Exception e)
{
if(this.logger is not null)
this.logger.LogError(e, "Error while fetching Qdrant info from Rust service.");
this.logger.LogError(e, "Error while fetching {DatabaseName} info from Rust service.", databaseName);
else
Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'.");
return new QdrantInfo
{
Status = QdrantStatus.UNAVAILABLE,
UnavailableReason = e.Message
};
Console.WriteLine($"Error while fetching {databaseName} info from Rust service: '{e}'.");
return unavailableFactory(e.Message);
}
}
}
public async Task ExecuteDatabaseOperation<TRequest>(string databaseName, string path, TRequest request, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMinutes(5));
using var response = await this.http.PostAsJsonAsync(path, request, this.jsonRustSerializerOptions, cts.Token);
response.EnsureSuccessStatusCode();
var operation = await response.Content.ReadFromJsonAsync<DatabaseOperationResponse>(this.jsonRustSerializerOptions, cts.Token);
if (operation is not { Success: true })
throw new InvalidOperationException(operation?.Issue ?? $"The {databaseName} operation failed.");
}
private sealed record DatabaseOperationResponse(bool Success, string Issue);
}

View File

@ -47,7 +47,7 @@ public sealed partial class RustService
foreach (var config in configs)
{
if (Guid.TryParse(config.Id, out var id))
environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, null));
environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, config.Source, config.SourceDetail, config.Slot, null));
else
this.logger!.LogWarning($"Skipping enterprise config with invalid ID: '{config.Id}'.");
}

View File

@ -12,6 +12,8 @@ namespace AIStudio.Tools;
public static class WorkspaceBehaviour
{
public readonly record struct TryCreateWorkspaceResult(bool Success, WorkspaceTreeWorkspace Workspace);
private sealed class WorkspaceChatCacheEntry
{
public Guid WorkspaceId { get; init; }
@ -76,9 +78,9 @@ public static class WorkspaceBehaviour
private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45);
private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces");
private static readonly string WORKSPACE_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "workspaces");
private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats");
private static readonly string TEMPORARY_CHATS_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "tempChats");
private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId)
{
@ -156,9 +158,9 @@ public static class WorkspaceBehaviour
private static async Task<List<WorkspaceChatCacheEntry>> ReadTemporaryChatsCoreAsync()
{
var chats = new List<WorkspaceChatCacheEntry>();
Directory.CreateDirectory(TemporaryChatsRootDirectory);
Directory.CreateDirectory(TEMPORARY_CHATS_ROOT_DIRECTORY);
foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory))
foreach (var tempChatPath in Directory.EnumerateDirectories(TEMPORARY_CHATS_ROOT_DIRECTORY))
{
if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId))
continue;
@ -188,8 +190,8 @@ public static class WorkspaceBehaviour
WORKSPACE_TREE_CACHE.Workspaces.Clear();
WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear();
Directory.CreateDirectory(WorkspaceRootDirectory);
foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory))
Directory.CreateDirectory(WORKSPACE_ROOT_DIRECTORY);
foreach (var workspacePath in Directory.EnumerateDirectories(WORKSPACE_ROOT_DIRECTORY))
{
if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId))
continue;
@ -230,6 +232,99 @@ public static class WorkspaceBehaviour
chats.RemoveAt(existingIndex);
}
private static IReadOnlyList<string> ParseSearchTerms(string searchText) => searchText
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(term => !string.IsNullOrWhiteSpace(term))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
private static IReadOnlyList<string> GetMissingTerms(string text, IReadOnlyList<string> terms) => terms
.Where(term => text.IndexOf(term, StringComparison.OrdinalIgnoreCase) < 0)
.ToList();
private static bool ChatThreadContainsTerms(ChatThread thread, IReadOnlyList<string> terms)
{
var matchedTerms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var block in thread.Blocks)
{
if (block.HideFromUser || block.Content is not ContentText textContent || string.IsNullOrWhiteSpace(textContent.Text))
continue;
foreach (var term in terms)
if (textContent.Text.Contains(term, StringComparison.OrdinalIgnoreCase))
matchedTerms.Add(term);
if (matchedTerms.Count == terms.Count)
return true;
}
return false;
}
private static bool WorkspaceNameExistsCore(string workspaceName, Guid excludedWorkspaceId = default)
{
return WORKSPACE_TREE_CACHE.Workspaces.Values.Any(workspace =>
workspace.WorkspaceId != excludedWorkspaceId &&
string.Equals(workspace.WorkspaceName.Trim(), workspaceName, StringComparison.OrdinalIgnoreCase));
}
private static async Task<bool> ThreadContainsTermsAsync(WorkspaceTreeChat chat, IReadOnlyList<string> terms, CancellationToken token)
{
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(ThreadContainsTermsAsync));
if (!acquired)
return false;
try
{
var threadPath = Path.Join(chat.ChatPath, "thread.json");
if (!File.Exists(threadPath))
return false;
var chatData = await File.ReadAllTextAsync(threadPath, Encoding.UTF8, token);
token.ThrowIfCancellationRequested();
var thread = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
return thread is not null && ChatThreadContainsTerms(thread, terms);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LOG.LogWarning(ex, "Failed to search chat thread for workspace '{WorkspaceId}', chat '{ChatId}'.", chat.WorkspaceId, chat.ChatId);
return false;
}
finally
{
semaphore.Release();
}
}
private static async Task<List<WorkspaceSearchResult>> SearchChatsAsync(IReadOnlyList<WorkspaceTreeChat> chats, IReadOnlyList<string> terms, bool includeThreadContents, CancellationToken token)
{
var results = new List<WorkspaceSearchResult>();
foreach (var chat in chats)
{
token.ThrowIfCancellationRequested();
var missingTerms = GetMissingTerms(chat.Name, terms);
if (missingTerms.Count == 0)
{
results.Add(new(chat, NameMatched: true, ThreadMatched: false));
continue;
}
if (!includeThreadContents)
continue;
var threadMatched = await ThreadContainsTermsAsync(chat, missingTerms, token);
if (threadMatched)
results.Add(new(chat, NameMatched: false, ThreadMatched: true));
}
return results;
}
private static async Task UpdateCacheAfterChatStored(Guid workspaceId, Guid chatId, string chatDirectory, string chatName, DateTimeOffset lastEditTime)
{
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
@ -348,6 +443,55 @@ public static class WorkspaceBehaviour
}
}
public static async Task<WorkspaceSearchSnapshot> SearchWorkspaceChatsAsync(string searchText, bool includeThreadContents, CancellationToken token = default)
{
var terms = ParseSearchTerms(searchText);
if (terms.Count == 0)
return new([], []);
List<WorkspaceTreeWorkspace> workspaces;
List<WorkspaceTreeChat> temporaryChats;
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(token);
try
{
await EnsureTreeShellLoadedCoreAsync();
workspaces = [];
foreach (var workspaceId in WORKSPACE_TREE_CACHE.WorkspaceOrder)
{
token.ThrowIfCancellationRequested();
if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
continue;
if (!workspace.ChatsLoaded)
{
workspace.Chats = await ReadWorkspaceChatsCoreAsync(workspaceId, workspace.WorkspacePath);
workspace.ChatsLoaded = true;
}
workspaces.Add(ToPublicWorkspace(workspace));
}
temporaryChats = WORKSPACE_TREE_CACHE.TemporaryChats.Select(ToPublicChat).ToList();
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
var matchingWorkspaces = new List<WorkspaceSearchWorkspace>();
foreach (var workspace in workspaces)
{
token.ThrowIfCancellationRequested();
var matchingChats = await SearchChatsAsync(workspace.Chats, terms, includeThreadContents, token);
if (matchingChats.Count > 0)
matchingWorkspaces.Add(new(workspace.WorkspaceId, workspace.WorkspacePath, workspace.Name, matchingChats));
}
var matchingTemporaryChats = await SearchChatsAsync(temporaryChats, terms, includeThreadContents, token);
return new(matchingWorkspaces, matchingTemporaryChats);
}
public static async Task TryPrefetchRemainingChatsAsync(Func<Guid, Task>? onWorkspaceUpdated = null, CancellationToken token = default)
{
while (true)
@ -452,6 +596,100 @@ public static class WorkspaceBehaviour
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static string NormalizeWorkspaceName(string workspaceName) => workspaceName.Trim();
public static async Task<bool> IsWorkspaceNameExistingAsync(string workspaceName, Guid excludedWorkspaceId = default)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return false;
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
return WorkspaceNameExistsCore(normalizedWorkspaceName, excludedWorkspaceId);
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static async Task<TryCreateWorkspaceResult> TryCreateWorkspaceAsync(string workspaceName)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return new(false, default);
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
if (WorkspaceNameExistsCore(normalizedWorkspaceName))
return new(false, default);
var workspaceId = Guid.NewGuid();
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
Directory.CreateDirectory(workspacePath);
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
var workspace = new WorkspaceCacheEntry
{
WorkspaceId = workspaceId,
WorkspacePath = workspacePath,
WorkspaceName = normalizedWorkspaceName,
Chats = [],
ChatsLoaded = false,
};
WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = workspace;
WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId);
return new(true, ToPublicWorkspace(workspace));
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static async Task<bool> RenameWorkspaceAsync(Guid workspaceId, string workspaceName)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return false;
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
return false;
var workspaceNamePath = Path.Join(workspace.WorkspacePath, "name");
if (string.Equals(workspace.WorkspaceName.Trim(), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
{
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
workspace.WorkspaceName = normalizedWorkspaceName;
return true;
}
if (WorkspaceNameExistsCore(normalizedWorkspaceName, workspaceId))
return false;
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
workspace.WorkspaceName = normalizedWorkspaceName;
return true;
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static bool IsChatExisting(LoadChat loadChat)
{
@ -533,7 +771,7 @@ public static class WorkspaceBehaviour
// Not in cache — read from disk and update cache in the same semaphore scope
// to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync:
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
string workspaceName;
@ -621,7 +859,7 @@ public static class WorkspaceBehaviour
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
{
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
if (!Path.Exists(workspacePath))
@ -651,4 +889,4 @@ public static class WorkspaceBehaviour
public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day");
public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers");
}
}

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct WorkspaceSearchResult(WorkspaceTreeChat Chat, bool NameMatched, bool ThreadMatched);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct WorkspaceSearchSnapshot(IReadOnlyList<WorkspaceSearchWorkspace> Workspaces, IReadOnlyList<WorkspaceSearchResult> TemporaryChats);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct WorkspaceSearchWorkspace(Guid WorkspaceId, string WorkspacePath, string Name, IReadOnlyList<WorkspaceSearchResult> Chats);

View File

@ -66,16 +66,6 @@
"MudBlazor": "8.11.0"
}
},
"Qdrant.Client": {
"type": "Direct",
"requested": "[1.18.1, )",
"resolved": "1.18.1",
"contentHash": "eBwFLihGMvN02/jr/BNdcop2XmtA10y8VMOclVZ7K2H8yheAhl7jbkf7I8e4X3RYpT+cAxgcalP4xmOhgs4KJg==",
"dependencies": {
"Google.Protobuf": "3.31.0",
"Grpc.Net.Client": "2.71.0"
}
},
"ReverseMarkdown": {
"type": "Direct",
"requested": "[5.0.0, )",
@ -90,33 +80,6 @@
"resolved": "3.2.449",
"contentHash": "uA9sYDy4VepL3xwzBTLcP2LyuVYMt0ZIT3gaSiXvGoX15Ob+rOP+hGydhevlSVd+rFo+Y+VQFEHDuWU8HBW+XA=="
},
"Google.Protobuf": {
"type": "Transitive",
"resolved": "3.31.0",
"contentHash": "OZXSf6igaJBeo+kAzMhYF0R5zp0nRgf4G0Uis/IsGKACc4RGP9bQPLpHLengIFuASl0lY92utMB8rRpTx4TaOg=="
},
"Grpc.Core.Api": {
"type": "Transitive",
"resolved": "2.71.0",
"contentHash": "QquqUC37yxsDzd1QaDRsH2+uuznWPTS8CVE2Yzwl3CvU4geTNkolQXoVN812M2IwT6zpv3jsZRc9ExJFNFslTg=="
},
"Grpc.Net.Client": {
"type": "Transitive",
"resolved": "2.71.0",
"contentHash": "U1vr20r5ngoT9nlb7wejF28EKN+taMhJsV9XtK9MkiepTZwnKxxiarriiMfCHuDAfPUm9XUjFMn/RIuJ4YY61w==",
"dependencies": {
"Grpc.Net.Common": "2.71.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0"
}
},
"Grpc.Net.Common": {
"type": "Transitive",
"resolved": "2.71.0",
"contentHash": "v0c8R97TwRYwNXlC8GyRXwYTCNufpDfUtj9la+wUrZFzVWkFJuNAltU+c0yI3zu0jl54k7en6u2WKgZgd57r2Q==",
"dependencies": {
"Grpc.Core.Api": "2.71.0"
}
},
"LuaCSharp.Annotations": {
"type": "Transitive",
"resolved": "0.5.5",

View File

@ -135,6 +135,13 @@
text-align: left;
}
.configuration-help-justified .mud-input-helper-text,
.configuration-help-justified .mud-form-helpertext {
text-align: justify;
hyphens: auto;
word-break: auto-phrase;
}
.code-block {
background-color: #2d2d2d;
color: #f8f8f2;

View File

@ -131,3 +131,30 @@ window.formatChatInputMarkdown = function (inputId, formatType) {
return nextValue
}
const escapeHandlers = new Map()
window.registerEscapeHandler = function (id, dotNetReference) {
window.unregisterEscapeHandler(id)
const handler = function (event) {
if (event.key !== 'Escape')
return
event.preventDefault()
event.stopPropagation()
dotNetReference.invokeMethodAsync('HandleEscapeKeyAsync').catch(() => {})
}
document.addEventListener('keydown', handler, true)
escapeHandlers.set(id, handler)
}
window.unregisterEscapeHandler = function (id) {
const handler = escapeHandlers.get(id)
if (!handler)
return
document.removeEventListener('keydown', handler, true)
escapeHandlers.delete(id)
}

View File

@ -1 +1,15 @@
# v26.6.1, build 241 (2026-06-xx xx:xx UTC)
- Added support for up to 100 thousand enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible.
- Added an enterprise configuration option to hide the quick start guide on the welcome page.
- Added support for managed custom root certificate bundles and host allowlists for external HTTPS requests, helping Flatpak deployments connect to organization-internal services with private root CAs while keeping built-in cloud provider endpoints on system trust.
- Added support for reading enterprise policy files from a Flatpak provisioning extension.
- Added startup path and Linux package type details to the information page to make support easier.
- Added the option to search for chats in all workspaces.
- Improved workspaces by highlighting the currently open chat in the workspace view.
- Improved workspaces by adding a shortcut to start a new chat directly from each workspace row.
- Improved workspaces by allowing new workspaces to be created while moving a chat.
- Improved voice recording shortcut labels so they match the user's keyboard layout after being configured.
- Improved the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used.
- Fixed workspace creation and renaming to prevent new workspaces from using an existing name.
- Fixed an issue on Microsoft Windows where reading attached documents could briefly open a terminal window while processing files.
- Upgraded dependencies.

View File

@ -13,76 +13,94 @@ namespace SourceCodeRules.UsageAnalyzers;
public sealed class EmptyStringAnalyzer : DiagnosticAnalyzer
{
private const string DIAGNOSTIC_ID = Identifier.EMPTY_STRING_ANALYZER;
private static readonly string TITLE = """
Use string.Empty instead of ""
""";
private static readonly string MESSAGE_FORMAT = """
Use string.Empty instead of ""
""";
private static readonly string DESCRIPTION = """Empty string literals ("") should be replaced with string.Empty for better code consistency and readability except in const contexts.""";
private static readonly string DESCRIPTION = """Empty string literals ("") should be replaced with string.Empty for better code consistency and readability except in contexts requiring compile-time constants.""";
private const string CATEGORY = "Usage";
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeEmptyStringLiteral, SyntaxKind.StringLiteralExpression);
}
private static void AnalyzeEmptyStringLiteral(SyntaxNodeAnalysisContext context)
{
var stringLiteral = (LiteralExpressionSyntax)context.Node;
if (stringLiteral.Token.ValueText != string.Empty)
return;
if (IsInConstContext(stringLiteral))
if (RequiresCompileTimeConstant(stringLiteral))
return;
if (IsInParameterDefaultValue(stringLiteral))
return;
var diagnostic = Diagnostic.Create(RULE, stringLiteral.GetLocation());
context.ReportDiagnostic(diagnostic);
}
private static bool IsInConstContext(LiteralExpressionSyntax stringLiteral)
private static bool RequiresCompileTimeConstant(LiteralExpressionSyntax stringLiteral)
{
return IsInConstDeclarationInitializer(stringLiteral)
|| IsInParameterDefaultValue(stringLiteral)
|| IsInAttributeArgument(stringLiteral)
|| IsInSwitchCaseLabel(stringLiteral)
|| IsInConstantPattern(stringLiteral);
}
private static bool IsInConstDeclarationInitializer(LiteralExpressionSyntax stringLiteral)
{
var variableDeclarator = stringLiteral.FirstAncestorOrSelf<VariableDeclaratorSyntax>();
if (variableDeclarator is null)
if (variableDeclarator?.Initializer is null || !ContainsNode(variableDeclarator.Initializer.Value, stringLiteral))
return false;
var declaration = variableDeclarator.Parent?.Parent;
return declaration switch
{
FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
LocalDeclarationStatementSyntax localDeclaration => localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
_ => false
};
}
private static bool IsInParameterDefaultValue(LiteralExpressionSyntax stringLiteral)
{
// Prüfen, ob das String-Literal Teil eines Parameter-Defaults ist
var parameter = stringLiteral.FirstAncestorOrSelf<ParameterSyntax>();
if (parameter is null)
return false;
// Überprüfen, ob das String-Literal im Default-Wert des Parameters verwendet wird
if (parameter.Default is not null &&
parameter.Default.Value == stringLiteral)
{
return true;
}
return false;
return parameter?.Default is not null && ContainsNode(parameter.Default.Value, stringLiteral);
}
private static bool IsInAttributeArgument(LiteralExpressionSyntax stringLiteral)
{
var attributeArgument = stringLiteral.FirstAncestorOrSelf<AttributeArgumentSyntax>();
return attributeArgument is not null && ContainsNode(attributeArgument.Expression, stringLiteral);
}
private static bool IsInSwitchCaseLabel(LiteralExpressionSyntax stringLiteral)
{
var caseSwitchLabel = stringLiteral.FirstAncestorOrSelf<CaseSwitchLabelSyntax>();
return caseSwitchLabel is not null && ContainsNode(caseSwitchLabel.Value, stringLiteral);
}
private static bool IsInConstantPattern(LiteralExpressionSyntax stringLiteral)
{
var constantPattern = stringLiteral.FirstAncestorOrSelf<ConstantPatternSyntax>();
return constantPattern is not null && ContainsNode(constantPattern.Expression, stringLiteral);
}
private static bool ContainsNode(SyntaxNode parent, SyntaxNode child)
{
return parent.SpanStart <= child.SpanStart && child.Span.End <= parent.Span.End;
}
}

View File

@ -50,13 +50,6 @@ You can now test your changes. To stop the application:
- Press ``Ctrl+C`` in the terminal where the app is running.
- Stop the process via your IDEs run/debug controls.
> ⚠️ Important: Stopping the app via ``Ctrl+C`` or the IDE may not terminate the Qdrant sidecar process, especially on Windows. This can lead to startup failures when restarting the app.
If you encounter issues with restarting Tauri, then manually kill the Qdrant process:
- **Linux/macOS:** Run pkill -f qdrant in your terminal.
- **Windows:** Open Task Manager → Find qdrant.exe → Right-click → “End task”.
- Restart your Tauri app.
## 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.
@ -68,4 +61,4 @@ In order to create a release:
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.
10. Building the release including virus scanning takes some time. Please be patient.

View File

@ -39,13 +39,15 @@ AI Studio supports loading multiple enterprise configurations simultaneously. Th
The preferred format is a fixed set of indexed pairs:
- Registry values `config_id0` to `config_id9` together with `config_server_url0` to `config_server_url9`
- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID0` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID9` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL0` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL9`
- Policy files `config0.yaml` to `config9.yaml`
- Registry values `config_id_00000` to `config_id_99999` together with `config_server_url_00000` to `config_server_url_99999`
- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_99999` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_99999`
- Policy files `config_00000.yaml` to `config_99999.yaml`
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to ten configurations are supported per device.
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots are supported per device.
If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `0`, then `1`, and so on up to `9`.
If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `00000`, then `00001`, and so on up to `99999`.
For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments.
### Windows registry example
@ -55,10 +57,10 @@ The Windows registry path is:
Example values:
- `config_id0` = `9072b77d-ca81-40da-be6a-861da525ef7b`
- `config_server_url0` = `https://intranet.example.org/ai-studio/configuration`
- `config_id1` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
- `config_server_url1` = `https://intranet.example.org/ai-studio/department-config`
- `config_id_00000` = `9072b77d-ca81-40da-be6a-861da525ef7b`
- `config_server_url_00000` = `https://intranet.example.org/ai-studio/configuration`
- `config_id_10503` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
- `config_server_url_10503` = `https://intranet.example.org/ai-studio/department-config`
- `config_encryption_secret` = `BASE64...`
This approach works well with GPOs because each slot can be managed independently without rewriting a shared combined string.
@ -77,6 +79,28 @@ AI Studio checks each directory listed in `$XDG_CONFIG_DIRS` and looks for a `mi
The directories from `$XDG_CONFIG_DIRS` are processed in order.
#### Flatpak policy directory
When AI Studio runs as a Flatpak, it first checks this sandbox path before the regular Linux policy directories:
`/app/etc/MindWorkAI/`
This path is intended for a Flatpak provisioning extension like:
```yaml
add-extensions:
org.MindWorkAI.AIStudio.provisioning:
directory: etc/MindWorkAI
no-autodownload: true
```
Policy files can then be provided on the host through the extension directories. For example:
- System-wide, read-only: `/var/lib/flatpak/extension/org.MindWorkAI.AIStudio.provisioning/x86_64/stable/`
- User-specific: `$XDG_DATA_HOME/flatpak/extension/org.MindWorkAI.AIStudio.provisioning/x86_64/stable/`
Files placed there are mounted into the sandbox at `/app/etc/MindWorkAI/`. Use the same policy file names and YAML format described below.
#### macOS policy directory
`/Library/Application Support/MindWork/AI Studio/`
@ -85,10 +109,10 @@ The directories from `$XDG_CONFIG_DIRS` are processed in order.
Configuration files:
- `config0.yaml`
- `config1.yaml`
- `config_00000.yaml`
- `config_00001.yaml`
- ...
- `config9.yaml`
- `config_99999.yaml`
Each configuration file contains one configuration ID and one server URL:
@ -110,10 +134,10 @@ config_encryption_secret: "BASE64..."
If you need the fallback environment-variable format, configure the values like this:
```bash
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID0=9072b77d-ca81-40da-be6a-861da525ef7b
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL0=https://intranet.example.org/ai-studio/configuration
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID1=a1b2c3d4-e5f6-7890-abcd-ef1234567890
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL1=https://intranet.example.org/ai-studio/department-config
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000=9072b77d-ca81-40da-be6a-861da525ef7b
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000=https://intranet.example.org/ai-studio/configuration
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_10503=a1b2c3d4-e5f6-7890-abcd-ef1234567890
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_10503=https://intranet.example.org/ai-studio/department-config
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET=BASE64...
```
@ -136,6 +160,38 @@ Finally, AI Studio will send a GET request and download the ZIP file. The ZIP fi
Approximately every 16 minutes, AI Studio checks the metadata of the ZIP file by reading the [ETag](https://en.wikipedia.org/wiki/HTTP_ETag). When the ETag was not changed, no download will be performed. Make sure that your web server supports this. When using multiple configurations, each configuration is checked independently.
### Custom root certificates for Flatpak deployments
On Linux, AI Studio normally relies on the operating system's trusted root certificates for external HTTPS requests. In a Flatpak package, however, the application may not be able to read organization-specific root certificates from the host system. This can affect connections to self-hosted AI providers, embedding providers, transcription providers, ERI servers, and enterprise configuration servers.
If your organization uses private root CAs, place a PEM bundle with the required root CA certificates in a location that is readable inside the Flatpak sandbox. The bundle should contain one or more certificates using the regular PEM marker:
```text
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
```
For the first enterprise configuration download, configure these environment variables before AI Studio starts:
```bash
MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;eri.example.org
```
You can also manage the same behavior from a configuration plugin after the plugin has been downloaded:
```lua
CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
```
This feature does not disable TLS verification. AI Studio first uses the system certificate validation. If that fails only because the certificate chain is not trusted, AI Studio tries again with the configured root CA bundle, but only for configured host patterns. Exact hosts such as `eri.intra.example.org` and one-label wildcards such as `*.intra.example.org` are supported. Hostname mismatches, missing certificates, expired certificates, and otherwise invalid chains are still rejected. Built-in cloud provider endpoints, such as OpenAI, Google, etc., never use configured custom root certificates.
As an alternative, your Flatpak launch environment can set `SSL_CERT_FILE` or `SSL_CERT_DIR` to a certificate bundle or directory that .NET/OpenSSL can read. This is useful when your deployment already manages a consistent PEM bundle for the sandbox.
## Configure the configuration web server
In principle, you can use any web server that can serve ZIP files from a folder. However, keep in mind that AI Studio queries the file's metadata using [ETag](https://en.wikipedia.org/wiki/HTTP_ETag). Your web server must support this feature. For security reasons, you should also make sure that users cannot list the contents of the directory. This is important because the different configurations may contain confidential information such as API keys. Each user should only know their own configuration ID. Otherwise, a user might try to use someone elses ID to gain access to exclusive resources.

View File

@ -3,10 +3,10 @@
240
9.0.117 (commit 6e241a69c1)
9.0.16 (commit a1e6809fb8)
1.95.0 (commit 59807616e)
1.96.0 (commit ac68faa20)
8.15.0
2.11.1
2.11.2
d05ff26e628, release
osx-arm64
148.0.7763.0
1.18.1
0.6.1

2267
runtime/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,24 @@ description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]
[build-dependencies]
tauri-build = { version = "2.6.1", features = [] }
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
tauri = { version = "2.11.1", features = [] }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-window-state = { version = "2.4.1" }
tauri-plugin-shell = "2.3.5"
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2.5.4"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_json = "1.0.150"
keyring-core = "1.0.0"
arboard = "3.6.1"
tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "process"] }
tokio-stream = "0.1.18"
futures = "0.3.32"
async-stream = "0.3.6"
flexi_logger = "0.31.8"
log = { version = "0.4.29", features = ["kv"] }
flexi_logger = "0.31.9"
log = { version = "0.4.30", features = ["kv"] }
once_cell = "1.21.4"
axum = { version = "0.8.9", features = ["http2", "json", "query", "tokio"] }
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
@ -31,8 +31,8 @@ rustls = { version = "0.23.28", default-features = false, features = ["aws_lc_rs
rand = "0.10.1"
rand_chacha = "0.10.0"
base64 = "0.22.1"
aes = "0.9.0"
cbc = "0.2.0"
aes = "0.9.1"
cbc = "0.2.1"
pbkdf2 = "0.13.0"
hmac = "0.13.0"
sha2 = "0.11.0"
@ -46,9 +46,19 @@ cfg-if = "1.0.4"
pptx-to-md = "0.4.0"
tempfile = "3.27.0"
strum_macros = "0.28.0"
sysinfo = "0.39.1"
sysinfo = "0.39.3"
bytes = "1.11.1"
tokenizers = "0.23.1"
qdrant-edge = "0.7.2"
[patch.crates-io]
# Issue: It was not possible to build qdrant-edge for macOS. See PR 9312: https://github.com/qdrant/qdrant/pull/9312
# State: The PR was merged, but not yet released. We use the git version for now.
qdrant-edge = { git = "https://github.com/SommerEngineering/qdrant.git", rev = "462c84d82ced126e4a2b7914544bfde16a509eb1" }
# Issue: This repo was not updated since 2020. The rand crate was outdated. We patched it to use a newer version of rand.
# State: There is a PR for a long time, but it was not merged. We use the git version for now.
permutation_iterator = { git = "https://github.com/SommerEngineering/permutation-iterator-rs.git", rev = "76836ed316d18dfef530ba908f58481c343e80d7" }
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = "0.6.1"

View File

@ -22,11 +22,6 @@
"name": "mindworkAIStudioServer",
"sidecar": true,
"args": true
},
{
"name": "qdrant",
"sidecar": true,
"args": true
}
]
}

82
runtime/patches/README.md Normal file
View File

@ -0,0 +1,82 @@
# Runtime Patches
This directory documents temporary patches for third-party Rust dependencies.
## Qdrant Edge
AI Studio temporarily uses a pinned commit from `SommerEngineering/qdrant` for `qdrant-edge`.
The fork commit exposes Qdrant's internal `lib/edge` crate as `qdrant-edge` and applies the
trait-solver fix from Qdrant PR #9312.
When updating to a newer Qdrant Edge version, replace the placeholder values first:
```bash
export QDRANT_EDGE_VERSION="0.7.2"
export QDRANT_BRANCH="ai-studio-qdrant-edge-${QDRANT_EDGE_VERSION}"
export AISTUDIO_REPO="xxx/mindwork-ai-studio"
export QDRANT_REPO="xxx/qdrant"
```
1. Sync the Qdrant fork with upstream:
```bash
cd "$QDRANT_REPO"
git remote add upstream https://github.com/qdrant/qdrant.git 2>/dev/null || true
git fetch upstream
git fetch origin
git switch master
git merge --ff-only upstream/master
git push origin master
```
2. Create a fresh AI Studio branch in the Qdrant fork:
```bash
cd "$QDRANT_REPO"
git switch -c "$QDRANT_BRANCH" master
```
3. Apply the AI Studio patch if upstream has not released the fix yet:
```bash
cd "$QDRANT_REPO"
git apply "$AISTUDIO_REPO/runtime/patches/qdrant-edge-ai-studio.patch"
```
4. Update the exposed `qdrant-edge` version in the fork:
```bash
cd "$QDRANT_REPO"
perl -0pi -e "s/name = \"qdrant-edge\"\\nversion = \"[^\"]+\"/name = \"qdrant-edge\"\\nversion = \"$ENV{QDRANT_EDGE_VERSION}\"/" lib/edge/Cargo.toml
```
5. Commit and push the fork branch:
```bash
cd "$QDRANT_REPO"
git diff
git add lib/edge/Cargo.toml lib/segment/src/common/anonymize.rs
git commit -m "Expose qdrant-edge ${QDRANT_EDGE_VERSION} package for AI Studio"
git push origin "$QDRANT_BRANCH"
export QDRANT_EDGE_COMMIT="$(git rev-parse HEAD)"
echo "$QDRANT_EDGE_COMMIT"
```
6. Update AI Studio to use the new Qdrant Edge version and fork commit:
```bash
cd "$AISTUDIO_REPO"
perl -0pi -e "s/qdrant-edge = \"[^\"]+\"/qdrant-edge = \"$ENV{QDRANT_EDGE_VERSION}\"/" runtime/Cargo.toml
perl -0pi -e "s/rev = \"[0-9a-f]+\"/rev = \"$ENV{QDRANT_EDGE_COMMIT}\"/" runtime/Cargo.toml
```
7. Refresh the AI Studio lock file and verify the Rust runtime:
```bash
cd "$AISTUDIO_REPO/runtime"
cargo update -p qdrant-edge
cargo check
```
Remove the patch and the `[patch.crates-io]` override once Qdrant publishes a fixed `qdrant-edge`
release on crates.io.

View File

@ -0,0 +1,26 @@
diff --git a/lib/edge/Cargo.toml b/lib/edge/Cargo.toml
index 7c2cf6037..d21e3c053 100644
--- a/lib/edge/Cargo.toml
+++ b/lib/edge/Cargo.toml
@@ -1,6 +1,6 @@
[package]
-name = "edge"
-version = "0.1.0"
+name = "qdrant-edge"
+version = "0.7.2"
authors = ["Qdrant Team <info@qdrant.tech>"]
license = "Apache-2.0"
edition = "2024"
diff --git a/lib/segment/src/common/anonymize.rs b/lib/segment/src/common/anonymize.rs
index 6b5d19b12..c73d24433 100644
--- a/lib/segment/src/common/anonymize.rs
+++ b/lib/segment/src/common/anonymize.rs
@@ -105,7 +105,7 @@ where
{
collection_opt
.as_ref()
- .map(|c| anonymize_collection_values(c))
+ .map(|c| anonymize_collection_values::<C, K, V>(c))
}
impl Anonymize for String {

View File

@ -1,354 +0,0 @@
log_level: INFO
# Logging configuration
# Qdrant logs to stdout. You may configure to also write logs to a file on disk.
# Be aware that this file may grow indefinitely.
# logger:
# # Logging format, supports `text` and `json`
# format: text
# on_disk:
# enabled: true
# log_file: path/to/log/file.log
# log_level: INFO
# # Logging format, supports `text` and `json`
# format: text
# buffer_size_bytes: 1024
storage:
snapshots_config:
# "local" or "s3" - where to store snapshots
snapshots_storage: local
# s3_config:
# bucket: ""
# region: ""
# access_key: ""
# secret_key: ""
# Where to store temporary files
# If null, temporary snapshots are stored in: storage/snapshots_temp/
temp_path: null
# If true - point payloads will not be stored in memory.
# It will be read from the disk every time it is requested.
# This setting saves RAM by (slightly) increasing the response time.
# Note: those payload values that are involved in filtering and are indexed - remain in RAM.
#
# Default: true
on_disk_payload: true
# Maximum number of concurrent updates to shard replicas
# If `null` - maximum concurrency is used.
update_concurrency: null
# Write-ahead-log related configuration
wal:
# Size of a single WAL segment
wal_capacity_mb: 32
# Number of WAL segments to create ahead of actual data requirement
wal_segments_ahead: 0
# Normal node - receives all updates and answers all queries
node_type: "Normal"
# Listener node - receives all updates, but does not answer search/read queries
# Useful for setting up a dedicated backup node
# node_type: "Listener"
performance:
# Number of parallel threads used for search operations. If 0 - auto selection.
max_search_threads: 0
# CPU budget, how many CPUs (threads) to allocate for an optimization job.
# If 0 - auto selection, keep 1 or more CPUs unallocated depending on CPU size
# If negative - subtract this number of CPUs from the available CPUs.
# If positive - use this exact number of CPUs.
optimizer_cpu_budget: 0
# Prevent DDoS of too many concurrent updates in distributed mode.
# One external update usually triggers multiple internal updates, which breaks internal
# timings. For example, the health check timing and consensus timing.
# If null - auto selection.
update_rate_limit: null
# Limit for number of incoming automatic shard transfers per collection on this node, does not affect user-requested transfers.
# The same value should be used on all nodes in a cluster.
# Default is to allow 1 transfer.
# If null - allow unlimited transfers.
#incoming_shard_transfers_limit: 1
# Limit for number of outgoing automatic shard transfers per collection on this node, does not affect user-requested transfers.
# The same value should be used on all nodes in a cluster.
# Default is to allow 1 transfer.
# If null - allow unlimited transfers.
#outgoing_shard_transfers_limit: 1
# Enable async scorer which uses io_uring when rescoring.
# Only supported on Linux, must be enabled in your kernel.
# See: <https://qdrant.tech/articles/io_uring/#and-what-about-qdrant>
#async_scorer: false
optimizers:
# The minimal fraction of deleted vectors in a segment, required to perform segment optimization
deleted_threshold: 0.2
# The minimal number of vectors in a segment, required to perform segment optimization
vacuum_min_vector_number: 1000
# Target amount of segments optimizer will try to keep.
# Real amount of segments may vary depending on multiple parameters:
# - Amount of stored points
# - Current write RPS
#
# It is recommended to select default number of segments as a factor of the number of search threads,
# so that each segment would be handled evenly by one of the threads.
# If `default_segment_number = 0`, will be automatically selected by the number of available CPUs
default_segment_number: 0
# Do not create segments larger this size (in KiloBytes).
# Large segments might require disproportionately long indexation times,
# therefore it makes sense to limit the size of segments.
#
# If indexation speed have more priority for your - make this parameter lower.
# If search speed is more important - make this parameter higher.
# Note: 1Kb = 1 vector of size 256
# If not set, will be automatically selected considering the number of available CPUs.
max_segment_size_kb: null
# Maximum size (in KiloBytes) of vectors allowed for plain index.
# Default value based on experiments and observations.
# Note: 1Kb = 1 vector of size 256
# To explicitly disable vector indexing, set to `0`.
# If not set, the default value will be used.
indexing_threshold_kb: 10000
# Interval between forced flushes.
flush_interval_sec: 5
# Max number of threads (jobs) for running optimizations per shard.
# Note: each optimization job will also use `max_indexing_threads` threads by itself for index building.
# If null - have no limit and choose dynamically to saturate CPU.
# If 0 - no optimization threads, optimizations will be disabled.
max_optimization_threads: null
# This section has the same options as 'optimizers' above. All values specified here will overwrite the collections
# optimizers configs regardless of the config above and the options specified at collection creation.
#optimizers_overwrite:
# deleted_threshold: 0.2
# vacuum_min_vector_number: 1000
# default_segment_number: 0
# max_segment_size_kb: null
# indexing_threshold_kb: 10000
# flush_interval_sec: 5
# max_optimization_threads: null
# Default parameters of HNSW Index. Could be overridden for each collection or named vector individually
hnsw_index:
# Number of edges per node in the index graph. Larger the value - more accurate the search, more space required.
m: 16
# Number of neighbours to consider during the index building. Larger the value - more accurate the search, more time required to build index.
ef_construct: 100
# Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search.
# This measures the total size of vectors being queried against.
# When the maximum estimated amount of points that a condition satisfies is smaller than
# `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index
# traversal for better performance.
# Note: 1Kb = 1 vector of size 256
full_scan_threshold_kb: 10000
# Number of parallel threads used for background index building.
# If 0 - automatically select.
# Best to keep between 8 and 16 to prevent likelihood of building broken/inefficient HNSW graphs.
# On small CPUs, less threads are used.
max_indexing_threads: 0
# Store HNSW index on disk. If set to false, index will be stored in RAM. Default: false
on_disk: false
# Custom M param for hnsw graph built for payload index. If not set, default M will be used.
payload_m: null
# Default shard transfer method to use if none is defined.
# If null - don't have a shard transfer preference, choose automatically.
# If stream_records, snapshot or wal_delta - prefer this specific method.
# More info: https://qdrant.tech/documentation/guides/distributed_deployment/#shard-transfer-method
shard_transfer_method: null
# Default parameters for collections
collection:
# Number of replicas of each shard that network tries to maintain
replication_factor: 1
# How many replicas should apply the operation for us to consider it successful
write_consistency_factor: 1
# Default parameters for vectors.
vectors:
# Whether vectors should be stored in memory or on disk.
on_disk: null
# shard_number_per_node: 1
# Default quantization configuration.
# More info: https://qdrant.tech/documentation/guides/quantization
quantization: null
# Default strict mode parameters for newly created collections.
#strict_mode:
# Whether strict mode is enabled for a collection or not.
#enabled: false
# Max allowed `limit` parameter for all APIs that don't have their own max limit.
#max_query_limit: null
# Max allowed `timeout` parameter.
#max_timeout: null
# Allow usage of unindexed fields in retrieval based (eg. search) filters.
#unindexed_filtering_retrieve: null
# Allow usage of unindexed fields in filtered updates (eg. delete by payload).
#unindexed_filtering_update: null
# Max HNSW value allowed in search parameters.
#search_max_hnsw_ef: null
# Whether exact search is allowed or not.
#search_allow_exact: null
# Max oversampling value allowed in search.
#search_max_oversampling: null
# Maximum number of collections allowed to be created
# If null - no limit.
max_collections: null
service:
# Maximum size of POST data in a single request in megabytes
max_request_size_mb: 32
# Number of parallel workers used for serving the api. If 0 - equal to the number of available cores.
# If missing - Same as storage.max_search_threads
max_workers: 0
# Host to bind the service on
host: 127.0.0.1
# HTTP(S) port to bind the service on
# http_port: 6333
# gRPC port to bind the service on.
# If `null` - gRPC is disabled. Default: null
# Comment to disable gRPC:
# grpc_port: 6334
# Enable CORS headers in REST API.
# If enabled, browsers would be allowed to query REST endpoints regardless of query origin.
# More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
# Default: true
enable_cors: false
# Enable HTTPS for the REST and gRPC API
# TLS is enabled in AI Studio through environment variables when instantiating Qdrant as a sidecar.
# enable_tls: false
# Check user HTTPS client certificate against CA file specified in tls config
verify_https_client_certificate: false
# Set an api-key.
# If set, all requests must include a header with the api-key.
# example header: `api-key: <API-KEY>`
#
# If you enable this you should also enable TLS.
# (Either above or via an external service like nginx.)
# Sending an api-key over an unencrypted channel is insecure.
#
# Uncomment to enable.
# api_key: your_secret_api_key_here
# Set an api-key for read-only operations.
# If set, all requests must include a header with the api-key.
# example header: `api-key: <API-KEY>`
#
# If you enable this you should also enable TLS.
# (Either above or via an external service like nginx.)
# Sending an api-key over an unencrypted channel is insecure.
#
# Uncomment to enable.
# read_only_api_key: your_secret_read_only_api_key_here
# Uncomment to enable JWT Role Based Access Control (RBAC).
# If enabled, you can generate JWT tokens with fine-grained rules for access control.
# Use generated token instead of API key.
#
# jwt_rbac: true
# Hardware reporting adds information to the API responses with a
# hint on how many resources were used to execute the request.
#
# Warning: experimental, this feature is still under development and is not supported yet.
#
# Uncomment to enable.
# hardware_reporting: true
#
# Uncomment to enable.
# Prefix for the names of metrics in the /metrics API.
# metrics_prefix: qdrant_
cluster:
# Use `enabled: true` to run Qdrant in distributed deployment mode
enabled: false
# Configuration of the inter-cluster communication
p2p:
# Port for internal communication between peers
port: 6335
# Use TLS for communication between peers
enable_tls: false
# Configuration related to distributed consensus algorithm
consensus:
# How frequently peers should ping each other.
# Setting this parameter to lower value will allow consensus
# to detect disconnected nodes earlier, but too frequent
# tick period may create significant network and CPU overhead.
# We encourage you NOT to change this parameter unless you know what you are doing.
tick_period_ms: 100
# Compact consensus operations once we have this amount of applied
# operations. Allows peers to join quickly with a consensus snapshot without
# replaying a huge amount of operations.
# If 0 - disable compaction
compact_wal_entries: 128
# Set to true to prevent service from sending usage statistics to the developers.
# Read more: https://qdrant.tech/documentation/guides/telemetry
telemetry_disabled: true
# TLS configuration.
# Required if either service.enable_tls or cluster.p2p.enable_tls is true.
tls:
# Server certificate chain file
# cert: ./tls/cert.pem
# Server private key file
# key: ./tls/key.pem
# Certificate authority certificate file.
# This certificate will be used to validate the certificates
# presented by other nodes during inter-cluster communication.
#
# If verify_https_client_certificate is true, it will verify
# HTTPS client certificate
#
# Required if cluster.p2p.enable_tls is true.
ca_cert: ./tls/cacert.pem
# TTL in seconds to reload certificate from disk, useful for certificate rotations.
# Only works for HTTPS endpoints. Does not support gRPC (and intra-cluster communication).
# If `null` - TTL is disabled.
cert_ttl: 3600

View File

@ -25,7 +25,7 @@ use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_serv
use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY};
use crate::log::switch_to_file_logging;
use crate::pdfium::PDFIUM_LIB_PATH;
use crate::qdrant::{start_qdrant_server, stop_qdrant_server};
use crate::qdrant_edge_database::{start_qdrant_edge_database, stop_qdrant_edge_database};
#[cfg(debug_assertions)]
use crate::dotnet::create_startup_env_file;
use crate::tokenizer::set_default_tokenizer_path;
@ -149,7 +149,7 @@ pub fn start_tauri() {
start_dotnet_server(app.handle().clone());
}
start_qdrant_server(app.handle().clone());
start_qdrant_edge_database(app.handle().clone());
set_default_tokenizer_path(app.handle().clone());
@ -186,7 +186,7 @@ pub fn start_tauri() {
RunEvent::ExitRequested { .. } => {
warn!(Source = "Tauri"; "Run event: exit was requested.");
stop_qdrant_server();
stop_qdrant_edge_database();
if is_prod() {
warn!("Try to stop the .NET server as well...");
stop_dotnet_server();
@ -540,7 +540,7 @@ pub async fn install_update(_token: APIToken) {
if is_prod() {
stop_dotnet_server();
stop_qdrant_server();
stop_qdrant_edge_database();
} else {
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
}
@ -1003,4 +1003,4 @@ mod tests {
assert!(!is_tauri_asset_url(&url));
assert!(!is_local_http_url(&url));
}
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More