wsj-filenaming/script.js
DJP 0939fbfb33 Initial commit: WSJ Filename Creator
Converted Activation Calendar to filename creator for digital banners.

Features:
- Filename format: JOBNUMBER_PROJECTNAME_SIZE_UNIT.png
- Conversational AI with Google Gemini 2.0
- Voice recognition support
- Auto-concatenation of filename parts
- Campaign save/load functionality
- WSJ styling: white background, black text, Georgia headlines
- Export to CSV and copy filenames to clipboard

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-16 13:40:59 -05:00

566 lines
20 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const spreadsheetDiv = document.getElementById('spreadsheet');
const commandInput = document.getElementById('commandInput');
const sendBtn = document.getElementById('sendBtn');
const saveBtn = document.getElementById('saveBtn');
const exportBtn = document.getElementById('exportBtn');
const loadingOverlay = document.getElementById('loadingOverlay');
const statsDisplay = document.getElementById('statsDisplay');
const micBtn = document.getElementById('micBtn');
let spreadsheet = null;
// --- Initial Load ---
loadData();
// --- API Interactions ---
async function loadData() {
showLoading(true);
try {
const response = await fetch(`api.php?action=load&t=${Date.now()}`);
const rawData = await response.json();
initSpreadsheet(rawData || []);
} catch (error) {
console.error('Error loading data:', error);
alert('Failed to load data.');
} finally {
showLoading(false);
}
}
function initSpreadsheet(data) {
if (spreadsheet) {
spreadsheet.destroy();
}
spreadsheet = jspreadsheet(spreadsheetDiv, {
data: data,
columns: [
{ type: 'text', title: 'Job Number', width: 120, name: 'JobNumber' },
{ type: 'text', title: 'Project Name', width: 150, name: 'ProjectName' },
{ type: 'text', title: 'Size', width: 120, name: 'Size' },
{ type: 'text', title: 'Unit', width: 120, name: 'Unit' },
{ type: 'text', title: 'Filename', width: 400, name: 'Filename', readOnly: true }
],
defaultColWidth: 100,
tableOverflow: true,
tableWidth: '100%',
tableHeight: '70vh',
// Context Menu
contextMenu: function (obj, x, y, e) {
var items = [];
if (y == null) {
// Header context menu
if (obj.options.allowInsertColumn == true) {
items.push({
title: obj.options.text.insertNewColumnBefore,
onclick: function () { obj.insertColumn(1, parseInt(x), 1); }
});
items.push({
title: obj.options.text.insertNewColumnAfter,
onclick: function () { obj.insertColumn(1, parseInt(x), 0); }
});
}
} else {
// Row context menu
items.push({
title: 'Delete Selected Rows',
onclick: function () {
document.getElementById('deleteSelectedBtn').click();
}
});
items.push({
title: 'Insert New Row',
onclick: function () { obj.insertRow(1, parseInt(y)); }
});
}
return items;
},
// Events
onchange: function (instance, cell, x, y, value) {
// Auto-update filename when any of the first 4 columns change
if (x < 4) {
updateFilename(y);
}
saveData();
},
oninsertrow: function () { saveData(); },
ondeleterow: function () { saveData(); }
});
}
function updateFilename(rowIndex) {
const row = spreadsheet.getRowData(rowIndex);
const job = row[0] || '0000000';
const project = row[1] || 'project';
const size = row[2] || '000x000';
const unit = row[3] || 'Platform';
const filename = `${job}_${project}_${size}_${unit}.png`;
spreadsheet.setValueFromCoords(4, rowIndex, filename, true);
}
async function saveData(dataOverride = null) {
try {
const data = dataOverride || spreadsheet.getJson();
const response = await fetch('api.php?action=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: data })
});
const result = await response.json();
if (result.success) {
console.log('Data saved.');
} else {
console.error('Save failed:', result.message);
}
} catch (error) {
console.error('Error saving data:', error);
}
}
let lastCommandText = '';
let conversationHistory = []; // Track AI conversation
async function sendCommand(overrideCommand = null, forceYolo = false) {
const command = overrideCommand || commandInput.value.trim();
if (!command) return;
lastCommandText = command;
const yoloMode = forceYolo || document.getElementById('yoloToggle').checked;
const aiOutput = document.getElementById('aiOutput');
const timestamp = new Date().toLocaleTimeString();
aiOutput.textContent += `\n[${timestamp}] Sending command: "${command}" (YOLO: ${yoloMode})...\n`;
// Clear input box
if (!overrideCommand) {
commandInput.value = '';
}
showLoading(true);
try {
// Build history string
const historyText = conversationHistory.map(h => `${h.role}: ${h.text}`).join('\n');
const response = await fetch('api.php?action=command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: command,
yolo_mode: yoloMode,
history: historyText
})
});
// Check if response is OK
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Try to parse JSON
const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch (e) {
aiOutput.textContent += `> Error: Invalid JSON response from server.\n`;
aiOutput.textContent += `> Raw response: ${text.substring(0, 500)}\n`;
showLoading(false);
return;
}
if (result.success) {
// Add user command to history
conversationHistory.push({ role: 'User', text: command });
if (result.question) {
// AI has a question - Show Modal
conversationHistory.push({ role: 'AI', text: result.question });
// Log to activity log for history
aiOutput.textContent += `\n[${new Date().toLocaleTimeString()}] 🤖 AI Question: ${result.question}\n`;
aiOutput.scrollTop = aiOutput.scrollHeight;
showAiQuestionModal(result.question);
} else {
// Success - clear history for fresh start
conversationHistory = [];
await loadData(); // Reload grid
commandInput.value = '';
aiOutput.textContent += `✅ Success! Processed ${result.count || 0} items.\n`;
if (result.debug_llm) {
aiOutput.textContent += `\n--- AI Response ---\n${result.debug_llm}\n`;
}
}
} else {
aiOutput.textContent += `❌ Error: ${result.message}\n`;
if (result.message && result.message.includes('Debug:')) {
aiOutput.textContent += `\n--- Debug Info ---\n${result.message}\n`;
}
if (result.debug_llm) {
aiOutput.textContent += `\n--- AI Response ---\n${result.debug_llm}\n`;
}
}
// Auto-scroll to bottom
aiOutput.scrollTop = aiOutput.scrollHeight;
} catch (error) {
console.error('Error sending command:', error);
aiOutput.textContent += `❌ Critical Error: ${error.message}\n`;
aiOutput.scrollTop = aiOutput.scrollHeight;
} finally {
showLoading(false);
}
}
// Clear Log Button
document.getElementById('clearLogBtn').addEventListener('click', () => {
document.getElementById('aiOutput').textContent = 'Waiting for command...';
});
function updateStats(count) {
statsDisplay.textContent = `${count} Deliverables`;
}
function showLoading(show) {
if (show) loadingOverlay.classList.add('active');
else loadingOverlay.classList.remove('active');
}
// --- Event Listeners ---
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
// Clear Sheet Logic
const clearSheetBtn = document.getElementById('clearSheetBtn');
if (clearSheetBtn) {
clearSheetBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to clear the ENTIRE sheet? This cannot be undone.')) {
spreadsheet.setData([]);
saveData([]); // Force save empty array
updateStats(0);
// Clear AI Log and History
document.getElementById('aiOutput').textContent = 'Waiting for command...';
conversationHistory = [];
}
});
}
// Delete Selected Logic
deleteSelectedBtn.addEventListener('click', () => {
const selected = spreadsheet.getSelected(); // [c1, r1, c2, r2]
if (!selected) {
alert('Please select a cell to delete its row.');
return;
}
// Normalize coordinates (handle reverse selection)
const y1 = parseInt(selected[1]);
const y2 = parseInt(selected[3]);
const startRow = Math.min(y1, y2);
const endRow = Math.max(y1, y2);
if (isNaN(startRow) || isNaN(endRow)) {
alert('Invalid selection.');
return;
}
const numberOfRows = endRow - startRow + 1;
const totalRows = spreadsheet.getData().length;
if (confirm(`Delete ${numberOfRows} row(s)?`)) {
// Special case: If deleting ALL rows (or the last remaining row),
// Jspreadsheet might block it or leave 1 empty row.
// We should just clear the data.
if (numberOfRows === totalRows) {
spreadsheet.setData([]); // Clear everything
saveData([]); // Force save empty array
} else {
spreadsheet.deleteRow(startRow, numberOfRows);
saveData();
}
spreadsheet.resetSelection();
}
});
sendBtn.addEventListener('click', () => stopMicAndSend());
commandInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
stopMicAndSend();
}
});
saveBtn.addEventListener('click', () => {
saveData();
alert('Saved!');
});
// Custom CSV Export
if (exportBtn) {
exportBtn.addEventListener('click', function () {
const data = spreadsheet.getData();
const headers = ["JobNumber", "ProjectName", "Size", "Unit", "Filename"];
let csvContent = headers.map(h => `"${h}"`).join(",") + "\n";
data.forEach(row => {
const csvRow = [
row[0] || "", // JobNumber
row[1] || "", // ProjectName
row[2] || "", // Size
row[3] || "", // Unit
row[4] || "" // Filename
];
// Escape quotes and wrap in quotes
const escapedRow = csvRow.map(field => {
const stringField = String(field);
if (stringField.includes('"')) {
return `"${stringField.replace(/"/g, '""')}"`;
}
return `"${stringField}"`;
});
csvContent += escapedRow.join(",") + "\n";
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", "filename_export.csv");
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
// Copy Filenames to Clipboard
const copyFilenamesBtn = document.getElementById('copyFilenamesBtn');
if (copyFilenamesBtn) {
copyFilenamesBtn.addEventListener('click', () => {
const data = spreadsheet.getData();
const filenames = data.map(row => row[4]).filter(f => f).join('\n');
navigator.clipboard.writeText(filenames)
.then(() => alert('Filenames copied to clipboard!'))
.catch(err => alert('Copy failed. Please try again.'));
});
}
// Load Campaign
const loadCampaignBtn = document.getElementById('loadCampaignBtn');
if (loadCampaignBtn) {
loadCampaignBtn.addEventListener('click', async () => {
const campaign = prompt('Enter campaign name to load:');
if (!campaign) return;
showLoading(true);
try {
const response = await fetch(`api.php?action=load_campaign&name=${campaign}`);
const result = await response.json();
if (result.success) {
initSpreadsheet(result.data);
const campaignNameInput = document.getElementById('campaignName');
if (campaignNameInput) campaignNameInput.value = campaign;
alert(`Campaign "${campaign}" loaded!`);
} else {
alert('Campaign not found.');
}
} catch (error) {
console.error('Error loading campaign:', error);
alert('Failed to load campaign.');
} finally {
showLoading(false);
}
});
}
// Save Campaign
const saveCampaignBtn = document.getElementById('saveCampaignBtn');
if (saveCampaignBtn) {
saveCampaignBtn.addEventListener('click', async () => {
const campaignNameInput = document.getElementById('campaignName');
const campaign = campaignNameInput ? campaignNameInput.value.trim() : '';
if (!campaign) {
alert('Please enter a campaign name in the input field above.');
return;
}
showLoading(true);
try {
const data = spreadsheet.getJson();
const response = await fetch('api.php?action=save_campaign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: campaign, data: data })
});
const result = await response.json();
if (result.success) {
alert(`Campaign "${campaign}" saved!`);
} else {
alert('Save failed.');
}
} catch (error) {
console.error('Error saving campaign:', error);
alert('Failed to save campaign.');
} finally {
showLoading(false);
}
});
}
// --- Voice Recognition ---
let recognition;
if ('webkitSpeechRecognition' in window) {
recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
micBtn.addEventListener('click', () => {
if (micBtn.classList.contains('listening')) {
recognition.stop();
} else {
recognition.start();
}
});
recognition.onstart = () => {
micBtn.classList.add('listening');
commandInput.placeholder = "Listening... (Press Enter to send)";
};
recognition.onend = () => {
micBtn.classList.remove('listening');
commandInput.placeholder = "Type or speak a command...";
};
recognition.onresult = (event) => {
let finalTranscript = '';
let interimTranscript = '';
for (let i = 0; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
} else {
interimTranscript += event.results[i][0].transcript;
}
}
commandInput.value = finalTranscript + interimTranscript;
};
} else {
micBtn.style.display = 'none';
}
function stopMicAndSend() {
if (recognition && micBtn.classList.contains('listening')) {
recognition.stop();
}
sendCommand();
}
// --- AI Question Modal Logic ---
const aiModal = document.getElementById('aiQuestionModal');
const aiModalInput = document.getElementById('aiModalInput');
const aiModalMicBtn = document.getElementById('aiModalMicBtn');
const aiModalSendBtn = document.getElementById('aiModalSendBtn');
const aiModalYoloBtn = document.getElementById('aiModalYoloBtn');
window.showAiQuestionModal = function (question) {
document.getElementById('aiQuestionText').textContent = question;
aiModal.classList.add('active');
aiModalInput.value = '';
aiModalInput.focus();
};
window.closeAiModal = function () {
aiModal.classList.remove('active');
if (recognition) recognition.stop();
};
// Modal Send
aiModalSendBtn.addEventListener('click', () => {
const answer = aiModalInput.value.trim();
if (answer) {
closeAiModal();
sendCommand(answer);
}
});
// Modal Enter Key
aiModalInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
aiModalSendBtn.click();
}
});
// Modal YOLO
aiModalYoloBtn.addEventListener('click', () => {
closeAiModal();
sendCommand(lastCommandText, true); // Resend last command with YOLO forced
});
// Modal Mic
if (recognition) {
aiModalMicBtn.addEventListener('click', () => {
if (aiModalMicBtn.classList.contains('listening')) {
recognition.stop();
} else {
// Temporarily override onresult for modal
recognition.onresult = (event) => {
let finalTranscript = '';
for (let i = 0; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
}
}
aiModalInput.value = finalTranscript;
};
recognition.onend = () => {
aiModalMicBtn.classList.remove('listening');
// Restore original onresult
restoreMainMicHandler();
};
recognition.start();
aiModalMicBtn.classList.add('listening');
}
});
} else {
aiModalMicBtn.style.display = 'none';
}
function restoreMainMicHandler() {
recognition.onresult = (event) => {
let finalTranscript = '';
let interimTranscript = '';
for (let i = 0; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
} else {
interimTranscript += event.results[i][0].transcript;
}
}
commandInput.value = finalTranscript + interimTranscript;
};
recognition.onend = () => {
micBtn.classList.remove('listening');
commandInput.placeholder = "Type or speak a command...";
};
}
});