src/lib/form/form-detector.ts

Total Symbols
5
Lines of Code
198
Avg Complexity
5.0
Avg Coverage
100.0%

File Relationships

graph LR detectFormFields["detectFormFields"] detectAllFields["detectAllFields"] deduplicateFields["deduplicateFields"] detectAllFieldsAsync["detectAllFieldsAsync"] detectFormFields -->|calls| detectAllFields detectAllFields -->|calls| deduplicateFields detectAllFieldsAsync -->|calls| deduplicateFields click detectFormFields "../symbols/f533b30bd49ac06c.html" click detectAllFields "../symbols/394718c982f136bb.html" click deduplicateFields "../symbols/10d773974adfc3cb.html" click detectAllFieldsAsync "../symbols/1b422b3353cdbe22.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'detectAllFieldsAsync' has cyclomatic complexity 18 (max 10)

Symbols by Kind

function 4
interface 1

All Symbols

Name Kind Visibility Status Lines Signature
detectFormFields function exported- 47-49 detectFormFields(): : FormField[]
DetectionResult interface exported- 51-53 interface DetectionResult
deduplicateFields function - 60-78 deduplicateFields( nativeFields: FormField[], customFields: FormField[], ): : FormField[]
detectAllFields function exported- 84-90 detectAllFields(): : DetectionResult
detectAllFieldsAsync function exported- 96-175 detectAllFieldsAsync(): : Promise<DetectionResult>

Full Source

/**
 * Form Detector
 *
 * Finds all fillable form fields on the page using a multi-strategy pipeline:
 *   1. Native inputs (input/select/textarea)   — nativeInputDetector
 *   2. Custom select components (Ant Design, MUI, React Select, etc.) — customSelectPageDetector
 *   3. Interactive widgets (date pickers, sliders, toggles, etc.) — interactivePageDetector
 *
 * All scanners are composed into DEFAULT_COLLECTION_PIPELINE.
 * Field-level classification goes through DEFAULT_PIPELINE (inside nativeInputDetector).
 *
 * Two exported variants:
 *   detectAllFields()       — SYNC, no AI (used by dom-watcher)
 *   detectAllFieldsAsync()  — ASYNC, full AI + learning pipeline
 */

import type { FormField, DetectionMethod } from "@/types";
import {
  DEFAULT_PIPELINE,
  DEFAULT_COLLECTION_PIPELINE,
  nativeInputDetector,
  detectNativeFieldsAsync,
  streamNativeFieldsAsync,
  classifyCustomFieldsSync,
  classifyCustomFieldsAsync,
  reclassifyFieldBySelector,
} from "./detectors/classifiers";
export {
  DEFAULT_PIPELINE,
  DEFAULT_COLLECTION_PIPELINE,
  reclassifyFieldBySelector,
};
import { detectCustomComponents } from "./adapters/adapter-registry";
import { createLogger } from "@/lib/logger";

const log = createLogger("FormDetector");
export type {
  FieldClassifier,
  ClassifierResult,
  PipelineResult,
  DetectionPipeline,
  PageDetector,
  FieldCollectionPipeline,
} from "./detectors/pipeline";

/** Convenience wrapper — returns only the detected fields array. */
export function detectFormFields(): FormField[] {
  return detectAllFields().fields;
}

export interface DetectionResult {
  fields: FormField[];
}

/**
 * Removes native fields whose underlying element is already contained within
 * a custom component wrapper. Adapter-detected fields take precedence because
 * they carry richer context (label, options, etc.).
 */
function deduplicateFields(
  nativeFields: FormField[],
  customFields: FormField[],
): FormField[] {
  if (customFields.length === 0) return nativeFields;

  // Collect all custom wrapper elements
  const customWrappers = new Set(customFields.map((f) => f.element));

  const filtered = nativeFields.filter((nf) => {
    // If the native element is a descendant of any custom wrapper, skip it
    for (const wrapper of customWrappers) {
      if (wrapper.contains(nf.element)) return false;
    }
    return true;
  });

  return [...filtered, ...customFields];
}

/**
 * Synchronous detection — used by dom-watcher and any context that cannot await.
 * Delegates to the PageDetectors in DEFAULT_COLLECTION_PIPELINE.
 */
export function detectAllFields(): DetectionResult {
  const nativeFields = nativeInputDetector.detect();
  const customFields = classifyCustomFieldsSync(detectCustomComponents());
  const fields = deduplicateFields(nativeFields, customFields);
  log.debug("fields detectados :", fields);
  return { fields };
}

