Skip to content

Commit a32de60

Browse files
authored
feat: InferPage visual & functional improvements (#122)
* chat main layout * file attachment * stop button + some cleaning * Use BackendType for backend config/UI Refactor backend selection to use BackendType everywhere and simplify API key handling. Added Extensions.GetApiKeyVariable to map backends to env vars; Program now sets Utils.BackendType from the CLI arg, prompts for missing API keys (and marks Utils.HasApiKey), and only registers MaIN services when a non-self backend is selected. Utils was simplified: removed per-backend booleans, added BackendType, HasApiKey, IsLocal helper and moved Reason flag. UI updates: NavBar shows backend and model badges (with color/display name logic including "Local Ollama"), and Home.razor now branches on BackendType and uses Utils.IsLocal for MessageType. Also trimmed launchSettings applicationUrl. * Color fix * fix: stream tokens progressively for file-based chat * fix theme color change (disco problem) * post merge fixes * update show-reasoning button * fix stop button * Add themeManager and replace eval-based theme access Introduce a small JS themeManager in App.razor that bootstraps theme on page load (reads localStorage, parses JSON, and sets documentElement data-theme for dark mode) and exposes save/load helpers. Replace prior eval-based localStorage/document access in NavBar.razor and Home.razor with calls to themeManager.load, and update component logic to derive UI mode/accent color from the returned value. This centralizes theme persistence, avoids using eval, and provides safer parsing and fallbacks. * Use LLMApiRegistry for API keys in IferPage Program.cs Remove the old BackendType extension and centralize API key metadata in LLMApiRegistry (moved to MaIN.Domain.Models.Concrete). Program.cs now looks up the registry entry for each BackendType to read ApiKeyEnvName instead of calling GetApiKeyVariable. Updated numerous LLM and image service files (and McpService) to reference the new namespace. This change consolidates API key configuration and removes the duplicated extension method. * fix MemoryStream leaks + multi-attachments issue * smarter scroll * Handle unregistered ai models; Support images input in cloud LLM Add multi-image support and extract image bytes from uploaded files for LLM services; improve model ID/instance handling and model selection flow. - Message: replace single byte[] Image with List<byte[]> Images and keep a backward-compatible Image getter/setter. - Chat: preserve raw ModelId string, safely try to resolve model instance (no throws), and sync ModelInstance with internal id field. - Home.razor: unify model resolution into a local variable and choose GenericLocalModel/GenericCloudModel when registry lookup fails. - AnthropicService & OpenAiCompatibleService: add ExtractImageFromFiles to load image file bytes into Message.Images, remove consumed file entries, update HasImages/BuildMessageContent to iterate images, and extend image type detection (HEIC/HEIF, AVIF and more extensions). These changes enable passing uploaded images to compatible LLM backends while maintaining backward compatibility and preventing exceptions when models are missing. * paste and drag&drop files/images * Add image attachment support and previews Add client-side support for image attachments: show inline thumbnails for selected images, history image previews, paste handling, dismiss buttons, and update input/send logic to include images alongside files. Introduce _selectedImages and ImageExtensions, update MessageExt to store AttachedImages, and ensure proper disposal of image streams. Add CSS for image-preview and history-image-preview styling. On the service side, route messages that include images through a SearchAsync + context-enhanced chat flow (streaming and non-streaming) and adjust token handling/return values accordingly. * Add image-generation support and UI Introduce image-generation capability across the app: add IImageGenerationModel and HasImageGeneration on AIModel; mark cloud models (DALL·E3, new gpt-image-1 and grok-2-image) as image generators. Update UI to render generated images with download and copy-to-clipboard actions (Home.razor changes, CopyImageToClipboard interop + editor.js). Improve visual/model detection in Utils to use ModelRegistry with a fallback set of known image-generation IDs. Increase SignalR hub max message size to 10MB (Program.cs) to allow larger image transfers and add CSS for generated image layout and controls. * Replace Visual flag with ImageGen and add vision support Rename the old "Visual" concept to a clearer "ImageGen" across the codebase and add vision detection/flags. Key changes: - Domain & storage: Chat.Visual -> Chat.ImageGen, ChatDocument.ImageGen, DTO and DB mappings updated (SQL/SQLite repos). - API: removed EnableVisual(); added WithModel(AIModel model, bool? imageGen = null) to allow explicit imageGen override (defaults to model capability). - Interfaces: removed EnableVisual from builder interfaces. - Services/handlers: ChatService, AgentService, StartCommandHandler, AnswerCommandHandler and step handlers updated to use ImageGen logic when routing to image-gen or LLM services; TTS gating now checks ImageGen. - Mappers: ChatMapper and DTO mappings updated to use ImageGen. - UI: InferPage and NavBar updated to show Image Gen and Vision badges; Home.razor now computes message content/reasoning via MessageExt.ComputedContent/ComputedReasoning; MessageExt gains HasReasoning and computed fields. - Utils: Reason is now computed from registered model capabilities and ImageGen is mutually exclusive with reasoning; added Vision detection and model lists. - Examples & tests: updated to call WithModel(..., imageGen: true) or model-based API accordingly. Why: clarifies semantics between image generation and visual/vision capabilities, centralizes model-driven behavior, and enables explicit overrides for image generation behavior. * Handle base64 images in OpenAiImageGenService Rename ImageGenDalleService to OpenAiImageGenService and update response handling to support OpenAI base64 (b64_json) image payloads. ProcessOpenAiResponse now returns byte[] and will decode b64_json or download from a URL; callers now receive image bytes directly. Add JsonPropertyName for b64_json, reorder/add necessary usings, and remove unused MaIN.Services.Services.LLMService.Utils imports from Gemini, OpenAi and Xai image services. These changes enable handling both base64 and URL image responses and standardize the OpenAI image service name. * Add missing IVisionModel in cloud models * versioning * fix typo
1 parent 0957664 commit a32de60

52 files changed

Lines changed: 1691 additions & 532 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Examples/Examples/Chat/ChatWithImageGenExample.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Examples.Utils;
22
using MaIN.Core.Hub;
3+
using MaIN.Domain.Models.Abstract;
34

45
namespace Examples.Chat;
56

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

1213
var result = await AIHub.Chat()
13-
.EnableVisual()
14+
.WithModel(new GenericLocalModel("FLUX.1_Shnell"), imageGen: true)
1415
.WithMessage("Generate cyberpunk godzilla cat warrior")
1516
.CompleteAsync();
1617

Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Examples.Utils;
22
using MaIN.Core.Hub;
3+
using MaIN.Domain.Configuration;
4+
using MaIN.Domain.Models.Abstract;
35

46
namespace Examples.Chat;
57

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

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

Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ public async Task Start()
1313

1414
var result = await AIHub.Chat()
1515
.WithModel<DallE3>()
16-
.EnableVisual()
1716
.WithMessage("Generate rock style cow playing guitar")
1817
.CompleteAsync();
1918

MaIN.Core.IntegrationTests/ChatTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using MaIN.Core.Hub;
33
using MaIN.Core.IntegrationTests.Helpers;
44
using MaIN.Domain.Entities;
5+
using MaIN.Domain.Models.Abstract;
56
using MaIN.Domain.Models.Concrete;
67

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

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

Releases/0.10.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 0.10.0 release
2+
3+
Improve InferPage:
4+
- Refreshed chat UI layout with improved theming and smarter scroll behavior
5+
- Extended attachments (drag & drop, paste), image previews, and improved image generation
6+
- Added support for unregistered models and vision-based image handling (no OCR)
7+
- Stability fixes, proper cancellation support, and internal service refactoring

src/MaIN.Core.UnitTests/ChatContextTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public async Task CompleteAsync_ShouldCallChatService()
8888
};
8989

9090

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

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

100100
// Assert
101-
_mockChatService.Verify(s => s.Completions(It.IsAny<Chat>(), false, false, null), Times.Once);
101+
_mockChatService.Verify(s => s.Completions(It.IsAny<Chat>(), false, false, null, It.IsAny<CancellationToken>()), Times.Once);
102102
Assert.Equal(chatResult, result);
103103
}
104104

@@ -128,6 +128,6 @@ await _chatContext.WithModel(model)
128128
.CompleteAsync();
129129

130130
// Assert
131-
_mockChatService.Verify(s => s.Completions(It.Is<Chat>(c => c.ModelId == _testModelId && c.ModelInstance == model), It.IsAny<bool>(), It.IsAny<bool>(), null), Times.Once);
131+
_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);
132132
}
133133
}

src/MaIN.Core/.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package>
33
<metadata>
44
<id>MaIN.NET</id>
5-
<version>0.9.4</version>
5+
<version>0.10.0</version>
66
<authors>Wisedev</authors>
77
<owners>Wisedev</owners>
88
<icon>favicon.png</icon>

src/MaIN.Core/Hub/Contexts/ChatContext.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ internal ChatContext(IChatService chatService, Chat existingChat)
4040
_chat = existingChat;
4141
}
4242

43-
public IChatMessageBuilder WithModel(AIModel model)
43+
public IChatMessageBuilder WithModel(AIModel model, bool? imageGen = null)
4444
{
4545
SetModel(model);
46+
_chat.ImageGen = imageGen ?? model is IImageGenerationModel;
4647
return this;
4748
}
4849

