Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active May 24, 2025 20:13
Show Gist options
  • Save celsowm/8fe24083a9e068cb2b51f7143faac3ff to your computer and use it in GitHub Desktop.
Save celsowm/8fe24083a9e068cb2b51f7143faac3ff to your computer and use it in GitHub Desktop.
OPEN EXCELLM
<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Leitor & Editor de Planilhas XLSX + AI</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/handsontable.full.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/handsontable.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/languages/pt-BR.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hyperformula.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
body {
background: #f8f9fa;
}
header {
border-bottom: 1px solid #dee2e6;
}
#spreadsheet {
border: 1px solid #ced4da;
box-shadow: 0 2px 4px rgba(0, 0, 0, .04);
overflow: auto;
}
:root {
--excel-border: #d0d0d0;
--excel-row-alt: #fafafa;
--excel-header-bg: #f3f6ff;
}
.ht_master .htCore td,
.ht_master .htCore th,
.ht_clone_top th,
.ht_clone_left th {
border: 1px solid var(--excel-border);
box-sizing: border-box;
}
.ht_master .htCore tr:nth-child(even) td {
background-color: var(--excel-row-alt);
}
.ht_clone_top th,
.ht_clone_left th {
background: var(--excel-header-bg);
font-weight: 600;
}
#formulaSuggestions {
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
display: none;
background-color: white;
border: 1px solid #ced4da;
border-top: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, .1);
z-index: 1000;
}
#formulaSuggestions .list-group-item {
padding: 8px 15px;
cursor: pointer;
border: none;
border-bottom: 1px solid #eee;
}
#formulaSuggestions .list-group-item:last-child {
border-bottom: none;
}
#formulaSuggestions .list-group-item:hover {
background-color: #f8f9fa;
}
.formula-cell {
background-color: #e6f7ff;
font-family: 'Courier New', monospace;
color: #333;
white-space: pre;
}
.formula-function {
color: #800080;
font-weight: bold;
}
.formula-reference {
color: #008000;
}
.formula-operator {
color: #ff4500;
}
.formula-string {
color: #a52a2a;
}
.formula-number {
color: #1a1aa6;
}
</style>
</head>
<body>
<header class="py-3 bg-white sticky-top">
<div class="container-xl">
<h1 class="h4 mb-3"><i class="fa-solid fa-table me-1"></i> Leitor & Editor de Planilhas XLSX</h1>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<label class="btn btn-outline-secondary mb-0">
<i class="fa-solid fa-file-import me-1"></i> Abrir XLSX
<input type="file" id="fileInput" accept=".xlsx" hidden>
</label>
<button id="saveButton" class="btn btn-primary" disabled>
<i class="fa-solid fa-floppy-disk me-1"></i> Salvar Planilha
</button>
<div class="input-group input-group-sm" style="max-width:450px;">
<span class="input-group-text"><i class="fa-solid fa-server"></i></span>
<input type="text" id="apiEndpointInput" class="form-control" placeholder="Endpoint da API Gemini"
value="https://generativelanguage.googleapis.com/v1beta/openai/chat/completions">
</div>
<div class="input-group input-group-sm" style="max-width:340px;">
<span class="input-group-text"><i class="fa-solid fa-key"></i></span>
<input type="password" id="apiKeyInput" class="form-control" placeholder="Bearer Gemini API Key">
</div>
<span id="fileName" class="text-muted ms-auto small"></span>
</div>
<input type="text" id="formulaBar" class="form-control form-control-sm"
placeholder="Barra de fórmulas e dados (Enter para aplicar)">
<div id="formulaSuggestions" class="list-group"></div>
</div>
</header>
<main class="container-xl my-4">
<div id="spreadsheet"></div>
</main>
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999;">
<div id="toastSaved" class="toast align-items-center text-bg-success border-0" role="alert">
<div class="d-flex">
<div class="toast-body"><i class="fa-solid fa-circle-check me-1"></i> Planilha salva!</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Configuração e variáveis
let GEMINI_API_KEY = localStorage.getItem('gemini_api_key') || '';
let GEMINI_API_ENDPOINT = localStorage.getItem('gemini_api_endpoint') || 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; // Endpoint padrão atualizado
// MODIFIED: Updated AI_REGEX to support single cell references (e.g., A1) as well as ranges (A1:B2)
const AI_REGEX = /^\s*=AI\(\s*"(.*?)"(?:,\s*([A-Z]+\d+(?::[A-Z]+\d+)?))?\s*\)\s*$/i;
let hot = null, workbookData = null;
let lastSelectedCell = null;
// Elementos
const container = document.getElementById('spreadsheet');
const fileInput = document.getElementById('fileInput');
const saveButton = document.getElementById('saveButton');
const fileNameUI = document.getElementById('fileName');
const apiKeyInput = document.getElementById('apiKeyInput');
const apiEndpointInput = document.getElementById('apiEndpointInput');
const formulaBar = document.getElementById('formulaBar');
const toast = new bootstrap.Toast(document.getElementById('toastSaved'));
const formulaSuggestions = document.getElementById('formulaSuggestions');
const functionNames = [
'SOMA', 'MÉDIA', 'SE', 'MÁXIMO', 'MÍNIMO', 'CONTAR',
'CONCATENAR', 'ESQUERDA', 'DIREITA', 'EXT.TEXTO', 'MAIÚSCULA',
'MINÚSCULA', 'PRI.MAIÚSCULA', 'NÚM.CARACT', 'ARRUMAR',
'DATA', 'HOJE', 'AGORA', 'ANO', 'MÊS', 'DIA',
'AI'
];
const commonFormulas = functionNames.map(name => name + '(');
if (GEMINI_API_KEY) apiKeyInput.value = GEMINI_API_KEY;
apiKeyInput.addEventListener('input', e => {
GEMINI_API_KEY = e.target.value.trim();
if (GEMINI_API_KEY) localStorage.setItem('gemini_api_key', GEMINI_API_KEY);
else localStorage.removeItem('gemini_api_key');
});
if (GEMINI_API_ENDPOINT) apiEndpointInput.value = GEMINI_API_ENDPOINT;
apiEndpointInput.addEventListener('input', e => {
GEMINI_API_ENDPOINT = e.target.value.trim();
if (GEMINI_API_ENDPOINT) localStorage.setItem('gemini_api_endpoint', GEMINI_API_ENDPOINT);
else localStorage.removeItem('gemini_api_endpoint');
});
function formatDataForLLM(dataArray) {
if (!dataArray || dataArray.length === 0) return "";
return dataArray.map(row => row.join(', ')).join('\n');
}
function cellStringToCoords(cellStr) {
const colMatch = cellStr.match(/[A-Z]+/);
const rowMatch = cellStr.match(/\d+/);
if (!colMatch || !rowMatch) throw new Error("Formato de célula inválido: " + cellStr);
const colStr = colMatch[0];
const rowNum = parseInt(rowMatch[0], 10);
let col = 0;
for (let i = 0; i < colStr.length; i++) {
col = col * 26 + (colStr.charCodeAt(i) - 'A'.charCodeAt(0) + 1);
}
return { row: rowNum - 1, col: col - 1 };
}
function coordsToCellString(row, col) { // 0-indexed row, col
let colStr = '';
let tempCol = col + 1; // Convert 0-indexed col to 1-indexed for calculation
while (tempCol > 0) {
let remainder = (tempCol - 1) % 26;
colStr = String.fromCharCode('A'.charCodeAt(0) + remainder) + colStr;
tempCol = Math.floor((tempCol - 1) / 26);
}
return colStr + (row + 1); // Convert 0-indexed row to 1-indexed row number
}
async function callGeminiAPI(promptText, rangeString = null) {
if (!GEMINI_API_KEY) {
console.error('#CHAVE? - Chave da API não configurada.');
return '#CHAVE?';
}
if (!GEMINI_API_ENDPOINT) {
console.error('#ENDPOINT? - Endpoint da API não configurado.');
return '#ENDPOINT?';
}
let systemInstruction = "Sua resposta deve ser extremamente concisa, objetiva e caber em uma única célula. Forneça apenas a resposta direta sem introduções, conclusões ou explicações detalhadas. Foque na solicitação principal.";
let fullPrompt = promptText;
if (rangeString && hot) {
try {
let startRow, startCol, endRow, endCol;
if (Handsontable && Handsontable.utils && Handsontable.utils.cell && typeof Handsontable.utils.cell.rangeStringToCellCoords === 'function') {
// MODIFIED: Ensure rangeString is in 'A1:B2' format for rangeStringToCellCoords if it's a single cell
let tempRangeStr = rangeString;
if (rangeString && !rangeString.includes(':')) { // Check if rangeString is not null/empty before .includes
tempRangeStr = `${rangeString}:${rangeString}`;
}
const coords = Handsontable.utils.cell.rangeStringToCellCoords(tempRangeStr);
startRow = coords[0]; startCol = coords[1]; endRow = coords[2]; endCol = coords[3];
} else {
console.warn("Handsontable.utils.cell.rangeStringToCellCoords não disponível, usando parser manual para o range.");
const parts = rangeString.split(':');
let startCellCoords, endCellCoords;
if (parts.length === 1) {
startCellCoords = cellStringToCoords(parts[0]);
endCellCoords = startCellCoords;
} else if (parts.length === 2) {
startCellCoords = cellStringToCoords(parts[0]);
endCellCoords = cellStringToCoords(parts[1]);
} else {
throw new Error("Formato de range inválido para parser manual: " + rangeString);
}
startRow = Math.min(startCellCoords.row, endCellCoords.row);
startCol = Math.min(startCellCoords.col, endCellCoords.col);
endRow = Math.max(startCellCoords.row, endCellCoords.row);
endCol = Math.max(startCellCoords.col, endCellCoords.col);
}
const rangeData = hot.getData(startRow, startCol, endRow, endCol);
fullPrompt += `\n\nDados da planilha (formato linha, coluna): \n${formatDataForLLM(rangeData)}`;
} catch (e) {
console.error('Erro ao processar o range da fórmula AI:', e);
return '#ERRO RANGE';
}
}
try {
let response;
let result;
let apiUrl = GEMINI_API_ENDPOINT;
let payload;
let headers = { 'Content-Type': 'application/json' };
if (GEMINI_API_ENDPOINT.includes('openai/chat/completions') || GEMINI_API_ENDPOINT.includes('openai.com')) {
payload = {
model: 'gemini-2.0-flash',
messages: [
{ role: "system", content: systemInstruction },
{ role: "user", content: fullPrompt }
]
};
headers['Authorization'] = `Bearer ${GEMINI_API_KEY}`;
} else {
payload = {
contents: [{ role: "user", parts: [{ text: fullPrompt }] }],
generationConfig: {}
};
if (!payload.system_instruction) {
payload.contents[0].parts[0].text = systemInstruction + "\n\nPROMPT DO USUÁRIO:\n" + payload.contents[0].parts[0].text;
}
apiUrl = `${GEMINI_API_ENDPOINT}?key=${GEMINI_API_KEY}`;
}
response = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Erro na API (${response.status}): ${errorBody}`);
return `#ERRO API ${response.status}`;
}
result = await response.json();
if (GEMINI_API_ENDPOINT.includes('openai/chat/completions') || GEMINI_API_ENDPOINT.includes('openai.com')) {
if (result.choices && result.choices.length > 0 && result.choices[0].message && result.choices[0].message.content) {
return result.choices[0].message.content;
} else if (result.choices && result.choices[0].text) {
return result.choices[0].text;
}
} else {
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
return result.candidates[0].content.parts[0].text;
}
}
console.error('Estrutura de resposta inesperada da API:', result);
return '#ERRO RESPOSTA';
} catch (err) {
console.error('Erro ao chamar a API:', err);
return '#ERRO API';
}
}
fileInput.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
const data = new Uint8Array(await file.arrayBuffer());
workbookData = XLSX.read(data, { type: 'array' });
const sheetName = workbookData.SheetNames[0];
const worksheet = workbookData.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false, defval: null });
initHot(jsonData);
fileNameUI.textContent = file.name;
saveButton.removeAttribute('disabled');
} catch (err) {
console.error(err);
console.error('Erro ao carregar o arquivo XLSX.');
}
});
function formulaRenderer(instance, TD, row, col, prop, value, cellProperties) {
Handsontable.renderers.TextRenderer.apply(this, arguments);
if (typeof value === 'string' && value.startsWith('=')) {
TD.classList.add('formula-cell');
let formulaContent = value.substring(1);
const highlightPatterns = [
{ regex: /"(.*?)"/g, className: 'formula-string' },
{ regex: new RegExp('\\b(' + functionNames.join('|') + ')\\b', 'gi'), className: 'formula-function' },
{ regex: /[A-Z]+\d+(?::[A-Z]+\d+)?/g, className: 'formula-reference' },
{ regex: /[+\-*\/\^=<>!&%]/g, className: 'formula-operator' },
{ regex: /\b\d+(\.\d+)?\b/g, className: 'formula-number' },
];
let processedContent = formulaContent;
highlightPatterns.forEach(pattern => {
processedContent = processedContent.replace(pattern.regex, (match) => {
if (match.includes('<span class="formula-')) return match; // Avoid double wrapping
return `<span class="${pattern.className}">${match}</span>`;
});
});
TD.innerHTML = `<span class="formula-operator">=</span>${processedContent}`;
} else {
TD.classList.remove('formula-cell', 'formula-function', 'formula-reference', 'formula-operator', 'formula-string', 'formula-number');
}
}
function initHot(data) {
if (!hot) {
hot = new Handsontable(container, {
data: data,
rowHeaders: true,
colHeaders: true,
language: 'pt-BR',
licenseKey: 'non-commercial-and-evaluation',
formulas: { engine: HyperFormula },
stretchH: 'all',
height: 'auto',
minRows: 200,
minCols: 26,
collapsibleColumns: true,
manualColumnResize: true,
autoFill: true, // autoFill must be enabled
cells: function (row, col, prop) {
const cellProperties = {};
cellProperties.renderer = formulaRenderer;
return cellProperties;
},
contextMenu: {
items: {
'undo': { name: 'Desfazer' },
'redo': { name: 'Refazer' },
'separator1': Handsontable.plugins.ContextMenu.SEPARATOR,
'row_above': { name: 'Inserir linha acima' },
'row_below': { name: 'Inserir linha abaixo' },
'col_left': { name: 'Inserir coluna à esquerda' },
'col_right': { name: 'Inserir coluna à direita' },
'remove_row': { name: 'Remover linha(s)' },
'remove_col': { name: 'Remover coluna(s)' },
}
}
});
let activeCellEditor = null;
hot.addHook('afterBeginEditing', function (row, col) {
const editor = hot.getActiveEditor();
const cellMeta = hot.getCellMeta(row, col);
const aiFormula = cellMeta ? cellMeta.aiFormula : undefined;
if (editor && aiFormula) {
if (editor.TEXTAREA) {
editor.TEXTAREA.value = aiFormula;
editor.TEXTAREA.select();
} else if (editor.input) {
editor.input.value = aiFormula;
editor.input.select();
}
formulaBar.value = aiFormula;
}
if (editor && editor.TEXTAREA) {
activeCellEditor = editor.TEXTAREA;
activeCellEditor.addEventListener('input', updateFormulaBarFromCell);
} else if (editor && editor.input) {
activeCellEditor = editor.input;
activeCellEditor.addEventListener('input', updateFormulaBarFromCell);
}
});
hot.addHook('afterEndEditing', function (row, col, newValue) {
if (activeCellEditor) {
activeCellEditor.removeEventListener('input', updateFormulaBarFromCell);
activeCellEditor = null;
}
syncFormulaBar();
});
function updateFormulaBarFromCell() {
if (activeCellEditor) {
formulaBar.value = activeCellEditor.value;
}
}
const syncFormulaBar = () => {
const sel = hot.getSelectedLast();
if (sel) {
const [r, c] = sel;
let cellMeta = hot.getCellMeta(r, c);
if (cellMeta && cellMeta.aiFormula) {
formulaBar.value = cellMeta.aiFormula;
} else {
let formula = hot.getSourceDataAtCell ? hot.getSourceDataAtCell(r, c) : null;
let value = hot.getDataAtCell(r, c);
if (typeof formula === 'string' && formula.trim().startsWith('=')) {
formulaBar.value = formula;
} else if (typeof value === 'string' && value.trim().startsWith('=')) {
formulaBar.value = value;
} else {
formulaBar.value = (value === null || value === undefined) ? '' : value;
}
}
lastSelectedCell = [r, c];
}
};
hot.addHook('afterSelectionEnd', syncFormulaBar);
formulaBar.addEventListener('input', () => {
const text = formulaBar.value;
if (text.startsWith('=')) {
const partialFormula = text.substring(1).toUpperCase();
const filteredSuggestions = commonFormulas.filter(formula =>
formula.toUpperCase().startsWith(partialFormula)
);
formulaSuggestions.innerHTML = '';
if (filteredSuggestions.length > 0 && partialFormula.length > 0) {
filteredSuggestions.forEach(suggestion => {
const item = document.createElement('button');
item.classList.add('list-group-item', 'list-group-item-action');
item.textContent = suggestion;
item.addEventListener('click', () => {
formulaBar.value = '=' + suggestion;
formulaSuggestions.style.display = 'none';
formulaBar.focus();
});
formulaSuggestions.appendChild(item);
});
formulaSuggestions.style.display = 'block';
} else {
formulaSuggestions.style.display = 'none';
}
} else {
formulaSuggestions.style.display = 'none';
}
});
formulaBar.addEventListener('blur', (e) => {
setTimeout(() => {
if (!formulaSuggestions.contains(document.activeElement)) {
formulaSuggestions.style.display = 'none';
}
}, 100);
});
formulaBar.addEventListener('keydown', e => {
if (e.key === 'Enter') {
formulaSuggestions.style.display = 'none';
e.preventDefault();
e.stopPropagation();
if (lastSelectedCell) {
const [r, c] = lastSelectedCell;
const newValue = formulaBar.value;
hot.setDataAtCell(r, c, newValue);
hot.render();
formulaBar.blur();
hot.selectCell(r, c);
} else {
console.warn('Nenhuma célula selecionada para aplicar a fórmula.');
}
}
});
hot.addHook('afterChange', async (changes, src) => {
if (!changes || src === 'loadData' || src === 'aiProcessing') return;
for (const change of changes) {
const [r, c, oldVal, newVal] = change;
let cellMeta = hot.getCellMeta(r, c);
if (typeof newVal === 'string') {
const match = newVal.match(AI_REGEX);
if (match) {
const promptText = match[1];
const rangeString = match[2] || null;
hot.setCellMeta(r, c, 'aiFormula', newVal);
hot.setDataAtCell(r, c, '⏳ AI…', 'aiProcessing');
const answer = await callGeminiAPI(promptText, rangeString);
hot.setDataAtCell(r, c, answer, 'aiProcessing');
} else {
if (cellMeta && cellMeta.aiFormula) {
hot.removeCellMeta(r, c, 'aiFormula');
}
}
} else {
if (cellMeta && cellMeta.aiFormula) {
hot.removeCellMeta(r, c, 'aiFormula');
}
}
}
});
// NEW: Hook for handling AI formulas during autofill
hot.addHook('afterAutofill', function (fillData, sourceRange, targetRange, direction) {
console.log('[AfterAutofill] Triggered. Direction:', direction);
console.log('[AfterAutofill] Source Range:', sourceRange, 'Target Range:', targetRange);
const sourceStartRow = sourceRange.from.row;
const sourceStartCol = sourceRange.from.col;
const sourceEndRow = sourceRange.to.row;
const sourceEndCol = sourceRange.to.col;
const targetStartRow = targetRange.from.row;
const targetStartCol = targetRange.from.col;
const targetEndRow = targetRange.to.row;
const targetEndCol = targetRange.to.col;
const changesToProcess = [];
for (let r = targetStartRow; r <= targetEndRow; r++) {
for (let c = targetStartCol; c <= targetEndCol; c++) {
console.log(`[AfterAutofill] Processing target cell visual (row ${r + 1}, col ${String.fromCharCode(65 + c)}), 0-indexed (${r},${c})`);
const isSourceCell = (r >= sourceStartRow && r <= sourceEndRow &&
c >= sourceStartCol && c <= sourceEndCol);
if (isSourceCell) {
console.log(`[AfterAutofill] Cell (${r},${c}) is a source cell. Skipping.`);
continue;
}
const relRowInSourceBlock = (r - sourceStartRow) % (sourceEndRow - sourceStartRow + 1);
const relColInSourceBlock = (c - sourceStartCol) % (sourceEndCol - sourceStartCol + 1);
const originalSourceRow = sourceStartRow + relRowInSourceBlock;
const originalSourceCol = sourceStartCol + relColInSourceBlock;
console.log(`[AfterAutofill] Corresponding original source cell for (${r},${c}) is (${originalSourceRow},${originalSourceCol})`);
const sourceMeta = hot.getCellMeta(originalSourceRow, originalSourceCol);
if (sourceMeta && sourceMeta.aiFormula) {
let originalAiFormula = sourceMeta.aiFormula;
console.log(`[AfterAutofill] Original AI Formula from sourceMeta (${originalSourceRow},${originalSourceCol}): '${originalAiFormula}'`);
let adjustedAiFormula = originalAiFormula; // Default to original, will be updated if possible
const aiMatch = originalAiFormula.match(AI_REGEX);
if (aiMatch) {
console.log('[AfterAutofill] AI_REGEX match successful. Matches:', aiMatch);
const prompt = aiMatch[1];
let rangeArg = aiMatch[2] ? aiMatch[2].trim() : null;
console.log(`[AfterAutofill] Extracted prompt: "${prompt}", Extracted rangeArg: "${rangeArg}"`);
if (rangeArg) {
console.log('[AfterAutofill] Attempting to adjust range argument:', rangeArg);
let newAdjustedRange = rangeArg; // Initialize with original, update if adjustment is successful
try {
const rowShift = r - originalSourceRow;
const colShift = c - originalSourceCol;
console.log(`[AfterAutofill] Calculated shifts: rowShift=${rowShift}, colShift=${colShift}`);
const useHandsontableUtils = Handsontable && Handsontable.utils && Handsontable.utils.cell &&
typeof Handsontable.utils.cell.rangeStringToCellCoords === 'function' &&
typeof Handsontable.utils.cell.cellCoordsToRangeString === 'function';
if (useHandsontableUtils) {
console.log('[AfterAutofill] Using Handsontable utils for range adjustment.');
let tempRangeStr = rangeArg;
const isSingleCell = !rangeArg.includes(':');
if (isSingleCell) {
tempRangeStr = `${rangeArg}:${rangeArg}`; // Needs to be A1:A1 for rangeStringToCellCoords
}
const coords = Handsontable.utils.cell.rangeStringToCellCoords(tempRangeStr);
const newStartRow = coords[0] + rowShift;
const newStartCol = coords[1] + colShift;
const newEndRow = coords[2] + rowShift;
const newEndCol = coords[3] + colShift;
if (newStartRow >= 0 && newStartCol >= 0 && newEndRow >= 0 && newEndCol >= 0) {
if (isSingleCell) {
newAdjustedRange = Handsontable.utils.cell.cellCoordsToRangeString(newStartRow, newStartCol);
} else {
newAdjustedRange = Handsontable.utils.cell.cellCoordsToRangeString(newStartRow, newStartCol, newEndRow, newEndCol);
}
} else {
console.warn(`[AfterAutofill] Adjusted range (Handsontable utils) for AI formula resulted in invalid coordinates for cell (${r},${c}). Using original range: ${rangeArg}`);
// newAdjustedRange remains original rangeArg
}
} else { // Fallback to custom parser
console.warn("[AfterAutofill] Handsontable cell utils not available. Using custom parser for range adjustment.");
const isSingleCell = !rangeArg.includes(':');
if (isSingleCell) {
const startCellCoords = cellStringToCoords(rangeArg); // {row, col} 0-indexed
const newStartRow = startCellCoords.row + rowShift;
const newStartCol = startCellCoords.col + colShift;
if (newStartRow >= 0 && newStartCol >= 0) {
newAdjustedRange = coordsToCellString(newStartRow, newStartCol);
} else {
console.warn(`[AfterAutofill] Adjusted single cell (custom parser) resulted in invalid coords for cell (${r},${c}). Using original range: ${rangeArg}`);
// newAdjustedRange remains original rangeArg
}
} else { // Range like A1:B2
const parts = rangeArg.split(':');
const startCellCoords = cellStringToCoords(parts[0]);
const endCellCoords = cellStringToCoords(parts[1]);
const newStartRow = startCellCoords.row + rowShift;
const newStartCol = startCellCoords.col + colShift;
const newEndRow = endCellCoords.row + rowShift;
const newEndCol = endCellCoords.col + colShift;
if (newStartRow >= 0 && newStartCol >= 0 && newEndRow >= 0 && newEndCol >= 0) {
newAdjustedRange = `${coordsToCellString(newStartRow, newStartCol)}:${coordsToCellString(newEndRow, newEndCol)}`;
} else {
console.warn(`[AfterAutofill] Adjusted range (custom parser) resulted in invalid coords for cell (${r},${c}). Using original range: ${rangeArg}`);
// newAdjustedRange remains original rangeArg
}
}
}
console.log(`[AfterAutofill] Final adjusted range string for this iteration: '${newAdjustedRange}'`);
adjustedAiFormula = `=AI("${prompt}", ${newAdjustedRange})`;
} catch (e) {
console.error(`[AfterAutofill] Error adjusting AI formula range for cell (${r},${c}):`, e, "Original range:", rangeArg);
// adjustedAiFormula remains originalAiFormula if an error occurs during adjustment attempt
}
} else { // No rangeArg in the original formula, e.g. =AI("prompt")
console.log('[AfterAutofill] No rangeArg to adjust. Using original formula (which had no range).');
// adjustedAiFormula remains originalAiFormula, which is correct
}
} else { // aiMatch failed on originalAiFormula
console.warn(`[AfterAutofill] Original AI formula '${originalAiFormula}' did NOT match AI_REGEX.`);
// adjustedAiFormula remains originalAiFormula
}
console.log(`[AfterAutofill] Final adjusted AI Formula for target cell (${r},${c}): '${adjustedAiFormula}'`);
changesToProcess.push([r, c, adjustedAiFormula]);
} else {
console.log(`[AfterAutofill] Source cell (${originalSourceRow},${originalSourceCol}) does not have an 'aiFormula' in its metadata.`);
}
}
}
if (changesToProcess.length > 0) {
console.log('[AfterAutofill] Batching setDataAtCell for changes:', changesToProcess);
hot.batch(() => {
for (const change of changesToProcess) {
const [row, col, formula] = change;
hot.setDataAtCell(row, col, formula, 'aiAutofill');
}
});
hot.render();
} else {
console.log('[AfterAutofill] No changes to process.');
}
});
} else {
hot.loadData(data);
}
}
saveButton.addEventListener('click', () => {
if (!workbookData || !hot) return;
try {
const sheetName = workbookData.SheetNames[0] || 'Sheet1';
const dataToSave = hot.getData().map((row, rIndex) => {
return row.map((cellValue, cIndex) => {
return cellValue;
});
});
workbookData.Sheets[sheetName] = XLSX.utils.aoa_to_sheet(dataToSave);
const excelArray = XLSX.write(workbookData, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([excelArray], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: url, download: 'planilha_editada.xlsx' });
a.click(); URL.revokeObjectURL(url);
toast.show();
} catch (err) {
console.error(err);
console.error('Erro ao salvar o arquivo XLSX.');
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment