feat: Implement multi-sheet management, enhance AI command parsing with detailed hierarchy and format extraction rules, and add activity logging.
This commit is contained in:
parent
27284570ed
commit
0b363dbfa8
12 changed files with 997 additions and 64 deletions
96
api.php
96
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));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
// API Configuration
|
||||
$GEMINI_API_KEY = 'AIzaSyBDMbLOvVqYMXvvFCOoLZOXCEb3YtKhsL0';
|
||||
$GEMINI_API_KEY = 'AIzaSyC2DrDCeNIhI531JcXCF9uolTMU_KBcjDY';
|
||||
|
||||
// User Configuration (hardcoded for now, will be SSO later)
|
||||
$CURRENT_USER = 'daveporter@oliver.agency';
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
[]
|
||||
23
index.php
23
index.php
|
|
@ -98,6 +98,29 @@
|
|||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- AI Question Modal -->
|
||||
<div id="aiQuestionModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>🤖 AI Needs Clarification</h3>
|
||||
<button class="close-modal-btn" onclick="closeAiModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="aiQuestionText" class="ai-question-text"></p>
|
||||
|
||||
<div class="modal-input-group">
|
||||
<input type="text" id="aiModalInput" placeholder="Type your answer..." autocomplete="off">
|
||||
<button id="aiModalMicBtn" class="btn btn-icon" title="Speak">🎤</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button id="aiModalSendBtn" class="btn btn-primary">Send Answer</button>
|
||||
<button id="aiModalYoloBtn" class="btn btn-secondary">🚀 Do what you think (YOLO)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</div> <!-- /main-content -->
|
||||
</div> <!-- /app-container -->
|
||||
|
|
|
|||
266
script.js
266
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 => `
|
||||
<div class="sheet-item ${sheet.id === activeSheetId ? 'active' : ''}" data-id="${sheet.id}">
|
||||
<div class="sheet-item-name">${sheet.name}</div>
|
||||
<div class="sheet-item-meta">${sheet.itemCount} items • ${new Date(sheet.modified).toLocaleDateString()}</div>
|
||||
<div class="sheet-item-content">
|
||||
<div class="sheet-item-name">${sheet.name}</div>
|
||||
<div class="sheet-item-meta">${sheet.itemCount} items • ${new Date(sheet.modified).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<div class="sheet-item-actions">
|
||||
<button class="sheet-action-btn" data-action="rename" title="Rename">✏️</button>
|
||||
<button class="sheet-action-btn" data-action="duplicate" title="Duplicate">📋</button>
|
||||
<button class="sheet-action-btn sheet-action-delete" data-action="delete" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 += `<div style="color: #ff9800; font-weight: bold; margin: 10px 0;">
|
||||
🤖 AI Question: ${result.question}
|
||||
</div>`;
|
||||
// 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...";
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
[]
|
||||
44
sheets/daveporter_at_oliver_agency_1763606241496.json
Normal file
44
sheets/daveporter_at_oliver_agency_1763606241496.json
Normal file
|
|
@ -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
|
||||
}
|
||||
]
|
||||
142
sheets/daveporter_at_oliver_agency_1763606271640.json
Normal file
142
sheets/daveporter_at_oliver_agency_1763606271640.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
72
sheets/daveporter_at_oliver_agency_1763606295286.json
Normal file
72
sheets/daveporter_at_oliver_agency_1763606295286.json
Normal file
|
|
@ -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
|
||||
}
|
||||
]
|
||||
210
sheets/daveporter_at_oliver_agency_1763606320579.json
Normal file
210
sheets/daveporter_at_oliver_agency_1763606320579.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
170
style.css
170
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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue