Converted Activation Calendar to filename creator for digital banners. Features: - Filename format: JOBNUMBER_PROJECTNAME_SIZE_UNIT.png - Conversational AI with Google Gemini 2.0 - Voice recognition support - Auto-concatenation of filename parts - Campaign save/load functionality - WSJ styling: white background, black text, Georgia headlines - Export to CSV and copy filenames to clipboard 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
493 lines
20 KiB
PHP
493 lines
20 KiB
PHP
<?php
|
||
header('Content-Type: application/json');
|
||
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() {
|
||
global $dataFile;
|
||
if (!file_exists($dataFile)) return [];
|
||
$content = file_get_contents($dataFile);
|
||
return json_decode($content, true) ?? [];
|
||
}
|
||
|
||
// Helper to save data
|
||
function saveData($data) {
|
||
global $dataFile;
|
||
file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT));
|
||
}
|
||
|
||
// Helper to generate ID
|
||
function generateId($data) {
|
||
$maxId = 0;
|
||
foreach ($data as $row) {
|
||
$num = intval(str_replace('DEL-', '', $row['Number'] ?? '0'));
|
||
if ($num > $maxId) $maxId = $num;
|
||
}
|
||
return 'DEL-' . str_pad($maxId + 1, 3, '0', STR_PAD_LEFT);
|
||
}
|
||
|
||
// Helper to generate filename
|
||
function generateFilename($item) {
|
||
$job = $item['JobNumber'] ?? '0000000';
|
||
$project = $item['ProjectName'] ?? 'project';
|
||
$size = $item['Size'] ?? '000x000';
|
||
$unit = $item['Unit'] ?? 'Platform';
|
||
return "{$job}_{$project}_{$size}_{$unit}.png";
|
||
}
|
||
|
||
$action = $_GET['action'] ?? '';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||
if ($action === 'load') {
|
||
echo json_encode(getData());
|
||
exit;
|
||
}
|
||
|
||
// Sheet Management Endpoints
|
||
if ($action === 'list_sheets') {
|
||
$sheets = getUserSheets($CURRENT_USER);
|
||
echo json_encode(['success' => true, 'sheets' => $sheets]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'load_sheet') {
|
||
$sheetId = $_GET['id'] ?? '';
|
||
$data = loadSheetData($CURRENT_USER, $sheetId);
|
||
if ($data !== null) {
|
||
echo json_encode(['success' => true, 'data' => $data]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => 'Sheet not found']);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// Load Campaign
|
||
if ($action === 'load_campaign') {
|
||
$name = $_GET['name'] ?? '';
|
||
$file = __DIR__ . '/campaigns/' . preg_replace('/[^a-z0-9_-]/i', '', $name) . '.json';
|
||
if (file_exists($file)) {
|
||
$data = json_decode(file_get_contents($file), true);
|
||
echo json_encode(['success' => true, 'data' => $data]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => 'Campaign not found']);
|
||
}
|
||
exit;
|
||
}
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$input = json_decode(file_get_contents('php://input'), true);
|
||
|
||
if ($action === 'save') {
|
||
if (isset($input['data']) && is_array($input['data'])) {
|
||
saveData($input['data']);
|
||
echo json_encode(['success' => true]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => 'Invalid data format']);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// Save Campaign
|
||
if ($action === 'save_campaign') {
|
||
$name = preg_replace('/[^a-z0-9_-]/i', '', $input['name'] ?? 'untitled');
|
||
$data = $input['data'] ?? [];
|
||
$file = __DIR__ . '/campaigns/' . $name . '.json';
|
||
if (!is_dir(__DIR__ . '/campaigns/')) {
|
||
mkdir(__DIR__ . '/campaigns/', 0755, true);
|
||
}
|
||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
|
||
echo json_encode(['success' => true]);
|
||
exit;
|
||
}
|
||
|
||
// Sheet Management POST Endpoints
|
||
if ($action === 'save_sheet') {
|
||
$name = $input['name'] ?? '';
|
||
$data = getData(); // Get current data
|
||
$sheet = createSheet($CURRENT_USER, $name, $data);
|
||
echo json_encode(['success' => true, 'sheet' => $sheet]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'duplicate_sheet') {
|
||
$sheetId = $input['id'] ?? '';
|
||
$sheet = duplicateSheet($CURRENT_USER, $sheetId);
|
||
if ($sheet) {
|
||
echo json_encode(['success' => true, 'sheet' => $sheet]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => 'Failed to duplicate sheet']);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_sheet') {
|
||
$sheetId = $input['id'] ?? '';
|
||
deleteSheet($CURRENT_USER, $sheetId);
|
||
echo json_encode(['success' => true]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'rename_sheet') {
|
||
$sheetId = $input['id'] ?? '';
|
||
$newName = $input['name'] ?? '';
|
||
$success = renameSheet($CURRENT_USER, $sheetId, $newName);
|
||
echo json_encode(['success' => $success]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'update_sheet') {
|
||
$sheetId = $input['id'] ?? '';
|
||
$data = $input['data'] ?? [];
|
||
updateSheet($CURRENT_USER, $sheetId, $data);
|
||
echo json_encode(['success' => true]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'command') {
|
||
$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
|
||
$correctionMap = [
|
||
'delivery balls' => 'deliverables',
|
||
'delivery ball' => 'deliverable',
|
||
'delivery' => 'deliverables',
|
||
'liver' => 'deliverables',
|
||
'rose' => 'rows',
|
||
'row' => 'rows',
|
||
'oh oh h' => 'OOH',
|
||
'out of home' => 'OOH'
|
||
];
|
||
foreach ($correctionMap as $wrong => $right) {
|
||
$commandLower = str_replace($wrong, $right, $commandLower);
|
||
}
|
||
|
||
// Pre-processing: Convert number words to digits
|
||
$numberMap = [
|
||
'one' => '1', 'two' => '2', 'three' => '3', 'four' => '4', 'five' => '5',
|
||
'six' => '6', 'seven' => '7', 'eight' => '8', 'nine' => '9', 'ten' => '10',
|
||
'eleven' => '11', 'twelve' => '12', 'twenty' => '20', 'thirty' => '30'
|
||
];
|
||
foreach ($numberMap as $word => $digit) {
|
||
$commandLower = preg_replace('/\b' . $word . '\b/', $digit, $commandLower);
|
||
}
|
||
|
||
// require_once 'config.php'; // Moved to top
|
||
|
||
|
||
// If API Key is present, use LLM
|
||
// Debug: Check if key is loaded
|
||
if (isset($GEMINI_API_KEY) && $GEMINI_API_KEY !== 'YOUR_API_KEY_HERE') {
|
||
|
||
// 1. Construct Prompt
|
||
$currentDate = date('Y-m-d');
|
||
$dataContext = json_encode($data);
|
||
$yoloMode = isset($input['yolo_mode']) && $input['yolo_mode'] ? 'TRUE' : 'FALSE';
|
||
|
||
// Get conversation history from input (if provided)
|
||
$conversationHistory = isset($input['history']) ? $input['history'] : '';
|
||
|
||
$prompt = "
|
||
You are an intelligent assistant managing a FILENAME CREATOR for digital banner campaigns.
|
||
Current Date: $currentDate
|
||
YOLO MODE: $yoloMode
|
||
|
||
CONVERSATION HISTORY:
|
||
$conversationHistory
|
||
|
||
CURRENT DATA (Context for your actions):
|
||
$dataContext
|
||
|
||
Data Schema:
|
||
- JobNumber (String, format: 0000XXX with leading zeros, user-entered)
|
||
- ProjectName (String, lowercase, no spaces - convert spaces to underscores, remove special chars)
|
||
- Size (String, dimensions like \"1080x1920\", \"300x250\" - convert \"by\" to \"x\")
|
||
- Unit (String, social media platform: Instagram, Facebook, TikTok, LinkedIn, Twitter, YouTube, Pinterest, Snapchat, Reddit, WhatsApp, Telegram, Discord, Twitch, etc.)
|
||
- Number (Auto-generated from JobNumber for compatibility, do not modify)
|
||
- Filename (Auto-generated, READ-ONLY: \"{JobNumber}_{ProjectName}_{Size}_{Unit}.png\" - DO NOT create this field, backend handles it)
|
||
|
||
Supported Operations:
|
||
1. 'create': Create new filename entries
|
||
Output: { \"operation\": \"create\", \"items\": [ { \"JobNumber\": \"0000123\", \"ProjectName\": \"newyear\", \"Size\": \"1080x1920\", \"Unit\": \"Instagram\" } ] }
|
||
|
||
2. 'update': Update existing entries
|
||
Output: { \"operation\": \"update\", \"target_ids\": [\"0000123\"], \"values\": { \"Size\": \"1080x1080\" } }
|
||
|
||
3. 'batch_update': Update multiple items with DIFFERENT values
|
||
Output: { \"operation\": \"batch_update\", \"updates\": [ { \"Number\": \"0000123\", \"values\": { \"ProjectName\": \"blackfriday\" } } ] }
|
||
|
||
4. 'question': Ask for clarification (ONLY if YOLO MODE is FALSE)
|
||
Output: { \"operation\": \"question\", \"text\": \"What's the job number for this campaign?\" }
|
||
|
||
CRITICAL RULES:
|
||
|
||
0. **MULTIPLE ITEMS CREATION**:
|
||
- When user says \"10 filenames\" or \"create 5 banners\", you MUST create that many SEPARATE items in the array.
|
||
- Example: \"Add 10 filenames\" = Create 10 separate objects in the items array
|
||
- **MATH VALIDATION**: If user says \"X filenames\" with multiple sizes/units, calculate: sizes × units = total. If total ≠ X, ask for clarification (unless YOLO mode)
|
||
- **CONFIRMATION CHECK**: If you just asked about count and user confirms, EXECUTE immediately
|
||
|
||
1. **PROJECT NAME NORMALIZATION (ULTRA IMPORTANT)**:
|
||
- ALWAYS convert to lowercase
|
||
- ALWAYS remove ALL spaces (don't use underscores, just concatenate)
|
||
- Remove special characters except hyphens
|
||
- Examples:
|
||
* \"New Year\" -> \"newyear\"
|
||
* \"Black Friday\" -> \"blackfriday\"
|
||
* \"Spring Sale 2025\" -> \"springsale2025\"
|
||
* \"Back-to-School\" -> \"back-to-school\"
|
||
|
||
2. **SIZE FORMAT EXTRACTION (STRICT)**:
|
||
- **STRICT RULE**: ALWAYS use 'x' as the separator for dimensions. NEVER use 'by'
|
||
- **Pixel Dimensions**: Convert '1080 by 1920' -> '1080x1920', '300 by 250' -> '300x250'
|
||
- **Physical Dimensions**: Convert '30 by 30 cm' -> '30x30cm'
|
||
- NO SPACES around 'x'
|
||
- Examples:
|
||
* \"1080 by 1920\" -> \"1080x1920\"
|
||
* \"300 by 250\" -> \"300x250\"
|
||
* \"728x90\" -> \"728x90\" (keep as-is)
|
||
|
||
3. **UNIT/PLATFORM HANDLING**:
|
||
- Accept full names or abbreviations: \"IG\" -> \"Instagram\", \"FB\" -> \"Facebook\", \"LI\" -> \"LinkedIn\"
|
||
- Common platforms: Instagram, Facebook, TikTok, LinkedIn, Twitter, YouTube, Pinterest, Snapchat, Reddit
|
||
- Keep user's capitalization preference when they provide full name
|
||
- If abbreviated, expand to full name
|
||
|
||
4. **JOB NUMBER HANDLING**:
|
||
- If user doesn't provide job number AND not in YOLO mode, ASK for it
|
||
- In YOLO mode: use \"0000001\" as default
|
||
- Keep leading zeros format (0000XXX)
|
||
- If user provides without leading zeros, add them (\"123\" -> \"0000123\")
|
||
|
||
5. **YOLO MODE (HIGHEST PRIORITY)**:
|
||
- If YOLO MODE is TRUE: **YOU ARE FORBIDDEN FROM ASKING QUESTIONS**
|
||
- You MUST GUESS any missing information:
|
||
* Missing job number -> use \"0000001\"
|
||
* Missing project name -> use \"campaign\"
|
||
* Missing size -> use \"1080x1920\"
|
||
* Missing unit -> use \"Instagram\"
|
||
- NEVER return 'question' operation when YOLO is TRUE
|
||
|
||
6. **CLARIFICATION RECOVERY (CRITICAL)**:
|
||
- **CONTEXT MERGING**: User's current input is likely an ANSWER to your previous question
|
||
- **DO NOT** treat the input as standalone - COMBINE with conversation history
|
||
- Example:
|
||
1. AI asks: \"What size?\"
|
||
2. User: \"300x250\"
|
||
-> Combine with previous context and EXECUTE, don't ask again
|
||
- If user seems frustrated or repeats info, JUST EXECUTE with best guesses
|
||
|
||
7. **CONTEXT IS KING**: Use 'CURRENT DATA' to resolve references like \"change all Instagram ones to Facebook\"
|
||
|
||
8. **PRECISE TARGETING**: Use Number field (which matches JobNumber) for updates via `target_ids`
|
||
|
||
CRITICAL: You MUST respond with ONLY valid JSON. No explanations, no conversational text, no markdown.
|
||
Your response must be a single JSON object starting with { and ending with }.
|
||
|
||
If you need clarification, use the 'question' operation:
|
||
{ \\\"operation\\\": \\\"question\\\", \\\"text\\\": \\\"Your question here\\\" }
|
||
|
||
User Command: \"$command\"
|
||
";
|
||
|
||
// 2. Call Gemini API
|
||
// User mentioned 2.5, likely referring to the new 2.0 Flash Experimental
|
||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=" . $GEMINI_API_KEY;
|
||
$dataPayload = [
|
||
"contents" => [
|
||
[
|
||
"parts" => [
|
||
["text" => $prompt]
|
||
]
|
||
]
|
||
]
|
||
];
|
||
|
||
$ch = curl_init($url);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($dataPayload));
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||
|
||
// FIX: Disable SSL check for local dev environments (MAMP/XAMPP often lack certs)
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||
|
||
$response = curl_exec($ch);
|
||
|
||
if (curl_errno($ch)) {
|
||
echo json_encode(['success' => false, 'message' => 'Curl error: ' . curl_error($ch)]);
|
||
exit;
|
||
}
|
||
|
||
curl_close($ch);
|
||
|
||
// 2. Parse LLM Response
|
||
$responseObj = json_decode($response, true);
|
||
|
||
// 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'),
|
||
'debug_raw' => $response
|
||
]);
|
||
exit;
|
||
}
|
||
|
||
$llmText = $responseObj['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||
|
||
if (empty($llmText)) {
|
||
echo json_encode([
|
||
'success' => false,
|
||
'message' => "AI returned an empty response.",
|
||
'debug_raw' => $response
|
||
]);
|
||
exit;
|
||
}
|
||
|
||
// Robust JSON Extraction
|
||
// Robust JSON Extraction
|
||
$start = strpos($llmText, '{');
|
||
$end = strrpos($llmText, '}');
|
||
|
||
if ($start !== false && $end !== false) {
|
||
$responseText = substr($llmText, $start, $end - $start + 1);
|
||
} else {
|
||
$responseText = $llmText; // Fallback
|
||
}
|
||
|
||
$llmAction = json_decode($responseText, true);
|
||
|
||
// Add debug info to response
|
||
$debugInfo = [
|
||
'debug_llm' => $llmText,
|
||
'debug_extracted' => $responseText,
|
||
'debug_json_error' => json_last_error_msg()
|
||
];
|
||
|
||
if (json_last_error() !== JSON_ERROR_NONE || !$llmAction) {
|
||
echo json_encode(array_merge([
|
||
'success' => false,
|
||
'message' => "Invalid JSON from AI",
|
||
], $debugInfo));
|
||
exit;
|
||
}
|
||
|
||
// 3. Execute Action
|
||
// Map $llmAction to $actionData for compatibility with existing logic below if needed,
|
||
// OR just use $llmAction directly.
|
||
// The previous code used $actionData. Let's stick to $llmAction as per my recent update intent,
|
||
// BUT the code below line 220 might expect $actionData.
|
||
// Let's check the next lines.
|
||
// The view showed `if (!$actionData) ... else { // 3. Execute Logic`.
|
||
// So I should probably replace the whole execution block or alias it.
|
||
|
||
// Let's just handle the execution here as per the NEW logic I wrote in step 528
|
||
// which used $llmAction.
|
||
|
||
if ($llmAction['operation'] === 'create') {
|
||
$newItems = $llmAction['items'];
|
||
$count = 0;
|
||
foreach ($newItems as $item) {
|
||
// Normalize project name (lowercase, remove spaces)
|
||
if (isset($item['ProjectName'])) {
|
||
$item['ProjectName'] = strtolower(str_replace(' ', '', $item['ProjectName']));
|
||
}
|
||
|
||
// Set Number field from JobNumber for compatibility
|
||
$item['Number'] = isset($item['JobNumber']) ? $item['JobNumber'] : '0000000';
|
||
|
||
// Generate filename
|
||
$item['Filename'] = generateFilename($item);
|
||
|
||
$data[] = $item;
|
||
$count++;
|
||
}
|
||
updateSheet($CURRENT_USER, $sheetId, $data);
|
||
logActivity("Created $count filenames via AI", 'SUCCESS');
|
||
echo json_encode(array_merge(['success' => true, 'message' => "Created $count filenames.", 'count' => $count], $debugInfo));
|
||
|
||
} elseif ($llmAction['operation'] === 'update') {
|
||
$updates = $llmAction['values'];
|
||
$targetIds = $llmAction['target_ids'] ?? [];
|
||
|
||
$count = 0;
|
||
foreach ($data as &$row) {
|
||
$match = false;
|
||
if (!empty($targetIds)) {
|
||
if (in_array($row['Number'], $targetIds)) {
|
||
$match = true;
|
||
}
|
||
} else {
|
||
// Fallback: Filter (simplified)
|
||
$match = true;
|
||
}
|
||
|
||
if ($match) {
|
||
foreach ($updates as $key => $val) {
|
||
$row[$key] = $val;
|
||
}
|
||
$count++;
|
||
}
|
||
}
|
||
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') {
|
||
$updates = $llmAction['updates'];
|
||
$count = 0;
|
||
foreach ($updates as $update) {
|
||
$id = $update['Number'];
|
||
$values = $update['values'];
|
||
foreach ($data as &$row) {
|
||
if ($row['Number'] === $id) {
|
||
foreach ($values as $key => $val) {
|
||
$row[$key] = $val;
|
||
}
|
||
$count++;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
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));
|
||
}
|
||
exit;
|
||
} // End if (isset($GEMINI_API_KEY))
|
||
} // End if ($action === 'command')
|
||
} // End if ($_SERVER['REQUEST_METHOD'] === 'POST')
|
||
?>
|