Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
56e08b2
chat main layout
Madzionator Feb 11, 2026
7cbbc9a
file attachment
Madzionator Feb 12, 2026
b7fad25
stop button + some cleaning
Madzionator Feb 12, 2026
1a55893
Use BackendType for backend config/UI
Madzionator Feb 12, 2026
1ed2540
Color fix
Madzionator Feb 19, 2026
828b724
fix: stream tokens progressively for file-based chat
Madzionator Feb 19, 2026
14d3a20
fix theme color change (disco problem)
Madzionator Feb 19, 2026
b1f1fb9
Merge branch 'main' into feature/infra-page-update
Madzionator Feb 19, 2026
0f50ad9
post merge fixes
Madzionator Feb 19, 2026
0542c12
update show-reasoning button
Madzionator Feb 19, 2026
658020f
fix stop button
Madzionator Feb 19, 2026
e0375fa
Add themeManager and replace eval-based theme access
Madzionator Feb 20, 2026
b6056bc
Use LLMApiRegistry for API keys in IferPage Program.cs
Madzionator Feb 20, 2026
80e043e
fix MemoryStream leaks + multi-attachments issue
Madzionator Feb 20, 2026
c4a0f57
smarter scroll
Madzionator Feb 20, 2026
ed05edf
Handle unregistered ai models; Support images input in cloud LLM
Madzionator Feb 24, 2026
580d7ac
paste and drag&drop files/images
Madzionator Feb 24, 2026
6c0af42
Add image attachment support and previews
Madzionator Feb 24, 2026
07034d1
Add image-generation support and UI
Madzionator Feb 26, 2026
5688c30
Replace Visual flag with ImageGen and add vision support
Madzionator Feb 27, 2026
1e8246e
Merge branch 'main' into feature/infra-page-update
Madzionator Feb 27, 2026
37a1f5f
Handle base64 images in OpenAiImageGenService
Madzionator Feb 27, 2026
53ded11
Add missing IVisionModel in cloud models
Madzionator Feb 27, 2026
2541958
versioning
Madzionator Feb 27, 2026
fef3f6a
fix typo
Madzionator Mar 3, 2026
729202d
Merge branch 'main' into feature/infra-page-update
Madzionator Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Examples/Examples/Chat/ChatWithImageGenExample.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Examples.Utils;
using MaIN.Core.Hub;
using MaIN.Domain.Models.Abstract;

namespace Examples.Chat;

Expand All @@ -10,7 +11,7 @@ public async Task Start()
Console.WriteLine("ChatExample with image gen is running!");

var result = await AIHub.Chat()
.EnableVisual()
.WithModel(new GenericLocalModel("FLUX.1_Shnell"), imageGen: true)
.WithMessage("Generate cyberpunk godzilla cat warrior")
.CompleteAsync();

Expand Down
4 changes: 3 additions & 1 deletion Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Examples.Utils;
using MaIN.Core.Hub;
using MaIN.Domain.Configuration;
using MaIN.Domain.Models.Abstract;

namespace Examples.Chat;

Expand All @@ -11,7 +13,7 @@ public async Task Start()
GeminiExample.Setup(); // We need to provide Gemini API key

var result = await AIHub.Chat()
.EnableVisual()
.WithModel(new GenericCloudModel("imagen-3", BackendType.Gemini), imageGen: true)
.WithMessage("Generate hamster as a astronaut on the moon")
.CompleteAsync();

Expand Down
1 change: 0 additions & 1 deletion Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public async Task Start()

var result = await AIHub.Chat()
.WithModel<DallE3>()
.EnableVisual()
.WithMessage("Generate rock style cow playing guitar")
.CompleteAsync();

Expand Down
3 changes: 2 additions & 1 deletion MaIN.Core.IntegrationTests/ChatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using MaIN.Core.Hub;
using MaIN.Core.IntegrationTests.Helpers;
using MaIN.Domain.Entities;
using MaIN.Domain.Models.Abstract;
using MaIN.Domain.Models.Concrete;

namespace MaIN.Core.IntegrationTests;
Expand Down Expand Up @@ -97,7 +98,7 @@ public async Task Should_GenerateImage_BasedOnPrompt()
const string extension = "png";

var result = await AIHub.Chat()
.EnableVisual()
.WithModel(new GenericLocalModel("FLUX.1_Shnell"), imageGen: true)
.WithMessage("Generate cat in Rome. Sightseeing, colloseum, ancient builidngs, Italy.")
.CompleteAsync();

Expand Down
7 changes: 7 additions & 0 deletions Releases/0.10.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 0.10.0 release