/**
 * Async detection — runs the full DEFAULT_COLLECTION_PIPELINE and adds
 * per-detector summary logging.
 */
export async function detectAllFieldsAsync(): Promise<DetectionResult> {
  const url = window.location.href;
  const t0 = performance.now();

  log.groupCollapsed(`🚀 Detecção iniciada — ${new URL(url).hostname}`);
  log.debug(`📄 URL: ${url}`);

  // Use the async pipeline so the Chrome AI classifier (detectAsync) is active
  // for native inputs. Custom selects also run the full async pipeline so TF.js
  // and Chrome AI can classify fields the adapter left as "unknown".
  const nativeFields = await detectNativeFieldsAsync();
  const customFields = await classifyCustomFieldsAsync(
    detectCustomComponents(),
  );
  const fields = deduplicateFields(nativeFields, customFields);

  const byMethod: Record<DetectionMethod, number> = {
    "html-type": 0,
    keyword: 0,
    tensorflow: 0,
    "chrome-ai": 0,
    "html-fallback": 0,
    "custom-select": 0,
    interactive: 0,
    "user-override": 0,
  };

  fields.forEach((field, idx) => {
    log.debug(`🔍 Campo #${idx + 1} detectado:`, field);
    const method = field.detectionMethod ?? "html-fallback";
    byMethod[method as DetectionMethod]++;

    const tag = field.element.tagName.toLowerCase();
    const htmlType =
      field.element instanceof HTMLInputElement ? field.element.type : "—";

    log.groupCollapsed(
      `#${idx + 1} <${tag} type="${htmlType}"> │ id="${field.id ?? ""}" name="${field.name ?? ""}"`,
    );
    log.debug(field);
    log.debug(`📌 Label: "${field.label ?? "(nenhum)"}"`);
    log.debug(`📡 Sinais: "${field.contextSignals || "(nenhum)"}"`);
    const fieldMs = field.detectionDurationMs ?? 0;
    const fieldMsStr =
      fieldMs >= 1
        ? `${fieldMs.toFixed(1)}ms`
        : `${(fieldMs * 1000).toFixed(0)}µs`;
    log.debug(
      `✅ Tipo final: "${field.fieldType}" [${method} | ${((field.detectionConfidence ?? 0) * 100).toFixed(0)}%] ⚡ ${fieldMsStr}`,
    );
    log.groupEnd();
  });

  const summary = (Object.entries(byMethod) as [DetectionMethod, number][])
    .filter(([, n]) => n > 0)
    .map(([m, n]) => `${m}: ${n}`)
    .join(" · ");

  log.info(`✅ ${fields.length} campo(s)  ·  ${summary}`);

  // ── Performance summary ────────────────────────────────────────────────────
  const totalMs = performance.now() - t0;
  const perfSorted = [...fields]
    .filter((f) => (f.detectionDurationMs ?? 0) > 0)
    .sort(
      (a, b) => (b.detectionDurationMs ?? 0) - (a.detectionDurationMs ?? 0),
    );
  const slowTop = perfSorted.slice(0, 3).map((f) => {
    const fIdx = fields.indexOf(f) + 1;
    const ms = (f.detectionDurationMs ?? 0).toFixed(1);
    const label = f.label ?? f.id ?? f.name ?? "?";
    return `#${fIdx} "${label}" ${ms}ms [${f.detectionMethod}]`;
  });
  log.debug(
    `⏱ ${totalMs.toFixed(0)}ms total${slowTop.length ? ` · 🐢 ${slowTop.join(" · ")}` : ""}`,
  );
  log.groupEnd();

  return { fields };
}

/**
 * Streaming detection — yields each FormField immediately after it is classified.
 * Native inputs run the full async pipeline (incl. Chrome AI); custom selects and
 * interactive fields are yielded synchronously at the end.
 *
 * Ideal for real-time UI updates: consumers can show each field's type as it
 * arrives rather than waiting for the entire scan to complete.
 */
export async function* streamAllFields(): AsyncGenerator<FormField> {
  for await (const field of streamNativeFieldsAsync()) {
    yield field;
  }
  // Yield custom component fields (antd, select2, …) with full async classification
  // so TF.js / Chrome AI can classify fields the adapter left as "unknown".
  const customFields = await classifyCustomFieldsAsync(
    detectCustomComponents(),
  );
  for (const field of customFields) {
    yield field;
  }
}