src/lib/form/form-filler.ts

Total Symbols
19
Lines of Code
884
Avg Complexity
8.7
Symbol Types
1

File Relationships

graph LR showAiFieldBadge["showAiFieldBadge"] positionBadge["positionBadge"] highlightField["highlightField"] createFieldLabelBadge["createFieldLabelBadge"] fillAllFields["fillAllFields"] doFillAllFields["doFillAllFields"] getAiFunction["getAiFunction"] fieldHasValue["fieldHasValue"] applyValueToField["applyValueToField"] showFillToast["showFillToast"] fillSingleField["fillSingleField"] fillContextualAI["fillContextualAI"] buildUserContextString["buildUserContextString"] handleSelectElement["handleSelectElement"] handleCheckboxOrRadio["handleCheckboxOrRadio"] setNativeValue["setNativeValue"] applyTemplate["applyTemplate"] showAiFieldBadge -->|calls| positionBadge highlightField -->|calls| createFieldLabelBadge fillAllFields -->|calls| doFillAllFields doFillAllFields -->|calls| getAiFunction doFillAllFields -->|calls| fieldHasValue doFillAllFields -->|calls| applyValueToField doFillAllFields -->|calls| highlightField doFillAllFields -->|calls| showAiFieldBadge doFillAllFields -->|calls| showFillToast fillSingleField -->|calls| getAiFunction fillSingleField -->|calls| applyValueToField fillSingleField -->|calls| highlightField fillContextualAI -->|calls| fieldHasValue fillContextualAI -->|calls| buildUserContextString fillContextualAI -->|calls| fillAllFields fillContextualAI -->|calls| applyValueToField fillContextualAI -->|calls| highlightField fillContextualAI -->|calls| showAiFieldBadge applyValueToField -->|calls| handleSelectElement applyValueToField -->|calls| handleCheckboxOrRadio applyValueToField -->|calls| setNativeValue applyTemplate -->|calls| applyValueToField applyTemplate -->|calls| highlightField click showAiFieldBadge "../symbols/91a8abe473f9bccf.html" click positionBadge "../symbols/15eeba24240cdb49.html" click highlightField "../symbols/d079b946779ebfc9.html" click createFieldLabelBadge "../symbols/a72190a773a67f24.html" click fillAllFields "../symbols/12dbe3fb8e692059.html" click doFillAllFields "../symbols/23fe5c1a0125e335.html" click getAiFunction "../symbols/da66edfde6d3db9f.html" click fieldHasValue "../symbols/5a7659a4d189d652.html" click applyValueToField "../symbols/59a962012828c5cb.html" click showFillToast "../symbols/f69df9743d44144e.html" click fillSingleField "../symbols/e1740bceca6b34c2.html" click fillContextualAI "../symbols/854e1a4562eb49e4.html" click buildUserContextString "../symbols/c7237ee5672c23a4.html" click handleSelectElement "../symbols/0a5f5d5c152645bc.html" click handleCheckboxOrRadio "../symbols/cc82ce08938d0fd2.html" click setNativeValue "../symbols/334bd99609d7c37c.html" click applyTemplate "../symbols/2ec007fc3b6a3513.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'doFillAllFields' has cyclomatic complexity 18 (max 10)
  • [warning] max-cyclomatic-complexity: 'fillContextualAI' has cyclomatic complexity 22 (max 10)
  • [warning] max-cyclomatic-complexity: 'captureFormValues' has cyclomatic complexity 14 (max 10)
  • [warning] max-cyclomatic-complexity: 'applyTemplate' has cyclomatic complexity 54 (max 10)
  • [warning] max-lines: 'doFillAllFields' has 108 lines (max 80)
  • [warning] max-lines: 'fillContextualAI' has 129 lines (max 80)
  • [warning] max-lines: 'applyTemplate' has 140 lines (max 80)

Symbols by Kind

function 19

All Symbols