Improve InferPage:
- Refreshed chat UI layout with improved theming and smarter scroll behavior
- Extended attachments (drag & drop, paste), image previews, and improved image generation
- Added support for unregistered models and vision-based image handling (no OCR)
- Stability fixes, proper cancellation support, and internal service refactoring
6 changes: 3 additions & 3 deletions src/MaIN.Core.UnitTests/ChatContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public async Task CompleteAsync_ShouldCallChatService()
};


_mockChatService.Setup(s => s.Completions(It.IsAny<Chat>(), It.IsAny<bool>(), It.IsAny<bool>(), null))
_mockChatService.Setup(s => s.Completions(It.IsAny<Chat>(), It.IsAny<bool>(), It.IsAny<bool>(), null, It.IsAny<CancellationToken>()))
.ReturnsAsync(chatResult);

_chatContext.WithMessage("User message");
Expand All @@ -98,7 +98,7 @@ public async Task CompleteAsync_ShouldCallChatService()
var result = await _chatContext.CompleteAsync();

// Assert
_mockChatService.Verify(s => s.Completions(It.IsAny<Chat>(), false, false, null), Times.Once);
_mockChatService.Verify(s => s.Completions(It.IsAny<Chat>(), false, false, null, It.IsAny<CancellationToken>()), Times.Once);
Assert.Equal(chatResult, result);
}

Expand Down Expand Up @@ -128,6 +128,6 @@ await _chatContext.WithModel(model)
.CompleteAsync();

// Assert
_mockChatService.Verify(s => s.Completions(It.Is<Chat>(c => c.ModelId == _testModelId && c.ModelInstance == model), It.IsAny<bool>(), It.IsAny<bool>(), null), Times.Once);
_mockChatService.Verify(s => s.Completions(It.Is<Chat>(c => c.ModelId == _testModelId && c.ModelInstance == model), It.IsAny<bool>(), It.IsAny<bool>(), null, It.IsAny<CancellationToken>()), Times.Once);
}
}
2 changes: 1 addition & 1 deletion src/MaIN.Core/.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package>
<metadata>
<id>MaIN.NET</id>
<version>0.9.4</version>
<version>0.10.0</version>
<authors>Wisedev</authors>
<owners>Wisedev</owners>
<icon>favicon.png</icon>
Expand Down
17 changes: 7 additions & 10 deletions src/MaIN.Core/Hub/Contexts/ChatContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ internal ChatContext(IChatService chatService, Chat existingChat)
_chat = existingChat;
}

public IChatMessageBuilder WithModel(AIModel model)
public IChatMessageBuilder WithModel(AIModel model, bool? imageGen = null)
{
SetModel(model);
_chat.ImageGen = imageGen ?? model is IImageGenerationModel;
return this;
}

Expand Down Expand Up @@ -82,12 +83,7 @@ private void SetModel(AIModel model)
_chat.ModelId = model.Id;
_chat.ModelInstance = model;
_chat.Backend = model.Backend;
}

public IChatMessageBuilder EnableVisual()
{
_chat.Visual = true;
return this;
_chat.ImageGen = model.HasImageGeneration;
}

public IChatMessageBuilder EnsureModelDownloaded()
Expand Down Expand Up @@ -116,7 +112,7 @@ public IChatConfigurationBuilder WithMemoryParams(MemoryParams memoryParams)

