diff --git a/app/controllers/ocr_controller.rb b/app/controllers/ocr_controller.rb index cd4ac44a..97a84760 100644 --- a/app/controllers/ocr_controller.rb +++ b/app/controllers/ocr_controller.rb @@ -33,6 +33,10 @@ def scan ocrresult.image.attach(file) ocrresult.save + if params[:files].length > 1 + params[:files][1..].each { |f| ocrresult.extra_images.attach(f) } + end + logger.debug "OCR data id stored in flash: #{ocrresult.id}" # If multiple recipes detected, redirect to selection page diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb index b8b171ac..a638ea07 100644 --- a/app/controllers/recipes_controller.rb +++ b/app/controllers/recipes_controller.rb @@ -61,6 +61,7 @@ def create ocrresult = OcrResult.find_by(id: params[:ocrresult_id]) if ocrresult.present? @recipe.recipe_images.attach(ocrresult.image.blob) if ocrresult.image.attached? + ocrresult.extra_images.each { |img| @recipe.recipe_images.attach(img.blob) } end end diff --git a/app/javascript/controllers/dropzone_controller.js b/app/javascript/controllers/dropzone_controller.js index 5c6063e7..0e552029 100644 --- a/app/javascript/controllers/dropzone_controller.js +++ b/app/javascript/controllers/dropzone_controller.js @@ -2,13 +2,20 @@ import { Controller } from "@hotwired/stimulus" import { Utils } from "../src/utils" export default class extends Controller { - static targets = ["dropzone", "fileInput", "spinner", "successMessage", "errorMessage", "resetButton"] - static values = { url: String } + static targets = ["dropzone", "fileInput", "spinner", "successMessage", "errorMessage", "resetButton", "previewContainer", "uploadButton"] + static values = { url: String, previewMode: Boolean } connect() { this.maxFiles = 10 this.acceptedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/heic', 'image/heif'] this.acceptedExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif'] + this.pendingFiles = [] + this.pendingObjectUrls = [] + + // Initialize button state in preview mode + if (this.previewModeValue && this.hasUploadButtonTarget) { + this.uploadButtonTarget.disabled = true + } } // Drag and drop handlers @@ -58,8 +65,9 @@ export default class extends Controller { // Hide any previous messages this.hideMessages() - // Validate file count - if (files.length > this.maxFiles) { + // Validate file count (account for already-queued files in preview mode) + const totalCount = this.previewModeValue ? this.pendingFiles.length + files.length : files.length + if (totalCount > this.maxFiles) { this.showError(`Maximum ${this.maxFiles} files allowed. You selected ${files.length} files.`) return } @@ -81,8 +89,84 @@ export default class extends Controller { return } - // All validations passed, upload files - this.uploadFiles(files) + if (this.previewModeValue) { + for (let i = 0; i < files.length; i++) { + this.pendingFiles.push(files[i]) + } + this.renderPreviews() + this.uploadButtonTarget.disabled = false + } else { + // All validations passed, upload files + this.uploadFiles(files) + } + } + + renderPreviews() { + // Revoke old object URLs before rebuilding + this.pendingObjectUrls.forEach(url => URL.revokeObjectURL(url)) + this.pendingObjectUrls = [] + + if (this.pendingFiles.length === 0) { + this.previewContainerTarget.classList.add('hidden') + this.previewContainerTarget.innerHTML = '' + return + } + + this.previewContainerTarget.classList.remove('hidden') + + const grid = document.createElement('div') + grid.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 mt-4' + + this.pendingFiles.forEach((file, index) => { + const objectUrl = URL.createObjectURL(file) + this.pendingObjectUrls.push(objectUrl) + + const card = document.createElement('div') + card.className = 'relative rounded-lg overflow-hidden border border-gray-200 bg-gray-50' + card.innerHTML = ` + ${this.escapeHtml(file.name)} +
${this.escapeHtml(file.name)}
+ + ${index === 0 ? '
AI
' : ''} + ` + grid.appendChild(card) + }) + + this.previewContainerTarget.innerHTML = '' + this.previewContainerTarget.appendChild(grid) + } + + removeFile(event) { + const index = parseInt(event.currentTarget.dataset.index, 10) + this.pendingFiles.splice(index, 1) + + if (this.pendingFiles.length === 0) { + this.uploadButtonTarget.disabled = true + this.pendingObjectUrls.forEach(url => URL.revokeObjectURL(url)) + this.pendingObjectUrls = [] + this.previewContainerTarget.classList.add('hidden') + this.previewContainerTarget.innerHTML = '' + } else { + this.renderPreviews() + } + } + + submit() { + const filesToUpload = this.pendingFiles.slice() + this.pendingFiles = [] + this.uploadButtonTarget.disabled = true + this.previewContainerTarget.classList.add('hidden') + this.uploadFiles(filesToUpload) + } + + escapeHtml(str) { + return str.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) } uploadFiles(files) { @@ -154,6 +238,18 @@ export default class extends Controller { // Clear file input this.fileInputTarget.value = '' + // Clear any pending preview state + this.pendingFiles = [] + this.pendingObjectUrls.forEach(url => URL.revokeObjectURL(url)) + this.pendingObjectUrls = [] + if (this.hasPreviewContainerTarget) { + this.previewContainerTarget.classList.add('hidden') + this.previewContainerTarget.innerHTML = '' + } + if (this.hasUploadButtonTarget) { + this.uploadButtonTarget.classList.add('hidden') + } + // Re-enable dropzone this.enableDropzone() } diff --git a/app/models/ocr_result.rb b/app/models/ocr_result.rb index 7429eb22..e9384a6b 100644 --- a/app/models/ocr_result.rb +++ b/app/models/ocr_result.rb @@ -1,3 +1,4 @@ class OcrResult < ApplicationRecord has_one_attached :image + has_many_attached :extra_images end diff --git a/app/views/ocr/select_image_for_reparse.html.erb b/app/views/ocr/select_image_for_reparse.html.erb index 4d35453c..89222f34 100644 --- a/app/views/ocr/select_image_for_reparse.html.erb +++ b/app/views/ocr/select_image_for_reparse.html.erb @@ -10,18 +10,23 @@ <%= t('recipes.select_image_for_reparse.description') %>

-
- - -
+
+ + <%= t 'ocr.ai_method.label' %>: + <%= t 'ocr.ai_method.mistral_openai' %> + + +
+ +
+
<% @recipe.recipe_images.each_with_index do |image, index| %> diff --git a/app/views/recipes/new_magic.html.erb b/app/views/recipes/new_magic.html.erb index 32799bf0..0c4395df 100644 --- a/app/views/recipes/new_magic.html.erb +++ b/app/views/recipes/new_magic.html.erb @@ -2,16 +2,21 @@ <%=t 'recipes.new_magic' %>
-
- - -
+
+ + <%= t 'ocr.ai_method.label' %>: + <%= t 'ocr.ai_method.mistral_openai' %> + + +
+ +
+
-<%= render 'shared/dropzone', url: scan_ocr_index_path %> +<%= render 'shared/dropzone', url: scan_ocr_index_path, preview_mode: true %> diff --git a/app/views/shared/_dropzone.html.erb b/app/views/shared/_dropzone.html.erb index c51e70f2..9a59b7d6 100644 --- a/app/views/shared/_dropzone.html.erb +++ b/app/views/shared/_dropzone.html.erb @@ -7,12 +7,14 @@ formats_key ||= 'ocr.dropzone.formats' uploading_key ||= 'ocr.dropzone.uploading' reset_button_key ||= 'ocr.dropzone.reset_button' + process_button_key ||= 'ocr.dropzone.process_button' accept ||= 'image/*,.heic,.heif' multiple ||= true wrapper_classes ||= 'mb-6 mt-4' + preview_mode ||= false %> -
+
+ + + + + <% if preview_mode %> +
+ +
+ <% end %> +