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