public IChatConfigurationBuilder Speak(TextToSpeechParams speechParams)
{
_chat.Visual = false;
_chat.ImageGen = false;
_chat.TextToSpeechParams = speechParams;
return this;
}
Expand Down Expand Up @@ -205,7 +201,8 @@ public IChatConfigurationBuilder DisableCache()
public async Task<ChatResult> CompleteAsync(
bool translate = false, // Move to WithTranslate
bool interactive = false, // Move to WithInteractive
Func<LLMTokenValue?, Task>? changeOfValue = null)
Func<LLMTokenValue?, Task>? changeOfValue = null,
CancellationToken cancellationToken = default)
{
if (_chat.ModelInstance is null)
{
Expand All @@ -231,7 +228,7 @@ public async Task<ChatResult> CompleteAsync(
{
await _chatService.Create(_chat);
}
var result = await _chatService.Completions(_chat, translate, interactive, changeOfValue);
var result = await _chatService.Completions(_chat, translate, interactive, changeOfValue, cancellationToken);
_files = [];
return result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext;
namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext;

public interface IChatBuilderEntryPoint : IChatActions
{
Expand All @@ -9,7 +9,7 @@ public interface IChatBuilderEntryPoint : IChatActions
/// <param name="model">The name of the AI model to be used.</param>
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
IChatMessageBuilder WithModel(string model);

/// <summary>
/// Configures a custom model with a specific path and project context.
/// </summary>
Expand All @@ -18,18 +18,11 @@ public interface IChatBuilderEntryPoint : IChatActions
/// <param name="mmProject">Optional multi-modal project identifier.</param>
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null);

/// <summary>
/// Enables visual/image generation mode. Use this method now if you do not plan to explicitly define the model.
/// Otherwise, you will be able to use this method in the next step, after defining the model.
/// </summary>
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
IChatMessageBuilder EnableVisual();


/// <summary>
/// Loads an existing chat session from the database using its unique identifier.
/// </summary>
/// <param name="chatId">The GUID of the existing chat.</param>
/// <returns>The context instance implementing <see cref="IChatConfigurationBuilder"/> for method chaining.</returns>
Task<IChatConfigurationBuilder> FromExisting(string chatId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ public interface IChatConfigurationBuilder : IChatActions
/// <param name="interactive">A flag indicating whether the chat session should be interactive. Default is false.</param>
/// <param name="changeOfValue">An optional callback invoked whenever a new token or update is received during streaming.</param>
/// <returns>A <see cref="ChatResult"/> object containing the result of the completed chat session.</returns>
Task<ChatResult> CompleteAsync(bool translate = false, bool interactive = false, Func<LLMTokenValue?, Task>? changeOfValue = null);
Task<ChatResult> CompleteAsync(bool translate = false, bool interactive = false, Func<LLMTokenValue?, Task>? changeOfValue = null, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,14 @@ namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext;

public interface IChatMessageBuilder : IChatActions
{
/// <summary>
/// Enables the visual output for the current chat session. This flag allows the AI to generate and return visual content,
/// such as images or charts, as part of its response.
/// </summary>
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
IChatMessageBuilder EnableVisual();

/// <summary>
/// Flags the chat to automatically ensure the selected local model is downloaded before completing.
/// If the model is already present the download is skipped; cloud models are silently ignored.
/// The actual download is deferred until <see cref="IChatConfigurationBuilder.CompleteAsync"/> is called.
/// </summary>
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
IChatMessageBuilder EnsureModelDownloaded();

/// <summary>
/// Adds a user message to the chat. This method captures the message content and assigns the "User" role to it.
/// It also timestamps the message for proper ordering.
Expand Down
14 changes: 10 additions & 4 deletions src/MaIN.Domain/Entities/Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,35 @@ public class Chat
{
public string Id { get; init; } = string.Empty;
public required string Name { get; init; }
private string? _modelId;
public required string ModelId
{
get => _modelInstance?.Id ?? string.Empty;
get => _modelInstance?.Id ?? _modelId ?? string.Empty;
set
{
_modelId = value;
if (string.IsNullOrEmpty(value))
{
_modelInstance = null;
return;
}

_modelInstance = ModelRegistry.GetById(value);
ModelRegistry.TryGetById(value, out _modelInstance);
}
}
private AIModel? _modelInstance;
public AIModel? ModelInstance
{
get => _modelInstance;
set => (_modelInstance, ModelId) = (value, value?.Id ?? string.Empty);
set
{
_modelInstance = value;
_modelId = value?.Id ?? string.Empty;
}
}
public List<Message> Messages { get; set; } = [];
public ChatType Type { get; set; } = ChatType.Conversation;
public bool Visual { get; set; }
public bool ImageGen { get; set; }
public InferenceParams InterferenceParams { get; set; } = new();
public MemoryParams MemoryParams { get; set; } = new();
public ToolsConfiguration? ToolsConfiguration { get; set; }
Expand Down
14 changes: 13 additions & 1 deletion src/MaIN.Domain/Entities/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,19 @@ public Message()
public List<LLMTokenValue> Tokens { get; set; } = [];
public bool Tool { get; init; }
public DateTime Time { get; set; }
public byte[]? Image { get; init; }
public List<byte[]>? Images { get; set; }

// Backward-compat wrapper – single image access
public byte[]? Image
{
get => Images?.Count > 0 ? Images[0] : null;
set
{
if (value == null) Images = null;
else Images = [value];
}
}

public byte[]? Speech { get; set; }
public List<FileInfo>? Files { get; set; }
public Dictionary<string, string> Properties { get; set; } = [];
Expand Down
3 changes: 3 additions & 0 deletions src/MaIN.Domain/Models/Abstract/AIModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public abstract record AIModel(

/// <summary> Checks if model supports vision/image input. </summary>
public bool HasVision => this is IVisionModel;

/// <summary> Checks if model generates images from text prompts. </summary>
public bool HasImageGeneration => this is IImageGenerationModel;
}

/// <summary> Base class for local models. </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/MaIN.Domain/Models/Abstract/IModelCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ public interface IEmbeddingModel
/// Interface for models that support text-to-speech.
/// </summary>
public interface ITTSModel;

/// <summary>
/// Interface for models that generate images from text prompts.
/// </summary>
public interface IImageGenerationModel;
Loading
Loading