@@ -82,12 +83,7 @@ private void SetModel(AIModel model)
8283
_chat.ModelId = model.Id;
8384
_chat.ModelInstance = model;
8485
_chat.Backend = model.Backend;
85-
}
86-
87-
public IChatMessageBuilder EnableVisual()
88-
{
89-
_chat.Visual = true;
90-
return this;
86+
_chat.ImageGen = model.HasImageGeneration;
9187
}
9288

9389
public IChatMessageBuilder EnsureModelDownloaded()
@@ -116,7 +112,7 @@ public IChatConfigurationBuilder WithMemoryParams(MemoryParams memoryParams)
116112

117113
public IChatConfigurationBuilder Speak(TextToSpeechParams speechParams)
118114
{
119-
_chat.Visual = false;
115+
_chat.ImageGen = false;
120116
_chat.TextToSpeechParams = speechParams;
121117
return this;
122118
}
@@ -205,7 +201,8 @@ public IChatConfigurationBuilder DisableCache()
205201
public async Task<ChatResult> CompleteAsync(
206202
bool translate = false, // Move to WithTranslate
207203
bool interactive = false, // Move to WithInteractive
208-
Func<LLMTokenValue?, Task>? changeOfValue = null)
204+
Func<LLMTokenValue?, Task>? changeOfValue = null,
205+
CancellationToken cancellationToken = default)
209206
{
210207
if (_chat.ModelInstance is null)
211208
{
@@ -231,7 +228,7 @@ public async Task<ChatResult> CompleteAsync(
231228
{
232229
await _chatService.Create(_chat);
233230
}
234-
var result = await _chatService.Completions(_chat, translate, interactive, changeOfValue);
231+
var result = await _chatService.Completions(_chat, translate, interactive, changeOfValue, cancellationToken);
235232
_files = [];
236233
return result;
237234
}
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext;
1+
namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext;
22

33
public interface IChatBuilderEntryPoint : IChatActions
44
{
@@ -9,7 +9,7 @@ public interface IChatBuilderEntryPoint : IChatActions
99
/// <param name="model">The name of the AI model to be used.</param>
1010
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
1111
IChatMessageBuilder WithModel(string model);
12-
12+
1313
/// <summary>
1414
/// Configures a custom model with a specific path and project context.
1515
/// </summary>
@@ -18,18 +18,11 @@ public interface IChatBuilderEntryPoint : IChatActions
1818
/// <param name="mmProject">Optional multi-modal project identifier.</param>
1919
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
2020
IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null);
21-
22-
/// <summary>
23-
/// Enables visual/image generation mode. Use this method now if you do not plan to explicitly define the model.
24-
/// Otherwise, you will be able to use this method in the next step, after defining the model.
25-
/// </summary>
26-
/// <returns>The context instance implementing <see cref="IChatMessageBuilder"/> for method chaining.</returns>
27-
IChatMessageBuilder EnableVisual();
28-
21+
2922
/// <summary>
3023
/// Loads an existing chat session from the database using its unique identifier.
3124
/// </summary>
3225
/// <param name="chatId">The GUID of the existing chat.</param>
3326
/// <returns>The context instance implementing <see cref="IChatConfigurationBuilder"/> for method chaining.</returns>
3427
Task<IChatConfigurationBuilder> FromExisting(string chatId);
35-
}
28+
}

src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,5 @@ public interface IChatConfigurationBuilder : IChatActions
104104
/// <param name="interactive">A flag indicating whether the chat session should be interactive. Default is false.</param>
105105
/// <param name="changeOfValue">An optional callback invoked whenever a new token or update is received during streaming.</param>
106106
/// <returns>A <see cref="ChatResult"/> object containing the result of the completed chat session.</returns>
107-
Task<ChatResult> CompleteAsync(bool translate = false, bool interactive = false, Func<LLMTokenValue?, Task>? changeOfValue = null);
107+
Task<ChatResult> CompleteAsync(bool translate = false, bool interactive = false, Func<LLMTokenValue?, Task>? changeOfValue = null, CancellationToken cancellationToken = default);
108108
}

0 commit comments

Comments
 (0)