- Tech Live: TECL → TL - TechLive Qatar: added (TLQ) - TechLive Cyber: added (TLCYB) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
593 lines
22 KiB
JavaScript
593 lines
22 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const spreadsheetDiv = document.getElementById('spreadsheet');
|
|
const commandInput = document.getElementById('commandInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const exportBtn = document.getElementById('exportBtn');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
const micBtn = document.getElementById('micBtn');
|
|
|
|
let spreadsheet = null;
|
|
let recognition = null;
|
|
let conversationHistory = [];
|
|
|
|
// --- Dropdown Options ---
|
|
const DOMAINS = ['PMKT', 'BRND', 'EVNT', 'B2B'];
|
|
|
|
const SUBTEAMS = ['ACQ', 'B2B', 'CMKT', 'ENGRT', 'ENT'];
|
|
|
|
const BRANDS = [
|
|
'WSJ', 'WSJ+', 'BAR', 'MW', 'DF', 'DJE', 'FAC', 'FE',
|
|
'GRI', 'NWS', 'OA', 'RSK', 'RSKC', 'DJRJ', 'R&C', 'WECR'
|
|
];
|
|
|
|
const EVENTS = [
|
|
'GH', 'DJRJS', 'WECR', 'FEOE', 'FOH', 'GFF', 'JH', 'TL', 'TLQ', 'TLCYB',
|
|
'FOE', 'WSJIL', 'BODC', 'CCOC', 'CEOC', 'CFOC',
|
|
'CMOC', 'CPOC', 'TECC', 'WSJLI'
|
|
];
|
|
|
|
// --- 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 || []);
|
|
|
|
// Check for builder params in URL
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('builder') === '1') {
|
|
const newRow = [
|
|
params.get('OMGID') || '',
|
|
params.get('Domain') || '',
|
|
params.get('Subteam') || '',
|
|
params.get('Brand') || '',
|
|
params.get('Event') || '',
|
|
params.get('Initiative') || '',
|
|
params.get('YY') || '',
|
|
params.get('Sequence') || '',
|
|
params.get('AssetName') || '',
|
|
params.get('Version') || '',
|
|
'' // Filename - will be generated
|
|
];
|
|
spreadsheet.insertRow(1, 0, true); // insert at top
|
|
for (let col = 0; col < newRow.length; col++) {
|
|
spreadsheet.setValueFromCoords(col, 0, newRow[col], true);
|
|
}
|
|
updateFilenameForRow(0);
|
|
saveData();
|
|
// Clear URL params without reload
|
|
window.history.replaceState({}, '', window.location.pathname);
|
|
}
|
|
|
|
if (spreadsheet && spreadsheet.getData().length > 0) {
|
|
updateAllFilenames();
|
|
}
|
|
} 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: 'OMGID', width: 90, name: 'OMGID' },
|
|
{ type: 'dropdown', title: 'Domain', width: 90, name: 'Domain', source: DOMAINS },
|
|
{ type: 'dropdown', title: 'Subteam', width: 90, name: 'Subteam', source: SUBTEAMS },
|
|
{ type: 'dropdown', title: 'Brand', width: 90, name: 'Brand', source: BRANDS },
|
|
{ type: 'dropdown', title: 'Event', width: 90, name: 'Event', source: EVENTS },
|
|
{ type: 'text', title: 'Initiative', width: 100, name: 'Initiative' },
|
|
{ type: 'text', title: 'YY', width: 50, name: 'YY' },
|
|
{ type: 'text', title: 'Seq', width: 50, name: 'Sequence' },
|
|
{ type: 'text', title: 'Asset Name', width: 140, name: 'AssetName' },
|
|
{ type: 'text', title: 'Ver', width: 50, name: 'Version' },
|
|
{ type: 'text', title: 'Filename', width: 450, name: 'Filename', readOnly: true }
|
|
],
|
|
defaultColWidth: 100,
|
|
tableOverflow: true,
|
|
tableWidth: '100%',
|
|
tableHeight: '70vh',
|
|
|
|
contextMenu: function (obj, x, y, e) {
|
|
var items = [];
|
|
if (y == null) {
|
|
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 {
|
|
items.push({
|
|
title: 'Delete Selected Rows',
|
|
onclick: function () {
|
|
const btn = document.getElementById('deleteSelectedBtn');
|
|
if (btn) btn.click();
|
|
}
|
|
});
|
|
items.push({
|
|
title: 'Insert New Row',
|
|
onclick: function () { obj.insertRow(1, parseInt(y)); }
|
|
});
|
|
}
|
|
return items;
|
|
},
|
|
|
|
onchange: function (instance, cell, x, y, value) {
|
|
// Update filename when any editable column changes (cols 0-9)
|
|
if (x >= 0 && x <= 9) {
|
|
updateFilenameForRow(y);
|
|
}
|
|
saveData();
|
|
},
|
|
oninsertrow: function () {
|
|
updateAllFilenames();
|
|
saveData();
|
|
},
|
|
ondeleterow: function () {
|
|
updateAllFilenames();
|
|
saveData();
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildFilename(row) {
|
|
// row is array: [OMGID, Domain, Subteam, Brand, Event, Initiative, YY, Sequence, AssetName, Version, Filename]
|
|
const omgid = (row[0] || '').trim();
|
|
const domain = (row[1] || '').trim();
|
|
const subteam = (row[2] || '').trim();
|
|
const brand = (row[3] || '').trim();
|
|
const event = (row[4] || '').trim();
|
|
const initiative = (row[5] || '').trim();
|
|
const yy = (row[6] || '').trim();
|
|
const seq = (row[7] || '').trim();
|
|
const assetName = (row[8] || '').trim();
|
|
const version = (row[9] || '').trim();
|
|
|
|
// Build the middle segment based on domain
|
|
let middlePart = '';
|
|
if (domain === 'EVNT') {
|
|
// Event format: EVNT-[EventAbbrev]-[YY]-[Seq]
|
|
const parts = ['EVNT'];
|
|
if (event) parts.push(event);
|
|
if (yy) parts.push(yy);
|
|
if (seq) parts.push(seq);
|
|
middlePart = parts.join('-');
|
|
} else {
|
|
// Normal format: [Domain]-[Subteam]-[Brand]-[Initiative]-[YY]-[Seq]
|
|
const parts = [];
|
|
if (domain) parts.push(domain);
|
|
if (subteam) parts.push(subteam);
|
|
if (brand) parts.push(brand);
|
|
if (initiative) parts.push(initiative);
|
|
if (yy) parts.push(yy);
|
|
if (seq) parts.push(seq);
|
|
middlePart = parts.join('-');
|
|
}
|
|
|
|
// Build suffix: _[AssetName]_v[Version]
|
|
let suffix = '';
|
|
if (assetName) suffix += '_' + assetName;
|
|
if (version) suffix += '_v' + version;
|
|
|
|
// Final: [OMGID] - [middlePart][suffix]
|
|
if (!omgid && !middlePart) return '';
|
|
return (omgid ? omgid + ' - ' : '') + middlePart + suffix;
|
|
}
|
|
|
|
function updateFilenameForRow(rowIndex) {
|
|
const row = spreadsheet.getRowData(rowIndex);
|
|
const filename = buildFilename(row);
|
|
spreadsheet.setValueFromCoords(10, rowIndex, filename, true);
|
|
}
|
|
|
|
function updateAllFilenames() {
|
|
const data = spreadsheet.getData();
|
|
data.forEach((row, index) => {
|
|
const filename = buildFilename(row);
|
|
spreadsheet.setValueFromCoords(10, index, 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 = '';
|
|
|
|
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`;
|
|
|
|
if (!overrideCommand) {
|
|
commandInput.value = '';
|
|
}
|
|
|
|
showLoading(true);
|
|
try {
|
|
const historyText = conversationHistory.map(h => `${h.role}: ${h.text}`).join('\n');
|
|
const campaignName = document.getElementById('campaignName')?.value.trim() || '';
|
|
|
|
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,
|
|
campaign_name: campaignName
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
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) {
|
|
conversationHistory.push({ role: 'User', text: command });
|
|
|
|
if (result.question) {
|
|
conversationHistory.push({ role: 'AI', text: result.question });
|
|
aiOutput.textContent += `\n[${new Date().toLocaleTimeString()}] AI Question: ${result.question}\n`;
|
|
aiOutput.scrollTop = aiOutput.scrollHeight;
|
|
showAiQuestionModal(result.question);
|
|
} else {
|
|
conversationHistory = [];
|
|
await loadData();
|
|
commandInput.value = '';
|
|
aiOutput.textContent += `Done! 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.debug_llm) {
|
|
aiOutput.textContent += `\n--- AI Response ---\n${result.debug_llm}\n`;
|
|
}
|
|
}
|
|
aiOutput.scrollTop = aiOutput.scrollHeight;
|
|
} catch (error) {
|
|
console.error('Error sending command:', error);
|
|
const aiOutput = document.getElementById('aiOutput');
|
|
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 showLoading(show) {
|
|
if (show) loadingOverlay.classList.add('active');
|
|
else loadingOverlay.classList.remove('active');
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
|
|
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([]);
|
|
document.getElementById('aiOutput').textContent = 'Waiting for command...';
|
|
conversationHistory = [];
|
|
}
|
|
});
|
|
}
|
|
|
|
sendBtn.addEventListener('click', () => stopMicAndSend());
|
|
|
|
commandInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
stopMicAndSend();
|
|
}
|
|
});
|
|
|
|
// Custom CSV Export
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', function () {
|
|
const data = spreadsheet.getData();
|
|
const headers = ["OMGID", "Domain", "Subteam", "Brand", "Event", "Initiative", "YY", "Sequence", "AssetName", "Version", "Filename"];
|
|
let csvContent = headers.map(h => `"${h}"`).join(",") + "\n";
|
|
|
|
data.forEach(row => {
|
|
const csvRow = row.map((field, i) => {
|
|
const val = String(field || '');
|
|
if (val.includes('"')) {
|
|
return `"${val.replace(/"/g, '""')}"`;
|
|
}
|
|
return `"${val}"`;
|
|
});
|
|
csvContent += csvRow.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", "dj_jobs_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[10]).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 ---
|
|
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 {
|
|
try {
|
|
recognition.start();
|
|
} catch (error) {
|
|
console.error('Error starting voice recognition:', error);
|
|
alert('Could not start voice recognition. Please check microphone permissions.');
|
|
}
|
|
}
|
|
});
|
|
|
|
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.onerror = (event) => {
|
|
console.error('Speech recognition error:', event.error);
|
|
micBtn.classList.remove('listening');
|
|
if (event.error === 'not-allowed' || event.error === 'permission-denied') {
|
|
alert('Microphone access denied.');
|
|
}
|
|
};
|
|
|
|
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();
|
|
};
|
|
|
|
aiModalSendBtn.addEventListener('click', () => {
|
|
const answer = aiModalInput.value.trim();
|
|
if (answer) {
|
|
closeAiModal();
|
|
sendCommand(answer);
|
|
}
|
|
});
|
|
|
|
aiModalInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
aiModalSendBtn.click();
|
|
}
|
|
});
|
|
|
|
aiModalYoloBtn.addEventListener('click', () => {
|
|
closeAiModal();
|
|
sendCommand(lastCommandText, true);
|
|
});
|
|
|
|
if (recognition) {
|
|
aiModalMicBtn.addEventListener('click', () => {
|
|
if (aiModalMicBtn.classList.contains('listening')) {
|
|
recognition.stop();
|
|
} else {
|
|
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');
|
|
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...";
|
|
};
|
|
}
|
|
});
|