Tutorial – Crie um Lançador por Voz com IA no Google Planilhas

Se você acompanhou o nosso tutorial clássico do “Lançador de Dados”, sabe como uma interface web simplifica a vida. Mas hoje, vamos levar essa ideia para o futuro. Chega de preencher campo por campo ou lutar com o teclado do celular.

Neste tutorial completo, vamos construir o Lançador IA de Elite. Você vai aprender a integrar o Google Gemini (a inteligência artificial do Google) diretamente na sua planilha. O resultado? Você apenas fala ou digita uma frase solta e a IA entende sozinha o que é a descrição, o valor e a categoria, organizando tudo na sua planilha em segundos. É o poder da IA generativa trabalhando para a sua produtividade, de forma 100% gratuita.

O que você vai aprender:

  • Integração com Google AI Studio: Como gerar e configurar sua chave de API para conectar sua planilha ao cérebro do Gemini.
  • Engenharia de Prompt para Apps Script: O segredo para criar instruções que fazem a IA devolver dados estruturados (JSON) sem erros.
  • Captura de Voz em Web Apps: O “pulo do gato” técnico para abrir um gravador externo e contornar os bloqueios de microfone do Apps Script, garantindo que o sistema funcione em qualquer celular.
  • Lógica de Filtro Inteligente: Como programar a IA para ignorar conversas aleatórias e focar apenas no que é despesa real, protegendo seu banco de dados.

Código.gs

const API_KEY = "xxxxxxxxxxxxxxxxxxxxx"; 

