diff --git a/app/controllers/templates_detect_fields_controller.rb b/app/controllers/templates_detect_fields_controller.rb
index 8355dcb2..337e8008 100644
--- a/app/controllers/templates_detect_fields_controller.rb
+++ b/app/controllers/templates_detect_fields_controller.rb
@@ -11,11 +11,14 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob)
+ documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
+
+ page_number = params[:page].present? ? params[:page].to_i : nil
documents.each do |document|
io = StringIO.new(document.download)
- Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
+ Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: })
end
end
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index 34340fda..bb43cdfb 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -373,6 +373,7 @@
:draw-field-type="drawFieldType"
:editable="editable"
:base-url="baseUrl"
+ :with-fields-detection="withFieldsDetection"
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
@drop-field="onDropfield"
@remove-area="removeArea"
@@ -381,6 +382,7 @@
@copy-selected-areas="copySelectedAreas"
@delete-selected-areas="deleteSelectedAreas"
@align-selected-areas="alignSelectedAreas"
+ @autodetect-fields="detectFieldsForPage"
/>
+
+
+
+
+
+ {{ (detectingFieldsAddedCount === 1 ? t('field_added') : t('fields_added')).replace('{count}', detectingFieldsAddedCount) }}
+
+
+
+
+ {{ Math.round(detectingAnalyzingProgress * 100) }}% {{ t('analyzing_') }}
+
+
+ {{ t('processing_') }}
+
+
+
+
+
+
0
+
+ const calculateIoU = (area1, area2) => {
+ const x1 = Math.max(area1.x, area2.x)
+ const y1 = Math.max(area1.y, area2.y)
+ const x2 = Math.min(area1.x + area1.w, area2.x + area2.w)
+ const y2 = Math.min(area1.y + area1.h, area2.y + area2.h)
+
+ const intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1)
+ const area1Size = area1.w * area1.h
+ const area2Size = area2.w * area2.h
+ const unionArea = area1Size + area2Size - intersectionArea
+
+ return unionArea > 0 ? intersectionArea / unionArea : 0
+ }
+
+ const hasOverlappingField = (newArea) => {
+ const pageAreas = this.fieldAreasIndex[newArea.attachment_uuid]?.[newArea.page] || []
+
+ return pageAreas.some(({ area: existingArea }) => {
+ return calculateIoU(existingArea, newArea) >= 0.1
+ })
+ }
+
+ const filterNonOverlappingFields = (detectedFields) => {
+ return detectedFields.filter((field) => {
+ return (field.areas || []).every((area) => !hasOverlappingField(area))
+ })
+ }
+
+ this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ attachment_uuid: attachmentUuid, page })
+ }).then(async (response) => {
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder('utf-8')
+ let buffer = ''
+ const fields = []
+
+ while (true) {
+ const { value, done } = await reader.read()
+
+ buffer += decoder.decode(value, { stream: true })
+
+ const lines = buffer.split('\n\n')
+
+ buffer = lines.pop()
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const jsonStr = line.replace(/^data: /, '')
+ const data = JSON.parse(jsonStr)
+
+ if (data.error) {
+ const errorFields = filterNonOverlappingFields(data.fields || fields)
+
+ if (errorFields.length) {
+ errorFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ this.insertField(f)
+ })
+
+ totalFieldsAdded += errorFields.length
+
+ this.save()
+ } else if (!(data.fields || fields).length) {
+ alert(data.error)
+ }
+
+ break
+ } else if (data.analyzing) {
+ this.detectingAnalyzingProgress = data.progress
+ } else if (data.completed) {
+ if (data.submitters) {
+ if (!hadFieldsBeforeDetection) {
+ this.template.submitters = data.submitters
+ this.selectedSubmitter = this.template.submitters[0]
+
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ } else {
+ const existingSubmitters = this.template.submitters
+ const submitterUuidMap = {}
+
+ data.submitters.forEach((newSubmitter) => {
+ const existingMatch = existingSubmitters.find(
+ (s) => s.name.toLowerCase() === newSubmitter.name.toLowerCase()
+ )
+
+ if (existingMatch) {
+ submitterUuidMap[newSubmitter.uuid] = existingMatch.uuid
+ } else {
+ submitterUuidMap[newSubmitter.uuid] = newSubmitter.uuid
+
+ if (!existingSubmitters.find((s) => s.uuid === newSubmitter.uuid)) {
+ this.template.submitters.push(newSubmitter)
+ }
+ }
+ })
+
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (f.submitter_uuid && submitterUuidMap[f.submitter_uuid]) {
+ f.submitter_uuid = submitterUuidMap[f.submitter_uuid]
+ } else if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ }
+ } else {
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ }
+
+ break
+ } else if (data.fields) {
+ data.fields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ fields.push(...data.fields)
+ }
+ }
+ }
+
+ if (done) break
+ }
+ }).catch(error => {
+ console.error('Error in streaming message: ', error)
+ }).finally(() => {
+ this.isDetectingPageFields = false
+ this.detectingAnalyzingProgress = null
+ this.detectingFieldsAddedCount = totalFieldsAdded
+
+ setTimeout(() => {
+ this.detectingFieldsAddedCount = null
+ }, 1000)
+ })
+ },
save ({ force } = { force: false }) {
this.pendingFieldAttachmentUuids = []
diff --git a/app/javascript/template_builder/context_menu.vue b/app/javascript/template_builder/context_menu.vue
index 9de39eb9..4ad6972b 100644
--- a/app/javascript/template_builder/context_menu.vue
+++ b/app/javascript/template_builder/context_menu.vue
@@ -185,6 +185,18 @@
Tab
+
+