ac-helper/script.js
2025-11-19 18:19:40 -05:00

480 lines
18 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;
// --- ISO Codes & Data ---
const statusOptions = ['To-do', 'Booked', 'In Progress', 'Done'];
// Full ISO 3166-1 alpha-2 Country Codes
const countryOptions = [
{ id: 'US', name: 'United States' },
{ id: 'GB', name: 'United Kingdom' },
{ id: 'CA', name: 'Canada' },
{ id: 'AU', name: 'Australia' },
{ id: 'DE', name: 'Germany' },
{ id: 'FR', name: 'France' },
{ id: 'IT', name: 'Italy' },
{ id: 'ES', name: 'Spain' },
{ id: 'JP', name: 'Japan' },
{ id: 'CN', name: 'China' },
{ id: 'IN', name: 'India' },
{ id: 'BR', name: 'Brazil' },
{ id: 'MX', name: 'Mexico' },
{ id: 'RU', name: 'Russia' },
{ id: 'ZA', name: 'South Africa' },
{ id: 'NL', name: 'Netherlands' },
{ id: 'SE', name: 'Sweden' },
{ id: 'NO', name: 'Norway' },
{ id: 'DK', name: 'Denmark' },
{ id: 'FI', name: 'Finland' },
{ id: 'KR', name: 'South Korea' },
{ id: 'SG', name: 'Singapore' },
{ id: 'NZ', name: 'New Zealand' },
{ id: 'IE', name: 'Ireland' },
{ id: 'CH', name: 'Switzerland' },
{ id: 'AT', name: 'Austria' },
{ id: 'BE', name: 'Belgium' },
{ id: 'PL', name: 'Poland' },
{ id: 'PT', name: 'Portugal' },
{ id: 'GR', name: 'Greece' },
{ id: 'TR', name: 'Turkey' },
{ id: 'AE', name: 'United Arab Emirates' },
{ id: 'SA', name: 'Saudi Arabia' },
{ id: 'IL', name: 'Israel' },
{ id: 'AR', name: 'Argentina' },
{ id: 'CL', name: 'Chile' },
{ id: 'CO', name: 'Colombia' },
{ id: 'PE', name: 'Peru' },
{ id: 'TH', name: 'Thailand' },
{ id: 'VN', name: 'Vietnam' },
{ id: 'ID', name: 'Indonesia' },
{ id: 'MY', name: 'Malaysia' },
{ id: 'PH', name: 'Philippines' }
// Add more as needed or fetch from an API
];
// Full ISO 639-1 Language Codes
const languageOptions = [
{ id: 'EN', name: 'English' },
{ id: 'FR', name: 'French' },
{ id: 'DE', name: 'German' },
{ id: 'IT', name: 'Italian' },
{ id: 'ES', name: 'Spanish' },
{ id: 'PT', name: 'Portuguese' },
{ id: 'RU', name: 'Russian' },
{ id: 'ZH', name: 'Chinese' },
{ id: 'JA', name: 'Japanese' },
{ id: 'KO', name: 'Korean' },
{ id: 'AR', name: 'Arabic' },
{ id: 'HI', name: 'Hindi' },
{ id: 'BN', name: 'Bengali' },
{ id: 'NL', name: 'Dutch' },
{ id: 'SV', name: 'Swedish' },
{ id: 'NO', name: 'Norwegian' },
{ id: 'DA', name: 'Danish' },
{ id: 'FI', name: 'Finnish' },
{ id: 'TR', name: 'Turkish' },
{ id: 'PL', name: 'Polish' },
{ id: 'EL', name: 'Greek' },
{ id: 'HE', name: 'Hebrew' },
{ id: 'TH', name: 'Thai' },
{ id: 'ID', name: 'Indonesian' },
{ id: 'MS', name: 'Malay' },
{ id: 'VI', name: 'Vietnamese' }
];
const categoryOptions = ['Digital', 'DOOH', 'Online - Banner - Celtra', 'OOH', 'Print', 'Video'];
const mediaOptions = ['POS', 'OOH', 'Social', 'Community management', 'Online advertising', 'Banner', 'Rich media', 'Landing page', 'Static Image', 'Video', 'Push notifications', '.com', 'Print', 'Digital', 'Direct mail', 'Packaging', 'TV', 'Cinema', 'Radio', 'VOD'];
// --- Initial Load ---
loadData();
// --- API Interactions ---
async function loadData() {
showLoading(true);
try {
// Cache buster
const response = await fetch(`api.php?action=load&t=${Date.now()}`);
const rawData = await response.json();
if (!rawData) {
initSpreadsheet([]);
return;
}
// Transform JSON object array to Array of Arrays for Jspreadsheet if needed,
// BUT Jspreadsheet supports JSON directly if we define columns matching keys.
// Let's use JSON support for simplicity.
initSpreadsheet(rawData);
updateStats(rawData.length);
} 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: 'hidden', title: 'Number', width: 100, readOnly: true, name: 'Number' }, // Hidden but present for logic
{ type: 'text', title: 'Title', width: 300, name: 'Title' },
{ type: 'dropdown', title: 'Status', width: 100, name: 'Status', source: statusOptions },
{ type: 'dropdown', title: 'Category', width: 120, name: 'Category', source: categoryOptions },
{ type: 'dropdown', title: 'Media', width: 120, name: 'Media', source: mediaOptions },
{ type: 'text', title: 'Sub-media', width: 150, name: 'Sub-media' },
{ type: 'calendar', title: 'Supply date', width: 100, name: 'Supply date', options: { format: 'YYYY-MM-DD' } },
{ type: 'calendar', title: 'Live date', width: 100, name: 'Live date', options: { format: 'YYYY-MM-DD' } },
{ type: 'dropdown', title: 'Language', width: 100, name: 'Language', source: languageOptions },
{ type: 'dropdown', title: 'Country', width: 100, name: 'Country', source: countryOptions },
{ type: 'hidden', title: 'Quantity', width: 50, name: 'Quantity' }
],
defaultColWidth: 100,
tableOverflow: true,
tableWidth: '100%',
tableHeight: '70vh', // Fixed height for scrolling
// 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 () {
// Use the same logic as the button
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-save on change? Or wait?
// User asked for "real time", but constant saving might be heavy.
// Let's save on every change for now to be safe.
saveData();
},
oninsertrow: function () { saveData(); },
ondeleterow: function () { saveData(); }
});
}
async function saveData(dataOverride = null) {
// showLoading(true); // Don't block UI for auto-save
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('Saved.');
updateStats(data.length);
} else {
console.error('Save failed:', result.message);
}
} catch (error) {
console.error('Error saving data:', error);
} finally {
// showLoading(false);
}
}
let lastCommandText = '';
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');
aiOutput.textContent = `> Sending command: "${command}" (YOLO: ${yoloMode})...\n`;
showLoading(true);
try {
const response = await fetch('api.php?action=command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: command,
yolo_mode: yoloMode
})
});
const result = await response.json();
if (result.success) {
if (result.question) {
// AI has a question
aiOutput.innerHTML += `<div style="color: #ff9800; font-weight: bold; margin: 10px 0;">
🤖 AI Question: ${result.question}
</div>`;
// Add "Do what you think" button
const yoloBtn = document.createElement('button');
yoloBtn.className = 'btn btn-primary';
yoloBtn.style.marginTop = '5px';
yoloBtn.innerText = '🚀 Do what you think (YOLO)';
yoloBtn.onclick = () => sendCommand(lastCommandText, true);
aiOutput.appendChild(yoloBtn);
aiOutput.innerHTML += '\nOr type your clarification above.\n';
} else {
// Success
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`;
}
}
// Auto-scroll to bottom
aiOutput.scrollTop = aiOutput.scrollHeight;
} catch (error) {
console.error('Error processing 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);
}
});
}
// 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();
// Strict Header Requirement
const headers = [
"Number", "Title", "Status", "Category", "Media", "Sub media",
"Destination", "Format", "Supply date", "Live date", "End date",
"Special instructions", "Language", "Country", "Quantity"
];
let csvContent = "Sep=,\n" + headers.map(h => `"${h}"`).join(",") + "\n";
data.forEach(row => {
// Map Jspreadsheet row indices to the required CSV format
// Row structure in Jspreadsheet:
// 0:Number, 1:Title, 2:Status, 3:Category, 4:Media, 5:Sub-media,
// 6:Format (New Index!), 7:Supply date, 8:Live date, 9:Language, 10:Country, 11:Quantity
const csvRow = [
"", // Number (Cleared as requested)
row[1] || "", // Title
row[2] || "", // Status
row[3] || "", // Category
row[4] || "", // Media
row[5] || "", // Sub media
"", // Destination (Empty)
row[6] || "", // Format (Now mapped!)
row[7] || "", // Supply date
row[8] || "", // Live date
"", // End date (Empty)
"", // Special instructions (Empty)
row[9] || "", // Language
row[10] || "", // Country
"1.00" // Quantity
];
// 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", "activation_calendar_export.csv");
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
// --- 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();
}
});