function doGet() {
  return HtmlService.createTemplateFromFile('Index')
      .evaluate()
      .setTitle('Lançador IA')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function interpretarTextoComIA(textoUsuario) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-preview:generateContent?key=${API_KEY}`;
  
  // Prompt aprimorado com instruções de bloqueio
  const prompt = `Analise a frase do usuário: "${textoUsuario}".
    1. Se a frase NÃO for um relato de gasto, despesa ou compra, retorne apenas: {"erro": "não_é_despesa"}.
    2. Se for uma despesa mas NÃO houver um valor numérico mencionado, retorne apenas: {"erro": "valor_ausente"}.
    3. Se for uma despesa válida, extraia para JSON. Categorias: [Mercado, Transporte, Lazer, Contas, Restaurante].
    Regra: A "descricao" deve começar com letra maiúscula.
    Retorne APENAS o JSON no formato: {"descricao": "string", "valor": number, "categoria": "string"}`;

  const payload = { 
    contents: [{ parts: [{ text: prompt }] }], 
    generationConfig: { temperature: 0.1, responseMimeType: "application/json" } 
  };
  
  const options = { 
    method: "post", 
    contentType: "application/json", 
    payload: JSON.stringify(payload), 
    muteHttpExceptions: true 
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const result = JSON.parse(response.getContentText());
    const extraido = JSON.parse(result.candidates[0].content.parts[0].text);
    
    // Retorna o objeto (seja ele de dados ou de erro)
    return extraido;
  } catch (e) { 
    throw new Error("A IA falhou ao processar."); 
  }
}

function lancarDespesa(dados) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const aba = ss.getSheets()[0];
    let valorLimpo = dados.valor.toString().replace(/[R$\s.]/g, '').replace(',', '.');
    const descFinal = dados.descricao.charAt(0).toUpperCase() + dados.descricao.slice(1);
    aba.appendRow([new Date(), descFinal, dados.categoria, parseFloat(valorLimpo)]);
    const ultimaLinha = aba.getLastRow();
    aba.getRange(ultimaLinha, 1).setNumberFormat("dd/mm/yyyy");
    aba.getRange(ultimaLinha, 4).setNumberFormat("R$ #,##0.00");
    return { success: true, message: "✨ Lançado com sucesso!" };
  } catch (e) { return { success: false, message: "Erro: " + e.message }; }
}

Index.html

<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <style>
    :root {
      --primary: #4f46e5;
      --bg: #f1f5f9;
      --header: #1e293b;
      --danger: #ef4444;
      --success: #10b981;
    }

    * {
      box-sizing: border-box;
    }

    body {
      font-family: 'Inter', sans-serif;
      background: var(--bg);
      margin: 0;
      padding: 20px;
    }

    .app-card {
      max-width: 450px;
      margin: 0 auto;
      background: white;
      border-radius: 20px;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }

    .app-header {
      background: var(--header);
      color: white;
      padding: 20px;
      text-align: center;
    }

    .app-header h2 {
      margin: 0;
      font-size: 1.1rem;
    }

    .content {
      padding: 20px;
    }

    /* Spinner de carregamento */
    .spinner {
      width: 20px;
      height: 20px;
      border: 3px solid rgba(255, 255, 255, 0.3);
      border-top: 3px solid white;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      display: inline-block;
      vertical-align: middle;
      margin-right: 10px;
    }

    @keyframes spin {
      0% {
        transform: rotate(0deg);
      }

      100% {
        transform: rotate(360deg);
      }
    }

    /* Barra Mágica */
    .magic-container {
      display: flex;
      align-items: center;
      background: #f8fafc;
      border: 2px solid #e2e8f0;
      border-radius: 12px;
      padding: 10px;
      margin-bottom: 15px;
    }

    .magic-container textarea {
      flex: 1;
      border: none;
      background: transparent;
      font-size: 1rem;
      outline: none;
      resize: none;
      font-family: inherit;
      font-weight: 500;
      height: 45px;
    }

    /* Microfone - Melhorado para ser Clicável */
    #micBtn {
      background: none;
      border: none;
      outline: none;
      padding: 10px;
      cursor: pointer;
      color: var(--primary);
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 50%;
      transition: 0.2s;
      position: relative;
      /* Garante que ele respeite a camada */
      z-index: 99;
      /* Joga o botão para a frente de tudo */
    }

    #micBtn:hover {
      background: #eef2ff;
    }

    #micBtn.active {
      color: var(--danger);
      animation: pulse 1s infinite;
      background: #fee2e2;
    }

    @keyframes pulse {
      0% {
        transform: scale(1);
      }

      50% {
        transform: scale(1.15);
      }

      100% {
        transform: scale(1);
      }
    }

    .btn-magic {
      width: 100%;
      padding: 14px;
      background: var(--primary);
      color: white;
      border: none;
      border-radius: 10px;
      font-weight: 700;
      cursor: pointer;
      margin-bottom: 20px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.9rem;
    }

    .btn-magic:disabled {
      opacity: 0.7;
      cursor: not-allowed;
    }

    /* Form */
    .form-group {
      margin-bottom: 15px;
    }

    .form-group label {
      display: block;
      font-size: 0.7rem;
      font-weight: 700;
      color: #64748b;
      text-transform: uppercase;
      margin-bottom: 5px;
    }

    .input-field {
      width: 100%;
      padding: 12px;
      border: 1px solid #e2e8f0;
      border-radius: 10px;
      font-size: 1rem;
      outline: none;
    }

    .btn-manual {
      width: 100%;
      padding: 16px;
      background: var(--header);
      color: white;
      border: none;
      border-radius: 12px;
      font-weight: 700;
      cursor: pointer;
      font-size: 1rem;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* Modal */
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(15, 23, 42, 0.8);
      backdrop-filter: blur(4px);
      display: none;
      align-items: center;
      justify-content: center;
      z-index: 1000;
      padding: 20px;
    }

    .modal {
      background: white;
      border-radius: 20px;
      padding: 25px;
      width: 100%;
      max-width: 350px;
    }

    .modal-data {
      background: #f8fafc;
      border-radius: 12px;
      padding: 15px;
      margin-bottom: 20px;
      border-left: 4px solid var(--primary);
      font-size: 0.9rem;
    }

    .btn-confirm {
      width: 100%;
      padding: 14px;
      background: var(--success);
      color: white;
      border: none;
      border-radius: 10px;
      font-weight: 700;
      cursor: pointer;
      margin-bottom: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .btn-row {
      display: flex;
      gap: 10px;
    }

    .btn-sec {
      flex: 1;
      padding: 12px;
      border-radius: 10px;
      border: none;
      font-weight: 600;
      cursor: pointer;
      font-size: 0.85rem;
      text-align: center;
    }

    .toast {
      position: fixed;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      padding: 14px 25px;
      border-radius: 50px;
      color: white;
      font-weight: 700;
      display: none;
      z-index: 2000;
      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
      white-space: nowrap;
    }
  </style>
</head>

<body>

  <div class="app-card">
    <div class="app-header">
      <h2>Gestor de Despesas</h2>
    </div>
    <div class="content">

      <label style="font-size: 0.7rem; font-weight: 800; color: var(--primary); text-transform: uppercase;">✨ Lançamento Inteligente</label>
      <div class="magic-container">
        <textarea id="textoIA" placeholder="Fale ou digite seu gasto..."></textarea>
        <!-- Mudança: microfone em um botão real para garantir clique -->
        <!-- Altere o onclick do botão no HTML -->
        <button id="micBtn" type="button" onclick="abrirGravadorExterno()">
          <span class="material-icons">mic</span>
        </button>
      </div>

      <button class="btn-magic" id="btnIA" onclick="processarComIA()">
      <span id="labelIA">Interpretar com IA</span>
    </button>

      <hr style="border: 0; border-top: 1px solid #eee; margin-bottom: 20px;">

      <form id="despesaForm">
        <div class="form-group">
          <label>Descrição</label>
          <input type="text" id="descricao" class="input-field" placeholder="Ex: Mercado">
        </div>
        <div style="display: flex; gap: 10px;">
          <div class="form-group" style="flex: 1;">
            <label>Valor</label>
            <input type="text" id="valor" class="input-field" placeholder="R$ 0,00" oninput="mascaraMoeda(this)">
          </div>
          <div class="form-group" style="flex: 1;">
            <label>Categoria</label>
            <select id="categoria" class="input-field">
            <option value="Outros">Outros</option>
            <option value="Mercado">Mercado</option>
            <option value="Transporte">Transporte</option>
            <option value="Lazer">Lazer</option>
            <option value="Contas">Contas</option>
            <option value="Contas">Restaurante</option>
          </select>
          </div>
        </div>
        <button type="button" id="btnManual" class="btn-manual" onclick="validarELancarManual()">
        <span id="labelManual">LANÇAR AGORA</span>
      </button>
      </form>
    </div>
  </div>

  <!-- Modal -->
  <div class="modal-overlay" id="modalConfirm">
    <div class="modal">
      <h3>Confirmar Lançamento?</h3>
      <div class="modal-data">
        <div id="mDesc" style="font-weight: 700; font-size: 1rem; margin-bottom: 5px;"></div>
        <div style="display: flex; justify-content: space-between; color: #64748b;">
          <span id="mCat"></span>
          <span id="mVal" style="color: var(--header); font-weight: 700;"></span>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn-confirm" id="btnConfirmFinal" onclick="finalizarLancamentoIA()">
        <span id="labelConfirmIA">SIM, PODE LANÇAR</span>
      </button>
        <div class="btn-row">
          <button class="btn-sec" style="background:#f1f5f9;" onclick="ajustarNoForm()">AJUSTAR</button>
          <button class="btn-sec" style="background:#fee2e2; color:var(--danger);" onclick="resetarTudo()">CANCELAR</button>
        </div>
      </div>
    </div>
  </div>

  <div id="toast" class="toast"></div>

  <script>
    let dadosIA = null;
  const moedaFmt = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });

  document.getElementById('textoIA').addEventListener('keydown', function(e) {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); processarComIA(); }
  });

  // MÁSCARA R$ 0,00
  function mascaraMoeda(i) {
    let v = i.value.replace(/\D/g,'');
    v = (v/100).toFixed(2) + '';
    v = v.replace(".", ",");
    v = v.replace(/(\d)(\d{3})(\d{3}),/g, "$1.$2.$3,");
    v = v.replace(/(\d)(\d{3}),/g, "$1.$2,");
    i.value = v !== "0,00" ? "R$ " + v : "";
  }

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition = null;
let isRecording = false;

if (SpeechRecognition) {
  recognition = new SpeechRecognition();
  recognition.lang = 'pt-BR';
  recognition.continuous = false;
  recognition.interimResults = false;

  recognition.onstart = () => { 
    isRecording = true;
    document.getElementById('micBtn').classList.add('active'); 
    showMsg("Ouvindo...", "#4f46e5");
  };

  recognition.onend = () => { 
    isRecording = false;
    document.getElementById('micBtn').classList.remove('active'); 
  };
  
  recognition.onresult = (event) => {
    const transcricao = event.results[0][0].transcript;
    document.getElementById('textoIA').value = transcricao;
    showMsg("Entendido!", "#10b981");
    // Opcional: Processar automaticamente após falar
    setTimeout(processarComIA, 500);
  };

  recognition.onerror = (event) => {
    isRecording = false;
    document.getElementById('micBtn').classList.remove('active');
    console.error("Erro Recognition:", event.error);
    
    if (event.error === 'not-allowed') {
      showMsg("Microfone bloqueado! Clique no cadeado na barra de endereços e permita o som.", "#ef4444");
    } else if (event.error === 'network') {
      showMsg("Erro de rede. Verifique sua conexão.", "#ef4444");
    }
  };
}

/**
 * FUNÇÃO PRINCIPAL DE ATIVAÇÃO
 * Resolve o problema de permissão no Android e Desktop
 */
async function toggleVoz() {
  if (!recognition) return showMsg("Navegador sem suporte a voz", "#ef4444");

  const btn = document.getElementById('micBtn');
  if (btn.classList.contains('active')) {
    recognition.stop();
    return;
  }

  try {
    recognition.start();
  } catch (e) {
    console.error("Erro ao iniciar:", e);
    // Se o erro for 'not-allowed', o Google está bloqueando o Iframe.
    if (e.name === 'NotAllowedError' || e.message.includes('not-allowed')) {
       showMsg("Bloqueio de Segurança do Google. Use a Solução do Popup abaixo.", "#ef4444");
    }
  }
}

  // --- LÓGICA DE PROCESSAMENTO IA ---
  function processarComIA() {
  const txtField = document.getElementById('textoIA');
  const txt = txtField.value;
  if (!txt) return;

  const btn = document.getElementById('btnIA');
  const label = document.getElementById('labelIA');
  
  btn.disabled = true;
  label.innerHTML = '<div class="spinner"></div> IA Analisando...';

  google.script.run
    .withSuccessHandler(res => {
      btn.disabled = false;
      label.innerHTML = 'Interpretar com IA';

      // --- VALIDAÇÃO COM LIMPEZA DE CAMPO ---
      if (res.erro) {
        txtField.value = ''; // Limpa o campo se a IA não entender
        if (res.erro === "não_é_despesa") {
          showMsg("❌ Isso não parece ser uma despesa.", "#ef4444");
        } else if (res.erro === "valor_ausente") {
          showMsg("💰 Qual o valor? Não identifiquei o preço.", "#f59e0b");
        }
        return;
      }

      if (!res.valor || res.valor <= 0) {
        txtField.value = ''; // Limpa se o valor vier zerado
        showMsg("⚠️ Valor inválido para lançamento.", "#f59e0b");
        return;
      }

      dadosIA = res;
      abrirModal(res);
    })
    .withFailureHandler(() => {
      showMsg("IA falhou!", "#ef4444");
      label.innerHTML = 'Interpretar com IA';
      btn.disabled = false;
    })
    .interpretarTextoComIA(txt);
}

  function abrirModal(d) {
    document.getElementById('mDesc').innerText = d.descricao;
    document.getElementById('mVal').innerText = moedaFmt.format(d.valor);
    document.getElementById('mCat').innerText = d.categoria;
    document.getElementById('modalConfirm').style.display = 'flex';
  }

  function ajustarNoForm() {
    document.getElementById('descricao').value = dadosIA.descricao;
    const vField = document.getElementById('valor');
    vField.value = dadosIA.valor.toFixed(2).replace('.', ',');
    mascaraMoeda(vField);
    document.getElementById('categoria').value = dadosIA.categoria;
    document.getElementById('modalConfirm').style.display = 'none';
  }

  function resetarTudo() {
    document.getElementById('despesaForm').reset();
    document.getElementById('textoIA').value = '';
    document.getElementById('modalConfirm').style.display = 'none';
    dadosIA = null;
  }

  function finalizarLancamentoIA() {
    const btn = document.getElementById('btnConfirmFinal');
    const label = document.getElementById('labelConfirmIA');
    btn.disabled = true;
    label.innerHTML = '<div class="spinner"></div> Gravando...';

    google.script.run
      .withSuccessHandler(res => {
        showMsg(res.message, "#10b981");
        resetarTudo();
        label.innerHTML = 'SIM, PODE LANÇAR';
        btn.disabled = false;
      })
      .lancarDespesa(dadosIA);
  }

  // --- LANÇAMENTO MANUAL (COM SPINNER) ---
  function validarELancarManual() {
    const descField = document.getElementById('descricao');
    const val = document.getElementById('valor').value;
    const cat = document.getElementById('categoria').value;

    if (!descField.value || !val || val === "R$ 0,00") return showMsg("Preencha Descrição e Valor!", "#f59e0b");

    // Força a primeira letra maiúscula no manual também
    descField.value = descField.value.charAt(0).toUpperCase() + descField.value.slice(1);

    const btn = document.getElementById('btnManual');
    const label = document.getElementById('labelManual');
    
    // Ativa o visual de processamento
    btn.disabled = true;
    label.innerHTML = '<div class="spinner"></div> Gravando...';

    google.script.run
      .withSuccessHandler(res => {
        showMsg(res.message, "#10b981");
        resetarTudo();
        label.innerHTML = 'LANÇAR AGORA';
        btn.disabled = false;
      })
      .withFailureHandler(err => {
        showMsg("Erro ao gravar", "#ef4444");
        label.innerHTML = 'LANÇAR AGORA';
        btn.disabled = false;
      })
      .lancarDespesa({
        descricao: descField.value,
        valor: val,
        categoria: cat
      });
  }

let toastTimer = null; 

function showMsg(t, c) {
  const toast = document.getElementById('toast');
  
  if (toastTimer) {
    clearTimeout(toastTimer);
  }

  toast.innerText = t; 
  toast.style.backgroundColor = c;
  toast.style.display = 'block';
  
  let tempo = 3000;
  if (c === "#ef4444") tempo = 5000;
  if (c === "#f59e0b") tempo = 4000;
  
  toastTimer = setTimeout(() => {
    toast.style.display = 'none';
    toastTimer = null; // Limpa a variável após esconder
  }, tempo);
}

function abrirGravadorExterno() {
    const largura = 380;
    const altura = 500;
    const esquerda = (screen.width / 2) - (largura / 2);
    const topo = (screen.height / 2) - (altura / 2);
    
    const win = window.open('', 'GravadorElite', `width=${largura},height=${altura},left=${esquerda},top=${topo}`);

    const htmlElite = `
      <!DOCTYPE html>
      <html lang="pt-br">
      <head>
        <meta charset="UTF-8">
        <!-- CRUCIAL PARA MOBILE: Força o navegador a renderizar no tamanho correto na hora -->
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <title>Ouvindo...</title>
        <style>
          /* Força 100% da tela para evitar o erro de 'canto superior esquerdo' */
          html, body { 
            width: 100%; 
            height: 100%; 
            margin: 0; 
            padding: 0; 
            background-color: #1e293b;
            overflow: hidden;
          }
          
          body { 
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            color: #ffffff;
            display: flex; 
            flex-direction: column; 
            align-items: center; 
            justify-content: center;
          }
          
          .container { text-align: center; width: 100%; }

          .mic-box {
            position: relative;
            width: 100px; height: 100px;
            margin: 0 auto 30px;
            display: flex; align-items: center; justify-content: center;
          }

          .mic-button {
            position: relative;
            z-index: 5;
            width: 85px; height: 85px;
            background-color: #4f46e5;
            border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            box-shadow: 0 8px 20px rgba(0,0,0,0.3);
          }

          .mic-icon { width: 40px; height: 40px; fill: white; }

          .pulse {
            position: absolute;
            width: 80px; height: 80px;
            background-color: #4f46e5;
            border-radius: 50%;
            opacity: 0.6;
            animation: ripple 2s infinite ease-out;
          }
          .pulse2 { animation-delay: 1s; }

          @keyframes ripple {
            0% { transform: scale(1); opacity: 0.6; }
            100% { transform: scale(2.8); opacity: 0; }
          }

          h2 { font-size: 1.4rem; font-weight: 700; margin: 0 0 10px; color: #fff; }
          p { color: #94a3b8; font-size: 1rem; margin: 0; padding: 0 30px; line-height: 1.5; min-height: 3em; }

          .status-label {
            position: absolute; bottom: 40px;
            font-size: 0.75rem; font-weight: 700;
            text-transform: uppercase; letter-spacing: 2px;
            color: #818cf8;
          }
        </style>
      </head>
      <body>
        <div class="container">
          <div class="mic-box">
            <div class="pulse"></div>
            <div class="pulse pulse2"></div>
            <div class="mic-button">
              <svg class="mic-icon" viewBox="0 0 24 24">
                <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
                <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
              </svg>
            </div>
          </div>
          <h2>IA Elite Voz</h2>
          <p id="feedback">Pode falar, estou ouvindo...</p>
          <div class="status-label">Ouvindo Áudio</div>
        </div>

        <script>
          const feedback = document.getElementById('feedback');
          const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
          
          if (!SpeechRecognition) {
            feedback.innerText = "Seu navegador não suporta voz.";
          } else {
            const recognition = new SpeechRecognition();
            recognition.lang = 'pt-BR';
            
            recognition.onresult = (event) => {
              const texto = event.results[0][0].transcript;
              feedback.innerText = '"' + texto + '"';
              feedback.style.color = "#10b981";
              
              if (window.opener) {
                window.opener.postMessage({tipo: 'VOZ_CAPTADA', texto: texto}, '*');
                setTimeout(() => window.close(), 1000);
              }
            };

            recognition.onerror = () => {
              feedback.innerText = "Erro ao captar áudio.";
              feedback.style.color = "#ef4444";
              setTimeout(() => window.close(), 2000);
            };

            recognition.onend = () => {
              setTimeout(() => {
                if(feedback.innerText === "Pode falar, estou ouvindo...") window.close();
              }, 4000);
            };

            recognition.start();
          }
        <\/script>
      </body>
      </html>
    `;

    win.document.open();
    win.document.write(htmlElite);
    win.document.close();
}

  // Escuta a resposta da janela de voz
  window.addEventListener('message', function(event) {
    if (event.data && event.data.tipo === 'VOZ_CAPTADA') {
      document.getElementById('textoIA').value = event.data.texto;
      showMsg("Voz captada!", "#10b981");
      setTimeout(processarComIA, 500);
    }
  });
  </script>
</body>

</html>

Acompanhe nas redes sociais

Projetos Personalizados

Precisa de uma solução sob medida?

© 2025 Transformando Planilhas. Todos os direitos reservados.