Last active
May 24, 2025 20:13
-
-
Save celsowm/8fe24083a9e068cb2b51f7143faac3ff to your computer and use it in GitHub Desktop.
OPEN EXCELLM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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