diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 3bd6ddf9..c39b90e0 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -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" diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs deleted file mode 100644 index 29369ccf..00000000 --- a/app/Build/Commands/Qdrant.cs +++ /dev/null @@ -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, - }; - } -} \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index f3b0799e..303edcd5 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -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 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+(?[0-9.]+)""")] private static partial Regex MudBlazorVersionRegex(); + [GeneratedRegex("""qdrant-edge\s+v(?[0-9.]+)""")] + private static partial Regex QdrantEdgeVersionRegex(); + [GeneratedRegex("""tauri\s+v(?[0-9.]+)""")] private static partial Regex TauriVersionRegex(); diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 763ac03a..731c813c 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -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 system’s 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 system’s 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" diff --git a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs index 6c3f204f..d5070da0 100644 --- a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs +++ b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs @@ -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); diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 9589ceb3..1c6a4ef2 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -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(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogReference = await this.DialogService.ShowAsync(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: diff --git a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs index 59ef82b2..33c896d1 100644 --- a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs @@ -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 SPELLCHECK_ATTRIBUTES = new(); diff --git a/app/MindWork AI Studio/Components/ConfigurationFile.razor b/app/MindWork AI Studio/Components/ConfigurationFile.razor new file mode 100644 index 00000000..ed2f9be2 --- /dev/null +++ b/app/MindWork AI Studio/Components/ConfigurationFile.razor @@ -0,0 +1,27 @@ +@inherits ConfigurationBaseCore + + + + + + @T("Choose File") + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationFile.razor.cs b/app/MindWork AI Studio/Components/ConfigurationFile.razor.cs new file mode 100644 index 00000000..82d56d18 --- /dev/null +++ b/app/MindWork AI Studio/Components/ConfigurationFile.razor.cs @@ -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 +{ + /// + /// The text used for the textfield. + /// + [Parameter] + public Func Text { get; set; } = () => string.Empty; + + /// + /// An action which is called when the text was changed. + /// + [Parameter] + public Action TextUpdate { get; set; } = _ => { }; + + /// + /// The icon to display next to the textfield. + /// + [Parameter] + public string Icon { get; set; } = Icons.Material.Filled.AttachFile; + + /// + /// The color of the icon to use. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Default; + + /// + /// The title of the file selection dialog. + /// + [Parameter] + public string FileDialogTitle { get; set; } = "Select File"; + + /// + /// The optional file type filter for the file selection dialog. + /// + [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 + + /// + 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 +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor index 41f3b9a3..67929235 100644 --- a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor +++ b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor @@ -3,7 +3,7 @@ - @if (string.IsNullOrWhiteSpace(this.Shortcut())) + @if (string.IsNullOrWhiteSpace(this.Data.Value())) { @T("No shortcut configured") } diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs index aaa600b7..e717787c 100644 --- a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs @@ -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!; /// - /// The current shortcut value. + /// The shortcut binding data. /// [Parameter] - public Func Shortcut { get; set; } = () => string.Empty; - - /// - /// An action which is called when the shortcut was changed. - /// - [Parameter] - public Action ShortcutUpdate { get; set; } = _ => { }; - - /// - /// The name/identifier of the shortcut (used for conflict detection and registration). - /// - [Parameter] - public Shortcut ShortcutId { get; init; } + public ConfigurationShortcutData Data { get; set; } = ConfigurationShortcutData.Empty; /// /// 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 { - { 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( @@ -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(); } diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcutData.cs b/app/MindWork AI Studio/Components/ConfigurationShortcutData.cs new file mode 100644 index 00000000..70541788 --- /dev/null +++ b/app/MindWork AI Studio/Components/ConfigurationShortcutData.cs @@ -0,0 +1,44 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Components; + +/// +/// UI binding data for a configurable keyboard shortcut. +/// +public sealed class ConfigurationShortcutData +{ + /// + /// Empty shortcut binding. + /// + public static ConfigurationShortcutData Empty { get; } = new(); + + /// + /// The name/identifier of the shortcut, used for conflict detection and registration. + /// + public Shortcut Id { get; init; } = Shortcut.NONE; + + /// + /// The current shortcut value. + /// + public Func Value { get; init; } = () => string.Empty; + + /// + /// An action that is called when the shortcut was changed. + /// + public Action ValueUpdate { get; init; } = _ => { }; + + /// + /// The optional user-facing shortcut label. + /// + public Func DisplayName { get; init; } = () => string.Empty; + + /// + /// The canonical shortcut value the optional user-facing label belongs to. + /// + public Func DisplaySource { get; init; } = () => string.Empty; + + /// + /// An action that is called when the user-facing shortcut label was changed. + /// + public Action DisplayUpdate { get; init; } = (_, _) => { }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentAssistantAudit.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentAssistantAudit.razor index b3f8cb6b..b91dcd6f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentAssistantAudit.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentAssistantAudit.razor @@ -3,9 +3,9 @@ - + @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.") - + @(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit")) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor index 2cb8ec00..c6f2b4bd 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor @@ -2,9 +2,9 @@ - + @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.") - + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor index 5077aace..e4b258cb 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor @@ -2,9 +2,9 @@ - + @T("Use Case: this agent is used to select the appropriate data sources for the current prompt.") - + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor index f6989939..061c3585 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor @@ -1,9 +1,9 @@ @inherits SettingsPanelBase - + @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.") - + @if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation) { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index 2237ebb0..7d2b6801 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -37,7 +37,7 @@ @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) { - + } @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings) @@ -46,12 +46,12 @@ @T("Enterprise Administration") - + @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.") @T("Read the Enterprise IT documentation for details.") - + @T("Generate an encryption secret and copy it to the clipboard") + + + @T("External HTTPS certificates") + + + + + } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index a5fbc06b..04022738 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -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 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; diff --git a/app/MindWork AI Studio/Components/Workspaces.razor b/app/MindWork AI Studio/Components/Workspaces.razor index f49864fc..74e52edd 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor +++ b/app/MindWork AI Studio/Components/Workspaces.razor @@ -11,6 +11,36 @@ } else { + @if (this.SearchVisible) + { + + + + + + + + + + + @T("Search chat contents") + + @if (this.isSearchRunning) + { + + } + + + } + @switch (item.Value) @@ -35,7 +65,7 @@ else
- + @if (string.IsNullOrWhiteSpace(treeItem.Text)) { @T("Empty chat") @@ -71,15 +101,22 @@ else @treeItem.Text -
- - - + @if (!this.HasSearchQuery) + { +
+ + + - - - -
+ + + + + + + +
+ }
diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index ef9c15c8..0848fa34 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -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 SearchVisibleChanged { get; set; } + private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1); private readonly List> treeItems = []; private readonly HashSet 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> 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 + { + 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>(); + foreach (var workspace in snapshot.Workspaces) + workspaceChildren.Add(this.CreateSearchWorkspaceTreeItem(workspace)); + + this.treeItems.Add(new TreeItemData + { + 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 + { + Expandable = false, + Value = new TreeDivider(), + }); + } + + if (snapshot.TemporaryChats.Count > 0) + { + var temporaryChatsChildren = new List>(); + 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 + { + 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 CreateSearchWorkspaceTreeItem(WorkspaceSearchWorkspace workspace) + { + var children = new List>(); + foreach (var chat in workspace.Chats) + children.Add(this.CreateChatTreeItem(chat.Chat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat)); + + return new TreeItemData + { + 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(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(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(T("Move Chat to Workspace"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogReference = await this.DialogService.ShowAsync(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 -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Dialogs/DialogOptions.cs b/app/MindWork AI Studio/Dialogs/DialogOptions.cs index e2373824..ddb1d090 100644 --- a/app/MindWork AI Studio/Dialogs/DialogOptions.cs +++ b/app/MindWork AI Studio/Dialogs/DialogOptions.cs @@ -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() { diff --git a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs index 9809b818..b7872203 100644 --- a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs @@ -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))); + } /// /// 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(); + + 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: diff --git a/app/MindWork AI Studio/Dialogs/ShortcutDialogResult.cs b/app/MindWork AI Studio/Dialogs/ShortcutDialogResult.cs new file mode 100644 index 00000000..c9b424c7 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ShortcutDialogResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Dialogs; + +public readonly record struct ShortcutDialogResult(string Shortcut, string DisplayName, string DisplaySource); \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs index c858b38c..301bf937 100644 --- a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs @@ -31,6 +31,9 @@ public partial class SingleInputDialog : MSGComponentBase [Parameter] public string EmptyInputErrorMessage { get; set; } = string.Empty; + [Parameter] + public Func? AdditionalValidation { get; set; } + private static readonly Dictionary 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(); diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor index 05493cff..460b1bd6 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor @@ -5,18 +5,57 @@ @this.Message - @foreach (var (workspaceName, workspaceId) in this.workspaces) + @foreach (var workspace in this.workspaces) { - + } - - @T("Cancel") - - - @this.ConfirmText - + + + + @if (this.showCreateWorkspaceForm) + { + + + + } + else + { + + @T("Create new workspace") + + } + + + + @T("Cancel") + + @if (this.showCreateWorkspaceForm) + { + + @T("Add workspace") + + } + else + { + + @this.ConfirmText + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs index ca4b625e..46cf6ea6 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs @@ -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 workspaces = new(); + private readonly List workspaces = []; + private readonly string escapeHandlerId = $"workspace-selection-dialog-{Guid.NewGuid():N}"; + private MudForm? createWorkspaceForm; + private MudTextField? newWorkspaceNameField; + private DotNetObjectReference? 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 } \ No newline at end of file diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 5421ffc9..a2247811 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -53,7 +53,6 @@ - diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index f35a00a6..a7b85d53 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -51,13 +51,16 @@ + + + - + } @@ -77,10 +80,13 @@ + + + - + } @@ -149,11 +155,14 @@ + + + - + } diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 0f271076..6f3d2fbd 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -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() diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index eae947ab..abf7ffb7 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -34,9 +34,12 @@ - - - + @if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide) + { + + + + } diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 40431683..b44724d0 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -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; } } diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index ab2a7958..1a339dab 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -48,6 +48,26 @@ + +
+ + @this.WorkingDirectory + + +
+
+ +
+ + @this.ExecutablePath + + +
+
+ @if (OperatingSystem.IsLinux()) + { + + } @switch (HasAnyActiveEnvironment) { @@ -89,18 +109,7 @@ { + Items="@this.BuildEnterpriseConfigurationItems(env)"/> } + Items="@this.BuildEnterpriseConfigurationItems(env)"/> continue; } } @@ -186,9 +167,32 @@ } + @if (ExternalHttpClientTimeout.CustomRootCertificateState.IsEnabled) + { + + + @(ExternalHttpClientTimeout.CustomRootCertificateState.IsUsable + ? T("External HTTPS custom root certificates are active.") + : T("External HTTPS custom root certificates are configured but not active.")) + + + + + + @(this.showExternalHttpCustomRootCertificateDetails ? T("Hide Details") : T("Show Details")) + + + } - + @T("Check for updates") @@ -285,7 +289,7 @@ } - + @@ -310,7 +314,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 7c018da2..21fe274c 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -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 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 BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null) + { + var items = new List + { + 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 BuildExternalHttpCustomRootCertificateItems() + { + var state = ExternalHttpClientTimeout.CustomRootCertificateState; + var items = new List + { + 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(); diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 9075425f..526d24a2 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -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"] = {} diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index a1646515..e2fa9f4d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -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 Tokenizer‑Bibliothek dient als Basis‑Framework für die Integration des DeepSeek‑Tokenizers." +-- 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 system’s 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" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 0f8ae35d..de25aecd 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -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 system’s 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 system’s 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" diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index 7be6cdc5..79aef2bc 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 5274358a..1f322788 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index fc739662..4f901f31 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -29,7 +29,7 @@ public abstract class BaseProvider : IProvider, ISecretId /// /// The HTTP client to use it for all requests. /// - protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(); + protected readonly HttpClient HttpClient; /// /// The logger to use. @@ -65,21 +65,26 @@ public abstract class BaseProvider : IProvider, ISecretId /// Constructor for the base provider. /// /// The provider enum value. - /// The base URL for the provider. + /// The base URI for the provider. + /// The trust policy for external HTTPS requests to this provider. /// The logger to use. - 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 /// public LLMProviders Provider { get; } + + /// + /// The base URI for all relative provider requests. + /// + public Uri BaseUri { get; } /// public abstract string Id { get; } diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index a24e6b3d..8de74942 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 2849f6c8..a8840873 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 07787c87..f6181c72 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index d83d21b7..5e12811e 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index ae59bf7d..ae7d13e9 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index df7fbe14..bc6647d2 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index b3728521..ddb16062 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -10,7 +10,7 @@ public sealed class ProviderHuggingFace : BaseProvider { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - 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)}'."); } diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 04ac9898..c4169b72 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 80161caf..56744f91 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -13,7 +13,7 @@ namespace AIStudio.Provider.OpenAI; /// /// The OpenAI provider. /// -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index d84431e3..6e09ef02 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -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"; diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index 8d714985..fce52bf9 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 595a94ef..cf3b858a 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index ecfa87b0..f187aa0c 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index ad027064..c9352514 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -57,6 +57,11 @@ public sealed class DataApp(Expression>? configSelection = n ///
public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); + /// + /// Should the quick start guide be visible on the home page? + /// + public bool ShowQuickStartGuide { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowQuickStartGuide, true); + /// /// The visibility setting for previews features. /// @@ -94,11 +99,36 @@ public sealed class DataApp(Expression>? configSelection = n ///
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty); + /// + /// The user-facing label for the voice recording shortcut, based on the user's keyboard layout. + /// + public string ShortcutVoiceRecordingDisplayName { get; set; } = string.Empty; + + /// + /// The canonical voice recording shortcut value this display label belongs to. + /// + public string ShortcutVoiceRecordingDisplaySource { get; set; } = string.Empty; + /// /// The HTTP timeout in seconds for external HTTP clients. /// public int HttpClientTimeoutSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HttpClientTimeoutSeconds, ExternalHttpClientTimeout.DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS); + /// + /// Should external HTTP clients trust additional root certificates from a configured PEM bundle? + /// + public bool ExternalHttpCustomRootCertificatesEnabled { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificatesEnabled, false); + + /// + /// Path to a PEM bundle containing additional root certificates for external HTTP clients. + /// + public string ExternalHttpCustomRootCertificateBundlePath { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificateBundlePath, string.Empty); + + /// + /// Hostnames for which external HTTP clients may use the additional root certificates. + /// + public HashSet ExternalHttpCustomRootCertificateAllowedHosts { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ExternalHttpCustomRootCertificateAllowedHosts, []); + /// /// Should the user be allowed to add providers? /// diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs index 55087784..3c5581d2 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs @@ -91,17 +91,10 @@ public sealed partial class DatabaseClientProvider(RustService rustService, ILog private async Task 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 diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs index f1e96623..363cf902 100644 --- a/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs @@ -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 points, CancellationToken token); diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs index 6f9eaf87..75ed54da 100644 --- a/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs @@ -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() + { + } } diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs new file mode 100644 index 00000000..7a5e61b9 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs @@ -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 CreateAsync( + RustService rustService, + ILogger logger, + ILogger 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 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 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 Points); + + private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath); + + private sealed record DeleteVectorStoreRequest(string StoreName); + // ReSharper restore NotAccessedPositionalProperty.Local +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs new file mode 100644 index 00000000..fc95ed38 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.Databases.VectorStore; + +public sealed record VectorStoragePoint( + string PointId, + IReadOnlyList Vector, + string DataSourceId, + string DataSourceName, + string DataSourceType, + string FilePath, + string FileName, + string RelativePath, + int ChunkIndex, + string Text, + string Fingerprint, + DateTime LastWriteUtc, + DateTime EmbeddedAtUtc); diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs index 389a90e3..1a11a59f 100644 --- a/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs +++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs @@ -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; diff --git a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs index 952ec3b2..abdffd4e 100644 --- a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs +++ b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs @@ -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; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 59074a2d..e73d3ee6 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -155,6 +155,16 @@ public enum Event /// Requests the chat workspace overlay to be toggled. /// WORKSPACE_TOGGLE_OVERLAY, + + /// + /// Notifies receivers that a workspace was renamed. + /// + WORKSPACE_RENAMED, + + /// + /// Notifies receivers that a workspace was created. + /// + WORKSPACE_CREATED, diff --git a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs index 2cb9fa45..3d465737 100644 --- a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs +++ b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs @@ -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 SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService()); + 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 LOGGER = new(() => Program.LOGGER_FACTORY.CreateLogger(nameof(ExternalHttpClientTimeout))); + private static readonly Lazy SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService()); + 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 ReadAllowedHostPatterns(string? envAllowedHosts) + { + IEnumerable rawPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts) + ? envAllowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + : SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts; + + var patterns = new HashSet(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() + .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 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 allowedHostPatterns) + { + if (allowedHostPatterns.Count == 0) + return "none"; + + return string.Join(", ", allowedHostPatterns); + } + + private readonly record struct CustomRootCertificateConfiguration(bool Enabled, string BundlePath, IReadOnlyList AllowedHostPatterns, string Source); + + private sealed record CustomRootCertificateCache( + string CacheKey, + X509Certificate2Collection Certificates, + ExternalHttpCustomRootCertificateState State); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ExternalHttpCustomRootCertificateState.cs b/app/MindWork AI Studio/Tools/ExternalHttpCustomRootCertificateState.cs new file mode 100644 index 00000000..bea07be5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ExternalHttpCustomRootCertificateState.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Tools; + +public sealed record ExternalHttpCustomRootCertificateState( + bool IsEnabled, + string Source, + string BundlePath, + IReadOnlyList AllowedHostPatterns, + bool IsUsable, + int CertificateCount, + IReadOnlyList CertificateFingerprints, + string Issue); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ExternalHttpTrustPolicy.cs b/app/MindWork AI Studio/Tools/ExternalHttpTrustPolicy.cs new file mode 100644 index 00000000..b866cbaa --- /dev/null +++ b/app/MindWork AI Studio/Tools/ExternalHttpTrustPolicy.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools; + +public enum ExternalHttpTrustPolicy +{ + SYSTEM_TRUST_ONLY, + ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs b/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs index 869f01ca..134c9587 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs @@ -5,7 +5,7 @@ public class I18N : ILang public static readonly I18N I = new(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); - private ILanguagePlugin? language = PluginFactory.BaseLanguage; + private ILanguagePlugin? language; private I18N() { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs index eeafa119..cae831ec 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs @@ -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; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 7cfb4a70..a5f744ce 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -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); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs index daf77fb0..d1e5507b 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs @@ -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) { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index c939899d..e076a842 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -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; diff --git a/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs b/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs index bc6fb15e..197b6143 100644 --- a/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs +++ b/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs @@ -1,3 +1,3 @@ namespace AIStudio.Tools.Rust; -public sealed record EnterpriseConfig(string Id, string ServerUrl); \ No newline at end of file +public sealed record EnterpriseConfig(string Id, string ServerUrl, string Source, string SourceDetail, string Slot); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 99e38e13..b4d81a11 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -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) diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs new file mode 100644 index 00000000..4e438f2f --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Tools.Rust; + +/// +/// The response of the Qdrant Edge information request. +/// +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 + }; +} diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs similarity index 72% rename from app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs rename to app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs index 10d6246a..fb06dfc5 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs @@ -1,8 +1,8 @@ namespace AIStudio.Tools.Rust; -public enum QdrantStatus +public enum QdrantEdgeStatus { STARTING, AVAILABLE, UNAVAILABLE, -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs deleted file mode 100644 index 30044596..00000000 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace AIStudio.Tools.Rust; - -/// -/// The response of the Qdrant information request. -/// -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; } -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs new file mode 100644 index 00000000..435e89c1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Rust; + +public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/Shortcut.cs b/app/MindWork AI Studio/Tools/Rust/Shortcut.cs index f8f783b3..d78ab8d5 100644 --- a/app/MindWork AI Studio/Tools/Rust/Shortcut.cs +++ b/app/MindWork AI Studio/Tools/Rust/Shortcut.cs @@ -14,4 +14,4 @@ public enum Shortcut /// Toggles voice recording on/off. /// VOICE_RECORDING_TOGGLE, -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs index 6db55a6c..90e8606b 100644 --- a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs +++ b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs @@ -14,7 +14,7 @@ public sealed class EnterpriseEnvironmentService(ILogger new EnterpriseEnvironmentSnapshot( environment.ConfigurationId, NormalizeServerUrl(environment.ConfigurationServerUrl), + environment.Source, + environment.SourceDetail, + environment.Slot, environment.ETag?.ToString())) .OrderBy(environment => environment.ConfigurationId) .ToList(); diff --git a/app/MindWork AI Studio/Tools/Services/RustService.App.cs b/app/MindWork AI Studio/Tools/Services/RustService.App.cs index 1602ecc4..9fd0227f 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.App.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.App.cs @@ -120,6 +120,12 @@ public sealed partial class RustService return await response.Content.ReadAsStringAsync(); } + public async Task GetRuntimeInfo() + { + var response = await this.http.GetFromJsonAsync("/system/runtime/info", this.jsonRustSerializerOptions); + return response; + } + /// /// Requests the Rust runtime to exit the entire desktop application. /// diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs index fda5bdd8..3f101d70 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -1,44 +1,53 @@ -using AIStudio.Tools.Databases; -using AIStudio.Tools.Rust; - namespace AIStudio.Tools.Services; public sealed partial class RustService { - public async Task GetQdrantInfo(CancellationToken cancellationToken = default) + public async Task GetDatabaseInfo( + string databaseName, + string infoPath, + Func unavailableFactory, + CancellationToken cancellationToken = default) { try { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(45)); - - return await this.http.GetFromJsonAsync("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token); + + var databaseInfo = await this.http.GetFromJsonAsync(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); } } -} \ No newline at end of file + + public async Task ExecuteDatabaseOperation(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(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); +} diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs index d78567f4..f1155645 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs @@ -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}'."); } diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index c03fccc8..aaa5bb04 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -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> ReadTemporaryChatsCoreAsync() { var chats = new List(); - 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 ParseSearchTerms(string searchText) => searchText + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(term => !string.IsNullOrWhiteSpace(term)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static IReadOnlyList GetMissingTerms(string text, IReadOnlyList terms) => terms + .Where(term => text.IndexOf(term, StringComparison.OrdinalIgnoreCase) < 0) + .ToList(); + + private static bool ChatThreadContainsTerms(ChatThread thread, IReadOnlyList terms) + { + var matchedTerms = new HashSet(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 ThreadContainsTermsAsync(WorkspaceTreeChat chat, IReadOnlyList 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(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> SearchChatsAsync(IReadOnlyList chats, IReadOnlyList terms, bool includeThreadContents, CancellationToken token) + { + var results = new List(); + 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 SearchWorkspaceChatsAsync(string searchText, bool includeThreadContents, CancellationToken token = default) + { + var terms = ParseSearchTerms(searchText); + if (terms.Count == 0) + return new([], []); + + List workspaces; + List 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(); + 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? 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 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 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 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"); -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs new file mode 100644 index 00000000..bd09ebff --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchResult(WorkspaceTreeChat Chat, bool NameMatched, bool ThreadMatched); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs new file mode 100644 index 00000000..9d1c2271 --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchSnapshot(IReadOnlyList Workspaces, IReadOnlyList TemporaryChats); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs new file mode 100644 index 00000000..1c8ce2ac --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchWorkspace(Guid WorkspaceId, string WorkspacePath, string Name, IReadOnlyList Chats); \ No newline at end of file diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 311fe569..65751edc 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -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", diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index 787fb272..a6631ea6 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -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; diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index a2f8f967..c2845f76 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -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) +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md index 7e4a82af..d3fd4a57 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -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. \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs index 5092d436..c4fe1392 100644 --- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs @@ -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 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(); - 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(); - 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(); + return attributeArgument is not null && ContainsNode(attributeArgument.Expression, stringLiteral); + } + + private static bool IsInSwitchCaseLabel(LiteralExpressionSyntax stringLiteral) + { + var caseSwitchLabel = stringLiteral.FirstAncestorOrSelf(); + return caseSwitchLabel is not null && ContainsNode(caseSwitchLabel.Value, stringLiteral); + } + + private static bool IsInConstantPattern(LiteralExpressionSyntax stringLiteral) + { + var constantPattern = stringLiteral.FirstAncestorOrSelf(); + 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; } } \ No newline at end of file diff --git a/documentation/Build.md b/documentation/Build.md index 8022cd7d..3301562e 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -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 IDE’s 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. \ No newline at end of file +10. Building the release including virus scanning takes some time. Please be patient. diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 221a24db..3d7a9c1b 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -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 else’s ID to gain access to exclusive resources. diff --git a/metadata.txt b/metadata.txt index 533a4c14..7883dc5d 100644 --- a/metadata.txt +++ b/metadata.txt @@ -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 \ No newline at end of file +0.6.1 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 4639fd67..ffc6b325 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -14,6 +23,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aes" version = "0.8.4" @@ -27,15 +42,29 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cipher 0.5.1", + "cipher 0.5.2", "cpubits", "cpufeatures 0.3.0", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.1", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -45,6 +74,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -61,10 +99,10 @@ dependencies = [ ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" @@ -76,10 +114,60 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.86" +name = "anstream" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apple-native-keyring-store" @@ -92,6 +180,15 @@ dependencies = [ "security-framework", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -117,7 +214,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -130,6 +227,21 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "asn1-rs" version = "0.7.1" @@ -139,10 +251,10 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", ] @@ -360,6 +472,23 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_refcell" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[package]] +name = "atomicwrites" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3" +dependencies = [ + "rustix 0.38.34", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -462,6 +591,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.5", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + [[package]] name = "base64" version = "0.21.7" @@ -474,6 +618,41 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "binout" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222fb4925a15bea6a68075021910e03d6aa2d04951d71ff1d956190a551d738f" + [[package]] name = "bit-set" version = "0.8.0" @@ -483,6 +662,12 @@ dependencies = [ "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -519,6 +704,46 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitm" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7edec3daafc233e78a219c85a77bcf535ee267b0fae7a1aad96bd1a67add5d3" +dependencies = [ + "dyn_size_of", +] + +[[package]] +name = "bitpacking" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" +dependencies = [ + "crunchy", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -586,6 +811,14 @@ dependencies = [ "piper", ] +[[package]] +name = "bm25" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "murmur3_32", +] + [[package]] name = "brotli" version = "8.0.2" @@ -615,9 +848,23 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "byteorder" @@ -730,7 +977,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -754,11 +1001,11 @@ dependencies = [ [[package]] name = "cbc" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher 0.5.1", + "cipher 0.5.2", ] [[package]] @@ -773,6 +1020,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cedarwood" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90" +dependencies = [ + "smallvec", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -812,6 +1068,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgroups-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc46cf39fc5922b840030e0e5b378ce5caa9a824a675a95c6dec2c2c9ce9468" +dependencies = [ + "bit-vec 0.6.3", + "libc", + "log", + "nix 0.25.1", + "thiserror 1.0.63", + "zbus", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -820,22 +1090,39 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "charabia" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51689ee7cc84c8de789fc2874711d816055b93406cfd4135c40d1c82dd24b928" +dependencies = [ + "aho-corasick", + "csv", + "either", + "fst", + "irg-kvariants", + "jieba-rs", + "serde", + "slice-group-by", + "unicode-normalization", + "whatlang", ] [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] @@ -850,11 +1137,11 @@ dependencies = [ [[package]] name = "cipher" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout 0.2.2", ] @@ -897,6 +1184,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -907,6 +1200,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "common" +version = "0.0.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "ahash", + "aligned-vec", + "atomicwrites", + "bincode 1.3.3", + "bitvec", + "bytemuck", + "chrono", + "fs-err", + "fs4", + "fs_extra", + "io-uring", + "itertools", + "log", + "memmap2", + "nix 0.31.3", + "num-traits", + "num_cpus", + "ordered-float 5.3.0", + "parking_lot", + "ph", + "procfs", + "quick_cache", + "rand 0.10.1", + "roaring", + "schemars", + "self_cell", + "semver", + "serde", + "serde_json", + "slab", + "strum", + "tap", + "tar", + "tempfile", + "thiserror 2.0.18", + "thread-priority", + "tokio", + "validator", + "walkdir", + "zerocopy", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -916,6 +1256,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -942,6 +1294,12 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1050,6 +1408,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1111,9 +1478,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -1141,6 +1508,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.8.0" @@ -1166,6 +1554,12 @@ dependencies = [ "cmov", ] +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + [[package]] name = "darling" version = "0.20.10" @@ -1186,7 +1580,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.117", ] @@ -1202,10 +1596,30 @@ dependencies = [ ] [[package]] -name = "data-encoding" -version = "2.10.0" +name = "dary_heap" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dataset" +version = "0.0.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "anyhow", + "flate2", + "fs-err", + "indicatif", + "reqwest", + "serde", + "serde_json", +] [[package]] name = "dbus" @@ -1266,7 +1680,7 @@ checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -1333,7 +1747,7 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] @@ -1355,7 +1769,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1404,6 +1818,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "docopt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" +dependencies = [ + "lazy_static", + "regex", + "serde", + "strsim 0.10.0", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -1464,6 +1890,17 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "duplicate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "proc-macro2-diagnostics", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1471,10 +1908,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "either" -version = "1.13.0" +name = "dyn_size_of" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "4a742b95783b1f45b900129082cbc47717b6a77ee8d17eea70a8ea62462f5de3" + +[[package]] +name = "earcut" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88459a2a8e3a514b6e6de38cf3aaa9250a894cb098f74a932db77fcc8341b6d0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ecow" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bac48c16a993694c703f2991527d422d9efb8ec5625756004ba30e97683720" +dependencies = [ + "serde", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "embed-resource" @@ -1496,6 +1957,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1532,6 +1999,49 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1556,7 +2066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1593,7 +2103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", - "half", + "half 2.7.1", "lebe", "miniz_oxide 0.8.5", "rayon-core", @@ -1657,29 +2167,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] -name = "flate2" -version = "1.1.2" +name = "fixedbitset" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide 0.8.5", + "zlib-rs", ] [[package]] name = "flexi_logger" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea7feddba9b4e83022270d49a58d4a1b3fdad04b34f78cf1ce471f698e42672" +checksum = "2e90140a77c0ffbe2e4839e062983ec4ec60d4473e41a4fcce0884809d1b76d6" dependencies = [ "chrono", "log", "nu-ansi-term", "regex", - "thiserror 2.0.12", + "thiserror 2.0.18", ] +[[package]] +name = "float_next_after" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37007738a80ea34f969af54a3390dd72cacdef654974cfd449c9f6f72dbaac10" + [[package]] name = "fnv" version = "1.0.7" @@ -1744,12 +2266,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "fs4" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e72ed92b67c146290f88e9c89d60ca163ea417a446f61ffd7b72df3e7f1dfd5" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -1960,6 +2504,60 @@ dependencies = [ "version_check", ] +[[package]] +name = "geo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30eb1fdc57c1e5cfd11826fe0caec4b9dc7901f3758263bb506228d88c8d9e9a" +dependencies = [ + "earcut", + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "rand 0.10.1", + "rand_pcg", + "robust", + "rstar", + "sif-itree", + "spade", +] + +[[package]] +name = "geo-types" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94776032c45f950d30a13af6113c2ad5625316c9abfbccee4dd5a6695f8fe0f5" +dependencies = [ + "approx", + "num-traits", + "rayon", + "rstar", + "serde", + "spade", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7" +dependencies = [ + "libm", +] + +[[package]] +name = "geohash" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f58890382f70caccc5fa388981f7ac80c913795042afce9f3e065695d8f7464" +dependencies = [ + "geo-types", + "libm", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1977,8 +2575,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2004,7 +2604,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "rand_core", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -2019,6 +2619,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gio" version = "0.18.4" @@ -2116,7 +2722,7 @@ dependencies = [ "objc2-app-kit", "once_cell", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "windows-sys 0.59.0", "x11rb", "xkeysym", @@ -2133,6 +2739,42 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gpu" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "log", + "parking_lot", + "zerocopy", +] + +[[package]] +name = "gridstore" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "ahash", + "bitvec", + "bytemuck", + "common", + "dataset", + "ecow", + "fs-err", + "itertools", + "log", + "lz4_flex", + "parking_lot", + "rand 0.10.1", + "serde", + "serde_cbor", + "serde_json", + "smallvec", + "tempfile", + "thiserror 2.0.18", + "zerocopy", +] + [[package]] name = "gtk" version = "0.18.2" @@ -2206,12 +2848,31 @@ dependencies = [ [[package]] name = "half" -version = "2.5.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", + "num-traits", + "serde", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", ] [[package]] @@ -2220,21 +2881,54 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -2390,6 +3084,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -2413,6 +3120,52 @@ dependencies = [ "tracing", ] +[[package]] +name = "i_float" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813145bb0ad5b60f55cbbf3c74cdceda1c0a9d253b35c4cc36ae0df7887cb78f" +dependencies = [ + "libm", +] + +[[package]] +name = "i_key_sort" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d122b937fca067feb0ad74f62388920272b27c356d4df2d0cfdd59e044cf0" +dependencies = [ + "rayon", +] + +[[package]] +name = "i_overlay" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd314b4668e2b3a12508f2e125558c82a6c0a8636fa5107a900f79ce414e450" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9eac533d7509a8ab87672b60ac610c17240f9ea4851d26227689fdfe349c8" +dependencies = [ + "i_float", +] + +[[package]] +name = "i_tree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4804bdc1dc124eb7e1aa9e144ecc04096bcf787a10a15fa44af682b51f0f6cce" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -2630,6 +3383,39 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "include-flate" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e233413926ef735f7d87024466cfda5a4b87467730846bd82ea7d504121347" +dependencies = [ + "include-flate-codegen", + "include-flate-compress", +] + +[[package]] +name = "include-flate-codegen" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e7148f24ef8922cc0e5574ebb908729ccdd3a110c440a45165733fedadd9969" +dependencies = [ + "include-flate-compress", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "include-flate-compress" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74783a9ed407e844e99d5e7a57bd650acbfa124cf6e97ffd790ba59d8ab8e7ff" +dependencies = [ + "libflate", + "zstd", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2653,6 +3439,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "rayon", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "infer" version = "0.19.0" @@ -2682,12 +3482,40 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "integer-encoding" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" + +[[package]] +name = "io-uring" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "irg-kvariants" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2af7c331f2536964a32b78a7d2e0963d78b42f4a76323b16cc7d94b1ddce26" +dependencies = [ + "csv", + "once_cell", + "serde", +] + [[package]] name = "iri-string" version = "0.7.8" @@ -2717,6 +3545,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -2755,6 +3589,53 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jieba-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348294e44ee7e3c42685da656490f8febc7359632544019621588902216da95c" +dependencies = [ + "phf_codegen", +] + +[[package]] +name = "jieba-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "766bd7012aa5ba49411ebdf4e93bddd59b182d2918e085d58dec5bb9b54b7105" +dependencies = [ + "cedarwood", + "include-flate", + "jieba-macros", + "phf", + "regex", + "rustc-hash", +] + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "jni" version = "0.21.1" @@ -2906,6 +3787,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libflate" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd96e993e5f3368b0cb8497dae6c860c22af8ff18388c61c6c0b86c58d86b5df" +dependencies = [ + "adler32", + "crc32fast", + "dary_heap", + "libflate_lz77", + "no_std_io2", +] + +[[package]] +name = "libflate_lz77" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7a10e427698aef6eef269482776debfef63384d30f13aad39a1a95e0e098fd" +dependencies = [ + "hashbrown 0.16.1", + "no_std_io2", + "rle-decode-fast", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2923,9 +3828,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.16" @@ -2935,15 +3846,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" -dependencies = [ - "zlib-rs", -] - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2964,12 +3866,12 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2980,9 +3882,21 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" [[package]] name = "lzma-rs" @@ -3005,6 +3919,32 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "macros" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -3034,6 +3974,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -3053,7 +4002,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" name = "mindwork-ai-studio" version = "26.5.5" dependencies = [ - "aes 0.9.0", + "aes 0.9.1", "apple-native-keyring-store", "arboard", "async-stream", @@ -3062,7 +4011,7 @@ dependencies = [ "base64 0.22.1", "bytes", "calamine", - "cbc 0.2.0", + "cbc 0.2.1", "cfg-if", "dbus-secret-service-keyring-store", "file-format", @@ -3075,8 +4024,9 @@ dependencies = [ "pbkdf2 0.13.0", "pdfium-render", "pptx-to-md", - "rand", - "rand_chacha", + "qdrant-edge", + "rand 0.10.1", + "rand_chacha 0.10.0", "rcgen", "rustls", "serde", @@ -3084,7 +4034,7 @@ dependencies = [ "sha2 0.11.0", "strum_macros", "sys-locale", - "sysinfo", + "sysinfo 0.39.3", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -3161,10 +4111,16 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "windows-sys 0.61.2", ] +[[package]] +name = "murmur3_32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e7c60ee0b5e809b81d443bab6a735f9c80fe2138ebc5d758cda7b4faa2cdbd" + [[package]] name = "ndk" version = "0.9.0" @@ -3195,6 +4151,45 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -3205,6 +4200,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "ntapi" version = "0.4.2" @@ -3247,6 +4251,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.6" @@ -3262,6 +4272,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3300,6 +4321,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -3318,7 +4350,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.117", @@ -3602,6 +4634,15 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -3617,6 +4658,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "open" version = "5.3.4" @@ -3641,6 +4688,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "bytemuck", + "num-traits", + "rand 0.8.6", + "schemars", + "serde", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3672,7 +4741,7 @@ dependencies = [ "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -3708,9 +4777,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3718,17 +4787,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ + "backtrace", "cfg-if", "libc", + "petgraph", "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -3797,6 +4874,39 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "permutation_iterator" +version = "0.1.2" +source = "git+https://github.com/SommerEngineering/permutation-iterator-rs.git?rev=76836ed316d18dfef530ba908f58481c343e80d7#76836ed316d18dfef530ba908f58481c343e80d7" +dependencies = [ + "blake2-rfc", + "rand 0.8.6", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + +[[package]] +name = "ph" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2fbaf8da280599aae4047ea0659a1e79cf61739bce5bdc50ca88dc7e6357060" +dependencies = [ + "aligned-vec", + "binout", + "bitm", + "dyn_size_of", + "rayon", + "seedable_hash", +] + [[package]] name = "phf" version = "0.13.1" @@ -3850,6 +4960,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -3932,6 +5062,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "posting_list" +version = "0.0.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "bitpacking", + "common", + "zerocopy", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3948,7 +5103,7 @@ dependencies = [ "image 0.24.9", "rayon", "roxmltree", - "thiserror 2.0.12", + "thiserror 2.0.18", "zip 2.5.0", ] @@ -4027,6 +5182,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4036,6 +5213,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.11.1", + "procfs-core", + "rustix 1.1.4", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.11.1", + "hex", +] + +[[package]] +name = "qdrant-edge" +version = "0.7.2" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "ahash", + "bm25", + "common", + "fs-err", + "itertools", + "log", + "ordered-float 5.3.0", + "parking_lot", + "rand 0.10.1", + "segment", + "serde", + "serde_json", + "shard", + "sparse", + "uuid", + "wal", +] + +[[package]] +name = "qdrant-rust-stemmers" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e61bd348ee10767d59d65d47ced0861921e8bb3ef0823aab63cc16c6c0f6d756" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "qoi" version = "0.4.1" @@ -4045,6 +5288,27 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quantization" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "arrayvec 0.7.6", + "bytemuck", + "cc", + "common", + "fs-err", + "num-traits", + "ordered-float 5.3.0", + "parking_lot", + "permutation_iterator", + "rand 0.10.1", + "rayon", + "serde", + "serde_json", + "strum", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -4065,10 +5329,78 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.36" +name = "quick_cache" +version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "d1c821816e9b928e20e92ed59bb3ac4aab321d16ca2316871c9fe7ca739cd477" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.1", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4079,6 +5411,34 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.1" @@ -4087,7 +5447,27 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -4097,7 +5477,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.1", ] [[package]] @@ -4106,6 +5505,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand 0.10.1", +] + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4114,9 +5532,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -4124,9 +5542,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4172,14 +5590,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4189,9 +5607,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4200,20 +5618,22 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -4224,6 +5644,7 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -4281,12 +5702,70 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "roaring" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + [[package]] name = "roxmltree" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4308,7 +5787,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -4334,7 +5813,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4344,6 +5823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -4370,6 +5850,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -4391,7 +5872,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4448,8 +5929,10 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ + "chrono", "dyn-clone", "indexmap 1.9.3", + "indexmap 2.14.0", "schemars_derive", "serde", "serde_json", @@ -4498,6 +5981,88 @@ dependencies = [ "libc", ] +[[package]] +name = "seedable_hash" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47190540123956611cf01db81ad6dee21ca70e1d94a8ff5a962cf6d93b217c7c" +dependencies = [ + "wyhash", + "xxhash-rust", +] + +[[package]] +name = "segment" +version = "0.6.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "ahash", + "atomic_refcell", + "atomicwrites", + "bincode 1.3.3", + "bitvec", + "bytemuck", + "byteorder", + "cc", + "cgroups-rs", + "charabia", + "chrono", + "common", + "data-encoding", + "duplicate", + "ecow", + "fnv", + "fs-err", + "fs_extra", + "geo", + "geohash", + "gpu", + "gridstore", + "half 2.7.1", + "indexmap 2.14.0", + "integer-encoding", + "io-uring", + "itertools", + "log", + "macro_rules_attribute", + "macros", + "memmap2", + "nom 8.0.0", + "num-cmp", + "num-derive", + "num-traits", + "ordered-float 5.3.0", + "parking_lot", + "posting_list", + "procfs", + "qdrant-rust-stemmers", + "quantization", + "rand 0.10.1", + "rayon", + "roaring", + "schemars", + "self_cell", + "serde", + "serde-untagged", + "serde-value", + "serde_cbor", + "serde_json", + "serde_variant", + "sha2 0.11.0", + "smallvec", + "sparse", + "strum", + "sysinfo 0.38.4", + "tap", + "tempfile", + "thiserror 2.0.18", + "tinyvec", + "uuid", + "validator", + "vaporetto", + "zerocopy", +] + [[package]] name = "selectors" version = "0.36.1" @@ -4517,6 +6082,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.23" @@ -4548,6 +6119,26 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4581,10 +6172,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -4644,6 +6236,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + [[package]] name = "serde_with" version = "3.9.0" @@ -4738,6 +6339,39 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "shard" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "ahash", + "chrono", + "common", + "fs-err", + "fs4", + "indexmap 2.14.0", + "itertools", + "log", + "ordered-float 5.3.0", + "parking_lot", + "rand 0.10.1", + "rmp-serde", + "schemars", + "segment", + "serde", + "serde_cbor", + "serde_json", + "smallvec", + "sparse", + "strum", + "tempfile", + "thiserror 2.0.18", + "tonic", + "uuid", + "validator", + "wal", +] + [[package]] name = "shared_child" version = "1.0.0" @@ -4754,6 +6388,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sif-itree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f45b8998ced5134fb1d75732c77842a3e888f19c1ff98481822e8fbfbf930b" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4777,18 +6417,21 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -4848,6 +6491,44 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spade" +version = "2.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" +dependencies = [ + "hashbrown 0.16.1", + "num-traits", + "robust", + "smallvec", +] + +[[package]] +name = "sparse" +version = "0.1.0" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "bincode 1.3.3", + "bitpacking", + "common", + "fs-err", + "gridstore", + "half 2.7.1", + "itertools", + "log", + "memmap2", + "ordered-float 5.3.0", + "parking_lot", + "rand 0.10.1", + "schemars", + "serde", + "serde_json", + "tempfile", + "typed-arena", + "validator", + "zerocopy", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4878,12 +6559,27 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + [[package]] name = "strum_macros" version = "0.28.0" @@ -4966,9 +6662,23 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.1" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", +] + +[[package]] +name = "sysinfo" +version = "0.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96" dependencies = [ "libc", "memchr", @@ -5044,10 +6754,16 @@ dependencies = [ ] [[package]] -name = "tar" -version = "0.4.45" +name = "tap" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -5062,9 +6778,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" dependencies = [ "anyhow", "bytes", @@ -5101,7 +6817,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tray-icon", "url", @@ -5113,9 +6829,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", @@ -5134,9 +6850,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" dependencies = [ "base64 0.22.1", "brotli", @@ -5152,7 +6868,7 @@ dependencies = [ "sha2 0.10.8", "syn 2.0.117", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "url", "uuid", @@ -5161,9 +6877,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5203,7 +6919,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.12", + "thiserror 2.0.18", "url", ] @@ -5226,7 +6942,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "toml 1.1.2+spec-1.1.0", "url", ] @@ -5243,7 +6959,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -5262,7 +6978,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", "url", "windows 0.61.3", "zbus", @@ -5285,7 +7001,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", ] @@ -5314,7 +7030,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "tokio", "url", @@ -5334,14 +7050,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "tauri-runtime" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" dependencies = [ "cookie", "dpi", @@ -5355,7 +7071,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -5364,9 +7080,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", @@ -5390,9 +7106,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" dependencies = [ "anyhow", "brotli", @@ -5418,7 +7134,7 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.12", + "thiserror 2.0.18", "toml 1.1.2+spec-1.1.0", "url", "urlpattern", @@ -5447,7 +7163,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5471,11 +7187,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -5491,15 +7207,29 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "thread-priority" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210811179577da3d54eb69ab0b50490ee40491a25d95b8c6011ba40771cb721" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.61.3", +] + [[package]] name = "tiff" version = "0.9.1" @@ -5552,6 +7282,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -5561,6 +7306,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5733,6 +7479,37 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "flate2", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -5741,9 +7518,12 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.14.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -5829,7 +7609,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -5839,6 +7619,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typed-path" version = "0.12.2" @@ -5915,24 +7701,51 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -5986,13 +7799,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "uuid" -version = "1.10.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "vaporetto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7437bd3d45100e1ed1a284187ce4e9ee863f1fdac97b7eaa614623741464c6" +dependencies = [ + "bincode 2.0.1", + "daachorse", + "hashbrown 0.15.2", ] [[package]] @@ -6016,6 +7878,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vswhom" version = "0.1.0" @@ -6036,6 +7904,25 @@ dependencies = [ "libc", ] +[[package]] +name = "wal" +version = "0.1.4" +source = "git+https://github.com/SommerEngineering/qdrant.git?rev=462c84d82ced126e4a2b7914544bfde16a509eb1#462c84d82ced126e4a2b7914544bfde16a509eb1" +dependencies = [ + "byteorder", + "crc32c", + "docopt", + "env_logger", + "fs-err", + "fs4", + "log", + "memmap2", + "rand 0.10.1", + "rand_distr", + "rustix 1.1.4", + "serde", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -6209,6 +8096,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.4" @@ -6305,7 +8202,7 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.18", "windows 0.61.3", "windows-core 0.61.2", ] @@ -6316,6 +8213,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "whatlang" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d1c1645d361eb782a1650b1786a8fb58dd625e681a04c09f5ff7c8764a7b0" +dependencies = [ + "hashbrown 0.14.5", + "once_cell", +] + [[package]] name = "whoami" version = "2.1.2" @@ -7080,7 +8987,7 @@ dependencies = [ "sha2 0.10.8", "soup3", "tao-macros", - "thiserror 2.0.12", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -7091,6 +8998,24 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyhash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf6e163c25e3fac820b4b453185ea2dea3b6a3e0a721d4d23d75bd33734c295" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -7139,11 +9064,11 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", ] @@ -7164,6 +9089,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "xz2" version = "0.1.7" @@ -7268,6 +9199,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.5" @@ -7340,7 +9291,7 @@ dependencies = [ "aes 0.8.4", "arbitrary", "bzip2", - "constant_time_eq", + "constant_time_eq 0.3.1", "crc32fast", "crossbeam-utils", "deflate64", @@ -7387,9 +9338,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] name = "zmij" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 8552913a..457d1f04 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -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" diff --git a/runtime/capabilities/default.json b/runtime/capabilities/default.json index 86f14897..edd9c22f 100644 --- a/runtime/capabilities/default.json +++ b/runtime/capabilities/default.json @@ -22,11 +22,6 @@ "name": "mindworkAIStudioServer", "sidecar": true, "args": true - }, - { - "name": "qdrant", - "sidecar": true, - "args": true } ] } diff --git a/runtime/patches/README.md b/runtime/patches/README.md new file mode 100644 index 00000000..865e67b2 --- /dev/null +++ b/runtime/patches/README.md @@ -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. diff --git a/runtime/patches/qdrant-edge-ai-studio.patch b/runtime/patches/qdrant-edge-ai-studio.patch new file mode 100644 index 00000000..3b33f565 --- /dev/null +++ b/runtime/patches/qdrant-edge-ai-studio.patch @@ -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 "] + 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)) + } + + impl Anonymize for String { diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml deleted file mode 100644 index 50f03e08..00000000 --- a/runtime/resources/databases/qdrant/config.yaml +++ /dev/null @@ -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: - #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: ` - # - # 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: ` - # - # 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 \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index f03f2102..7f2fe904 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -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)); } -} \ No newline at end of file +} diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 3f8dd43c..400b2fa8 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -11,13 +11,25 @@ use sys_locale::get_locale; const DEFAULT_LANGUAGE: &str = "en-US"; -const ENTERPRISE_CONFIG_SLOT_COUNT: usize = 10; +const ENTERPRISE_CONFIG_SLOT_MAX: u32 = 99_999; +const ENTERPRISE_CONFIG_SLOT_WIDTH: usize = 5; + +const ENTERPRISE_CONFIG_ID_KEY_PREFIX: &str = "config_id"; +const ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX: &str = "config_server_url"; #[cfg(target_os = "windows")] const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\Enterprise IT"; const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml"; +#[cfg(any(target_os = "linux", test))] +const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI"; + +const ENTERPRISE_ENV_CONFIG_ID_PREFIX: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID"; +const ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL"; +const ENTERPRISE_ENV_CONFIGS: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS"; +const ENTERPRISE_ENV_CONFIG_ENCRYPTION_SECRET: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET"; + /// The data directory where the application stores its data. pub static DATA_DIRECTORY: OnceLock = OnceLock::new(); @@ -51,6 +63,59 @@ pub async fn read_user_name(_token: APIToken) -> String { }) } +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct RuntimeInfo { + pub working_directory: String, + pub executable_path: String, + pub linux_package_type: String, +} + +pub async fn get_runtime_info(_token: APIToken) -> Json { + Json(RuntimeInfo { + working_directory: env::current_dir() + .map(|path| path.to_string_lossy().into_owned()) + .unwrap_or_default(), + executable_path: env::current_exe() + .map(|path| path.to_string_lossy().into_owned()) + .unwrap_or_default(), + linux_package_type: detect_linux_package_type().to_string(), + }) +} + +#[cfg(target_os = "linux")] +fn detect_linux_package_type() -> &'static str { + if is_flatpak() { + "flatpak" + } else if is_appimage() { + "appimage" + } else { + "unknown" + } +} + +#[cfg(not(target_os = "linux"))] +fn detect_linux_package_type() -> &'static str { + "not_applicable" +} + +#[cfg(target_os = "linux")] +fn is_flatpak() -> bool { + env_var_has_value("FLATPAK_ID") + || Path::new("/.flatpak-info").is_file() + || env::var("container") + .is_ok_and(|value| value.trim().eq_ignore_ascii_case("flatpak")) +} + +#[cfg(target_os = "linux")] +fn is_appimage() -> bool { + env_var_has_value("APPIMAGE") || env_var_has_value("APPDIR") +} + +#[cfg(target_os = "linux")] +fn env_var_has_value(key: &str) -> bool { + env::var(key).is_ok_and(|value| !value.trim().is_empty()) +} + /// Returns true if the application is running in development mode. pub fn is_dev() -> bool { cfg!(debug_assertions) @@ -187,8 +252,53 @@ pub async fn read_user_language(_token: APIToken) -> String { pub struct EnterpriseConfig { pub id: String, pub server_url: String, + pub source: String, + pub source_detail: String, + pub slot: String, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct EnterpriseSourceValue { + value: String, + source_detail: String, +} + +impl EnterpriseSourceValue { + fn new(value: String, source_detail: String) -> Self { + Self { + value, + source_detail, + } + } +} + +trait EnterpriseSourceValueAccess { + fn value(&self) -> &str; + fn source_detail(&self) -> &str; +} + +impl EnterpriseSourceValueAccess for EnterpriseSourceValue { + fn value(&self) -> &str { + &self.value + } + + fn source_detail(&self) -> &str { + &self.source_detail + } +} + +impl EnterpriseSourceValueAccess for String { + fn value(&self) -> &str { + self + } + + fn source_detail(&self) -> &str { + "" + } +} + +type EnterpriseSourceValues = HashMap; + #[derive(Clone, Debug, Default, PartialEq, Eq)] struct EnterpriseSourceData { source_name: String, @@ -292,7 +402,7 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData { info!(r"Trying to read enterprise configuration metadata from 'HKEY_CURRENT_USER\{}'.", ENTERPRISE_REGISTRY_KEY_PATH); - let mut values = HashMap::new(); + let mut values = EnterpriseSourceValues::new(); let key = match CURRENT_USER.open(ENTERPRISE_REGISTRY_KEY_PATH) { Ok(key) => key, Err(_) => { @@ -304,32 +414,40 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData { } }; - for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT { - insert_registry_value(&mut values, &key, &format!("config_id{index}")); - insert_registry_value(&mut values, &key, &format!("config_server_url{index}")); - } + match key.values() { + Ok(registry_values) => { + for (key_name, value) in registry_values { + let Some(source_key_name) = enterprise_registry_value_key_name(&key_name) else { + continue; + }; - for key_name in [ - "configs", - "config_id", - "config_server_url", - "config_encryption_secret", - ] { - insert_registry_value(&mut values, &key, key_name); + match String::try_from(value) { + Ok(value) => { + values.insert(source_key_name, EnterpriseSourceValue::new(value, String::new())); + }, + + Err(error) => { + warn!(r"Could not read enterprise registry value 'HKEY_CURRENT_USER\{}\{}' as string: {}.", ENTERPRISE_REGISTRY_KEY_PATH, key_name, error); + }, + } + } + }, + + Err(error) => { + warn!(r"Could not enumerate enterprise registry values from 'HKEY_CURRENT_USER\{}': {}.", ENTERPRISE_REGISTRY_KEY_PATH, error); + }, } parse_enterprise_source_values("Windows registry", &values) } #[cfg(target_os = "windows")] -fn insert_registry_value( - values: &mut HashMap, - key: &windows_registry::Key, - key_name: &str, -) { - if let Ok(value) = key.get_string(key_name) { - values.insert(String::from(key_name), value); +fn enterprise_registry_value_key_name(key_name: &str) -> Option { + if is_legacy_enterprise_source_key(key_name) { + return Some(String::from(key_name)); } + + enterprise_indexed_source_key_name(key_name) } fn load_policy_file_enterprise_source() -> EnterpriseSourceData { @@ -342,26 +460,85 @@ fn load_policy_file_enterprise_source() -> EnterpriseSourceData { fn load_environment_enterprise_source() -> EnterpriseSourceData { info!("Trying to read enterprise configuration metadata from environment variables."); - let mut values = HashMap::new(); - for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT { - insert_env_value(&mut values, &format!("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID{index}"), &format!("config_id{index}")); - insert_env_value(&mut values, &format!("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL{index}"), &format!("config_server_url{index}")); + let mut values = EnterpriseSourceValues::new(); + for (env_name, value) in env::vars() { + if let Some(source_key_name) = enterprise_environment_key_name(&env_name) { + let source_detail = enterprise_environment_source_detail(&source_key_name, &env_name); + values.insert(source_key_name, EnterpriseSourceValue::new(value, source_detail)); + } } - insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS", "configs"); - insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID", "config_id"); - insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL", "config_server_url"); - insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET", "config_encryption_secret"); - parse_enterprise_source_values("environment variables", &values) } -fn insert_env_value(values: &mut HashMap, env_name: &str, key_name: &str) { - if let Ok(value) = env::var(env_name) { - values.insert(String::from(key_name), value); +fn enterprise_environment_source_detail(source_key_name: &str, env_name: &str) -> String { + if source_key_name == "config_id" + || enterprise_source_key_suffix(source_key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX).is_some() { + String::from(env_name) + } else { + String::new() } } +fn enterprise_environment_key_name(env_name: &str) -> Option { + if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIGS) { + return Some(String::from("configs")); + } + + if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_ID_PREFIX) { + return Some(String::from("config_id")); + } + + if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) { + return Some(String::from("config_server_url")); + } + + if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_ENCRYPTION_SECRET) { + return Some(String::from("config_encryption_secret")); + } + + if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_ID_PREFIX) { + return Some(format!("config_id{suffix}")); + } + + if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) { + return Some(format!("config_server_url{suffix}")); + } + + None +} + +#[cfg(target_os = "windows")] +fn enterprise_env_key_equals(env_name: &str, expected: &str) -> bool { + env_name.eq_ignore_ascii_case(expected) +} + +#[cfg(not(target_os = "windows"))] +fn enterprise_env_key_equals(env_name: &str, expected: &str) -> bool { + env_name == expected +} + +#[cfg(target_os = "windows")] +fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a str> { + if env_name.len() < prefix.len() { + return None; + } + + let (raw_prefix, suffix) = env_name.split_at(prefix.len()); + if raw_prefix.eq_ignore_ascii_case(prefix) { + normalize_enterprise_slot_suffix(suffix) + } else { + None + } +} + +#[cfg(not(target_os = "windows"))] +fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a str> { + env_name + .strip_prefix(prefix) + .and_then(normalize_enterprise_slot_suffix) +} + #[cfg(target_os = "windows")] fn enterprise_policy_directories() -> Vec { let base = env::var_os("ProgramData") @@ -373,7 +550,7 @@ fn enterprise_policy_directories() -> Vec { #[cfg(target_os = "linux")] fn enterprise_policy_directories() -> Vec { let xdg_config_dirs = env::var("XDG_CONFIG_DIRS").ok(); - linux_policy_directories_from_xdg(xdg_config_dirs.as_deref()) + linux_policy_directories_from_xdg(xdg_config_dirs.as_deref(), is_flatpak()) } #[cfg(target_os = "macos")] @@ -389,36 +566,72 @@ fn enterprise_policy_directories() -> Vec { } #[cfg(any(target_os = "linux", test))] -fn linux_policy_directories_from_xdg(xdg_config_dirs: Option<&str>) -> Vec { +fn linux_policy_directories_from_xdg(xdg_config_dirs: Option<&str>, include_flatpak_provisioning: bool) -> Vec { let mut directories = Vec::new(); + if include_flatpak_provisioning { + directories.push(PathBuf::from(FLATPAK_ENTERPRISE_POLICY_DIRECTORY)); + } + + let mut has_linux_policy_directory = false; if let Some(raw_directories) = xdg_config_dirs { for path in raw_directories.split(':') { if let Some(path) = normalize_enterprise_value(path) { directories.push(PathBuf::from(path).join("mindwork-ai-studio")); + has_linux_policy_directory = true; } } } - if directories.is_empty() { + if !has_linux_policy_directory { directories.push(PathBuf::from("/etc/xdg/mindwork-ai-studio")); } directories } -fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap { - let mut values = HashMap::new(); +fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSourceValues { + let mut values = EnterpriseSourceValues::new(); for directory in directories { info!("Checking enterprise policy directory '{}'.", directory.display()); - for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT { - let path = directory.join(format!("config{index}.yaml")); + let entries = match fs::read_dir(directory) { + Ok(entries) => entries, + Err(error) => { + info!("Could not enumerate enterprise policy directory '{}': {}.", directory.display(), error); + continue; + }, + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(error) => { + warn!("Could not read an entry from enterprise policy directory '{}': {}.", directory.display(), error); + continue; + }, + }; + + let file_name = entry.file_name(); + let Some(file_name) = file_name.to_str() else { + continue; + }; + + let Some(suffix) = enterprise_policy_file_slot_suffix(file_name) else { + continue; + }; + + let path = entry.path(); if let Some(config_values) = read_policy_yaml_mapping(&path) { + let source_detail = path + .canonicalize() + .unwrap_or_else(|_| path.clone()) + .to_string_lossy() + .into_owned(); if let Some(id) = config_values.get("id") { - insert_first_non_empty_value(&mut values, &format!("config_id{index}"), id); + insert_first_non_empty_value(&mut values, &format!("config_id{suffix}"), id, &source_detail); } if let Some(server_url) = config_values.get("server_url") { - insert_first_non_empty_value(&mut values, &format!("config_server_url{index}"), server_url); + insert_first_non_empty_value(&mut values, &format!("config_server_url{suffix}"), server_url, &source_detail); } } } @@ -426,13 +639,21 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap Option<&str> { + let suffix = file_name + .strip_prefix("config")? + .strip_suffix(".yaml")?; + + normalize_enterprise_slot_suffix(suffix) +} + fn read_policy_yaml_mapping(path: &Path) -> Option> { if !path.exists() { return None; @@ -516,27 +737,118 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option { Some(String::from(trimmed)) } -fn insert_first_non_empty_value(values: &mut HashMap, key: &str, raw_value: &str) { +fn insert_first_non_empty_value(values: &mut EnterpriseSourceValues, key: &str, raw_value: &str, source_detail: &str) { if let Some(value) = normalize_enterprise_value(raw_value) { - values.entry(String::from(key)).or_insert(value); + values + .entry(String::from(key)) + .or_insert_with(|| EnterpriseSourceValue::new(value, String::from(source_detail))); } } -fn parse_enterprise_source_values( +#[cfg(target_os = "windows")] +fn is_legacy_enterprise_source_key(key_name: &str) -> bool { + matches!( + key_name, + "configs" | "config_id" | "config_server_url" | "config_encryption_secret" + ) +} + +#[cfg(target_os = "windows")] +fn enterprise_indexed_source_key_name(key_name: &str) -> Option { + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX) { + return Some(format!("config_id{suffix}")); + } + + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX) { + return Some(format!("config_server_url{suffix}")); + } + + None +} + +fn enterprise_source_key_suffix<'a>(key_name: &'a str, prefix: &str) -> Option<&'a str> { + key_name + .strip_prefix(prefix) + .and_then(normalize_enterprise_slot_suffix) +} + +fn normalize_enterprise_slot_suffix(raw_suffix: &str) -> Option<&str> { + let suffix = raw_suffix.strip_prefix('_').unwrap_or(raw_suffix); + if is_enterprise_slot_suffix(suffix) { + Some(suffix) + } else { + None + } +} + +fn is_enterprise_slot_suffix(suffix: &str) -> bool { + !suffix.is_empty() + && suffix.len() <= ENTERPRISE_CONFIG_SLOT_WIDTH + && suffix.chars().all(|c| c.is_ascii_digit()) + && suffix.parse::().is_ok_and(|index| index <= ENTERPRISE_CONFIG_SLOT_MAX) +} + +fn collect_enterprise_config_slots(values: &HashMap) -> Vec { + let mut slots = HashSet::new(); + for key_name in values.keys() { + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX) + && is_enterprise_slot_suffix(suffix) { + slots.insert(String::from(suffix)); + continue; + } + + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX) + && is_enterprise_slot_suffix(suffix) { + slots.insert(String::from(suffix)); + } + } + + let mut slots: Vec = slots.into_iter().collect(); + slots.sort_by(|left, right| { + let left_index = left.parse::().unwrap_or(ENTERPRISE_CONFIG_SLOT_MAX); + let right_index = right.parse::().unwrap_or(ENTERPRISE_CONFIG_SLOT_MAX); + + left_index + .cmp(&right_index) + .then_with(|| enterprise_slot_width_rank(left).cmp(&enterprise_slot_width_rank(right))) + .then_with(|| left.len().cmp(&right.len())) + .then_with(|| left.cmp(right)) + }); + slots +} + +fn enterprise_slot_width_rank(suffix: &str) -> u8 { + if suffix.len() == ENTERPRISE_CONFIG_SLOT_WIDTH { + 0 + } else { + 1 + } +} + +fn indexed_enterprise_source_value<'a, T: EnterpriseSourceValueAccess>( + values: &'a HashMap, + prefix: &str, + suffix: &str, +) -> Option<&'a T> { + let separated_key = format!("{prefix}_{suffix}"); + values + .get(&separated_key) + .or_else(|| values.get(&format!("{prefix}{suffix}"))) +} + +fn parse_enterprise_source_values( source_name: &str, - values: &HashMap, + values: &HashMap, ) -> EnterpriseSourceData { let mut configs = Vec::new(); let mut seen_ids = HashSet::new(); - for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT { - let id_key = format!("config_id{index}"); - let server_url_key = format!("config_server_url{index}"); + for suffix in collect_enterprise_config_slots(values) { add_enterprise_config_pair( source_name, - &format!("indexed slot {index}"), - values.get(&id_key).map(String::as_str), - values.get(&server_url_key).map(String::as_str), + &format!("indexed slot {suffix}"), + indexed_enterprise_source_value(values, ENTERPRISE_CONFIG_ID_KEY_PREFIX, &suffix), + indexed_enterprise_source_value(values, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX, &suffix), &mut configs, &mut seen_ids, ); @@ -544,7 +856,7 @@ fn parse_enterprise_source_values( if let Some(combined) = values .get("configs") - .and_then(|value| normalize_enterprise_value(value)) + .and_then(|value| normalize_enterprise_value(value.value())) { add_combined_enterprise_configs(source_name, &combined, &mut configs, &mut seen_ids); } @@ -552,15 +864,15 @@ fn parse_enterprise_source_values( add_enterprise_config_pair( source_name, "legacy single configuration", - values.get("config_id").map(String::as_str), - values.get("config_server_url").map(String::as_str), + values.get("config_id"), + values.get("config_server_url"), &mut configs, &mut seen_ids, ); let encryption_secret = values .get("config_encryption_secret") - .and_then(|value| normalize_enterprise_value(value)) + .and_then(|value| normalize_enterprise_value(value.value())) .unwrap_or_default(); EnterpriseSourceData { @@ -572,26 +884,32 @@ fn parse_enterprise_source_values( fn add_enterprise_config_pair( source_name: &str, - context: &str, - raw_id: Option<&str>, - raw_server_url: Option<&str>, + slot: &str, + raw_id: Option<&impl EnterpriseSourceValueAccess>, + raw_server_url: Option<&impl EnterpriseSourceValueAccess>, configs: &mut Vec, seen_ids: &mut HashSet, ) { - let id = raw_id.and_then(normalize_enterprise_config_id); - let server_url = raw_server_url.and_then(normalize_enterprise_value); + let id = raw_id.and_then(|value| normalize_enterprise_config_id(value.value())); + let server_url = raw_server_url.and_then(|value| normalize_enterprise_value(value.value())); match (id, server_url) { (Some(id), Some(server_url)) => { if seen_ids.insert(id.clone()) { - configs.push(EnterpriseConfig { id, server_url }); + configs.push(EnterpriseConfig { + id, + server_url, + source: String::from(source_name), + source_detail: raw_id.map(|value| String::from(value.source_detail())).unwrap_or_default(), + slot: String::from(slot), + }); } else { - info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, context); + info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, slot); } } (Some(_), None) | (None, Some(_)) => { - warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, context); + warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, slot); } (None, None) => {} @@ -615,11 +933,13 @@ fn add_combined_enterprise_configs( continue; }; + let id = EnterpriseSourceValue::new(String::from(raw_id), String::new()); + let server_url = EnterpriseSourceValue::new(String::from(raw_server_url), String::new()); add_enterprise_config_pair( source_name, &format!("combined legacy entry {}", index + 1), - Some(raw_id), - Some(raw_server_url), + Some(&id), + Some(&server_url), configs, seen_ids, ); @@ -642,10 +962,11 @@ fn normalize_enterprise_config_id(value: &str) -> Option { #[cfg(test)] mod tests { use super::{ + enterprise_environment_key_name, enterprise_policy_file_slot_suffix, linux_policy_directories_from_xdg, load_policy_values_from_directories, normalize_locale_tag, parse_enterprise_source_values, select_effective_enterprise_config_source, select_effective_enterprise_secret_source, - EnterpriseConfig, EnterpriseSourceData, + EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues, }; use std::collections::HashMap; use std::fs; @@ -656,6 +977,30 @@ mod tests { const TEST_ID_B: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; const TEST_ID_C: &str = "11111111-2222-3333-4444-555555555555"; + fn enterprise_config( + id: &str, + server_url: &str, + source: &str, + source_detail: &str, + slot: &str, + ) -> EnterpriseConfig { + EnterpriseConfig { + id: String::from(id), + server_url: String::from(server_url), + source: String::from(source), + source_detail: String::from(source_detail), + slot: String::from(slot), + } + } + + fn policy_path(path: PathBuf) -> String { + path + .canonicalize() + .unwrap_or(path) + .to_string_lossy() + .into_owned() + } + #[test] fn normalize_locale_tag_supports_common_linux_formats() { assert_eq!( @@ -707,18 +1052,9 @@ mod tests { assert_eq!( source.configs, vec![ - EnterpriseConfig { - id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), - server_url: String::from("https://indexed.example.org"), - }, - EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://combined.example.org"), - }, - EnterpriseConfig { - id: String::from(TEST_ID_C), - server_url: String::from("https://legacy.example.org"), - }, + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://indexed.example.org", "test", "", "indexed slot 0"), + enterprise_config(TEST_ID_B, "https://combined.example.org", "test", "", "combined legacy entry 2"), + enterprise_config(TEST_ID_C, "https://legacy.example.org", "test", "", "legacy single configuration"), ] ); assert_eq!(source.encryption_secret, "secret"); @@ -743,35 +1079,164 @@ mod tests { assert_eq!( source.configs, vec![ - EnterpriseConfig { - id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), - server_url: String::from("https://slot0.example.org"), - }, - EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://slot4.example.org"), - }, + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 0"), + enterprise_config(TEST_ID_B, "https://slot4.example.org", "test", "", "indexed slot 4"), ] ); } + #[test] + fn parse_enterprise_source_values_supports_padded_and_high_indexed_slots() { + let mut values = HashMap::new(); + values.insert(String::from("config_id_00000"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url_00000"), + String::from("https://slot0.example.org"), + ); + values.insert(String::from("config_id_10503"), String::from(TEST_ID_B)); + values.insert( + String::from("config_server_url_10503"), + String::from("https://slot10503.example.org"), + ); + + let source = parse_enterprise_source_values("test", &values); + + assert_eq!( + source.configs, + vec![ + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 00000"), + enterprise_config(TEST_ID_B, "https://slot10503.example.org", "test", "", "indexed slot 10503"), + ] + ); + } + + #[test] + fn parse_enterprise_source_values_treats_slot_widths_as_distinct_slots() { + let mut values = HashMap::new(); + values.insert(String::from("config_id_00001"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url_00001"), + String::from("https://padded.example.org"), + ); + values.insert(String::from("config_id1"), String::from(TEST_ID_B)); + values.insert( + String::from("config_server_url1"), + String::from("https://legacy-slot.example.org"), + ); + + let source = parse_enterprise_source_values("test", &values); + + assert_eq!( + source.configs, + vec![ + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://padded.example.org", "test", "", "indexed slot 00001"), + enterprise_config(TEST_ID_B, "https://legacy-slot.example.org", "test", "", "indexed slot 1"), + ] + ); + } + + #[test] + fn parse_enterprise_source_values_ignores_invalid_slot_suffixes() { + let mut values = HashMap::new(); + values.insert(String::from("config_id_99999"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url_99999"), + String::from("https://valid.example.org"), + ); + values.insert(String::from("config_id_100000"), String::from(TEST_ID_B)); + values.insert( + String::from("config_server_url_100000"), + String::from("https://too-high.example.org"), + ); + values.insert(String::from("config_id_abc"), String::from(TEST_ID_C)); + values.insert( + String::from("config_server_url_abc"), + String::from("https://letters.example.org"), + ); + + let source = parse_enterprise_source_values("test", &values); + + assert_eq!( + source.configs, + vec![enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://valid.example.org", "test", "", "indexed slot 99999")] + ); + } + + #[test] + fn enterprise_environment_key_name_maps_indexed_and_legacy_names() { + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_10503"), + Some(String::from("config_id10503")) + ); + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000"), + Some(String::from("config_server_url00000")) + ); + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS"), + Some(String::from("configs")) + ); + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_100000"), + None + ); + } + + #[test] + fn parse_enterprise_source_values_keeps_environment_id_variable_as_source_detail() { + let mut values = EnterpriseSourceValues::new(); + values.insert( + String::from("config_id00000"), + EnterpriseSourceValue::new( + String::from(TEST_ID_A), + String::from("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000"), + ), + ); + values.insert( + String::from("config_server_url00000"), + EnterpriseSourceValue::new(String::from("https://env.example.org"), String::new()), + ); + + let source = parse_enterprise_source_values("environment variables", &values); + + assert_eq!( + source.configs, + vec![enterprise_config( + "9072b77d-ca81-40da-be6a-861da525ef7b", + "https://env.example.org", + "environment variables", + "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000", + "indexed slot 00000" + )] + ); + } + + #[test] + fn enterprise_policy_file_slot_suffix_accepts_valid_slot_file_names() { + assert_eq!(enterprise_policy_file_slot_suffix("config0.yaml"), Some("0")); + assert_eq!( + enterprise_policy_file_slot_suffix("config_00000.yaml"), + Some("00000") + ); + assert_eq!( + enterprise_policy_file_slot_suffix("config_10503.yaml"), + Some("10503") + ); + assert_eq!(enterprise_policy_file_slot_suffix("config_100000.yaml"), None); + assert_eq!(enterprise_policy_file_slot_suffix("config_abc.yaml"), None); + } + #[test] fn select_effective_enterprise_config_source_uses_first_source_with_configs_only() { let selected = select_effective_enterprise_config_source(vec![ EnterpriseSourceData { source_name: String::from("registry"), - configs: vec![EnterpriseConfig { - id: TEST_ID_A.to_lowercase(), - server_url: String::from("https://registry.example.org"), - }], + configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")], encryption_secret: String::new(), }, EnterpriseSourceData { source_name: String::from("environment"), - configs: vec![EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://env.example.org"), - }], + configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")], encryption_secret: String::from("ENV-SECRET"), }, ]); @@ -791,10 +1256,7 @@ mod tests { }, EnterpriseSourceData { source_name: String::from("environment"), - configs: vec![EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://env.example.org"), - }], + configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")], encryption_secret: String::new(), }, ]); @@ -809,10 +1271,7 @@ mod tests { let selected = select_effective_enterprise_secret_source(vec![ EnterpriseSourceData { source_name: String::from("registry"), - configs: vec![EnterpriseConfig { - id: TEST_ID_A.to_lowercase(), - server_url: String::from("https://registry.example.org"), - }], + configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")], encryption_secret: String::new(), }, EnterpriseSourceData { @@ -863,7 +1322,7 @@ mod tests { #[test] fn linux_policy_directories_from_xdg_preserves_order_and_falls_back() { assert_eq!( - linux_policy_directories_from_xdg(Some(" /opt/company:/etc/xdg ")), + linux_policy_directories_from_xdg(Some(" /opt/company:/etc/xdg "), false), vec![ PathBuf::from("/opt/company/mindwork-ai-studio"), PathBuf::from("/etc/xdg/mindwork-ai-studio"), @@ -871,15 +1330,35 @@ mod tests { ); assert_eq!( - linux_policy_directories_from_xdg(Some(" : ")), + linux_policy_directories_from_xdg(Some(" : "), false), vec![PathBuf::from("/etc/xdg/mindwork-ai-studio")] ); assert_eq!( - linux_policy_directories_from_xdg(None), + linux_policy_directories_from_xdg(None, false), vec![PathBuf::from("/etc/xdg/mindwork-ai-studio")] ); } + #[test] + fn linux_policy_directories_from_xdg_checks_flatpak_provisioning_first() { + assert_eq!( + linux_policy_directories_from_xdg(Some(" /opt/company:/etc/xdg "), true), + vec![ + PathBuf::from("/app/etc/MindWorkAI"), + PathBuf::from("/opt/company/mindwork-ai-studio"), + PathBuf::from("/etc/xdg/mindwork-ai-studio"), + ] + ); + + assert_eq!( + linux_policy_directories_from_xdg(None, true), + vec![ + PathBuf::from("/app/etc/MindWorkAI"), + PathBuf::from("/etc/xdg/mindwork-ai-studio"), + ] + ); + } + #[test] fn load_policy_values_from_directories_uses_first_directory_wins() { let directory_a = tempdir().unwrap(); @@ -918,19 +1397,19 @@ mod tests { ]); assert_eq!( - values.get("config_id0").map(String::as_str), + values.get("config_id0").map(|value| value.value.as_str()), Some("9072b77d-ca81-40da-be6a-861da525ef7b") ); assert_eq!( - values.get("config_server_url0").map(String::as_str), + values.get("config_server_url0").map(|value| value.value.as_str()), Some("https://org.example.org") ); assert_eq!( - values.get("config_id1").map(String::as_str), + values.get("config_id1").map(|value| value.value.as_str()), Some("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") ); assert_eq!( - values.get("config_encryption_secret").map(String::as_str), + values.get("config_encryption_secret").map(|value| value.value.as_str()), Some("SECRET-A") ); } @@ -956,14 +1435,40 @@ mod tests { assert_eq!( source.configs, vec![ - EnterpriseConfig { - id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), - server_url: String::from("https://slot0.example.org"), - }, - EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://slot4.example.org"), - }, + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config0.yaml")), "indexed slot 0"), + enterprise_config(TEST_ID_B, "https://slot4.example.org", "policy files", &policy_path(directory.path().join("config4.yaml")), "indexed slot 4"), + ] + ); + } + + #[test] + fn load_policy_values_from_directories_supports_padded_and_high_policy_slots() { + let directory = tempdir().unwrap(); + + fs::write( + directory.path().join("config_00000.yaml"), + "id: \"9072b77d-ca81-40da-be6a-861da525ef7b\"\nserver_url: \"https://slot0.example.org\"", + ) + .unwrap(); + fs::write( + directory.path().join("config_10503.yaml"), + "id: \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\nserver_url: \"https://slot10503.example.org\"", + ) + .unwrap(); + fs::write( + directory.path().join("config_100000.yaml"), + "id: \"11111111-2222-3333-4444-555555555555\"\nserver_url: \"https://ignored.example.org\"", + ) + .unwrap(); + + let values = load_policy_values_from_directories(&[directory.path().to_path_buf()]); + let source = parse_enterprise_source_values("policy files", &values); + + assert_eq!( + source.configs, + vec![ + enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config_00000.yaml")), "indexed slot 00000"), + enterprise_config(TEST_ID_B, "https://slot10503.example.org", "policy files", &policy_path(directory.path().join("config_10503.yaml")), "indexed slot 10503"), ] ); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8cab601a..55984c91 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,7 +13,7 @@ pub mod file_data; pub mod metadata; pub mod pdfium; pub mod pandoc; -pub mod qdrant; +pub mod qdrant_edge_database; pub mod certificate_factory; pub mod runtime_api_token; pub mod stale_process_cleanup; diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 76b8bb0b..9569857b 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -34,7 +34,7 @@ async fn main() { info!(".. MudBlazor: v{mud_blazor_version}", mud_blazor_version = metadata.mud_blazor_version); info!(".. Tauri: v{tauri_version}", tauri_version = metadata.tauri_version); info!(".. PDFium: v{pdfium_version}", pdfium_version = metadata.pdfium_version); - info!(".. Qdrant: v{qdrant_version}", qdrant_version = metadata.qdrant_version); + info!(".. Vector store: v{vector_store_version}", vector_store_version = metadata.vector_store_version); if is_dev() { warn!("Running in development mode."); diff --git a/runtime/src/metadata.rs b/runtime/src/metadata.rs index fa56dd68..df72640b 100644 --- a/runtime/src/metadata.rs +++ b/runtime/src/metadata.rs @@ -16,7 +16,7 @@ pub struct MetaData { pub app_commit_hash: String, pub architecture: String, pub pdfium_version: String, - pub qdrant_version: String, + pub vector_store_version: String, } impl MetaData { @@ -40,7 +40,7 @@ impl MetaData { let app_commit_hash = metadata_lines.next().unwrap(); let architecture = metadata_lines.next().unwrap(); let pdfium_version = metadata_lines.next().unwrap(); - let qdrant_version = metadata_lines.next().unwrap(); + let vector_store_version = metadata_lines.next().unwrap(); let metadata = MetaData { architecture: architecture.to_string(), @@ -54,7 +54,7 @@ impl MetaData { rust_version: rust_version.to_string(), tauri_version: tauri_version.to_string(), pdfium_version: pdfium_version.to_string(), - qdrant_version: qdrant_version.to_string(), + vector_store_version: vector_store_version.to_string(), }; *META_DATA.lock().unwrap() = Some(metadata.clone()); diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs index 82270059..b49c0c28 100644 --- a/runtime/src/pandoc.rs +++ b/runtime/src/pandoc.rs @@ -12,6 +12,12 @@ use crate::metadata::META_DATA; static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new(); static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new(); +/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000. +/// It starts console applications without opening a console window: +/// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + pub struct PandocExecutable { pub executable: String, pub is_local_installation: bool, @@ -99,6 +105,9 @@ impl PandocProcessBuilder { let pandoc_executable = Self::pandoc_executable_path(); let mut command = Command::new(&pandoc_executable.executable); + + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); command.args(&arguments); PandocPreparedProcess { diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs deleted file mode 100644 index 639dd7c7..00000000 --- a/runtime/src/qdrant.rs +++ /dev/null @@ -1,374 +0,0 @@ -use std::collections::HashMap; -use std::{fs}; -use std::error::Error; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::sync::{Arc, Mutex, OnceLock}; -use std::time::Duration; -use log::{debug, error, info, warn}; -use once_cell::sync::Lazy; -use axum::Json; -use serde::Serialize; -use crate::api_token::{APIToken}; -use crate::environment::{is_dev, DATA_DIRECTORY}; -use crate::certificate_factory::generate_certificate; -use std::path::PathBuf; -use tauri::Manager; -use tauri::path::BaseDirectory; -use tempfile::{TempDir, Builder}; -use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; -use crate::sidecar_types::SidecarType; -use tokio::time; -use tauri_plugin_shell::process::{CommandChild, CommandEvent}; -use tauri_plugin_shell::ShellExt; - -// Qdrant server process started in a separate process and can communicate -// via HTTP or gRPC with the .NET server and the runtime process -static QDRANT_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); - -// Qdrant server port (default is 6333 for HTTP and 6334 for gRPC) -static QDRANT_SERVER_PORT_HTTP: Lazy = Lazy::new(|| { - crate::network::get_available_port().unwrap_or(6333) -}); - -static QDRANT_SERVER_PORT_GRPC: Lazy = Lazy::new(|| { - crate::network::get_available_port().unwrap_or(6334) -}); - -pub static CERTIFICATE_FINGERPRINT: OnceLock = OnceLock::new(); -static API_TOKEN: Lazy = Lazy::new(|| { - crate::api_token::generate_api_token() -}); - -static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); -static QDRANT_STATUS: Lazy> = Lazy::new(|| Mutex::new(QdrantStatusInfo::default())); - -const PID_FILE_NAME: &str = "qdrant.pid"; -const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; -const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); -const STARTUP_CHECK_INTERVAL: Duration = Duration::from_millis(250); - -#[derive(Clone, Copy, Default, Serialize, PartialEq, Eq)] -enum QdrantStatus { - #[default] - Starting, - Available, - Unavailable, -} - -#[derive(Default)] -struct QdrantStatusInfo { - status: QdrantStatus, - unavailable_reason: Option, -} - -fn qdrant_base_path() -> PathBuf { - let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" }; - Path::new(DATA_DIRECTORY.get().unwrap()) - .join("databases") - .join(qdrant_directory) -} - -#[derive(Serialize)] -pub struct ProvideQdrantInfo { - status: QdrantStatus, - path: String, - port_http: u16, - port_grpc: u16, - fingerprint: String, - api_token: String, - is_available: bool, - unavailable_reason: Option, -} - -pub async fn qdrant_port(_token: APIToken) -> Json { - let status = QDRANT_STATUS.lock().unwrap(); - let current_status = status.status; - let is_available = current_status == QdrantStatus::Available; - let unavailable_reason = status.unavailable_reason.clone(); - - Json(ProvideQdrantInfo { - status: current_status, - path: if is_available { - qdrant_base_path().to_string_lossy().to_string() - } else { - String::new() - }, - port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 }, - port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 }, - fingerprint: if is_available { - CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default() - } else { - String::new() - }, - api_token: if is_available { - API_TOKEN.to_hex_text().to_string() - } else { - String::new() - }, - is_available, - unavailable_reason, - }) -} - -/// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server(app_handle: tauri::AppHandle){ - set_qdrant_starting(); - tauri::async_runtime::spawn(async move { - cleanup_qdrant(); - start_qdrant_server_internal(app_handle); - }); -} - -fn start_qdrant_server_internal(app_handle: tauri::AppHandle){ - let path = qdrant_base_path(); - if !path.exists() && let Err(e) = fs::create_dir_all(&path){ - error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); - set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); - return; - } - - let (cert_path, key_path) = match create_temp_tls_files(&path) { - Ok(paths) => paths, - Err(e) => { - error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}"); - set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}")); - return; - } - }; - - let storage_path = path.join("storage").to_string_lossy().to_string(); - let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); - let init_path = path.join(".qdrant-initialized"); - let init_path_environment = init_path.to_string_lossy().to_string(); - - let qdrant_server_environment: HashMap = HashMap::from_iter([ - (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), - (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), - (String::from("QDRANT_INIT_FILE_PATH"), init_path_environment), - (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), - (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), - (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()), - (String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()), - (String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()), - (String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()), - ]); - - let server_spawn_clone = QDRANT_SERVER.clone(); - let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml"; - let qdrant_source_path = match app_handle.path().resolve(qdrant_relative_source_path, BaseDirectory::Resource) { - Ok(path) => path, - Err(_) => { - let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved."); - error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant."); - set_qdrant_unavailable(reason); - return; - } - }; - - let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string(); - tauri::async_runtime::spawn(async move { - let shell = app_handle.shell(); - - let sidecar = match shell.sidecar("qdrant") { - Ok(sidecar) => sidecar, - Err(e) => { - let reason = format!("Failed to create sidecar for Qdrant: {e}"); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - return; - } - }; - - let (mut rx, child) = match sidecar - .args(["--config-path", qdrant_source_path_display.as_str()]) - .envs(qdrant_server_environment) - .spawn() - { - Ok(process) => process, - Err(e) => { - let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - return; - } - }; - - let server_pid = child.pid(); - info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); - log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); - - // Save the server process to stop it later: - *server_spawn_clone.lock().unwrap() = Some(child); - - let init_path_clone = init_path.clone(); - tauri::async_runtime::spawn(async move { - if wait_for_qdrant_startup(init_path_clone).await { - set_qdrant_available(); - info!(Source = "Qdrant"; "Qdrant is available."); - } else { - let reason = "Qdrant did not become available within the startup timeout.".to_string(); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - } - }); - - // Log the output of the Qdrant server: - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => { - let line_utf8 = String::from_utf8_lossy(&line).to_string(); - let line = line_utf8.trim_end(); - if line.contains("INFO") || line.contains("info") { - info!(Source = "Qdrant Server"; "{line}"); - } else if line.contains("WARN") || line.contains("warning") { - warn!(Source = "Qdrant Server"; "{line}"); - } else if line.contains("ERROR") || line.contains("error") { - error!(Source = "Qdrant Server"; "{line}"); - } else { - debug!(Source = "Qdrant Server"; "{line}"); - } - }, - - CommandEvent::Stderr(line) => { - let line_utf8 = String::from_utf8_lossy(&line).to_string(); - error!(Source = "Qdrant Server (stderr)"; "{line_utf8}"); - }, - - _ => {} - } - } - - let is_available = QDRANT_STATUS.lock().unwrap().status == QdrantStatus::Available; - let unavailable_reason = if is_available { - "Qdrant server process stopped.".to_string() - } else { - "Qdrant server process stopped before it became available.".to_string() - }; - set_qdrant_unavailable(unavailable_reason); - }); -} - -/// Stops the Qdrant server process. -pub fn stop_qdrant_server() { - if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { - let server_kill_result = server_process.kill(); - match server_kill_result { - Ok(_) => { - set_qdrant_unavailable("Qdrant server was stopped.".to_string()); - warn!(Source = "Qdrant"; "Qdrant server process was stopped.") - }, - Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), - } - } else { - warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); - } - - drop_tmpdir(); - cleanup_qdrant(); -} - -async fn wait_for_qdrant_startup(init_path: PathBuf) -> bool { - let mut elapsed = Duration::ZERO; - while elapsed < STARTUP_TIMEOUT { - if init_path.exists() { - return true; - } - - time::sleep(STARTUP_CHECK_INTERVAL).await; - elapsed += STARTUP_CHECK_INTERVAL; - } - - false -} - -/// Create a temporary directory with TLS relevant files -pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box> { - let cert = generate_certificate(); - - let temp_dir = init_tmpdir_in(path); - let cert_path = temp_dir.join("cert.pem"); - let key_path = temp_dir.join("key.pem"); - - let mut cert_file = File::create(&cert_path)?; - cert_file.write_all(&cert.certificate)?; - - let mut key_file = File::create(&key_path)?; - key_file.write_all(&cert.private_key)?; - - CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint."); - - Ok((cert_path, key_path)) -} - -pub fn init_tmpdir_in>(path: P) -> PathBuf { - let mut guard = TMPDIR.lock().unwrap(); - let dir = guard.get_or_insert_with(|| { - Builder::new() - .prefix("cert-") - .tempdir_in(path) - .expect("failed to create tempdir") - }); - - dir.path().to_path_buf() -} - -pub fn drop_tmpdir() { - let mut guard = TMPDIR.lock().unwrap(); - *guard = None; - warn!(Source = "Qdrant"; "Temporary directory for TLS was dropped."); -} - -/// Remove old Pid files and kill the corresponding processes -pub fn cleanup_qdrant() { - let path = qdrant_base_path(); - let pid_path = path.join(PID_FILE_NAME); - if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) { - warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); - } - if let Err(e) = delete_old_certificates(path) { - warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); - } - -} - -fn set_qdrant_available() { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Available; - status.unavailable_reason = None; -} - -fn set_qdrant_starting() { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Starting; - status.unavailable_reason = None; -} - -fn set_qdrant_unavailable(reason: String) { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Unavailable; - status.unavailable_reason = Some(reason); -} - -pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box> { - if !path.exists() { - return Ok(()); - } - - for entry in fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - let file_name = entry.file_name(); - let folder_name = file_name.to_string_lossy(); - - if folder_name.starts_with("cert-") { - fs::remove_dir_all(&path)?; - warn!(Source="Qdrant"; "Removed old certificates in: {}", path.display()); - } - } - } - Ok(()) -} \ No newline at end of file diff --git a/runtime/src/qdrant_edge_database.rs b/runtime/src/qdrant_edge_database.rs new file mode 100644 index 00000000..89f33bc4 --- /dev/null +++ b/runtime/src/qdrant_edge_database.rs @@ -0,0 +1,588 @@ +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use axum::Json; +use log::{error, info, warn}; +use once_cell::sync::Lazy; +use qdrant_edge::external::serde_json::json; +use qdrant_edge::external::uuid::Uuid; +use qdrant_edge::{ + Condition, Distance, EdgeConfig, EdgeOptimizersConfig, EdgeShard, EdgeVectorParams, + FieldCondition, Filter, HnswIndexConfig, Match, MatchValue, PointId, PointInsertOperations, + PointOperations, PointStruct, UpdateOperation, ValueVariants, Vectors, +}; +use serde::{Deserialize, Serialize}; +use tauri::Manager; + +use crate::api_token::APIToken; +use crate::environment::DATA_DIRECTORY; +use crate::metadata::META_DATA; + +const VECTOR_NAME: &str = "embedding"; +const HNSW_M: usize = 16; +const HNSW_EF_CONSTRUCT: usize = 100; +const HNSW_FULL_SCAN_THRESHOLD_KB: usize = 10_000; +const HNSW_MAX_INDEXING_THREADS: usize = 0; +const VECTOR_INDEXING_THRESHOLD_KB: usize = 10_000; + +type QdrantEdgeResult = Result>; + +static QDRANT_EDGE_DATABASE: Lazy>> = + Lazy::new(|| Mutex::new(None)); + +static QDRANT_EDGE_STATUS: Lazy> = + Lazy::new(|| Mutex::new(QdrantEdgeStatusInfo::default())); + +#[derive(Default)] +struct QdrantEdgeStatusInfo { + status: QdrantEdgeStatus, + unavailable_reason: Option, +} + +#[derive(Clone, Copy, Default, Serialize, PartialEq, Eq)] +pub enum QdrantEdgeStatus { + #[default] + Starting, + Available, + Unavailable, +} + +#[derive(Serialize)] +pub struct QdrantEdgeServiceInfo { + pub status: QdrantEdgeStatus, + pub name: String, + pub version: String, + pub path: String, + pub stores_count: usize, + pub is_available: bool, + pub unavailable_reason: Option, +} + +#[derive(Clone, Deserialize)] +pub struct QdrantEdgeStoragePoint { + pub point_id: String, + pub vector: Vec, + pub data_source_id: String, + pub data_source_name: String, + pub data_source_type: String, + pub file_path: String, + pub file_name: String, + pub relative_path: String, + pub chunk_index: i32, + pub text: String, + pub fingerprint: String, + pub last_write_utc: String, + pub embedded_at_utc: String, +} + +#[derive(Deserialize)] +pub struct EnsureQdrantEdgeStoreRequest { + pub store_name: String, + pub vector_size: usize, +} + +#[derive(Deserialize)] +pub struct InsertQdrantEdgeEmbeddingRequest { + pub store_name: String, + pub points: Vec, +} + +#[derive(Deserialize)] +pub struct DeleteQdrantEdgeEmbeddingByFileRequest { + pub store_name: String, + pub file_path: String, +} + +#[derive(Deserialize)] +pub struct DeleteQdrantEdgeStoreRequest { + pub store_name: String, +} + +#[derive(Serialize)] +pub struct QdrantEdgeOperationResponse { + pub success: bool, + pub issue: String, +} + +#[derive(Clone, Serialize)] +pub struct QdrantEdgeInfo { + pub name: String, + pub version: String, + pub path: String, + pub stores_count: usize, +} + +pub struct QdrantEdgeDatabase { + base_path: PathBuf, + shards: HashMap, +} + +impl QdrantEdgeDatabase { + pub fn new(base_path: PathBuf) -> Self { + Self { + base_path, + shards: HashMap::new(), + } + } + + fn store_path(&self, store_name: &str) -> QdrantEdgeResult { + validate_store_name(store_name)?; + Ok(self.base_path.join("stores").join(store_name)) + } + + // To ensure a shard exists and that you can insert a vector + fn get_or_create_store(&mut self, store_name: &str, vector_size: usize) -> QdrantEdgeResult<&EdgeShard> { + if self.shards.contains_key(store_name) { + return Ok(self.shards.get(store_name).unwrap()); + } + + let path = self.store_path(store_name)?; + let shard = if has_existing_store(&path) { + EdgeShard::load(&path, None)? + } else { + fs::create_dir_all(&path)?; + EdgeShard::new(&path, edge_config(vector_size))? + }; + + self.shards.insert(store_name.to_string(), shard); + Ok(self.shards.get(store_name).unwrap()) + } + + // To check whether a shard exists so you can delete a file from it + fn get_existing_store(&mut self, store_name: &str) -> QdrantEdgeResult> { + if self.shards.contains_key(store_name) { + return Ok(self.shards.get(store_name)); + } + + let path = self.store_path(store_name)?; + if !has_existing_store(&path) { + return Ok(None); + } + + let shard = EdgeShard::load(&path, None)?; + self.shards.insert(store_name.to_string(), shard); + Ok(self.shards.get(store_name)) + } + + fn info(&self) -> QdrantEdgeResult { + let stores_path = self.base_path.join("stores"); + let stores_count = if stores_path.exists() { + fs::read_dir(stores_path)? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .count() + } else { + 0 + }; + + Ok(QdrantEdgeInfo { + name: "Qdrant Edge".to_string(), + version: vector_store_version()?, + path: self.base_path.to_string_lossy().to_string(), + stores_count, + }) + } + + fn ensure_store_exists(&mut self, store_name: &str, vector_size: usize) -> QdrantEdgeResult<()> { + validate_vector_size(vector_size)?; + self.get_or_create_store(store_name, vector_size)?; + Ok(()) + } + + fn insert_embedding(&mut self, store_name: &str, points: Vec) -> QdrantEdgeResult<()> { + let Some(first_point) = points.first() else { + return Ok(()); + }; + + let vector_size = first_point.vector.len(); + validate_vector_size(vector_size)?; + if points.iter().any(|point| point.vector.len() != vector_size) { + return Err("All vectors in one insert request must have the same size.".into()); + } + + let shard = self.get_or_create_store(store_name, vector_size)?; + let points = points + .into_iter() + .map(to_qdrant_edge_point) + .collect::>(); + + shard.update(UpdateOperation::PointOperation( + PointOperations::UpsertPoints(PointInsertOperations::PointsList(points)), + ))?; + shard.flush(); + Ok(()) + } + + fn delete_embedding_by_file(&mut self, store_name: &str, file_path: &str) -> QdrantEdgeResult<()> { + let Some(shard) = self.get_existing_store(store_name)? else { + return Ok(()); + }; + + shard.update(UpdateOperation::PointOperation( + PointOperations::DeletePointsByFilter(match_keyword_filter("file_path", file_path)?), + ))?; + shard.flush(); + Ok(()) + } + + fn delete_store(&mut self, store_name: &str) -> QdrantEdgeResult<()> { + self.shards.remove(store_name); + + let path = self.store_path(store_name)?; + if path.exists() { + fs::remove_dir_all(path)?; + } + + Ok(()) + } + + fn base_path(&self) -> PathBuf { + self.base_path.clone() + } +} + +fn qdrant_edge_base_path() -> QdrantEdgeResult { + let data_directory = DATA_DIRECTORY + .get() + .ok_or("The data directory has not been initialized.")?; + + Ok(Path::new(data_directory) + .join("databases") + .join("vector_database")) +} + +pub async fn qdrant_edge_info(_token: APIToken) -> Json { + let status = QDRANT_EDGE_STATUS.lock().unwrap(); + let current_status = status.status; + let unavailable_reason = status.unavailable_reason.clone(); + drop(status); + + let database_guard = QDRANT_EDGE_DATABASE.lock().unwrap(); + let database_info = database_guard + .as_ref() + .and_then(|database| database.info().ok()); + + let is_available = current_status == QdrantEdgeStatus::Available && database_info.is_some(); + Json(QdrantEdgeServiceInfo { + status: current_status, + name: database_info.as_ref().map(|info| info.name.clone()).unwrap_or_default(), + version: database_info.as_ref().map(|info| info.version.clone()).unwrap_or_default(), + path: database_info.as_ref().map(|info| info.path.clone()).unwrap_or_default(), + stores_count: database_info.as_ref().map(|info| info.stores_count).unwrap_or_default(), + is_available, + unavailable_reason, + }) +} + +pub async fn ensure_qdrant_edge_store(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.ensure_store_exists(&request.store_name, request.vector_size) + }) +} + +pub async fn insert_qdrant_edge_embedding(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.insert_embedding(&request.store_name, request.points) + }) +} + +pub async fn delete_qdrant_edge_embedding_by_file(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.delete_embedding_by_file(&request.store_name, &request.file_path) + }) +} + +pub async fn delete_qdrant_edge_store(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.delete_store(&request.store_name) + }) +} + +pub fn start_qdrant_edge_database(app_handle: tauri::AppHandle) { + set_qdrant_edge_starting(); + remove_obsolete_qdrant_sidecar_files(&app_handle); + + let path = match qdrant_edge_base_path() { + Ok(path) => path, + Err(e) => { + let reason = format!("Qdrant Edge cannot be started: {e}"); + error!(Source = "Qdrant Edge"; "{reason}"); + set_qdrant_edge_unavailable(reason); + return; + }, + }; + + match fs::create_dir_all(&path) { + Ok(_) => { + let database = QdrantEdgeDatabase::new(path.clone()); + *QDRANT_EDGE_DATABASE.lock().unwrap() = Some(database); + set_qdrant_edge_available(); + info!(Source = "Qdrant Edge"; "Qdrant Edge is available at '{}'.", path.display()); + }, + Err(e) => { + let reason = format!("The Qdrant Edge data directory could not be created: {e}"); + error!(Source = "Qdrant Edge"; "{reason}"); + set_qdrant_edge_unavailable(reason); + }, + } +} + +pub fn stop_qdrant_edge_database() { + if let Some(database) = QDRANT_EDGE_DATABASE.lock().unwrap().take() { + info!(Source = "Qdrant Edge"; "Stopping Qdrant Edge at '{}'.", database.base_path().display()); + drop(database); + } + + set_qdrant_edge_unavailable("Qdrant Edge was stopped.".to_string()); +} + +fn execute_qdrant_edge_operation(operation: F) -> Json +where + F: FnOnce(&mut QdrantEdgeDatabase) -> QdrantEdgeResult<()>, +{ + let mut database_guard = QDRANT_EDGE_DATABASE.lock().unwrap(); + let Some(database) = database_guard.as_mut() else { + return Json(QdrantEdgeOperationResponse { + success: false, + issue: "Qdrant Edge is not available.".to_string(), + }); + }; + + match operation(database) { + Ok(_) => Json(QdrantEdgeOperationResponse { + success: true, + issue: String::new(), + }), + Err(e) => { + let issue = e.to_string(); + error!(Source = "Qdrant Edge"; "Qdrant Edge operation failed: {issue}"); + Json(QdrantEdgeOperationResponse { + success: false, + issue, + }) + }, + } +} + +fn set_qdrant_edge_available() { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Available; + status.unavailable_reason = None; +} + +fn set_qdrant_edge_starting() { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Starting; + status.unavailable_reason = None; +} + +fn set_qdrant_edge_unavailable(reason: String) { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Unavailable; + status.unavailable_reason = Some(reason); +} + +fn remove_obsolete_qdrant_sidecar_files(app_handle: &tauri::AppHandle) { + let mut paths = Vec::new(); + + if let Some(data_directory) = DATA_DIRECTORY.get() { + let databases_directory = Path::new(data_directory).join("databases"); + paths.push(databases_directory.join("qdrant")); + paths.push(databases_directory.join("qdrant_test")); + } + + if let Ok(resource_dir) = app_handle.path().resource_dir() { + paths.push(resource_dir.join("target").join("databases").join("qdrant")); + paths.push(resource_dir.join("resources").join("databases").join("qdrant")); + } + + cfg_if::cfg_if! { + if #[cfg(any(target_os = "windows", target_os = "macos"))]{ + if let Ok(current_exe) = std::env::current_exe() && let Some(exe_dir) = current_exe.parent() { + if exe_dir.to_string_lossy().contains("MindWork AI Studio") { + paths.push(exe_dir.join("target").join("databases").join("qdrant")); + paths.push(exe_dir.join("qdrant.exe")); + paths.push(exe_dir.join("qdrant")); + } + } + } + } + + for path in paths { + remove_obsolete_qdrant_path(&path); + } +} + +fn remove_obsolete_qdrant_path(path: &Path) { + if !path.exists() { + info!(Source = "Qdrant Edge"; "Obsolete file or directory '{}' was not found.", path.display()); + return; + } + + let result = if path.is_dir() { + fs::remove_dir_all(path) + } else { + fs::remove_file(path) + }; + + match result { + Ok(_) => warn!(Source = "Qdrant Edge"; "Removed obsolete Qdrant sidecar file or directory '{}'.", path.display()), + Err(e) => warn!(Source = "Qdrant Edge"; "Could not remove obsolete Qdrant sidecar file or directory '{}': {e}", path.display()), + } +} + +fn edge_config(vector_size: usize) -> EdgeConfig { + EdgeConfig { + on_disk_payload: true, + vectors: HashMap::from([( + VECTOR_NAME.to_string(), + EdgeVectorParams { + size: vector_size, + distance: Distance::Cosine, + on_disk: Some(true), + quantization_config: None, + multivector_config: None, + datatype: None, + hnsw_config: Some(hnsw_config()), + }, + )]), + sparse_vectors: HashMap::new(), + hnsw_config: hnsw_config(), + quantization_config: None, + optimizers: edge_optimizers_config(), + wal_options: None, + } +} + +fn hnsw_config() -> HnswIndexConfig { + HnswIndexConfig { + m: HNSW_M, + ef_construct: HNSW_EF_CONSTRUCT, + full_scan_threshold: HNSW_FULL_SCAN_THRESHOLD_KB, + max_indexing_threads: HNSW_MAX_INDEXING_THREADS, + on_disk: Some(true), + payload_m: None, + inline_storage: None, + } +} + +fn edge_optimizers_config() -> EdgeOptimizersConfig { + EdgeOptimizersConfig { + indexing_threshold: Some(VECTOR_INDEXING_THRESHOLD_KB), + prevent_unoptimized: Some(false), + ..Default::default() + } +} + +fn has_existing_store(path: &Path) -> bool { + path.join("edge_config.json").exists() || path.join("segments").exists() +} + +fn validate_vector_size(vector_size: usize) -> QdrantEdgeResult<()> { + if vector_size == 0 { + return Err("Vector size must be greater than zero.".into()); + } + + Ok(()) +} + +fn vector_store_version() -> QdrantEdgeResult { + let metadata = META_DATA + .lock() + .map_err(|_| "Metadata lock was poisoned.")?; + let Some(metadata) = metadata.as_ref() else { + return Err("Metadata was not initialized.".into()); + }; + + Ok(metadata.vector_store_version.clone()) +} + +fn to_qdrant_edge_point(point: QdrantEdgeStoragePoint) -> qdrant_edge::PointStructPersisted { + PointStruct::new( + to_point_id(&point.point_id), + Vectors::new_named([(VECTOR_NAME, point.vector)]), + json!({ + "data_source_id": point.data_source_id, + "data_source_name": point.data_source_name, + "data_source_type": point.data_source_type, + "file_path": point.file_path, + "file_name": point.file_name, + "relative_path": point.relative_path, + "chunk_index": point.chunk_index, + "text": point.text, + "fingerprint": point.fingerprint, + "last_write_utc": point.last_write_utc, + "embedded_at_utc": point.embedded_at_utc, + }), + ) + .into() +} + +fn to_point_id(point_id: &str) -> PointId { + Uuid::parse_str(point_id) + .map(PointId::Uuid) + .unwrap_or_else(|_| PointId::NumId(stable_u64(point_id))) +} + +fn stable_u64(value: &str) -> u64 { + let mut hash = 0xcbf29ce484222325_u64; + for byte in value.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + + hash +} + +fn match_keyword_filter(field_name: &str, value: &str) -> QdrantEdgeResult { + Ok(Filter { + should: None, + min_should: None, + must: Some(vec![Condition::Field(FieldCondition::new_match( + field_name + .try_into() + .map_err(|_| format!("Invalid payload field name '{field_name}'."))?, + Match::Value(MatchValue { + value: ValueVariants::String(value.to_string()), + }), + ))]), + must_not: None, + }) +} + +fn validate_store_name(store_name: &str) -> QdrantEdgeResult<()> { + if store_name.is_empty() { + return Err("Vector store name cannot be empty.".into()); + } + + if matches!(store_name, "." | "..") { + return Err(format!("Vector store name '{store_name}' is not supported.").into()); + } + + if store_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') + { + return Ok(()); + } + + Err(format!("Vector store name '{store_name}' contains unsupported characters.").into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_store_name_allows_safe_store_names() { + assert!(validate_store_name("rag_1234-abcd.ef").is_ok()); + } + + #[test] + fn validate_store_name_rejects_path_traversal_names() { + assert!(validate_store_name(".").is_err()); + assert!(validate_store_name("..").is_err()); + } +} diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 35b8da3b..f50913f2 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -32,8 +32,12 @@ pub fn start_runtime_api() { let app = Router::new() .route("/system/dotnet/port", get(crate::dotnet::dotnet_port)) .route("/system/dotnet/ready", get(crate::dotnet::dotnet_ready)) - .route("/system/qdrant/info", get(crate::qdrant::qdrant_port)) .route("/system/tokenizer/info", get(crate::tokenizer::tokenizer_info)) + .route("/system/qdrant-edge/info", get(crate::qdrant_edge_database::qdrant_edge_info)) + .route("/system/qdrant-edge/ensure", post(crate::qdrant_edge_database::ensure_qdrant_edge_store)) + .route("/system/qdrant-edge/insert", post(crate::qdrant_edge_database::insert_qdrant_edge_embedding)) + .route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file)) + .route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store)) .route("/clipboard/set", post(crate::clipboard::set_clipboard)) .route("/events", get(crate::app_window::get_event_stream)) .route("/updates/check", get(crate::app_window::check_for_update)) @@ -48,6 +52,7 @@ pub fn start_runtime_api() { .route("/secrets/delete", post(crate::secret::delete_secret)) .route("/system/directories/config", get(crate::environment::get_config_directory)) .route("/system/directories/data", get(crate::environment::get_data_directory)) + .route("/system/runtime/info", get(crate::environment::get_runtime_info)) .route("/system/language", get(crate::environment::read_user_language)) .route("/system/username", get(crate::environment::read_user_name)) .route("/system/enterprise/config/id", get(crate::environment::read_enterprise_env_config_id)) @@ -86,4 +91,4 @@ fn install_rustls_crypto_provider() { RUSTLS_CRYPTO_PROVIDER_INIT.call_once(|| { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); }); -} \ No newline at end of file +} diff --git a/runtime/src/sidecar_types.rs b/runtime/src/sidecar_types.rs index 7e5bfde0..973aa603 100644 --- a/runtime/src/sidecar_types.rs +++ b/runtime/src/sidecar_types.rs @@ -2,14 +2,12 @@ pub enum SidecarType { Dotnet, - Qdrant, } impl fmt::Display for SidecarType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SidecarType::Dotnet => write!(f, ".Net"), - SidecarType::Qdrant => write!(f, "Qdrant"), } } } \ No newline at end of file diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 1e1a96e9..e29bb1a4 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -24,11 +24,9 @@ "icons/icon.ico" ], "externalBin": [ - "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "target/databases/qdrant/qdrant" + "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer" ], "resources": [ - "resources/databases/qdrant/config.yaml", "resources/libraries/*" ], "macOS": {