Name Kind Visibility Status Lines Signature
setNativeValue function - 35-62 setNativeValue(element: HTMLElement, value: string): : void
handleSelectElement function - 64-93 handleSelectElement(element: HTMLSelectElement, value: string): : void
handleCheckboxOrRadio function - 95-98 handleCheckboxOrRadio(element: HTMLInputElement): : void
showFillToast function - 100-163 showFillToast(filled: number, total: number, aiCount: number): : void
showAiFieldBadge function - 165-209 showAiFieldBadge(element: HTMLElement): : void
positionBadge function - 198-202 positionBadge(): : void
highlightField function - 211-226 highlightField(element: HTMLElement, detectedLabel?: string): : void
createFieldLabelBadge function - 228-262 createFieldLabelBadge( target: HTMLElement, label: string, ): : HTMLElement
fillAllFields function exported- 270-279 fillAllFields(options?: { fillEmptyOnly?: boolean; }): : Promise<GenerationResult[]>
fieldHasValue function - 281-290 fieldHasValue(field: FormField): : boolean
doFillAllFields function - 292-399 doFillAllFields(options?: { fillEmptyOnly?: boolean; }): : Promise<GenerationResult[]>
fillSingleField function exported- 406-457 fillSingleField( field: FormField, ): : Promise<GenerationResult | null>
buildUserContextString function - 463-479 buildUserContextString( context?: AIContextPayload, ): : string | undefined
fillContextualAI function exported- 489-617 fillContextualAI( context?: AIContextPayload, ): : Promise<GenerationResult[]>
waitForDomSettle function - 619-642 waitForDomSettle(ms: number): : Promise<void>
applyValueToField function - 644-670 applyValueToField( field: FormField, value: string, ): : Promise<void>
getAiFunction function - 672-701 getAiFunction( settings: Settings, ): : Promise<((field: FormField) => Promise<string>) | undefined>
captureFormValues function exported- 706-737 captureFormValues(): : Promise<Record<string, string>>
applyTemplate function exported- 744-883 applyTemplate( form: SavedForm, ): : Promise<{ filled: number }>

Full Source

/**
 * Form filler — fills detected fields with generated or saved values
 */

import type {
  FieldType,
  FormField,
  GenerationResult,
  SavedForm,
  Settings,
  AIContextPayload,
} from "@/types";
import { detectAllFieldsAsync, streamAllFields } from "./form-detector";
import { resolveFieldValue } from "@/lib/rules/rule-engine";
import {
  generateFieldValueViaProxy as chromeAiGenerate,
  isAvailableViaProxy as isChromeAiAvailable,
  generateFormContextValuesViaProxy,
} from "@/lib/ai/chrome-ai-proxy";
import type { FormContextFieldInput } from "@/lib/ai/prompts";
import { generateWithTensorFlow } from "@/lib/ai/tensorflow-generator";
import { getSettings, getIgnoredFieldsForUrl } from "@/lib/storage/storage";
import { setFillingInProgress } from "./dom-watcher";
import {
  fillCustomComponent,
  extractCustomComponentValue,
} from "./adapters/adapter-registry";
import { generate } from "@/lib/generators";
import { deriveFieldValueFromTemplate } from "@/lib/form/field-type-aliases";
import { createLogger, logAuditFill } from "@/lib/logger";
import { createProgressNotification } from "./progress-notification";

const log = createLogger("FormFiller");

function setNativeValue(element: HTMLElement, value: string): void {
  // Trigger React/Vue/Angular change detection
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
    window.HTMLInputElement.prototype,
    "value",
  )?.set;

  const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
    window.HTMLTextAreaElement.prototype,
    "value",
  )?.set;

  if (element instanceof HTMLInputElement && nativeInputValueSetter) {
    nativeInputValueSetter.call(element, value);
  } else if (
    element instanceof HTMLTextAreaElement &&
    nativeTextAreaValueSetter
  ) {
    nativeTextAreaValueSetter.call(element, value);
  } else if (element instanceof HTMLSelectElement) {
    element.value = value;
  }

  // Dispatch events to notify frameworks
  element.dispatchEvent(new Event("input", { bubbles: true }));
  element.dispatchEvent(new Event("change", { bubbles: true }));
  element.dispatchEvent(new Event("blur", { bubbles: true }));
}

