|
66 | 66 | else |
67 | 67 | { |
68 | 68 | <FluentCard class="@(conversation.Message.Role == "User" ? "message-card user-message" : "message-card bot-message")"> |
69 | | - @if (conversation.Message.Role == "User" && conversation.AttachedFiles.Any()) |
| 69 | + @if (conversation.Message.Role == "User" && (conversation.AttachedFiles.Any() || conversation.AttachedImages.Any())) |
70 | 70 | { |
71 | 71 | <div class="attached-files-display"> |
| 72 | + @foreach (var image in conversation.AttachedImages) |
| 73 | + { |
| 74 | + <div class="history-image-preview" title="@image.Name"> |
| 75 | + <img src="data:image/png;base64,@image.Base64" alt="@image.Name" /> |
| 76 | + </div> |
| 77 | + } |
72 | 78 | @foreach (var fileName in conversation.AttachedFiles) |
73 | 79 | { |
74 | 80 | <span class="attached-file-tag"> |
|
140 | 146 | <div id="bottom" @ref="_bottomElement"></div> |
141 | 147 | </div> |
142 | 148 | <div class="chat-input-section"> |
143 | | - @if (_selectedFiles.Any()) |
| 149 | + @if (_selectedImages.Any() || _selectedFiles.Any()) |
144 | 150 | { |
145 | | - <div class="selected-files-container"> |
| 151 | + <div class="selected-attachments-container"> |
| 152 | + @foreach (var image in _selectedImages) |
| 153 | + { |
| 154 | + <div class="image-preview" title="@image.File.Name"> |
| 155 | + <img src="data:image/@image.File.Extension.TrimStart('.');base64,@image.Base64Preview" alt="@image.File.Name" /> |
| 156 | + <span class="image-dismiss-button" @onclick="@(() => RemoveImage(image))"> |
| 157 | + <FluentIcon Value="@(new Icons.Regular.Size12.Dismiss())" Style="fill: var(--accent-base-color);" /> |
| 158 | + </span> |
| 159 | + </div> |
| 160 | + } |
146 | 161 | @foreach (var file in _selectedFiles) |
147 | 162 | { |
148 | 163 | <div class="file-badge" title="@file.Name"> |
|
202 | 217 | private string? _errorMessage; |
203 | 218 | private string? _incomingMessage; |
204 | 219 | private string? _incomingReasoning; |
205 | | - private readonly string? _displayName = Utils.Model; |
206 | 220 | private IChatMessageBuilder? ctx; |
207 | 221 | private CancellationTokenSource? _cancellationTokenSource; |
208 | 222 | private Chat Chat { get; } = new() { Name = "MaIN Infer", ModelId = Utils.Model! }; |
|
211 | 225 | private ElementReference _editorRef; |
212 | 226 | private DotNetObjectReference<Home>? _dotNetRef; |
213 | 227 | private List<FileInfo> _selectedFiles = new(); |
| 228 | + private List<(FileInfo File, string Base64Preview)> _selectedImages = new(); |
214 | 229 | private int _inputKey; |
215 | 230 |
|
| 231 | + private static readonly HashSet<string> ImageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]; |
| 232 | + |
216 | 233 | private readonly MarkdownPipeline _markdownPipeline = new MarkdownPipelineBuilder() |
217 | 234 | .UseAdvancedExtensions() |
218 | 235 | .UseSoftlineBreakAsHardlineBreak() |
|
291 | 308 | if (_isLoading) return; |
292 | 309 |
|
293 | 310 | var msg = await JS.InvokeAsync<string>("editorManager.getInnerText", _editorRef); |
294 | | - if (!string.IsNullOrWhiteSpace(msg) || _selectedFiles.Any()) |
| 311 | + if (!string.IsNullOrWhiteSpace(msg) || _selectedFiles.Any() || _selectedImages.Any()) |
295 | 312 | { |
296 | 313 | await JS.InvokeVoidAsync("editorManager.clearContent", _editorRef); |
297 | 314 | await SendAsync(msg?.Trim() ?? string.Empty); |
|
307 | 324 | { |
308 | 325 | await file.OpenReadStream(20 * 1024 * 1024).CopyToAsync(ms); |
309 | 326 | ms.Position = 0; |
310 | | - _selectedFiles.Add(new FileInfo |
| 327 | + |
| 328 | + var extension = Path.GetExtension(file.Name).ToLowerInvariant(); |
| 329 | + var fileInfo = new FileInfo |
311 | 330 | { |
312 | 331 | Name = file.Name, |
313 | | - Extension = Path.GetExtension(file.Name), |
| 332 | + Extension = extension, |
314 | 333 | StreamContent = ms |
315 | | - }); |
| 334 | + }; |
| 335 | + |
| 336 | + if (ImageExtensions.Contains(extension)) |
| 337 | + { |
| 338 | + var base64 = Convert.ToBase64String(ms.ToArray()); |
| 339 | + ms.Position = 0; |
| 340 | + _selectedImages.Add((fileInfo, base64)); |
| 341 | + } |
| 342 | + else |
| 343 | + { |
| 344 | + _selectedFiles.Add(fileInfo); |
| 345 | + } |
316 | 346 | } |
317 | 347 | catch (Exception ex) |
318 | 348 | { |
|
329 | 359 | _selectedFiles.Remove(file); |
330 | 360 | } |
331 | 361 |
|
| 362 | + private void RemoveImage((FileInfo File, string Base64Preview) image) |
| 363 | + { |
| 364 | + image.File.StreamContent?.Dispose(); |
| 365 | + _selectedImages.Remove(image); |
| 366 | + } |
| 367 | + |
332 | 368 | [JSInvokable] |
333 | | - public async Task OnFilePasted(string fileName, string extension, string base64Data) |
| 369 | + public async Task OnFileReceived(string fileName, string extension, string base64Data) |
334 | 370 | { |
335 | | - var bytes = Convert.FromBase64String(base64Data); |
336 | | - var ms = new MemoryStream(bytes); |
337 | | - _selectedFiles.Add(new FileInfo |
| 371 | + try |
338 | 372 | { |
339 | | - Name = fileName, |
340 | | - Extension = extension, |
341 | | - StreamContent = ms |
342 | | - }); |
| 373 | + var bytes = Convert.FromBase64String(base64Data); |
| 374 | + var ms = new MemoryStream(bytes); |
| 375 | + var fileInfo = new FileInfo |
| 376 | + { |
| 377 | + Name = fileName, |
| 378 | + Extension = extension, |
| 379 | + StreamContent = ms |
| 380 | + }; |
| 381 | + |
| 382 | + if (ImageExtensions.Contains(extension.ToLowerInvariant())) |
| 383 | + { |
| 384 | + _selectedImages.Add((fileInfo, base64Data)); |
| 385 | + } |
| 386 | + else |
| 387 | + { |
| 388 | + _selectedFiles.Add(fileInfo); |
| 389 | + } |
| 390 | + } |
| 391 | + catch (Exception ex) |
| 392 | + { |
| 393 | + _errorMessage = $"Failed to load file {fileName}: {ex.Message}"; |
| 394 | + } |
| 395 | + |
343 | 396 | _isDragging = false; |
344 | 397 | await InvokeAsync(StateHasChanged); |
345 | 398 | } |
|
365 | 418 |
|
366 | 419 | private async Task SendAsync(string msg) |
367 | 420 | { |
368 | | - if (string.IsNullOrWhiteSpace(msg) && !_selectedFiles.Any()) |
| 421 | + if (string.IsNullOrWhiteSpace(msg) && !_selectedFiles.Any() && !_selectedImages.Any()) |
369 | 422 | { |
370 | 423 | return; |
371 | 424 | } |
|
377 | 430 | _isLoading = true; |
378 | 431 | StateHasChanged(); |
379 | 432 |
|
380 | | - var attachments = new List<FileInfo>(_selectedFiles); |
| 433 | + var attachedFiles = new List<FileInfo>(_selectedFiles); |
| 434 | + var attachedImages = new List<(FileInfo File, string Base64Preview)>(_selectedImages); |
381 | 435 | try |
382 | 436 | { |
383 | 437 | _selectedFiles.Clear(); |
| 438 | + _selectedImages.Clear(); |
384 | 439 | _inputKey++; |
385 | 440 | StateHasChanged(); |
386 | 441 |
|
|
392 | 447 | }; |
393 | 448 | Chat.Messages.Add(newMsg); |
394 | 449 |
|
395 | | - var attachedFileNames = attachments.Select(f => f.Name).ToList(); |
396 | 450 | Messages.Add(new MessageExt |
397 | 451 | { |
398 | 452 | Message = newMsg, |
399 | | - AttachedFiles = attachedFileNames |
| 453 | + AttachedFiles = attachedFiles.Select(f => f.Name).ToList(), |
| 454 | + AttachedImages = attachedImages.Select(i => (i.File.Name, i.Base64Preview)).ToList() |
400 | 455 | }); |
401 | 456 |
|
402 | 457 | Chat.ModelId = Utils.Model!; |
|
410 | 465 | await JS.InvokeVoidAsync("scrollManager.scrollToBottomSmooth", "messages-container"); |
411 | 466 |
|
412 | 467 | var request = ctx!.WithMessage(msg); |
413 | | - if (attachments.Count != 0) |
| 468 | + |
| 469 | + // Combine files and images - images go as files, ExtractImageFromFiles will handle them |
| 470 | + var allFiles = new List<FileInfo>(attachedFiles); |
| 471 | + allFiles.AddRange(attachedImages.Select(i => i.File)); |
| 472 | + |
| 473 | + if (allFiles.Count != 0) |
414 | 474 | { |
415 | | - request.WithFiles(attachments); |
| 475 | + request.WithFiles(allFiles); |
416 | 476 | } |
417 | 477 |
|
418 | 478 | cancellationToken.ThrowIfCancellationRequested(); |
|
502 | 562 | } |
503 | 563 | finally |
504 | 564 | { |
505 | | - foreach (var attachment in attachments) |
| 565 | + foreach (var attachment in attachedFiles) |
506 | 566 | attachment.StreamContent?.Dispose(); |
507 | | - attachments.Clear(); |
| 567 | + attachedFiles.Clear(); |
| 568 | + |
| 569 | + foreach (var image in attachedImages) |
| 570 | + image.File.StreamContent?.Dispose(); |
| 571 | + attachedImages.Clear(); |
508 | 572 |
|
509 | 573 | _isLoading = false; |
510 | 574 | _isThinking = false; |
|
548 | 612 | .Where(m => m.AttachedFiles.Any()) |
549 | 613 | .ToDictionary(m => m.Message, m => m.AttachedFiles); |
550 | 614 |
|
| 615 | + var existingImagesMap = Messages |
| 616 | + .Where(m => m.AttachedImages.Any()) |
| 617 | + .ToDictionary(m => m.Message, m => m.AttachedImages); |
| 618 | + |
| 619 | + var existingReasonMap = Messages |
| 620 | + .ToDictionary(m => m.Message, m => m.ShowReason); |
| 621 | + |
551 | 622 | Messages = Chat.Messages.Select(x => new MessageExt |
552 | 623 | { |
553 | 624 | Message = x, |
554 | 625 | AttachedFiles = existingFilesMap.TryGetValue(x, out var files) ? files : new List<string>(), |
555 | | - ShowReason = false |
| 626 | + AttachedImages = existingImagesMap.TryGetValue(x, out var images) ? images : new List<(string, string)>(), |
| 627 | + ShowReason = existingReasonMap.TryGetValue(x, out var show) && show |
556 | 628 | }).ToList(); |
557 | 629 | } |
558 | 630 |
|
|
0 commit comments