From 0b363dbfa84c566519db290b3afa72f23469922b Mon Sep 17 00:00:00 2001 From: DJP Date: Wed, 19 Nov 2025 21:59:18 -0500 Subject: [PATCH] feat: Implement multi-sheet management, enhance AI command parsing with detailed hierarchy and format extraction rules, and add activity logging. --- api.php | 96 +++++-- config.php | 2 +- data.json | 1 - index.php | 23 ++ script.js | 266 +++++++++++++++--- ...porter_at_oliver_agency_1763598776100.json | 1 - ...porter_at_oliver_agency_1763606241496.json | 44 +++ ...porter_at_oliver_agency_1763606271640.json | 142 ++++++++++ ...porter_at_oliver_agency_1763606295286.json | 72 +++++ ...porter_at_oliver_agency_1763606320579.json | 210 ++++++++++++++ sheets_metadata.json | 34 ++- style.css | 170 +++++++++++ 12 files changed, 997 insertions(+), 64 deletions(-) delete mode 100644 sheets/daveporter_at_oliver_agency_1763598776100.json create mode 100644 sheets/daveporter_at_oliver_agency_1763606241496.json create mode 100644 sheets/daveporter_at_oliver_agency_1763606271640.json create mode 100644 sheets/daveporter_at_oliver_agency_1763606295286.json create mode 100644 sheets/daveporter_at_oliver_agency_1763606320579.json diff --git a/api.php b/api.php index 19f4a14..b4e2197 100644 --- a/api.php +++ b/api.php @@ -4,6 +4,15 @@ require_once 'config.php'; require_once 'sheet_helpers.php'; $dataFile = 'data.json'; +$logFile = 'activity.log'; + +// Helper to log activity +function logActivity($message, $type = 'INFO') { + global $logFile, $CURRENT_USER; + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[$timestamp] [$type] [$CURRENT_USER] $message\n"; + file_put_contents($logFile, $logEntry, FILE_APPEND); +} // Helper to read data function getData() { @@ -113,9 +122,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if ($action === 'command') { - $data = getData(); // Load existing data first! + $sheetId = $input['sheet_id'] ?? ''; + if (empty($sheetId)) { + echo json_encode(['success' => false, 'message' => 'Please create or select a sheet first.']); + exit; + } + + $data = loadSheetData($CURRENT_USER, $sheetId) ?? []; $command = trim($input['command']); + // Log the incoming command + logActivity("Command received: $command", 'COMMAND'); + $commandLower = strtolower($command); // Pre-processing: Common speech-to-text corrections @@ -175,7 +193,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { - Status (Enum: Booked, To-do, In Progress, Done) - Default to 'Booked' - Category (Enum: 'Digital', 'DOOH', 'Online - Banner - Celtra', 'OOH', 'Print', 'Video') - Media (Enum: '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') - - Sub-media (String) + - Sub-media (Dependent on Media): + * POS: Wobbler, Display, Sticker, Poster, Shelf Talker + * OOH: Billboard, City Light, Bus Stop, Digital Screen, Transit + * Social: Instagram, Facebook, TikTok, LinkedIn, Twitter/X, Pinterest, Snapchat, YouTube + * Community management: Post, Reply, Story, Moderation + * Online advertising: Display, Native, Programmatic + * Banner: Standard, Animated, HTML5, Static + * Rich media: Expandable, Interstitial, Video + * Landing page: Microsite, Product Page, Campaign Page + * Static Image: JPEG, PNG, Hero Image + * Video: TVC, OLV, Social Video, Bumper, Long Form + * Push notifications: App Push, Web Push + * .com: Homepage, Product Page, Blog + * Print: Magazine, Newspaper, Flyer, Poster, Brochure + * Digital: Banner, Email, Website, App + * Direct mail: Letter, Postcard, Catalog + * Packaging: Box, Label, Bag + * TV: Spot, Sponsorship + * Cinema: Pre-roll, Spot + * Radio: Spot, Sponsorship + * VOD: Pre-roll, Mid-roll - Format (String) - Extract sizes/dimensions here! e.g., '300x250', 'A4', '10x15cm', 'Full Page', '1080p'. - Supply date (YYYY-MM-DD) - Live date (YYYY-MM-DD) @@ -213,19 +251,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 1. **FORMAT EXTRACTION (Ultra Complex Logic)**: - - Look for ANY size or format indication. - - Pixels: '300x250', '300 by 250', '1920x1080'. Normalize to 'WxH'. - - Print: 'A4', 'A3', 'Letter', 'Full Page', 'Half Page'. - - Dimensions: '10x15cm', '5 inches', '4x6\"'. Keep units. - - If user says \"social media sizes\", INFER standard sizes (e.g., 1080x1080 for Instagram) if possible, or ASK if unsure (unless YOLO is ON). + - **STRICT RULE**: ALWAYS use 'x' as the separator for dimensions. NEVER use 'by'. + - **Pixel Dimensions**: Convert '300 by 250' -> '300x250'. Convert '1920 by 1080' -> '1920x1080'. + - **Physical Dimensions**: Convert '30 by 30 cm' -> '30x30cm'. Convert '10 x 15 cm' -> '10x15cm'. Keep the unit attached or separated by space, but use 'x' for the numbers. + - **Print**: 'A4', 'A3', 'Letter', 'Full Page', 'Half Page'. + - **Social**: If user says \"social media sizes\", INFER standard sizes (e.g., 1080x1080 for Instagram) if possible, or ASK if unsure (unless YOLO is ON). + - **Examples**: + * \"300 by 300\" -> \"300x300\" + * \"30x30 cm\" -> \"30x30cm\" + * \"30 by 30 cm\" -> \"30x30cm\" - 2. **YOLO MODE**: - - If YOLO MODE is TRUE: NEVER ask questions. GUESS. Make the best assumption and DO IT. - - If YOLO MODE is FALSE: If the request is ambiguous (e.g., \"next Friday\" when it's unclear, or missing critical category info), return operation 'question'. + 2. **YOLO MODE (HIGHEST PRIORITY)**: + - If YOLO MODE is TRUE: **YOU ARE FORBIDDEN FROM ASKING QUESTIONS.** + - You MUST GUESS any missing information. + - Example: If user says \"banners\" and nothing else, create 1 banner with default settings. + - Example: If user says \"2027\" in YOLO mode, assume it's the date for the previous request and EXECUTE. + - NEVER return 'question' operation when YOLO is TRUE. + + 3. **CLARIFICATION RECOVERY (CRITICAL)**: + - **CONTEXT MERGING**: The user's current input is likely an ANSWER to your previous question. + - **DO NOT** treat the input (e.g., \"2027\", \"300x300\") as a standalone command. + - **COMBINE** it with the previous user messages in the history to form a complete request. + - **Example Flow**: + 1. User: \"Create ads\" (Missing format) + 2. AI: \"What format?\" + 3. User: \"300x250\" + -> **INTERNAL THOUGHT**: \"User said 300x250. Previous was 'Create ads'. Combined: 'Create ads 300x250'.\" + -> **ACTION**: Execute the creation. DO NOT ask again. + - If the user provides *some* missing info but not *all*, AND YOLO is FALSE, you *can* ask for the remaining info, but acknowledge what you received. + - **BUT**: If the user seems frustrated or repeats info, JUST EXECUTE with best guesses. + + 4. **CONTEXT IS KING**: Use 'CURRENT DATA' to resolve references like \"the French ones\". - 3. **CONTEXT IS KING**: Use 'CURRENT DATA' to resolve references like \"the French ones\". - - 4. **PRECISE TARGETING**: Use `target_ids` for updates. + 5. **PRECISE TARGETING**: Use `target_ids` for updates. 5. **INFER FIELDS**: - \"OOH\" -> Category='OOH', Media='OOH'. @@ -283,6 +341,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Check for API Level Errors (e.g. Invalid Key) if (isset($responseObj['error'])) { + $errorMsg = $responseObj['error']['message'] ?? 'Unknown Error'; + logActivity("Gemini API Error: $errorMsg", 'ERROR'); echo json_encode([ 'success' => false, 'message' => "Gemini API Error: " . ($responseObj['error']['message'] ?? 'Unknown Error'), @@ -362,7 +422,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $data[] = $item; $count++; } - saveData($data); + updateSheet($CURRENT_USER, $sheetId, $data); + logActivity("Created $count items via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Created $count items.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'update') { @@ -388,7 +449,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $count++; } } - saveData($data); + updateSheet($CURRENT_USER, $sheetId, $data); + logActivity("Updated $count items via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Updated $count items.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'batch_update') { @@ -407,10 +469,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } } - saveData($data); + updateSheet($CURRENT_USER, $sheetId, $data); + logActivity("Batch updated $count items via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Batch updated $count items.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'question') { + logActivity("AI asked question: " . $llmAction['text'], 'QUESTION'); echo json_encode(array_merge(['success' => true, 'question' => $llmAction['text']], $debugInfo)); } else { echo json_encode(array_merge(['success' => false, 'message' => 'Unknown operation: ' . $llmAction['operation']], $debugInfo)); diff --git a/config.php b/config.php index 5cb3792..e8d0914 100644 --- a/config.php +++ b/config.php @@ -1,6 +1,6 @@ + + + diff --git a/script.js b/script.js index 20ee512..1699ce3 100644 --- a/script.js +++ b/script.js @@ -94,6 +94,32 @@ document.addEventListener('DOMContentLoaded', () => { 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']; + const subMediaMap = { + 'POS': ['Wobbler', 'Display', 'Sticker', 'Poster', 'Shelf Talker'], + 'OOH': ['Billboard', 'City Light', 'Bus Stop', 'Digital Screen', 'Transit'], + 'Social': ['Instagram', 'Facebook', 'TikTok', 'LinkedIn', 'Twitter/X', 'Pinterest', 'Snapchat', 'YouTube'], + 'Community management': ['Post', 'Reply', 'Story', 'Moderation'], + 'Online advertising': ['Display', 'Native', 'Programmatic'], + 'Banner': ['Standard', 'Animated', 'HTML5', 'Static'], + 'Rich media': ['Expandable', 'Interstitial', 'Video'], + 'Landing page': ['Microsite', 'Product Page', 'Campaign Page'], + 'Static Image': ['JPEG', 'PNG', 'Hero Image'], + 'Video': ['TVC', 'OLV', 'Social Video', 'Bumper', 'Long Form'], + 'Push notifications': ['App Push', 'Web Push'], + '.com': ['Homepage', 'Product Page', 'Blog'], + 'Print': ['Magazine', 'Newspaper', 'Flyer', 'Poster', 'Brochure'], + 'Digital': ['Banner', 'Email', 'Website', 'App'], + 'Direct mail': ['Letter', 'Postcard', 'Catalog'], + 'Packaging': ['Box', 'Label', 'Bag'], + 'TV': ['Spot', 'Sponsorship'], + 'Cinema': ['Pre-roll', 'Spot'], + 'Radio': ['Spot', 'Sponsorship'], + 'VOD': ['Pre-roll', 'Mid-roll'] + }; + + // Flatten unique sub-media options for the source + const allSubMediaOptions = [...new Set(Object.values(subMediaMap).flat())].sort(); + // --- Sheet Management --- let activeSheetId = localStorage.getItem('activeSheetId') || null; let sheets = []; @@ -120,17 +146,43 @@ document.addEventListener('DOMContentLoaded', () => { sheetList.innerHTML = sheets.map(sheet => `
-
${sheet.name}
-
${sheet.itemCount} items • ${new Date(sheet.modified).toLocaleDateString()}
+
+
${sheet.name}
+
${sheet.itemCount} items • ${new Date(sheet.modified).toLocaleDateString()}
+
+
+ + + +
`).join(''); // Add click handlers document.querySelectorAll('.sheet-item').forEach(item => { - item.addEventListener('click', () => loadSheet(item.dataset.id)); - item.addEventListener('contextmenu', (e) => { - e.preventDefault(); - showSheetContextMenu(e, item.dataset.id); + const sheetId = item.dataset.id; + + // Click on the card itself (but not on action buttons) + item.addEventListener('click', (e) => { + if (!e.target.closest('.sheet-item-actions')) { + loadSheet(sheetId); + } + }); + + // Action button handlers + item.querySelectorAll('.sheet-action-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent card click + const action = btn.dataset.action; + + if (action === 'rename') { + renameSheet(sheetId); + } else if (action === 'duplicate') { + duplicateSheet(sheetId); + } else if (action === 'delete') { + deleteSheet(sheetId); + } + }); }); }); } @@ -189,7 +241,7 @@ document.addEventListener('DOMContentLoaded', () => { } async function deleteSheet(sheetId) { - if (!confirm('Are you sure you want to delete this sheet?')) return; + if (!confirm('Are you sure you want to delete this sheet? This cannot be undone.')) return; try { const response = await fetch('api.php?action=delete_sheet', { @@ -202,7 +254,7 @@ document.addEventListener('DOMContentLoaded', () => { if (activeSheetId === sheetId) { activeSheetId = null; localStorage.removeItem('activeSheetId'); - initSpreadsheet([]); + initSpreadsheet([]); // Clear grid } await loadSheetList(); } @@ -253,9 +305,23 @@ document.addEventListener('DOMContentLoaded', () => { async function loadData() { showLoading(true); try { - // Cache buster - const response = await fetch(`api.php?action=load&t=${Date.now()}`); - const rawData = await response.json(); + let rawData; + + // If there's an active sheet, load from that sheet + if (activeSheetId) { + const response = await fetch(`api.php?action=load_sheet&id=${activeSheetId}&t=${Date.now()}`); + const result = await response.json(); + if (result.success) { + rawData = result.data; + } else { + console.error('Failed to load sheet:', result.message); + rawData = []; + } + } else { + // Fallback: load from data.json (legacy) + const response = await fetch(`api.php?action=load&t=${Date.now()}`); + rawData = await response.json(); + } if (!rawData) { initSpreadsheet([]); @@ -290,7 +356,21 @@ document.addEventListener('DOMContentLoaded', () => { { 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: 'dropdown', + title: 'Sub-media', + width: 150, + name: 'Sub-media', + source: allSubMediaOptions, + filter: function (instance, cell, c, r, source) { + const jexcel = instance.jexcel || instance; + const mediaValue = jexcel.getValueFromCoords(4, r); // Media is at index 4 + if (subMediaMap[mediaValue]) { + return subMediaMap[mediaValue]; + } + return source; + } + }, { type: 'text', title: 'Format', width: 120, name: 'Format' }, { 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' } }, @@ -351,17 +431,36 @@ document.addEventListener('DOMContentLoaded', () => { // 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); + + // If there's an active sheet, save to that sheet + if (activeSheetId) { + const response = await fetch('api.php?action=update_sheet', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: activeSheetId, data: data }) + }); + const result = await response.json(); + if (result.success) { + console.log('Sheet saved.'); + updateStats(data.length); + await loadSheetList(); // Refresh sheet list to update item counts + } else { + console.error('Save failed:', result.message); + } } else { - console.error('Save failed:', result.message); + // Fallback: save to data.json (legacy behavior) + 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 to data.json.'); + updateStats(data.length); + } else { + console.error('Save failed:', result.message); + } } } catch (error) { console.error('Error saving data:', error); @@ -381,7 +480,8 @@ document.addEventListener('DOMContentLoaded', () => { const yoloMode = forceYolo || document.getElementById('yoloToggle').checked; const aiOutput = document.getElementById('aiOutput'); - aiOutput.textContent = `> Sending command: "${command}" (YOLO: ${yoloMode})...\n`; + const timestamp = new Date().toLocaleTimeString(); + aiOutput.textContent += `\n[${timestamp}] Sending command: "${command}" (YOLO: ${yoloMode})...\n`; // Clear input box if (!overrideCommand) { @@ -399,7 +499,8 @@ document.addEventListener('DOMContentLoaded', () => { body: JSON.stringify({ command: command, yolo_mode: yoloMode, - history: historyText + history: historyText, + sheet_id: activeSheetId // Pass active sheet ID }) }); @@ -425,22 +526,14 @@ document.addEventListener('DOMContentLoaded', () => { conversationHistory.push({ role: 'User', text: command }); if (result.question) { - // AI has a question + // AI has a question - Show Modal conversationHistory.push({ role: 'AI', text: result.question }); - aiOutput.innerHTML += `
- 🤖 AI Question: ${result.question} -
`; + // Log to activity log for history + aiOutput.textContent += `\n[${new Date().toLocaleTimeString()}] 🤖 AI Question: ${result.question}\n`; + aiOutput.scrollTop = aiOutput.scrollHeight; - // 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'; + showAiQuestionModal(result.question); } else { // Success - clear history for fresh start @@ -448,13 +541,13 @@ document.addEventListener('DOMContentLoaded', () => { await loadData(); // Reload grid commandInput.value = ''; - aiOutput.textContent += `> Success! Processed ${result.count || 0} items.\n`; + 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`; + aiOutput.textContent += `❌ Error: ${result.message}\n`; if (result.message && result.message.includes('Debug:')) { aiOutput.textContent += `\n--- Debug Info ---\n${result.message}\n`; } @@ -466,7 +559,7 @@ document.addEventListener('DOMContentLoaded', () => { aiOutput.scrollTop = aiOutput.scrollHeight; } catch (error) { console.error('Error sending command:', error); - aiOutput.textContent += `> Critical Error: ${error.message}\n`; + aiOutput.textContent += `❌ Critical Error: ${error.message}\n`; aiOutput.scrollTop = aiOutput.scrollHeight; } finally { showLoading(false); @@ -665,4 +758,97 @@ document.addEventListener('DOMContentLoaded', () => { } 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..."; + }; + } }); diff --git a/sheets/daveporter_at_oliver_agency_1763598776100.json b/sheets/daveporter_at_oliver_agency_1763598776100.json deleted file mode 100644 index 0637a08..0000000 --- a/sheets/daveporter_at_oliver_agency_1763598776100.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/sheets/daveporter_at_oliver_agency_1763606241496.json b/sheets/daveporter_at_oliver_agency_1763606241496.json new file mode 100644 index 0000000..f032ad2 --- /dev/null +++ b/sheets/daveporter_at_oliver_agency_1763606241496.json @@ -0,0 +1,44 @@ +[ + { + "Number": "DEL-001", + "Title": "Video Ad", + "Status": "Booked", + "Category": "Video", + "Media": "OOH", + "Sub-media": "Social Video", + "Format": "", + "Supply date": "", + "Live date": "2025-11-28", + "Language": "JA", + "Country": "JP", + "Quantity": 1 + }, + { + "Number": "DEL-002", + "Title": "Video Ad", + "Status": "Booked", + "Category": "Video", + "Media": "Video", + "Sub-media": "", + "Format": "", + "Supply date": "", + "Live date": "2025-11-28", + "Language": "JA", + "Country": "JP", + "Quantity": 1 + }, + { + "Number": "DEL-003", + "Title": "Video Ad", + "Status": "Booked", + "Category": "Video", + "Media": "Video", + "Sub-media": "", + "Format": "", + "Supply date": "", + "Live date": "2025-11-28", + "Language": "JA", + "Country": "JP", + "Quantity": 1 + } +] \ No newline at end of file diff --git a/sheets/daveporter_at_oliver_agency_1763606271640.json b/sheets/daveporter_at_oliver_agency_1763606271640.json new file mode 100644 index 0000000..ee8e3a1 --- /dev/null +++ b/sheets/daveporter_at_oliver_agency_1763606271640.json @@ -0,0 +1,142 @@ +[ + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-001" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-002" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-003" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-004" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-005" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-006" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-007" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-008" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-009" + }, + { + "Title": "Digital Deliverable", + "Status": "Booked", + "Category": "Digital", + "Media": "Digital", + "Sub-media": "TBD", + "Format": "TBD", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-010" + } +] \ No newline at end of file diff --git a/sheets/daveporter_at_oliver_agency_1763606295286.json b/sheets/daveporter_at_oliver_agency_1763606295286.json new file mode 100644 index 0000000..22810ad --- /dev/null +++ b/sheets/daveporter_at_oliver_agency_1763606295286.json @@ -0,0 +1,72 @@ +[ + { + "Number": "DEL-001", + "Title": "Social Media Banner", + "Status": "Booked", + "Category": "Digital", + "Media": "Social", + "Sub-media": "Banner", + "Format": "1080x1080", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "EN", + "Country": "GB", + "Quantity": 1 + }, + { + "Number": "DEL-002", + "Title": "Social Media Banner", + "Status": "Booked", + "Category": "Digital", + "Media": "Social", + "Sub-media": "Banner", + "Format": "1080x1080", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "EN", + "Country": "GB", + "Quantity": 1 + }, + { + "Number": "DEL-003", + "Title": "Social Media Banner", + "Status": "Booked", + "Category": "Digital", + "Media": "Social", + "Sub-media": "Banner", + "Format": "1080x1080", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "EN", + "Country": "GB", + "Quantity": 1 + }, + { + "Number": "DEL-004", + "Title": "Social Media Banner", + "Status": "Booked", + "Category": "Digital", + "Media": "Social", + "Sub-media": "Banner", + "Format": "1080x1080", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "EN", + "Country": "GB", + "Quantity": 1 + }, + { + "Number": "DEL-005", + "Title": "Social Media Banner", + "Status": "Booked", + "Category": "Digital", + "Media": "Social", + "Sub-media": "Banner", + "Format": "1080x1080", + "Supply date": "2025-11-20", + "Live date": "2025-11-20", + "Language": "EN", + "Country": "GB", + "Quantity": 1 + } +] \ No newline at end of file diff --git a/sheets/daveporter_at_oliver_agency_1763606320579.json b/sheets/daveporter_at_oliver_agency_1763606320579.json new file mode 100644 index 0000000..50ae72f --- /dev/null +++ b/sheets/daveporter_at_oliver_agency_1763606320579.json @@ -0,0 +1,210 @@ +[ + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-001" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-002" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "GB", + "Country": "GB", + "Quantity": 1, + "Number": "DEL-003" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "GB", + "Country": "GB", + "Quantity": 1, + "Number": "DEL-004" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "DE", + "Country": "DE", + "Quantity": 1, + "Number": "DEL-005" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "DE", + "Country": "DE", + "Quantity": 1, + "Number": "DEL-006" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "ES", + "Country": "ES", + "Quantity": 1, + "Number": "DEL-007" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "50x70cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "ES", + "Country": "ES", + "Quantity": 1, + "Number": "DEL-008" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-009" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "FR", + "Country": "FR", + "Quantity": 1, + "Number": "DEL-010" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "GB", + "Country": "GB", + "Quantity": 1, + "Number": "DEL-011" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "GB", + "Country": "GB", + "Quantity": 1, + "Number": "DEL-012" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "DE", + "Country": "DE", + "Quantity": 1, + "Number": "DEL-013" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "DE", + "Country": "DE", + "Quantity": 1, + "Number": "DEL-014" + }, + { + "Title": "Christmas campaign men", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "ES", + "Country": "ES", + "Quantity": 1, + "Number": "DEL-015" + }, + { + "Title": "Christmas campaign women", + "Status": "Booked", + "Category": "Print", + "Media": "Print", + "Format": "100x160cm", + "Supply date": "2025-12-01", + "Live date": "2020-12-25", + "Language": "ES", + "Country": "ES", + "Quantity": 1, + "Number": "DEL-016" + } +] \ No newline at end of file diff --git a/sheets_metadata.json b/sheets_metadata.json index 6faa0f9..d70be86 100644 --- a/sheets_metadata.json +++ b/sheets_metadata.json @@ -1,11 +1,35 @@ { "daveporter@oliver.agency": [ { - "id": "1763598776100", - "name": "HM 1", - "created": "2025-11-20T00:32:56+00:00", - "modified": "2025-11-20T00:32:56+00:00", - "itemCount": 0, + "id": "1763606241496", + "name": "1234568", + "created": "2025-11-20T02:37:21+00:00", + "modified": "2025-11-20T02:43:21+00:00", + "itemCount": 3, + "user": "daveporter@oliver.agency" + }, + { + "id": "1763606271640", + "name": "1212", + "created": "2025-11-20T02:37:51+00:00", + "modified": "2025-11-20T02:37:51+00:00", + "itemCount": 10, + "user": "daveporter@oliver.agency" + }, + { + "id": "1763606295286", + "name": "asdsaasd", + "created": "2025-11-20T02:38:15+00:00", + "modified": "2025-11-20T02:38:26+00:00", + "itemCount": 5, + "user": "daveporter@oliver.agency" + }, + { + "id": "1763606320579", + "name": "aaasdasd", + "created": "2025-11-20T02:38:40+00:00", + "modified": "2025-11-20T02:39:11+00:00", + "itemCount": 16, "user": "daveporter@oliver.agency" } ] diff --git a/style.css b/style.css index 169e69c..0768ad9 100644 --- a/style.css +++ b/style.css @@ -70,6 +70,10 @@ body { margin-bottom: 8px; cursor: pointer; transition: all 0.2s; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; } .sheet-item:hover { @@ -84,6 +88,11 @@ body { border-color: var(--accent-color); } +.sheet-item-content { + flex: 1; + min-width: 0; +} + .sheet-item-name { font-weight: 600; margin-bottom: 4px; @@ -99,6 +108,54 @@ body { opacity: 0.8; } +.sheet-item-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.2s; +} + +.sheet-item:hover .sheet-item-actions { + opacity: 1; +} + +.sheet-item.active .sheet-item-actions { + opacity: 1; +} + +.sheet-action-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.sheet-action-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +.sheet-item.active .sheet-action-btn { + background: rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.3); +} + +.sheet-item.active .sheet-action-btn:hover { + background: rgba(0, 0, 0, 0.3); +} + +.sheet-action-delete:hover { + background: var(--danger-color) !important; + border-color: var(--danger-color) !important; +} + + .loading-sheets { text-align: center; padding: 20px; @@ -663,4 +720,117 @@ input:checked+.slider:before { display: inline-block; width: 20px; transition: transform 0.3s ease; +} + +/* AI Question Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.modal-overlay.active { + opacity: 1; + pointer-events: all; +} + +.modal-content { + background: #1e293b; + border: 1px solid var(--accent-color); + border-radius: 12px; + width: 90%; + max-width: 500px; + padding: 0; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + transform: translateY(20px); + transition: transform 0.3s ease; +} + +.modal-overlay.active .modal-content { + transform: translateY(0); +} + +.modal-header { + padding: 15px 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(0, 0, 0, 0.2); + border-radius: 12px 12px 0 0; +} + +.modal-header h3 { + margin: 0; + color: var(--accent-color); + font-size: 1.1rem; +} + +.close-modal-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.close-modal-btn:hover { + color: white; +} + +.modal-body { + padding: 20px; +} + +.ai-question-text { + font-size: 1.1rem; + line-height: 1.5; + margin-top: 0; + margin-bottom: 20px; + color: white; +} + +.modal-input-group { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#aiModalInput { + flex: 1; + padding: 12px; + background: #0f172a; + border: 1px solid var(--border-color); + border-radius: 8px; + color: white; + font-size: 1rem; +} + +#aiModalInput:focus { + outline: none; + border-color: var(--accent-color); +} + +.modal-actions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.modal-actions .btn { + width: 100%; + justify-content: center; } \ No newline at end of file