diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor
index f7887f6a..1d636d32 100644
--- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor
+++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor
@@ -15,6 +15,23 @@
}
break;
+ case AssistantUiCompontentType.IMAGE:
+ if (component is AssistantImage assistantImage)
+ {
+ var resolvedSource = this.ResolveImageSource(assistantImage);
+ if (!string.IsNullOrWhiteSpace(resolvedSource))
+ {
+ var image = assistantImage;
+
+
+ @if (!string.IsNullOrWhiteSpace(image.Caption))
+ {
+ @image.Caption
+ }
+
+ }
+ }
+ break;
case AssistantUiCompontentType.WEB_CONTENT_READER:
if (component is AssistantWebContentReader webContent && this.webContentFields.TryGetValue(webContent.Name, out var webState))
{
diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
index 72c4c3bb..b3ad6667 100644
--- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
+++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
@@ -1,4 +1,7 @@
-using AIStudio.Dialogs.Settings;
+using System;
+using System.IO;
+
+using AIStudio.Dialogs.Settings;
using AIStudio.Tools.PluginSystem;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem.Assistants;
@@ -36,6 +39,9 @@ public partial class AssistantDynamic : AssistantBaseCore
private Dictionary switchFields = new();
private Dictionary webContentFields = new();
private Dictionary fileContentFields = new();
+ private readonly Dictionary imageCache = new();
+ private string pluginPath = string.Empty;
+ const string PLUGIN_SCHEME = "plugin://";
protected override void OnInitialized()
{
@@ -50,6 +56,7 @@ public partial class AssistantDynamic : AssistantBaseCore
this.submitText = assistantPlugin.SubmitText;
this.allowProfiles = assistantPlugin.AllowProfiles;
this.showFooterProfileSelection = !assistantPlugin.HasEmbeddedProfileSelection;
+ this.pluginPath = assistantPlugin.PluginPath;
}
foreach (var component in this.RootComponent!.Children)
@@ -118,6 +125,61 @@ public partial class AssistantDynamic : AssistantBaseCore
return false;
}
+ private string ResolveImageSource(AssistantImage image)
+ {
+ if (string.IsNullOrWhiteSpace(image.Src))
+ return string.Empty;
+
+ if (this.imageCache.TryGetValue(image.Src, out var cached) && !string.IsNullOrWhiteSpace(cached))
+ return cached;
+
+ var resolved = image.Src;
+
+ if (resolved.StartsWith(PLUGIN_SCHEME, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(this.pluginPath))
+ {
+ var relative = resolved[PLUGIN_SCHEME.Length..].TrimStart('/', '\\').Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
+ var filePath = Path.Join(this.pluginPath, relative);
+ if (File.Exists(filePath))
+ {
+ var mime = GetImageMimeType(filePath);
+ var data = Convert.ToBase64String(File.ReadAllBytes(filePath));
+ resolved = $"data:{mime};base64,{data}";
+ }
+ else
+ {
+ resolved = string.Empty;
+ }
+ }
+ else if (Uri.TryCreate(resolved, UriKind.Absolute, out var uri))
+ {
+ if (uri.Scheme is not ("http" or "https" or "data"))
+ resolved = string.Empty;
+ }
+ else
+ {
+ resolved = string.Empty;
+ }
+
+ this.imageCache[image.Src] = resolved;
+ return resolved;
+ }
+
+ private static string GetImageMimeType(string path)
+ {
+ var extension = Path.GetExtension(path).TrimStart('.').ToLowerInvariant();
+ return extension switch
+ {
+ "svg" => "image/svg+xml",
+ "png" => "image/png",
+ "jpg" => "image/jpeg",
+ "jpeg" => "image/jpeg",
+ "gif" => "image/gif",
+ "webp" => "image/webp",
+ "bmp" => "image/bmp",
+ _ => "image/png",
+ };
+ }
+
private string? ValidateCustomLanguage(string value) => string.Empty;
private string CollectUserPrompt()
diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md
index fab3f910..9adc5bbc 100644
--- a/app/MindWork AI Studio/Plugins/assistants/README.md
+++ b/app/MindWork AI Studio/Plugins/assistants/README.md
@@ -15,8 +15,11 @@ Supported types (matching the Blazor UI components):
- `PROVIDER_SELECTION` / `PROFILE_SELECTION`: hooks into the shared provider/profile selectors.
- `WEB_CONTENT_READER`: renders `ReadWebContent`; include `Name`, `UserPrompt`, `Preselect`, `PreselectContentCleanerAgent`.
- `FILE_CONTENT_READER`: renders `ReadFileContent`; include `Name`, `UserPrompt`.
+- `IMAGE`: embeds a static illustration; `Props` must include `Src` plus optionally `Alt` and `Caption`. `Src` can be an HTTP/HTTPS URL, a `data:` URI, or a plugin-relative path (`plugin://assets/your-image.png`). The runtime will convert plugin-relative paths into `data:` URLs (base64).
- `HEADING`, `TEXT`, `LIST`: descriptive helpers.
+Images referenced via the `plugin://` scheme must exist in the plugin directory (e.g., `assets/example.png`). Drop the file there and point `Src` at it. The component will read the file at runtime, encode it as Base64, and render it inside the assistant UI.
+
## Prompt Assembly
Each component exposes a `UserPrompt` string. When the assistant runs, `AssistantDynamic` iterates over `RootComponent.Children` and, for each component that has a prompt, emits:
@@ -36,6 +39,4 @@ For switches the “value” is the boolean `true/false`; for readers it is the
2. Keep in mind that components and their properties are case-sensitive (e.g. if you write `["Type"] = "heading"` instead of `["Type"] = "HEADING"` the component will not be registered). Always copy-paste the component from the `plugin.lua` manifest to avoid this.
3. When you expect default content (e.g., a textarea with instructions), keep `UserPrompt` but also set `PrefillText` so the user starts with a hint.
4. If you need extra explanatory text (before or after the interactive controls), use `TEXT` or `HEADING` components.
-5. Keep `Preselect`/`PreselectContentCleanerAgent` flags in `WEB_CONTENT_READER` to simplify the initial UI for the user.
-
-The sample `plugin.lua` in this directory is the live reference. Adjust it, reload the assistant plugin via the desktop app, and verify that the prompt log contains the blocked `context`/`user prompt` pairs that you expect.
+5. Keep `Preselect`/`PreselectContentCleanerAgent` flags in `WEB_CONTENT_READER` to simplify the initial UI for the user.
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Plugins/assistants/plugin.lua b/app/MindWork AI Studio/Plugins/assistants/plugin.lua
index 25041807..fa19e5f9 100644
--- a/app/MindWork AI Studio/Plugins/assistants/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua
@@ -144,6 +144,14 @@ ASSISTANT = {
}
}
},
+ {
+ ["Type"] = "IMAGE",
+ ["Props"] = {
+ ["Src"] = "plugin://assets/example.png",
+ ["Alt"] = "SVG-inspired placeholder",
+ ["Caption"] = "Static illustration via the IMAGE component."
+ }
+ },
{
["Type"] = "WEB_CONTENT_READER", -- allows the user to fetch a URL and clean it
["Props"] = {
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs
index dae13bf7..8b20a9d0 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs
@@ -37,6 +37,8 @@ public class AssistantComponentFactory
return new AssistantWebContentReader { Props = props, Children = children };
case AssistantUiCompontentType.FILE_CONTENT_READER:
return new AssistantFileContentReader { Props = props, Children = children };
+ case AssistantUiCompontentType.IMAGE:
+ return new AssistantImage { Props = props, Children = children };
default:
LOGGER.LogError($"Unknown assistant component type!\n{type} is not a supported assistant component type");
throw new Exception($"Unknown assistant component type: {type}");
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs
new file mode 100644
index 00000000..8cbcbd80
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs
@@ -0,0 +1,32 @@
+namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
+
+public class AssistantImage : AssistantComponentBase
+{
+ public override AssistantUiCompontentType Type => AssistantUiCompontentType.IMAGE;
+ public Dictionary Props { get; set; } = new();
+ public List Children { get; set; } = new();
+
+ public string Src
+ {
+ get => this.Props.TryGetValue(nameof(this.Src), out var v)
+ ? v.ToString() ?? string.Empty
+ : string.Empty;
+ set => this.Props[nameof(this.Src)] = value;
+ }
+
+ public string Alt
+ {
+ get => this.Props.TryGetValue(nameof(this.Alt), out var v)
+ ? v.ToString() ?? string.Empty
+ : string.Empty;
+ set => this.Props[nameof(this.Alt)] = value;
+ }
+
+ public string Caption
+ {
+ get => this.Props.TryGetValue(nameof(this.Caption), out var v)
+ ? v.ToString() ?? string.Empty
+ : string.Empty;
+ set => this.Props[nameof(this.Caption)] = value;
+ }
+}
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs
index 99d264c2..91fbf965 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs
@@ -14,4 +14,5 @@ public enum AssistantUiCompontentType
LIST,
WEB_CONTENT_READER,
FILE_CONTENT_READER,
+ IMAGE,
}
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs
index 41001325..03a4d514 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs
@@ -53,5 +53,9 @@ public static class ComponentPropSpecs
required: ["Name"],
optional: ["UserPrompt"]
),
+ [AssistantUiCompontentType.IMAGE] = new(
+ required: ["Src"],
+ optional: ["Alt", "Caption"]
+ ),
};
}