Conversation
There was a problem hiding this comment.
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.
| _currentImageBase64 = Convert.ToBase64String(imageBytes); | ||
|
|
||
| using var ms = new MemoryStream(imageBytes); | ||
| var oldImage = _imgPreview.Image; | ||
| _imgPreview.Image = Image.FromStream(ms); | ||
| oldImage?.Dispose(); | ||
|
|
There was a problem hiding this comment.
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.
| _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); |
| _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('/'); | ||
|
|
There was a problem hiding this comment.
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.
| _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('/'); | |
| } |
| { | ||
| _baseUrl = baseUrl.TrimEnd('/'); | ||
| _model = model; | ||
| _http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) }; |
There was a problem hiding this comment.
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.
| 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('/'); |
There was a problem hiding this comment.
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.
| 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)); |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| if (!File.Exists(path)) | ||
| { | ||
| _db = new TranslationDatabase(); |
There was a problem hiding this comment.
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.
No description provided.