function handleSelectElement(element: HTMLSelectElement, value: string): void {
  const options = Array.from(element.options);

  // Try to match by value first
  const byValue = options.find((opt) => opt.value === value);
  if (byValue) {
    element.value = byValue.value;
    element.dispatchEvent(new Event("change", { bubbles: true }));
    return;
  }

  // Try to match by text
  const byText = options.find((opt) =>
    opt.text.toLowerCase().includes(value.toLowerCase()),
  );
  if (byText) {
    element.value = byText.value;
    element.dispatchEvent(new Event("change", { bubbles: true }));
    return;
  }

  // Pick a random non-empty option
  const validOptions = options.filter((opt) => opt.value);
  if (validOptions.length > 0) {
    const random =
      validOptions[Math.floor(Math.random() * validOptions.length)];
    element.value = random.value;
    element.dispatchEvent(new Event("change", { bubbles: true }));
  }
}

function handleCheckboxOrRadio(element: HTMLInputElement): void {
  element.checked = true;
  element.dispatchEvent(new Event("change", { bubbles: true }));
}

function showFillToast(filled: number, total: number, aiCount: number): void {
  const toast = document.createElement("div");
  toast.setAttribute("data-fill-all-toast", "1");

  const aiNote =
    aiCount > 0
      ? `<span style="opacity:0.85;font-size:11px"> (${aiCount} via ✨ IA)</span>`
      : "";
  toast.innerHTML = `
    <span style="font-size:15px;margin-right:6px">✅</span>
    <span><strong>${filled}</strong> de <strong>${total}</strong> campos preenchidos${aiNote}</span>
    <button data-fill-all-toast-close style="
      background:none;border:none;color:inherit;cursor:pointer;
      font-size:14px;margin-left:12px;opacity:0.7;padding:0;line-height:1;
    " title="Fechar">×</button>
  `;

  toast.style.cssText = `
    position: fixed;
    bottom: 24px;
    right: 24px;
    z-index: 2147483647;
    display: flex;
    align-items: center;
    gap: 4px;
    background: #1e1e2e;
    color: #e2e8f0;
    font-family: system-ui, -apple-system, sans-serif;
    font-size: 13px;
    padding: 10px 14px;
    border-radius: 10px;
    box-shadow: 0 4px 18px rgba(0,0,0,0.38);
    border: 1px solid rgba(255,255,255,0.08);
    transition: opacity 0.3s ease, transform 0.3s ease;
    opacity: 0;
    transform: translateY(12px);
    max-width: 320px;
  `;

  document.body.appendChild(toast);

  // Animate in
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      toast.style.opacity = "1";
      toast.style.transform = "translateY(0)";
    });
  });

  const dismiss = (): void => {
    toast.style.opacity = "0";
    toast.style.transform = "translateY(12px)";
    setTimeout(() => toast.remove(), 350);
  };

  const closeBtn = toast.querySelector<HTMLButtonElement>(
    "[data-fill-all-toast-close]",
  );
  closeBtn?.addEventListener("click", dismiss);

  const timer = setTimeout(dismiss, 4000);
  toast.addEventListener("mouseenter", () => clearTimeout(timer));
  toast.addEventListener("mouseleave", () => setTimeout(dismiss, 1500));
}

