diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 022339505..b83140579 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -6343,6 +6343,39 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T639143005"] = "Text Fil
-- All Office Files
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T709668067"] = "All Office Files"
+-- Text
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1041509726"] = "Text"
+
+-- Office Files
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1063218378"] = "Office Files"
+
+-- Executable
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1364437037"] = "Executable"
+
+-- Image
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1494001562"] = "Image"
+
+-- Video
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1533528076"] = "Video"
+
+-- Source Code
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1569048941"] = "Source Code"
+
+-- Config
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1779622119"] = "Config"
+
+-- Audio
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2291602489"] = "Audio"
+
+-- Custom
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Custom"
+
+-- Media
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Media"
+
+-- Document
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
+
-- Pandoc Installation
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc Installation"
diff --git a/app/MindWork AI Studio/Chat/FileAttachment.cs b/app/MindWork AI Studio/Chat/FileAttachment.cs
index 1208303f3..42a696c06 100644
--- a/app/MindWork AI Studio/Chat/FileAttachment.cs
+++ b/app/MindWork AI Studio/Chat/FileAttachment.cs
@@ -58,11 +58,14 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi
/// extracting the filename, and reading the file size.
///
/// The full path to the file.
+ /// Optional: The allowed file types.
/// A FileAttachment instance with populated properties.
- public static FileAttachment FromPath(string filePath)
+ public static FileAttachment FromPath(string filePath, FileType[]? allowedTypes=null)
{
var fileName = Path.GetFileName(filePath);
var fileSize = File.Exists(filePath) ? new FileInfo(filePath).Length : 0;
+ if (allowedTypes != null && !IsAllowed(filePath, allowedTypes))
+ return new FileAttachment(FileAttachmentType.FORBIDDEN, fileName, filePath, fileSize);
var type = DetermineFileType(filePath);
return type switch
@@ -76,7 +79,7 @@ public static FileAttachment FromPath(string filePath)
///
/// Determines the file attachment type based on the file extension.
- /// Uses centrally defined file type filters from .
+ /// Uses centrally defined file types from .
///
/// The file path to analyze.
/// The corresponding FileAttachmentType.
@@ -85,21 +88,28 @@ private static FileAttachmentType DetermineFileType(string filePath)
var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant();
// Check if it's an image file:
- if (FileTypeFilter.AllImages.FilterExtensions.Contains(extension))
+ if (FileTypes.OnlyAllowTypes(FileTypes.IMAGE).Contains(extension))
+ {
return FileAttachmentType.IMAGE;
+ }
// Check if it's an audio file:
- if (FileTypeFilter.AllAudio.FilterExtensions.Contains(extension))
+ if (FileTypes.OnlyAllowTypes(FileTypes.AUDIO).Contains(extension))
return FileAttachmentType.AUDIO;
// Check if it's an allowed document file (PDF, Text, or Office):
- if (FileTypeFilter.PDF.FilterExtensions.Contains(extension) ||
- FileTypeFilter.Text.FilterExtensions.Contains(extension) ||
- FileTypeFilter.AllOffice.FilterExtensions.Contains(extension) ||
- FileTypeFilter.AllSourceCode.FilterExtensions.Contains(extension))
+ if (FileTypes.OnlyAllowTypes(FileTypes.DOCUMENT).Contains(extension))
+ {
return FileAttachmentType.DOCUMENT;
+ }
// All other file types are forbidden:
return FileAttachmentType.FORBIDDEN;
}
+
+ private static bool IsAllowed(string filePath, FileType[] allowedTypes)
+ {
+ var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant();
+ return FileTypes.OnlyAllowTypes(allowedTypes).Contains(extension);
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
index acfc0dd2f..be83d51bc 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -48,6 +48,9 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public bool UseSmallForm { get; set; }
+ [Parameter]
+ public FileType[]? AllowedFileTypes { get; set; }
+
///
/// When true, validate media file types before attaching. Default is true. That means that
/// the user cannot attach unsupported media file types when the provider or model does not
@@ -181,8 +184,7 @@ protected override async Task OnInitializedAsync()
{
if(!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
continue;
-
- this.DocumentPaths.Add(FileAttachment.FromPath(path));
+ this.DocumentPaths.Add(FileAttachment.FromPath(path, this.AllowedFileTypes));
}
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
@@ -226,7 +228,7 @@ private async Task AddFilesManually()
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.ValidateMediaFileTypes, this.Provider))
continue;
- this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
+ this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath, this.AllowedFileTypes));
}
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs
index 9caf3cd77..d8712e965 100644
--- a/app/MindWork AI Studio/Components/SelectFile.razor.cs
+++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs
@@ -23,7 +23,7 @@ public partial class SelectFile : MSGComponentBase
public string FileDialogTitle { get; set; } = "Select File";
[Parameter]
- public FileTypeFilter? Filter { get; set; }
+ public FileType[]? Filter { get; set; }
[Parameter]
public Func Validation { get; set; } = _ => null;
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
index 85e6e6eff..6e5a595ba 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
@@ -1,5 +1,6 @@
@using AIStudio.Provider
@using AIStudio.Provider.SelfHosted
+@using AIStudio.Tools.Rust
@inherits MSGComponentBase
@@ -124,6 +125,7 @@
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
+
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
index 6520b7ee7..a3b66dbe6 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
@@ -1,3 +1,4 @@
+using AIStudio.Chat;
using AIStudio.Components;
using AIStudio.Provider;
using AIStudio.Settings;
@@ -96,7 +97,8 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private readonly List availableModels = new();
private readonly Encryption encryption = Program.ENCRYPTION;
private readonly ProviderValidation providerValidation;
-
+ private HashSet chatDocumentPaths = [];
+
public EmbeddingProviderDialog()
{
this.providerValidation = new()
diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs
index 27e5244e5..e57afdd80 100644
--- a/app/MindWork AI Studio/Tools/PandocExport.cs
+++ b/app/MindWork AI Studio/Tools/PandocExport.cs
@@ -2,6 +2,7 @@
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Tools.PluginSystem;
+using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
@@ -16,7 +17,7 @@ public static class PandocExport
public static async Task ToMicrosoftWord(RustService rustService, IDialogService dialogService, string dialogTitle, IContent markdownContent)
{
- var response = await rustService.SaveFile(dialogTitle, new("Microsoft Word", ["docx"]));
+ var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]);
if (response.UserCancelled)
{
LOGGER.LogInformation("User cancelled the save dialog.");
diff --git a/app/MindWork AI Studio/Tools/Rust/FileType.cs b/app/MindWork AI Studio/Tools/Rust/FileType.cs
new file mode 100644
index 000000000..c333a6913
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/FileType.cs
@@ -0,0 +1,41 @@
+namespace AIStudio.Tools.Rust;
+
+///
+/// Represents a file type that can optionally contain child file types.
+/// Use the static helpers , and to build readable trees.
+///
+/// Display name of the type (e.g., "Document").
+/// File extensions belonging to this type (without dot).
+/// Nested file types that are included when this type is selected.
+public sealed record FileType(string FilterName, string[] FilterExtensions, IReadOnlyList Children)
+{
+ ///
+ /// Factory for a leaf node.
+ /// Example: FileType.Leaf(".NET", "cs", "razor")
+ ///
+ public static FileType Leaf(string name, params string[] extensions) =>
+ new(name, extensions, []);
+
+ ///
+ /// Factory for a parent node that only has children.
+ /// Example: FileType.Parent("Source Code", dotnet, java)
+ ///
+ public static FileType Parent(string name, params FileType[]? children) =>
+ new(name, [], children ?? []);
+
+ ///
+ /// Factory for a composite node that has its own extensions in addition to children.
+ ///
+ public static FileType Composite(string name, string[] extensions, params FileType[] children) =>
+ new(name, extensions, children);
+
+ ///
+ /// Collects all extensions for this type, including children.
+ ///
+ public IEnumerable FlattenExtensions()
+ {
+ return this.FilterExtensions
+ .Concat(this.Children.SelectMany(child => child.FlattenExtensions()))
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+ }
+}
\ 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
new file mode 100644
index 000000000..ff6fcd88c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs
@@ -0,0 +1,84 @@
+using AIStudio.Tools.PluginSystem;
+
+namespace AIStudio.Tools.Rust;
+
+///
+/// Central definition of supported file types with parent/child relationships and helpers
+/// to build extension whitelists (e.g., for file pickers or validation).
+///
+public static class FileTypes
+{
+ private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileType).Namespace, nameof(FileType));
+
+ // Source code hierarchy: SourceCode -> (.NET, Java, Python, Web, C/C++, Config, ...)
+ public static readonly FileType DOTNET = FileType.Leaf(".NET", "cs", "razor", "vb", "fs", "aspx", "cshtml", "csproj");
+ public static readonly FileType JAVA = FileType.Leaf("Java", "java");
+ public static readonly FileType PYTHON = FileType.Leaf("Python", "py");
+ public static readonly FileType JAVASCRIPT = FileType.Leaf("JavaScript/TypeScript", "js", "ts");
+ public static readonly FileType CFAMILY = FileType.Leaf("C/C++", "c", "cpp", "h", "hpp");
+ public static readonly FileType RUBY = FileType.Leaf("Ruby", "rb");
+ public static readonly FileType GO = FileType.Leaf("Go", "go");
+ public static readonly FileType RUST = FileType.Leaf("Rust", "rs");
+ public static readonly FileType LUA = FileType.Leaf("Lua", "lua");
+ public static readonly FileType PHP = FileType.Leaf("PHP", "php");
+ public static readonly FileType WEB = FileType.Leaf("HTML/CSS", "html", "css");
+ public static readonly FileType APP = FileType.Leaf("Swift/Kotlin", "swift", "kt");
+ public static readonly FileType SHELL = FileType.Leaf("Shell", "sh", "bash", "zsh");
+ public static readonly FileType LOG = FileType.Leaf("Log", "log");
+ public static readonly FileType JSON = FileType.Leaf("JSON", "json");
+ public static readonly FileType XML = FileType.Leaf("XML", "xml");
+ public static readonly FileType YAML = FileType.Leaf("YAML", "yaml", "yml");
+ public static readonly FileType CONFIG = FileType.Leaf(TB("Config"), "ini", "cfg", "toml", "plist");
+
+ public static readonly FileType SOURCE_CODE = FileType.Parent(TB("Source Code"),
+ DOTNET, JAVA, PYTHON, JAVASCRIPT, CFAMILY, RUBY, GO, RUST, LUA, PHP, WEB, APP, SHELL, LOG, JSON, XML, YAML, CONFIG);
+
+ // Document hierarchy
+ public static readonly FileType PDF = FileType.Leaf("PDF", "pdf");
+ public static readonly FileType TEXT = FileType.Leaf(TB("Text"), "txt", "md");
+ public static readonly FileType MS_WORD = FileType.Leaf("Microsoft Word", "docx");
+ public static readonly FileType WORD = FileType.Composite("Word", ["docx"], MS_WORD);
+ public static readonly FileType EXCEL = FileType.Leaf("Excel", "xls", "xlsx");
+ public static readonly FileType POWER_POINT = FileType.Leaf("PowerPoint", "ppt", "pptx");
+
+ public static readonly FileType OFFICE_FILES = FileType.Parent(TB("Office Files"),
+ WORD, EXCEL, POWER_POINT, PDF);
+ public static readonly FileType DOCUMENT = FileType.Parent(TB("Document"),
+ TEXT, OFFICE_FILES, SOURCE_CODE);
+
+ // Media hierarchy
+ public static readonly FileType IMAGE = FileType.Leaf(TB("Image"),
+ "jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "webp", "heic");
+ public static readonly FileType AUDIO = FileType.Leaf(TB("Audio"),
+ "mp3", "wav", "wave", "aac", "flac", "ogg", "m4a", "wma", "alac", "aiff", "m4b");
+ public static readonly FileType VIDEO = FileType.Leaf(TB("Video"),
+ "mp4", "m4v", "avi", "mkv", "mov", "wmv", "flv", "webm");
+
+ public static readonly FileType MEDIA = FileType.Parent(TB("Media"), IMAGE, AUDIO, VIDEO);
+
+ // Other standalone types
+ public static readonly FileType EXECUTABLES = FileType.Leaf(TB("Executable"), "exe", "app", "bin", "appimage");
+
+ ///
+ /// Builds a distinct, lower-cased list of extensions allowed for the provided types.
+ /// Accepts both composite types (e.g., Document) and leaves (e.g., Pdf).
+ ///
+ public static string[] OnlyAllowTypes(params FileType[] types)
+ {
+ if (types.Length == 0)
+ return [];
+
+ return types
+ .SelectMany(t => t.FlattenExtensions())
+ .Select(ext => ext.ToLowerInvariant())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+ }
+
+ public static FileType? AsOneFileType(params FileType[]? types)
+ {
+ if (types == null || types.Length == 0)
+ return null;
+ return FileType.Composite(TB("Custom"), OnlyAllowTypes(types));
+ }
+}
diff --git a/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs
index 107e581a7..f1300ac17 100644
--- a/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs
+++ b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs
@@ -6,5 +6,5 @@ public class SaveFileOptions
public PreviousFile? PreviousFile { get; init; }
- public FileTypeFilter? Filter { get; init; }
+ public FileType? Filter { get; init; }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs
index 28d16809a..fac7d5f4e 100644
--- a/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs
+++ b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs
@@ -6,5 +6,5 @@ public sealed class SelectFileOptions
public PreviousFile? PreviousFile { get; init; }
- public FileTypeFilter? Filter { get; init; }
+ public FileType? Filter { get; init; }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
index 4a498b016..c55b6a8b7 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
@@ -17,13 +17,13 @@ public async Task SelectDirectory(string title, stri
return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions);
}
- public async Task SelectFile(string title, FileTypeFilter? filter = null, string? initialFile = null)
+ public async Task SelectFile(string title, FileType[]? filter = null, string? initialFile = null)
{
var payload = new SelectFileOptions
{
Title = title,
PreviousFile = initialFile is null ? null : new (initialFile),
- Filter = filter
+ Filter = FileTypes.AsOneFileType(filter)
};
var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions);
@@ -36,13 +36,13 @@ public async Task SelectFile(string title, FileTypeFilter
return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions);
}
- public async Task SelectFiles(string title, FileTypeFilter? filter = null, string? initialFile = null)
+ public async Task SelectFiles(string title, FileType[]? filter = null, string? initialFile = null)
{
var payload = new SelectFileOptions
{
Title = title,
PreviousFile = initialFile is null ? null : new (initialFile),
- Filter = filter
+ Filter = FileTypes.AsOneFileType(filter)
};
var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
@@ -63,13 +63,13 @@ public async Task SelectFiles(string title, FileTypeFilt
/// An optional initial file path to pre-fill in the dialog.
/// A object containing information about whether the user canceled the
/// operation and whether the select operation was successful.
- public async Task SaveFile(string title, FileTypeFilter? filter = null, string? initialFile = null)
+ public async Task SaveFile(string title, FileType[]? filter = null, string? initialFile = null)
{
var payload = new SaveFileOptions
{
Title = title,
PreviousFile = initialFile is null ? null : new (initialFile),
- Filter = filter
+ Filter = FileTypes.AsOneFileType(filter)
};
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
diff --git a/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs b/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs
index 02a978d1a..a9a873670 100644
--- a/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs
+++ b/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs
@@ -44,7 +44,7 @@ public enum UseCase
public static async Task IsExtensionValidWithNotifyAsync(UseCase useCae, string filePath, bool validateMediaFileTypes = true, Settings.Provider? provider = null)
{
var ext = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant();
- if(FileTypeFilter.Executables.FilterExtensions.Contains(ext))
+ if(FileTypes.EXECUTABLES.FlattenExtensions().Contains(ext))
{
await MessageBus.INSTANCE.SendError(new(
Icons.Material.Filled.AppBlocking,
@@ -53,7 +53,7 @@ await MessageBus.INSTANCE.SendError(new(
}
var capabilities = provider?.GetModelCapabilities() ?? new();
- if (FileTypeFilter.AllImages.FilterExtensions.Contains(ext))
+ if (FileTypes.IMAGE.FlattenExtensions().Contains(ext))
{
switch (useCae)
{
@@ -88,7 +88,7 @@ await MessageBus.INSTANCE.SendWarning(new(
}
}
- if(FileTypeFilter.AllVideos.FilterExtensions.Contains(ext))
+ if(FileTypes.VIDEO.FlattenExtensions().Contains(ext))
{
await MessageBus.INSTANCE.SendWarning(new(
Icons.Material.Filled.FeaturedVideo,
@@ -96,7 +96,7 @@ await MessageBus.INSTANCE.SendWarning(new(
return false;
}
- if(FileTypeFilter.AllAudio.FilterExtensions.Contains(ext))
+ if(FileTypes.AUDIO.FlattenExtensions().Contains(ext))
{
await MessageBus.INSTANCE.SendWarning(new(
Icons.Material.Filled.AudioFile,
@@ -123,7 +123,7 @@ await MessageBus.INSTANCE.SendError(new(
return false;
}
- if (!Array.Exists(FileTypeFilter.AllImages.FilterExtensions, x => x.Equals(ext, StringComparison.OrdinalIgnoreCase)))
+ if (FileTypes.IMAGE.FlattenExtensions().Any(x => x.Equals(ext, StringComparison.OrdinalIgnoreCase)))
{
await MessageBus.INSTANCE.SendError(new(
Icons.Material.Filled.ImageNotSupported,