ac-helper/api.php

490 lines
22 KiB
PHP
Raw Permalink 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);
}
$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;
}
}
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;
}
// 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 an Activation Calendar.
Current Date: $currentDate
YOLO MODE: $yoloMode
CONVERSATION HISTORY:
$conversationHistory
CURRENT DATA (Context for your actions):
$dataContext
Data Schema:
- Number (Auto-generated, do not invent)
- Title (String)
- Status (Enum: Booked, To-do, In Progress, Done) - Default to 'Booked'
- Category (Enum: 'Digital', 'Print', 'Out of Home', 'Video')
- Media (Dependent on Category - see below)
- Sub-media (Dependent on Category AND Media - see below)
HIERARCHY RULES (Strictly follow these combinations):
1. Category: Digital
- Media: Online Advertising -> Sub-media: Banner, Rich Media, Landing Page, Static Image, Video, Push notifications, .com
- Media: Social -> Sub-media: GIF, Video, Static Image, Multi-Asset Build
- Media: Community management -> Sub-media: (None)
- Media: POS -> Sub-media: Digital
2. Category: Print
- Media: POS -> Sub-media: Print
- Media: Out of Home -> Sub-media: Print
- Media: Direct Mail -> Sub-media: Print
- Media: Packaging -> Sub-media: Print
3. Category: Out of Home
- Media: Out of Home -> Sub-media: Print, Digital
4. Category: Video
- Media: POS -> Sub-media: Video
- Media: Online Advertising -> Sub-media: Video
- Media: Social -> Sub-media: Video
- Media: Broadcast -> Sub-media: TV, Cinema, Radio, VOD
- Format (String) - Extract sizes/dimensions here! e.g., '300x250', 'A4', '10x15cm', 'Full Page', '1080p'.
- Supply date (YYYY-MM-DD)
- Live date (YYYY-MM-DD)
- Language (ISO 2-letter code, UPPERCASE, e.g., 'EN', 'FR', 'ES')
- Country (ISO 2-letter code, UPPERCASE, e.g., 'GB', 'FR', 'ES')
- Quantity (Integer)
Supported Operations:
1. 'create': Create new items.
Output: { \"operation\": \"create\", \"items\": [ { \"Title\": \"...\", \"Format\": \"300x250\", ... } ] }
2. 'update': Update existing items.
Output: { \"operation\": \"update\", \"target_ids\": [\"DEL-001\"], \"values\": { \"Status\": \"Done\" } }
3. 'batch_update': Update multiple items with DIFFERENT values.
Output: { \"operation\": \"batch_update\", \"updates\": [ { \"Number\": \"DEL-001\", \"values\": { \"Title\": \"Row 1\" } } ] }
4. 'question': Ask for clarification (ONLY if YOLO MODE is FALSE).
Output: { \"operation\": \"question\", \"text\": \"Did you mean 2025 or 2026?\" }
IMPORTANT BRAIN RULES:
0. **CRITICAL - MULTIPLE ITEMS vs QUANTITY**:
- When user says \"add 10 deliverables\" or \"create 5 banners\", you MUST create that many SEPARATE items in the array.
- NEVER use Quantity field to represent the count. Quantity should always be 1 unless explicitly stated otherwise.
- Example: \"Add 10 digital deliverables\" = Create 10 separate objects in the items array, each with Quantity=1.
- **MATH VALIDATION (MANDATORY)**:
* BEFORE creating items, COUNT how many items your pattern will create.
* If the user says \"X items\" but your pattern creates Y items where X ≠ Y, you MUST use 'question' operation.
* Example: User says \"10 print ads, 2 formats, 4 countries, alternating men/women\"
Your calculation: 2 formats × 4 countries × 2 titles = 16 items
Since 16 ≠ 10, return: {\"operation\": \"question\", \"text\": \"I calculated 16 items (2 formats × 4 countries × 2 titles). Did you mean 16 items total, or should I create only 10 items with a different pattern?\"}
* ONLY create items if the math matches OR if the user didn't specify a total count.
* **EXCEPTION - CONFIRMATION**: If the user's input is a confirmation of the count (e.g., \"Yes\", \"20\", \"Correct\"), DO NOT ask again. EXECUTE with the confirmed count.
* **CHECK HISTORY**: If you just asked \"I calculated X...\", and user says \"X\", PROCEED immediately.
- If the request is vague (no title, format, etc.), ask for clarification using the 'question' operation.
1. **FORMAT EXTRACTION (Ultra Complex Logic)**:
- **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 (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\".
5. **PRECISE TARGETING**: Use `target_ids` for updates.
5. **INFER FIELDS**:
- \"OOH\" -> Category='OOH', Media='OOH'.
- \"Instagram Story\" -> Category='Digital', Media='Social', Sub-media='Instagram', Format='9:16'.
- \"English\" -> Language='EN', \"French\" -> Language='FR', \"Spanish\" -> Language='ES'
- \"Great Britain\" or \"UK\" -> Country='GB', \"France\" -> Country='FR', \"Spain\" -> Country='ES'
6. **PATTERN RECOGNITION**:
- If user says \"repeat 3 times\" with different languages, create 3 sets of items.
- Extract formats from phrases like \"200 by 200\", \"300x300\", \"400x400 banner\".
- Pay attention to sequences: \"first 5\", \"next 4\", \"remaining\" for language/country assignments.
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) {
// Generate ID
$lastId = 0;
foreach ($data as $row) {
$num = intval(str_replace('DEL-', '', $row['Number']));
if ($num > $lastId) $lastId = $num;
}
$newId = 'DEL-' . str_pad($lastId + 1, 3, '0', STR_PAD_LEFT);
$item['Number'] = $newId;
// Ensure defaults
if (empty($item['Status'])) $item['Status'] = 'Booked';
if (empty($item['Quantity'])) $item['Quantity'] = 1;
$data[] = $item;
$count++;
}
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') {
$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')
?>