function showAiFieldBadge(element: HTMLElement): void {
  // Remove any existing AI badge on this element
  const existingBadge = element.parentElement?.querySelector(
    "[data-fill-all-ai-badge]",
  );
  existingBadge?.remove();

  const badge = document.createElement("span");
  badge.setAttribute("data-fill-all-ai-badge", "1");
  badge.title = "Preenchido por IA (Fill All) — clique para remover";
  badge.style.cssText = `
    display: inline-flex;
    align-items: center;
    gap: 3px;
    position: absolute;
    font-size: 10px;
    font-family: system-ui, -apple-system, sans-serif;
    font-weight: 700;
    padding: 2px 6px 2px 5px;
    border-radius: 4px;
    z-index: 2147483646;
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    color: #fff;
    box-shadow: 0 1px 4px rgba(99,102,241,0.45);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    pointer-events: auto;
    line-height: 1.4;
    letter-spacing: 0.1px;
  `;
  badge.innerHTML = `<span style="font-size:11px">✨</span>AI<span style="opacity:0.7;font-size:9px;margin-left:2px">×</span>`;

  function positionBadge(): void {
    const rect = element.getBoundingClientRect();
    badge.style.top = `${rect.top + window.scrollY - 20}px`;
    badge.style.left = `${rect.right + window.scrollX - 42}px`;
  }

  positionBadge();
  badge.style.position = "absolute";
  document.body.appendChild(badge);

  badge.addEventListener("click", () => badge.remove());
}

function highlightField(element: HTMLElement, detectedLabel?: string): void {
  const original = element.style.outline;
  element.style.outline = "2px solid #4F46E5";
  element.style.outlineOffset = "1px";

  let badge: HTMLElement | null = null;
  if (detectedLabel) {
    badge = createFieldLabelBadge(element, detectedLabel);
  }

  setTimeout(() => {
    element.style.outline = original;
    element.style.outlineOffset = "";
    badge?.remove();
  }, 2000);
}

function createFieldLabelBadge(
  target: HTMLElement,
  label: string,
): HTMLElement {
  const rect = target.getBoundingClientRect();
  const badge = document.createElement("div");
  badge.textContent = label;

  const top = rect.top - 20;
  const left = rect.left;

  badge.style.cssText = `
    position: fixed;
    top: ${Math.max(2, top)}px;
    left: ${left}px;
    background: rgba(79, 70, 229, 0.9);
    color: #fff;
    font-size: 10px;
    font-family: system-ui, -apple-system, sans-serif;
    font-weight: 600;
    padding: 1px 6px;
    border-radius: 3px;
    z-index: 2147483645;
    pointer-events: none;
    white-space: nowrap;
    max-width: 180px;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.5;
    box-shadow: 0 1px 4px rgba(0,0,0,0.25);
    letter-spacing: 0.2px;
  `;
  document.body.appendChild(badge);
  return badge;
}

/**
 * Fills every detected form field on the current page.
 * Resolves values through the priority chain (rules → saved forms → AI → generator).
 * @param options.fillEmptyOnly When set, overrides the stored setting for this call only
 * @returns Array of generation results for each filled field
 */
export async function fillAllFields(options?: {
  fillEmptyOnly?: boolean;
}): Promise<GenerationResult[]> {
  setFillingInProgress(true);
  try {
    return await doFillAllFields(options);
  } finally {
    setFillingInProgress(false);
  }
}

function fieldHasValue(field: FormField): boolean {
  const el = field.element;
  if (el instanceof HTMLInputElement) {
    if (el.type === "checkbox" || el.type === "radio") return el.checked;
    return el.value.trim() !== "";
  }
  if (el instanceof HTMLTextAreaElement) return el.value.trim() !== "";
  if (el instanceof HTMLSelectElement) return el.value !== "";
  return false;
}

async function doFillAllFields(options?: {
  fillEmptyOnly?: boolean;
}): Promise<GenerationResult[]> {
  const url = window.location.href;
  const settings = await getSettings();
  const fillEmptyOnly = options?.fillEmptyOnly ?? settings.fillEmptyOnly;
  const results: GenerationResult[] = [];

  // Determine AI function based on settings
  const aiGenerateFn = await getAiFunction(settings);

  // Load ignored fields for current URL
  const ignoredFields = await getIgnoredFieldsForUrl(url);
  const ignoredSelectors = new Set(ignoredFields.map((f) => f.selector));

  // Create progress notification
  const progress = createProgressNotification();
  progress.show();

  let totalFields = 0;

  // Stream detection + fill progressively (field by field)
  for await (const field of streamAllFields()) {
    totalFields++;

    // Show detecting state
    progress.addDetecting(field);

    // Detection already done by the stream — update badge
    progress.updateDetected(field);

    // Skip ignored fields
    if (ignoredSelectors.has(field.selector)) continue;

    // Skip fields that already have a value when fillEmptyOnly is enabled
    if (fillEmptyOnly && fieldHasValue(field)) continue;

    const fieldLabel =
      field.label ??
      field.name ??
      field.id ??
      field.fieldType ??
      field.selector;
    log.info(`⏳ Preenchendo [${field.fieldType}] "${fieldLabel}"...`);
    const start = Date.now();

    // Show filling state
    progress.addFilling(field);

    try {
      const result = await resolveFieldValue(
        field,
        url,
        aiGenerateFn,
        settings.forceAIFirst,
        settings.aiTimeoutMs,
      );

      await applyValueToField(field, result.value);

      logAuditFill({
        selector: field.selector,
        fieldType: field.fieldType,
        source: result.source,
        value: String(result.value),
      });

      log.info(
        `✅ Preenchido em ${Date.now() - start}ms via ${result.source}: "${String(result.value).slice(0, 40)}"`,
      );

      if (settings.highlightFilled) {
        highlightField(
          field.element,
          field.label ?? field.fieldType ?? undefined,
        );
      }

      if (settings.showAiBadge && result.source === "ai") {
        showAiFieldBadge(field.element);
      }

      // Update progress — filled
      progress.updateFilled(field, result);

      results.push(result);
    } catch (error) {
      log.warn(
        `❌ Falhou em ${Date.now() - start}ms — campo ${field.selector}:`,
        error,
      );
      progress.updateError(
        field,
        error instanceof Error ? error.message : "falhou",
      );
    }
  }

  // Show summary
  progress.done(results.length, totalFields);

  const aiFilledCount = results.filter((r) => r.source === "ai").length;
  if (settings.showFillToast !== false) {
    showFillToast(results.length, totalFields, aiFilledCount);
  }

  return results;
}

/**
 * Fills a single form field using the same priority chain as {@link fillAllFields}.
 * @param field - The detected form field to fill
 * @returns The generation result, or `null` on failure
 */
export async function fillSingleField(
  field: FormField,
): Promise<GenerationResult | null> {
  const url = window.location.href;
  const ignoredFields = await getIgnoredFieldsForUrl(url);
  const ignoredSelectors = new Set(ignoredFields.map((f) => f.selector));
  if (ignoredSelectors.has(field.selector)) {
    log.debug(`Campo ignorado — skip: ${field.selector}`);
    return null;
  }
  const settings = await getSettings();
  const aiGenerateFn = await getAiFunction(settings);
  const fieldLabel =
    field.label ?? field.name ?? field.id ?? field.fieldType ?? field.selector;
  log.info(`⏳ Preenchendo [${field.fieldType}] "${fieldLabel}"...`);
  const start = Date.now();

  try {
    const result = await resolveFieldValue(
      field,
      url,
      aiGenerateFn,
      settings.forceAIFirst,
      settings.aiTimeoutMs,
    );
    await applyValueToField(field, result.value);
    logAuditFill({
      selector: field.selector,
      fieldType: field.fieldType,
      source: result.source,
      value: String(result.value),
    });
    log.info(
      `✅ Preenchido em ${Date.now() - start}ms via ${result.source}: "${String(result.value).slice(0, 40)}"`,
    );

    if (settings.highlightFilled) {
      highlightField(
        field.element,
        field.label ?? field.fieldType ?? undefined,
      );
    }

    return result;
  } catch (error) {
    log.warn(
      `❌ Falhou em ${Date.now() - start}ms — campo ${field.selector}:`,
      error,
    );
    return null;
  }
}

/**
 * Aggregates all parts of an AIContextPayload into a single plaintext
 * string to be forwarded to the AI prompt.
 */
