Skip to content

feat: add Ollama integration, matching models, and modular UI files - #2

Merged
arhadnane merged 1 commit into
masterfrom
develop
Feb 21, 2026
Merged

feat: add Ollama integration, matching models, and modular UI files#2
arhadnane merged 1 commit into
masterfrom
develop

Conversation

@arhadnane

Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings February 21, 2026 21:38
@arhadnane
arhadnane merged commit e7e805d into master Feb 21, 2026
4 of 5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds significant new functionality including Ollama AI integration for diagnostic assistance and image analysis, refactors the UI into modular partial classes, introduces an extensible IMatchingModel interface for matching algorithms, and optimizes the translation service with O(1) dictionary lookups.

Changes:

  • Adds Ollama AI integration with diagnostic assessment, medication suggestions, general image analysis, and blood microscope analysis
  • Refactors MainForm into 10 partial class files for better maintainability
  • Implements IMatchingModel interface with Jaccard, Cosine, and NaiveBayes models
  • Optimizes TranslationService with dictionary-based O(1) lookups instead of O(n) linear searches
  • Adds comprehensive test coverage for synonym service and matching models
  • Adds 7 new specification documents covering architecture, UX/UI, security, data, decision rules, non-functional requirements, and future extensions

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Services/OllamaService.cs New service for AI integration with local Ollama instance via HTTP
Services/IMatchingModel.cs Interface defining matching algorithm contract
Services/JaccardModel.cs, CosineModel.cs, NaiveBayesModel.cs Extracted matching algorithms implementing IMatchingModel
Services/TranslationService.cs Optimized with dictionary-based lookups for O(1) performance
UI/MainForm.*.cs 10 partial class files decomposing 2500-line MainForm
tests/SymptomChecker.Tests/*.cs New test coverage for synonyms and matching models
specs/*.md 7 comprehensive specification documents

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +228 to +234
_currentImageBase64 = Convert.ToBase64String(imageBytes);

using var ms = new MemoryStream(imageBytes);
var oldImage = _imgPreview.Image;
_imgPreview.Image = Image.FromStream(ms);
oldImage?.Dispose();

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image upload allows files up to 20MB, but there's no validation of the actual image content. A malicious file could be uploaded with a valid image extension but invalid content, potentially causing issues when the image is loaded or sent to Ollama. Consider adding image format validation after loading the bytes.

Suggested change
_currentImageBase64 = Convert.ToBase64String(imageBytes);
using var ms = new MemoryStream(imageBytes);
var oldImage = _imgPreview.Image;
_imgPreview.Image = Image.FromStream(ms);
oldImage?.Dispose();
try
{
using var ms = new MemoryStream(imageBytes);
using var loadedImage = Image.FromStream(ms);
// Basic validation: ensure non-zero dimensions
if (loadedImage.Width <= 0 || loadedImage.Height <= 0)
{
MessageBox.Show(this,
"The selected file is not a valid image or is corrupted.",
"Invalid Image",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
var oldImage = _imgPreview.Image;
// Clone the validated image so the preview holds its own instance
_imgPreview.Image = (Image)loadedImage.Clone();
oldImage?.Dispose();
}
catch (Exception)
{
MessageBox.Show(this,
"The selected file is not a valid image or is corrupted.",
"Invalid Image",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
// Only store Base64 after successful image validation
_currentImageBase64 = Convert.ToBase64String(imageBytes);

Copilot uses AI. Check for mistakes.
Comment thread Services/OllamaService.cs
Comment on lines +37 to +44
_baseUrl = baseUrl.TrimEnd('/');
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}

public void SetModel(string model) => _model = model;
public void SetBaseUrl(string url) => _baseUrl = url.TrimEnd('/');

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SetModel and SetBaseUrl methods don't validate their inputs. SetBaseUrl should validate URL format, and SetModel should check for non-empty strings. Invalid values will only cause failures during API calls.

Suggested change
_baseUrl = baseUrl.TrimEnd('/');
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}
public void SetModel(string model) => _model = model;
public void SetBaseUrl(string url) => _baseUrl = url.TrimEnd('/');
SetBaseUrl(baseUrl);
SetModel(model);
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}
public void SetModel(string model)
{
if (string.IsNullOrWhiteSpace(model))
{
throw new ArgumentException("Model name must be a non-empty string.", nameof(model));
}
_model = model;
}
public void SetBaseUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentException("Base URL must be a non-empty string.", nameof(url));
}
if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult) ||
(uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
{
throw new ArgumentException("Base URL must be a valid absolute HTTP or HTTPS URL.", nameof(url));
}
_baseUrl = uriResult.ToString().TrimEnd('/');
}

Copilot uses AI. Check for mistakes.
Comment thread Services/OllamaService.cs
{
_baseUrl = baseUrl.TrimEnd('/');
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HttpClient timeout is set to 120 seconds, but AI operations (especially vision models) can take longer. Consider making the timeout configurable per operation type, with higher limits for image analysis operations which can take 30-120 seconds according to the UI comments.

Copilot uses AI. Check for mistakes.
Comment thread Services/OllamaService.cs
Comment on lines +35 to +43
public OllamaService(string baseUrl = "http://localhost:11434", string model = "llama3")
{
_baseUrl = baseUrl.TrimEnd('/');
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}

public void SetModel(string model) => _model = model;
public void SetBaseUrl(string url) => _baseUrl = url.TrimEnd('/');

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HttpClient is created in the constructor without checking if the baseUrl is valid. An invalid URL will only fail later during requests. Consider validating the URL format in the constructor or SetBaseUrl method to fail fast.

Suggested change
public OllamaService(string baseUrl = "http://localhost:11434", string model = "llama3")
{
_baseUrl = baseUrl.TrimEnd('/');
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}
public void SetModel(string model) => _model = model;
public void SetBaseUrl(string url) => _baseUrl = url.TrimEnd('/');
/// <summary>
/// Validate and normalize a base URL string.
/// Ensures the URL is an absolute HTTP/HTTPS URI and trims trailing slashes.
/// </summary>
/// <param name="url">The base URL to validate.</param>
/// <param name="paramName">The parameter name for exception messages.</param>
/// <returns>A normalized base URL string.</returns>
/// <exception cref="ArgumentException">Thrown when the URL is invalid.</exception>
private static string NormalizeAndValidateBaseUrl(string url, string paramName)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentException("Base URL cannot be null, empty, or whitespace.", paramName);
}
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
throw new ArgumentException("Base URL must be a valid absolute HTTP or HTTPS URL.", paramName);
}
return uri.ToString().TrimEnd('/');
}
public OllamaService(string baseUrl = "http://localhost:11434", string model = "llama3")
{
_baseUrl = NormalizeAndValidateBaseUrl(baseUrl, nameof(baseUrl));
_model = model;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
}
public void SetModel(string model) => _model = model;
public void SetBaseUrl(string url) => _baseUrl = NormalizeAndValidateBaseUrl(url, nameof(url));

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 82
public TranslationService(string path)
{
if (!File.Exists(path))
{
_db = new TranslationDatabase();
return;
}
var json = File.ReadAllText(path);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
_db = JsonSerializer.Deserialize<TranslationDatabase>(json, options) ?? new TranslationDatabase();
else
{
var json = File.ReadAllText(path);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
_db = JsonSerializer.Deserialize<TranslationDatabase>(json, options) ?? new TranslationDatabase();
}

// Build O(1) dictionaries
var cmp = StringComparer.OrdinalIgnoreCase;
_uiMap = new Dictionary<string, UiString>(cmp);
foreach (var u in _db.Ui)
if (!string.IsNullOrEmpty(u.Key) && !_uiMap.ContainsKey(u.Key))
_uiMap[u.Key] = u;

_msgMap = new Dictionary<string, MessageString>(cmp);
foreach (var m in _db.Messages)
if (!string.IsNullOrEmpty(m.Key) && !_msgMap.ContainsKey(m.Key))
_msgMap[m.Key] = m;

_detailsMap = new Dictionary<string, UiDetailsString>(cmp);
foreach (var d in _db.Ui_Details)
if (!string.IsNullOrEmpty(d.Key) && !_detailsMap.ContainsKey(d.Key))
_detailsMap[d.Key] = d;

_symptomMap = new Dictionary<string, SymptomString>(cmp);
foreach (var s in _db.Symptoms)
if (!string.IsNullOrEmpty(s.Key) && !_symptomMap.ContainsKey(s.Key))
_symptomMap[s.Key] = s;

_conditionMap = new Dictionary<string, ConditionString>(cmp);
foreach (var c in _db.Conditions)
if (!string.IsNullOrEmpty(c.Key) && !_conditionMap.ContainsKey(c.Key))
_conditionMap[c.Key] = c;

_categoryMap = new Dictionary<string, CategoryString>(cmp);
foreach (var c in _db.Categories)
if (!string.IsNullOrEmpty(c.Key) && !_categoryMap.ContainsKey(c.Key))
_categoryMap[c.Key] = c;
}

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor initializes several dictionaries but doesn't handle the case where _db is null (when file doesn't exist). This will cause a NullReferenceException when trying to iterate over _db.Ui, _db.Messages, etc. The else block at line 44 should be removed and the file existence check should throw an exception or the dictionaries should be initialized with empty values.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 42
if (!File.Exists(path))
{
_db = new TranslationDatabase();

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code at line 42 initializes an empty TranslationDatabase if the file doesn't exist, but then continues to try to build dictionaries from the empty database. However, the empty database will have null collections for Ui, Messages, etc., causing NullReferenceExceptions when iterating. The constructor should either initialize these collections in the empty database or skip dictionary building.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants