🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Google Forms: AI Form Filler (Gemini)

One-click AI autofill for Google Forms using Gemini with optional one-time instruction override + quick cleaner

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Google Forms: AI Form Filler (Gemini)
// @namespace    https://www.fiverr.com/web_coder_nsd
// @version      1.1.2
// @icon         https://cdn-icons-png.flaticon.com/512/720/720311.png
// @description  One-click AI autofill for Google Forms using Gemini with optional one-time instruction override + quick cleaner
// @match        https://docs.google.com/forms/*/viewform
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  // UI buttons
  const btn = document.createElement('button');        // Run
  const btnEdit = document.createElement('button');    // One-time prompt
  const btnClean = document.createElement('button');   // Clean all

  Object.assign(btn.style, {
    position: 'fixed', right: '16px', top: '25px', zIndex: 2147483647,
    width: '44px', height: '44px', borderRadius: '50%', border: '0',
    background: '#111', color: '#fff', fontSize: '14px', lineHeight: '44px',
    textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none'
  });
  btn.textContent = '▶';
  document.documentElement.appendChild(btn);

  Object.assign(btnEdit.style, {
    position: 'fixed', right: '9px', top: '19px', zIndex: 2147483647,
    width: '24px', height: '24px', borderRadius: '50%', border: '0',
    background: '#444', color: '#fff', fontSize: '17px', lineHeight: '14px',
    textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none'
  });
  btnEdit.title = 'Add one-time instructions';
  btnEdit.textContent = '✎';
  document.documentElement.appendChild(btnEdit);

  Object.assign(btnClean.style, {
    position: 'fixed', right: '16px', top: '72px', zIndex: 2147483647,
    width: '44px', height: '44px', borderRadius: '50%', border: '0',
    background: '#7a1d1d', color: '#fff', fontSize: '18px', lineHeight: '44px',
    textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none'
  });
  btnClean.title = 'Clear all inputs';
  btnClean.textContent = '🧹';
  document.documentElement.appendChild(btnClean);

  // one-time extra instructions, cleared after each run
  let sessionExtraInstructions = '';
  btnEdit.addEventListener('click', () => {
    const next = prompt('One-time instructions for this run only.\nExample: "email should be [email protected]"');
    if (next == null) return;
    sessionExtraInstructions = (next || '').trim();
    btnEdit.style.background = sessionExtraInstructions ? '#0a7' : '#444';
  });

  // drag all three as a group (drag using main ▶ button)
  let drag = { x: 0, y: 0, sx: 0, sy: 0, down: false };
  const onDown = (e) => {
    drag.down = true; drag.sx = e.clientX; drag.sy = e.clientY;
    const r = btn.getBoundingClientRect(); drag.x = r.left; drag.y = r.top; e.preventDefault();
  };
  const onMove = (e) => {
    if (!drag.down) return;
    const nx = drag.x + (e.clientX - drag.sx);
    const ny = drag.y + (e.clientY - drag.sy);
    [ [btn, 0], [btnClean, 47] ].forEach(([el, dy]) => {
      el.style.left = nx + 'px'; el.style.top = (ny + dy) + 'px';
      el.style.right = 'auto'; el.style.bottom = 'auto';
    });
    // the small ✎ button sits near the ▶ button
    btnEdit.style.left = (nx + 28) + 'px';
    btnEdit.style.top = (ny - 6) + 'px';
    btnEdit.style.right = 'auto';
    btnEdit.style.bottom = 'auto';
  };
  const onUp = () => { drag.down = false; };
  btn.addEventListener('mousedown', onDown);
  window.addEventListener('mousemove', onMove);
  window.addEventListener('mouseup', onUp);

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  // Gemini client
  class GeminiClient {
    constructor() {
      this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
      this.primaryModel = 'gemini-2.0-flash';
      this.fallbackModel = 'gemini-1.5-flash';
      this.apiKey = null;
    }
    async init() {
      if (!this.apiKey) {
        let k = localStorage.getItem('gemini_api_key');
        if (!k) {
          k = prompt('Enter Gemini API key');
          if (!k) throw new Error('API key is required');
          localStorage.setItem('gemini_api_key', k.trim());
        }
        this.apiKey = k.trim();
      }
    }
    async fetchFromModel(model, prompt) {
      const res = await fetch(`${this.baseUrl}/${model}:generateContent?key=${this.apiKey}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
      });
      return res;
    }
    async generateContent(prompt) {
      if (!this.apiKey) await this.init();
      let response = await this.fetchFromModel(this.primaryModel, prompt);
      if (response.status === 429) response = await this.fetchFromModel(this.fallbackModel, prompt);
      if (!response.ok) throw new Error(`HTTP error ${response.status}`);
      const data = await response.json();
      return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
    }
  }

  // DOM helpers
  const getClosestListItem = (el) => el.closest('[role="listitem"]') || el.closest('.Qr7Oae') || el.closest('.geS5n');
  const getHeadingText = (container) => {
    let t = '';
    if (!container) return t;
    const heading = container.querySelector('[role="heading"] .M7eMe') || container.querySelector('[role="heading"]') || container.querySelector('.HoXoMd .M7eMe');
    if (heading) t = heading.textContent.trim();
    return t || (container.querySelector('label, .aDTYNe, .OIC90c')?.textContent?.trim() || '');
  };
  const plain = (s) => (s || '').replace(/\s+/g, ' ').trim();
  const uid = () => Math.random().toString(36).slice(2, 10);
  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
  const isVisible = (el) => !!(el && el.offsetParent !== null);

  // prompt builder
  function buildPrompt(schema, oneTimeExtra) {
    const now = new Date().toISOString();
    const extraBlock = oneTimeExtra
      ? `\nUser instructions for this run only. If they conflict, override defaults:\n${oneTimeExtra}\n`
      : '';
    return `
You are a data faker for autofilling Google Forms. Decide realistic but safe dummy values from the provided schema.
Rules:
- Only use provided options for radio, checkbox, and select.
- For "date" fields, return ISO YYYY-MM-DD. Respect "min" and "max" if given, otherwise pick within ±365 days from today.
- For "time" fields, return {"hour":1-12,"minute":0-59,"meridiem":"AM|PM"}.
- Keep text answers short and plausible. Email looks like a real address.
- Return STRICT JSON only. No markdown.
${extraBlock}
Schema (JSON):
${JSON.stringify(schema, null, 2)}

Output JSON array of objects, each:
{ "id": "<schema.id>", "value": <string | string[] | {"hour":number,"minute":number,"meridiem":string}> }

Now generate values. Current time: ${now}.
`;
  }

  // safe JSON
  function safeJson(txt) {
    const cleaned = txt.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
    try { return JSON.parse(cleaned); } catch (e) {
      const m = cleaned.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
      if (m) return JSON.parse(m[0]);
      throw e;
    }
  }

  // setters
  function setNativeValue(el, value) {
    let proto = el; let setter = null;
    while ((proto = Object.getPrototypeOf(proto)) && !setter) {
      const d = Object.getOwnPropertyDescriptor(proto, 'value');
      if (d && typeof d.set === 'function') setter = d.set;
    }
    if (setter) setter.call(el, value); else el.value = value;
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
  }

  // Select helpers
  function clickOptionInListbox(listbox, wanted) {
    const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
    const open = () => { if (listbox.getAttribute('aria-expanded') === 'false') listbox.click(); };
    const close = () => { if (listbox.getAttribute('aria-expanded') === 'true') listbox.click(); };
    open();
    let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m));
    let options = popup ? Array.from(popup.querySelectorAll('[role="option"]'))
                        : Array.from(listbox.querySelectorAll('[role="option"]'));
    let target = options.find(o => norm(o.getAttribute('data-value')) === norm(wanted))
             || options.find(o => norm(o.textContent) === norm(wanted))
             || options.find(o => {
                  const txt = norm(o.getAttribute('data-value') || o.textContent);
                  return txt && txt !== 'choose';
                });
    if (target) target.click();
    close();
  }
  function resetListboxToChooseOrFirst(listbox) {
    const open = () => { if (listbox.getAttribute('aria-expanded') === 'false') listbox.click(); };
    const close = () => { if (listbox.getAttribute('aria-expanded') === 'true') listbox.click(); };
    open();
    let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m));
    let options = popup ? Array.from(popup.querySelectorAll('[role="option"]'))
                        : Array.from(listbox.querySelectorAll('[role="option"]'));
    // Prefer "Choose" / empty value
    let target = options.find(o => /choose/i.test(o.textContent || '') || (o.getAttribute('data-value') || '') === '');
    if (!target && options.length) target = options[0];
    if (target) target.click();
    close();
  }

  // CLEARING LOGIC
  function clearCheckboxesOnly() {
    // Uncheck all checked checkboxes (requested: auto-remove previously set checkmarks before run)
    Array.from(document.querySelectorAll('[role="checkbox"][aria-checked="true"]')).forEach(cb => cb.click());
  }

  function clearAllInputs() {
    // Text-like inputs
    document.querySelectorAll('input.whsOnd[type="text"], input.whsOnd[type="email"], input.whsOnd[type="number"]').forEach(el => setNativeValue(el, ''));
    // Textareas
    document.querySelectorAll('textarea').forEach(el => setNativeValue(el, ''));
    // Dates
    document.querySelectorAll('input[type="date"].whsOnd').forEach(el => setNativeValue(el, ''));
    // Time (Hour/Minute)
    document.querySelectorAll('.PfQ8Lb input[aria-label="Hour"]').forEach(el => setNativeValue(el, ''));
    document.querySelectorAll('.PfQ8Lb input[aria-label="Minute"]').forEach(el => setNativeValue(el, ''));
    // Time AM/PM reset to first option (usually AM)
    document.querySelectorAll('.PfQ8Lb [role="listbox"][aria-label="AM or PM"]').forEach(lb => resetListboxToChooseOrFirst(lb));
    // Checkboxes
    Array.from(document.querySelectorAll('[role="checkbox"][aria-checked="true"]')).forEach(cb => cb.click());
    // Radios (try to deselect active; Forms may keep one selected if required)
    Array.from(document.querySelectorAll('[role="radio"][aria-checked="true"]')).forEach(r => r.click());
    // Select listboxes reset to default
    document.querySelectorAll('[role="listbox"].jgvuAb, [role="listbox"].cGN2le, [role="listbox"].t9kgXb').forEach(lb => resetListboxToChooseOrFirst(lb));
  }

  btnClean.addEventListener('click', () => {
    clearAllInputs();
  });

  // fill from plan
  async function fillFromPlan(plan) {
    document.querySelectorAll('[data-ai-field-id^="text_"]').forEach(input => {
      const id = input.dataset.aiFieldId;
      const p = plan.find(x => x.id === id);
      if (p && typeof p.value === 'string') setNativeValue(input, p.value);
    });

    document.querySelectorAll('[data-ai-field-id^="textarea_"]').forEach(ta => {
      const id = ta.dataset.aiFieldId;
      const p = plan.find(x => x.id === id);
      if (p && typeof p.value === 'string') setNativeValue(ta, p.value);
    });

    document.querySelectorAll('[role="radiogroup"][data-ai-field-id]').forEach(group => {
      const id = group.dataset.aiFieldId;
      const p = plan.find(x => x.id === id);
      if (!p || typeof p.value !== 'string') return;
      const target = Array.from(group.querySelectorAll('[role="radio"]')).find(r => {
        const v = r.getAttribute('data-value') || r.getAttribute('aria-label') || r.textContent;
        return plain(v).toLowerCase() === plain(p.value).toLowerCase();
      }) || Array.from(group.querySelectorAll('[role="radio"]'))[0];
      if (target) target.click();
    });

    const groupIds = new Set(plan.filter(p => Array.isArray(p.value) || typeof p.value === 'string').map(p => p.id));
    groupIds.forEach(id => {
      const planned = plan.find(p => p.id === id);
      const values = Array.isArray(planned?.value) ? planned.value : [planned?.value].filter(Boolean);
      const cbs = Array.from(document.querySelectorAll(`[role="checkbox"][data-ai-field-id="${id}"]`));
      cbs.forEach(cb => { if (cb.getAttribute('aria-checked') === 'true') cb.click(); });
      values.forEach(val => {
        const target = cbs.find(cb => {
          const v = cb.getAttribute('data-answer-value') || cb.getAttribute('aria-label') || cb.textContent;
          return plain(v).toLowerCase() === plain(val).toLowerCase();
        }) || null;
        if (target) target.click();
      });
    });

    document.querySelectorAll('[role="listbox"][data-ai-field-id^="select_"]').forEach(lb => {
      const id = lb.dataset.aiFieldId;
      const p = plan.find(x => x.id === id);
      if (!p || typeof p.value !== 'string') return;
      clickOptionInListbox(lb, p.value);
    });

    document.querySelectorAll('input[type="date"][data-ai-field-id^="date_"]').forEach(dateEl => {
      const id = dateEl.dataset.aiFieldId;
      const p = plan.find(x => x.id === id);
      const min = dateEl.getAttribute('min');
      const max = dateEl.getAttribute('max');
      const value = (p && typeof p.value === 'string') ? p.value : randDate(min, max);
      setNativeValue(dateEl, value);
    });

    const timeGroups = {};
    document.querySelectorAll('[data-ai-field-id^="time_"]').forEach(el => {
      const id = el.dataset.aiFieldId.replace(/_(hour|minute|ampm)$/, '');
      if (!timeGroups[id]) timeGroups[id] = {};
      if (/_hour$/.test(el.dataset.aiFieldId)) timeGroups[id].hourEl = el;
      if (/_minute$/.test(el.dataset.aiFieldId)) timeGroups[id].minuteEl = el;
      if (/_ampm$/.test(el.dataset.aiFieldId)) timeGroups[id].ampmEl = el;
    });
    Object.keys(timeGroups).forEach(id => {
      const grp = timeGroups[id];
      const p = plan.find(x => x.id === id);
      let obj = null;
      if (p && typeof p.value === 'object' && p.value) {
        obj = { hour: clamp(parseInt(p.value.hour, 10) || 9, 1, 12),
                minute: clamp(parseInt(p.value.minute, 10) || 0, 0, 59),
                meridiem: String(p.value.meridiem || 'AM').toUpperCase() };
      } else if (p && typeof p.value === 'string') {
        obj = parseTimeLike(p.value);
      }
      if (!obj) obj = { hour: 10, minute: 30, meridiem: 'AM' };
      if (grp.hourEl) setNativeValue(grp.hourEl, String(obj.hour).padStart(2, '0'));
      if (grp.minuteEl) setNativeValue(grp.minuteEl, String(obj.minute).padStart(2, '0'));
      if (grp.ampmEl) clickOptionInListbox(grp.ampmEl, obj.meridiem);
    });
  }

  // schema scraper
  async function scrapeSchema() {
    const schema = [];

    document.querySelectorAll('input[type="text"].whsOnd, input[type="email"].whsOnd, input[type="number"].whsOnd').forEach((input, idx) => {
      const wrap = getClosestListItem(input);
      const label = getHeadingText(wrap);
      const id = `text_${idx}_${uid()}`;
      input.dataset.aiFieldId = id;
      schema.push({ id, type: 'text', label: plain(label), required: input.required || /[*]$/.test(label) });
    });

    document.querySelectorAll('textarea.KHxj8b, textarea').forEach((ta, idx) => {
      const wrap = getClosestListItem(ta);
      const label = getHeadingText(wrap);
      const id = `textarea_${idx}_${uid()}`;
      ta.dataset.aiFieldId = id;
      schema.push({ id, type: 'textarea', label: plain(label), required: ta.required || /[*]$/.test(label) });
    });

    document.querySelectorAll('[role="radiogroup"]').forEach((rg, idx) => {
      const wrap = getClosestListItem(rg);
      const label = getHeadingText(wrap);
      const options = Array.from(rg.querySelectorAll('[role="radio"]')).map(r => {
        const v = r.getAttribute('data-value') || r.getAttribute('aria-label') || r.textContent;
        return plain(v);
      }).filter(Boolean);
      if (!options.length) return;
      const id = `radio_${idx}_${uid()}`;
      rg.dataset.aiFieldId = id;
      schema.push({ id, type: 'radio', label: plain(label), options: Array.from(new Set(options)) });
    });

    const checkboxEls = Array.from(document.querySelectorAll('[role="checkbox"][data-answer-value], [role="checkbox"][aria-label]'));
    const groups = new Map();
    checkboxEls.forEach((cb) => {
      const wrap = getClosestListItem(cb);
      const groupLabel = getHeadingText(wrap) || 'Checkbox Group';
      const fieldId = cb.getAttribute('data-field-id') || groupLabel;
      const key = `${groupLabel}::${fieldId}`;
      if (!groups.has(key)) groups.set(key, []);
      const v = cb.getAttribute('data-answer-value') || cb.getAttribute('aria-label') || cb.textContent;
      groups.get(key).push({ el: cb, value: plain(v) });
    });
    let cidx = 0;
    for (const [key, items] of groups.entries()) {
      const [groupLabel] = key.split('::');
      const id = `checkbox_${cidx++}_${uid()}`;
      items.forEach(i => { i.el.dataset.aiFieldId = id; });
      const options = Array.from(new Set(items.map(i => i.value))).filter(Boolean);
      if (options.length) schema.push({ id, type: 'checkbox', label: plain(groupLabel), options });
    }

    // Select listboxes and floating popup
    const listboxes = Array.from(document.querySelectorAll('[role="listbox"].jgvuAb, [role="listbox"].cGN2le, [role="listbox"].t9kgXb'));
    for (let idx = 0; idx < listboxes.length; idx++) {
      const lb = listboxes[idx];
      const wrap = getClosestListItem(lb);
      const label = getHeadingText(wrap) || 'Select';
      const id = `select_${idx}_${uid()}`;
      lb.dataset.aiFieldId = id;

      if (lb.getAttribute('aria-expanded') === 'false') lb.click();
      await sleep(10);

      let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m));
      let opts = popup
        ? Array.from(popup.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent))
        : Array.from(lb.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent));

      if (lb.getAttribute('aria-expanded') === 'true') lb.click();

      opts = opts.filter(v => v && !/^choose$/i.test(v));
      if (opts.length) schema.push({ id, type: 'select', label: plain(label), options: Array.from(new Set(opts)) });
    }

    document.querySelectorAll('input[type="date"].whsOnd').forEach((dateEl, idx) => {
      const wrap = getClosestListItem(dateEl);
      const label = getHeadingText(wrap) || 'Date';
      const id = `date_${idx}_${uid()}`;
      dateEl.dataset.aiFieldId = id;
      schema.push({
        id, type: 'date', label: plain(label),
        min: dateEl.getAttribute('min') || null,
        max: dateEl.getAttribute('max') || null
      });
    });

    document.querySelectorAll('.PfQ8Lb').forEach((timeWrap, idx) => {
      const wrap = getClosestListItem(timeWrap);
      const label = getHeadingText(wrap) || 'Time';
      const hour = timeWrap.querySelector('input[aria-label="Hour"]');
      const minute = timeWrap.querySelector('input[aria-label="Minute"]');
      const meridianBox = timeWrap.querySelector('[role="listbox"][aria-label="AM or PM"]');
      if (hour && minute && meridianBox) {
        const id = `time_${idx}_${uid()}`;
        hour.dataset.aiFieldId = `${id}_hour`;
        minute.dataset.aiFieldId = `${id}_minute`;
        meridianBox.dataset.aiFieldId = `${id}_ampm`;
        const options = Array.from(meridianBox.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent));
        schema.push({ id, type: 'time', label: plain(label), options: Array.from(new Set(options)) });
      }
    });

    return schema;
  }

  // main
  async function run() {
    const original = btn.textContent;
    btn.disabled = true; btn.textContent = '...';
    try {
      // Auto-remove previously set checkmarks (checkboxes) before running fill
      clearCheckboxesOnly();

      const schema = await scrapeSchema();
      if (!schema.length) throw new Error('No fields detected');

      const prompt = buildPrompt(schema, sessionExtraInstructions);
      // clear one-time instructions after use
      sessionExtraInstructions = '';
      btnEdit.style.background = '#444';

      const gemini = new GeminiClient();
      const txt = await gemini.generateContent(prompt);
      const plan = safeJson(txt);
      await fillFromPlan(plan);
      btn.textContent = '✓';
      await sleep(1200);
    } catch (e) {
      console.error(e);
      alert(e.message || 'AI fill failed');
    } finally {
      btn.disabled = false; btn.textContent = original;
    }
  }

  btn.addEventListener('click', run);
})();