function buildUserContextString(
  context?: AIContextPayload,
): string | undefined {
  if (!context) return undefined;

  const parts: string[] = [];
  if (context.text?.trim()) parts.push(context.text.trim());
  if (context.audioTranscript?.trim())
    parts.push(`Audio transcript: ${context.audioTranscript.trim()}`);
  if (context.csvText?.trim())
    parts.push(`CSV data:\n${context.csvText.trim()}`);
  if (context.pdfText?.trim())
    parts.push(`PDF document content:\n${context.pdfText.trim()}`);
  // imageDataUrl is passed separately as multimodal input — not included in text

  return parts.length > 0 ? parts.join("\n\n") : undefined;
}

/**
 * Fills all form fields on the page using Chrome AI contextual generation.
 * All values are generated in a single AI call, producing a cohesive fictional
 * identity (same person/company across every field).
 * Falls back to {@link fillAllFields} if AI is unavailable or returns no data.
 * @param context - Optional user-provided context (text, CSV, audio, image) to guide AI
 * @returns Array of generation results for each filled field
 */
export async function fillContextualAI(
  context?: AIContextPayload,
): Promise<GenerationResult[]> {
  setFillingInProgress(true);
  const progress = createProgressNotification();
  progress.show();
  try {
    const { fields } = await detectAllFieldsAsync();
    if (fields.length === 0) {
      progress.destroy();
      return [];
    }

    const url = window.location.href;
    const settings = await getSettings();
    const ignoredFields = await getIgnoredFieldsForUrl(url);
    const ignoredSelectors = new Set(ignoredFields.map((f) => f.selector));

    const eligibleFields = fields.filter(
      (f) => !ignoredSelectors.has(f.selector),
    );

    const fillableFields = settings.fillEmptyOnly
      ? eligibleFields.filter((f) => !fieldHasValue(f))
      : eligibleFields;

    if (fillableFields.length === 0) {
      progress.destroy();
      return [];
    }

    // Build compact descriptors for the AI
    const contextInputs: FormContextFieldInput[] = fillableFields.map(
      (f, i) => {
        const input: FormContextFieldInput = {
          index: i,
          label: f.label ?? f.name ?? f.id ?? undefined,
          fieldType: f.fieldType,
          inputType:
            f.element instanceof HTMLInputElement
              ? f.element.type || "text"
              : undefined,
        };

        if (f.element instanceof HTMLSelectElement) {
          const opts = Array.from(f.element.options)
            .filter((o) => o.value.trim().length > 0)
            .map((o) => o.text.trim() || o.value)
            .slice(0, 6);
          if (opts.length > 0) return { ...input, options: opts };
        }

        return input;
      },
    );

    // Show spinner while the single batch AI call is in-flight
    progress.showAiGenerating();

    const contextMap = await generateFormContextValuesViaProxy(
      contextInputs,
      buildUserContextString(context),
      context?.imageDataUrl,
      context?.pdfPageDataUrls,
    );

    progress.hideAiGenerating();

    if (!contextMap || Object.keys(contextMap).length === 0) {
      log.warn(
        "fillContextualAI: AI não retornou valores, usando fallback fillAllFields",
      );
      progress.destroy();
      return fillAllFields();
    }

    const results: GenerationResult[] = [];

    for (let i = 0; i < fillableFields.length; i++) {
      const field = fillableFields[i]!;
      const value = contextMap[String(i)];

      if (!value) continue;

      progress.addFilling(field);

      try {
        await applyValueToField(field, value);

        logAuditFill({
          selector: field.selector,
          fieldType: field.fieldType,
          source: "ai",
          value,
        });

        if (settings.highlightFilled) {
          highlightField(
            field.element,
            field.label ?? field.fieldType ?? undefined,
          );
        }

        if (settings.showAiBadge) {
          showAiFieldBadge(field.element);
        }

        const result: GenerationResult = {
          fieldSelector: field.selector,
          value,
          source: "ai",
        };
        progress.updateFilled(field, result);
        results.push(result);
      } catch (err) {
        log.warn(`fillContextualAI: falhou no campo ${field.selector}:`, err);
        progress.updateError(
          field,
          err instanceof Error ? err.message : "falhou",
        );
      }
    }

    progress.done(results.length, fillableFields.length);
    return results;
  } finally {
    setFillingInProgress(false);
  }
}

function waitForDomSettle(ms: number): Promise<void> {
  return new Promise((resolve) => {
    let timer: ReturnType<typeof setTimeout>;
    const observer = new MutationObserver(() => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        observer.disconnect();
        resolve();
      }, 200);
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
    });

    // Fallback: resolve after max wait
    timer = setTimeout(() => {
      observer.disconnect();
      resolve();
    }, ms);
  });
}

async function applyValueToField(
  field: FormField,
  value: string,
): Promise<void> {
  // Delegate to custom adapter if the field was detected by one
  if (field.adapterName) {
    const handled = await fillCustomComponent(field, value);
    if (handled) return;
  }

  const el = field.element;

  if (el instanceof HTMLSelectElement) {
    handleSelectElement(el, value);
    return;
  }

  if (
    el instanceof HTMLInputElement &&
    (el.type === "checkbox" || el.type === "radio")
  ) {
    handleCheckboxOrRadio(el);
    return;
  }

  setNativeValue(el, value);
}

async function getAiFunction(
  settings: Settings,
): Promise<((field: FormField) => Promise<string>) | undefined> {
  log.debug(
    `useChromeAI=${settings.useChromeAI} | defaultStrategy=${settings.defaultStrategy} | forceAIFirst=${settings.forceAIFirst}`,
  );

  if (settings.useChromeAI) {
    const available = await isChromeAiAvailable();
    log.debug(`Chrome AI disponível: ${available}`);
    if (available) {
      log.debug("Usando Chrome AI (Gemini Nano).");
      return chromeAiGenerate;
    }
  }

  // Fallback to TF.js when strategy is "ai" or "tensorflow"
  if (
    settings.defaultStrategy === "tensorflow" ||
    settings.defaultStrategy === "ai"
  ) {
    log.debug("Usando TensorFlow.js como fallback de AI.");
    return async (field: FormField) => await generateWithTensorFlow(field);
  }

  log.warn(
    "Nenhuma função de AI configurada. Será usado apenas o gerador padrão.",
  );
  return undefined;
}

/**
 * Captures current form values and returns them as a map
 */
export async function captureFormValues(): Promise<Record<string, string>> {
  const { fields } = await detectAllFieldsAsync();
  const values: Record<string, string> = {};

  for (const field of fields) {
    const el = field.element;
    const key = field.id || field.name || field.selector;

    // if this field came from a custom adapter try to extract its value
    if (field.adapterName) {
      const custom = extractCustomComponentValue(field);
      if (custom !== null) {
        values[key] = custom;
        continue;
      }
    }

    if (el instanceof HTMLSelectElement) {
      values[key] = el.value;
    } else if (el instanceof HTMLInputElement) {
      if (el.type === "checkbox" || el.type === "radio") {
        values[key] = el.checked ? "true" : "false";
      } else {
        values[key] = el.value;
      }
    } else {
      values[key] = (el as HTMLTextAreaElement).value;
    }
  }

  return values;
}

/**
 * Applies a saved form template to the current page.
 * Uses templateFields (new format) if available, otherwise falls back to legacy fields.
 * For generator-mode fields, calls the appropriate generator to produce a fresh value.
 */
export async function applyTemplate(
  form: SavedForm,
): Promise<{ filled: number }> {
  const { fields: allDetectedFields } = await detectAllFieldsAsync();
  const settings = await getSettings();
  const url = window.location.href;

  // Skip fields the user has marked as ignored
  const ignoredFields = await getIgnoredFieldsForUrl(url);
  const ignoredSelectors = new Set(ignoredFields.map((f) => f.selector));
  const detectedFields = allDetectedFields.filter(
    (f) => !ignoredSelectors.has(f.selector),
  );

  let filled = 0;

  if (form.templateFields && form.templateFields.length > 0) {
    // Pre-generate ONE consistent value per field type (fixed or generator).
    // Generator fields are produced once so that derivations remain coherent —
    // e.g. first-name and last-name derived from the same generated full-name.
    const templateValueMap = new Map<FieldType, string>();
    for (const tField of form.templateFields) {
      if (!tField.matchByFieldType) continue;
      if (tField.mode === "generator" && tField.generatorType) {
        templateValueMap.set(
          tField.matchByFieldType,
          generate(tField.generatorType, tField.generatorParams ?? undefined),
        );
      } else if (tField.mode === "fixed" && tField.fixedValue) {
        templateValueMap.set(tField.matchByFieldType, tField.fixedValue);
      }
    }

    // For type-based templates (matchByFieldType), track which fields were already handled
    const handledSelectors = new Set<string>();

    for (const tField of form.templateFields) {
      // Type-based matching: find ALL fields of the given type
      if (tField.matchByFieldType) {
        const matchedFields = detectedFields.filter(
          (f) =>
            f.fieldType === tField.matchByFieldType &&
            !handledSelectors.has(f.selector),
        );
        for (const matchedField of matchedFields) {
          // Use the pre-generated value to keep derived fields coherent
          const value = templateValueMap.get(tField.matchByFieldType) ?? "";
          if (!value) continue;
          await applyValueToField(matchedField, value);
          if (settings.highlightFilled) {
            highlightField(
              matchedField.element,
              matchedField.label ?? matchedField.fieldType ?? undefined,
            );
          }
          handledSelectors.add(matchedField.selector);
          filled++;
        }
        continue;
      }

      // Selector-based matching (legacy / saved-from-page templates)
      const matchedField = detectedFields.find(
        (f) =>
          f.selector === tField.key ||
          f.id === tField.key ||
          f.name === tField.key,
      );
      if (!matchedField) continue;

      let value: string;
      if (tField.mode === "generator" && tField.generatorType) {
        value = generate(
          tField.generatorType,
          tField.generatorParams ?? undefined,
        );
      } else {
        value = tField.fixedValue ?? "";
      }

      if (!value && tField.mode === "fixed") continue;

      await applyValueToField(matchedField, value);
      if (settings.highlightFilled) {
        highlightField(
          matchedField.element,
          matchedField.label ?? matchedField.fieldType ?? undefined,
        );
      }
      handledSelectors.add(matchedField.selector);
      filled++;
    }

    // Fallback: fill detected fields not covered by the template.
    // Priority: (1) direct match in map (same type, missed the loop somehow)
    //           (2) smart derivation from a related type (e.g. first-name ← full-name)
    for (const field of detectedFields) {
      if (handledSelectors.has(field.selector) || !field.fieldType) continue;

      const value =
        templateValueMap.get(field.fieldType) ??
        deriveFieldValueFromTemplate(field.fieldType, templateValueMap);

      if (!value) continue;
      await applyValueToField(field, value);
      if (settings.highlightFilled) {
        highlightField(
          field.element,
          field.label ?? field.fieldType ?? undefined,
        );
      }
      filled++;
    }
  } else {
    // Legacy format: fields Record<string, string>
    for (const detectedField of detectedFields) {
      const key =
        detectedField.id || detectedField.name || detectedField.selector;
      const value =
        form.fields[detectedField.selector] ??
        form.fields[key] ??
        (detectedField.name ? form.fields[detectedField.name] : undefined) ??
        (detectedField.id ? form.fields[detectedField.id] : undefined);

      if (value === undefined) continue;

      await applyValueToField(detectedField, value);
      if (settings.highlightFilled) {
        highlightField(
          detectedField.element,
          detectedField.label ?? detectedField.fieldType ?? undefined,
        );
      }
      filled++;
    }
  }

  log.info(`Template "${form.name}" aplicado: ${filled} campos`);
  return { filled };
}