wsj-filenaming/api.php
DJP 0939fbfb33 Initial commit: WSJ Filename Creator
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>
2025-12-16 13:40:59 -05:00

493 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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')
?>