( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
<?php
// index.php - Simple PHP frontend to drive the n8n workflow in custom-ui.json
// Assumptions:
// - n8n is reachable at http://localhost:5678
// - The initial webhook path is "test" (from custom-ui.json)
// - The workflow returns a resumeUrl that we will POST a base64 image to
// - The resume response returns { summary, products, resumeUrl } to render
// Config (override via environment variables if needed)
$N8N_BASE_URL = getenv('N8N_BASE_URL') ?: 'https://thebrandai.app.n8n.cloud';
$WEBHOOK_PATH = getenv('N8N_WEBHOOK_PATH') ?: 'test';
$N8N_AUTH_TOKEN = getenv('N8N_AUTH_TOKEN') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZDcwMDk1Mi1hNjhiLTRhMDMtODFkZC02NTY3OWRmOGE4YWEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzYxNTA3MzY2LCJleHAiOjE3NjQwMTgwMDB9.w-J43tTtQz_qeTf7CCLIeH9cqsbLKjPRnLyMp0Opn-M';
// Robust AJAX error handling: ensure JSON is returned even on fatal errors
error_reporting(E_ALL);
ini_set('display_errors', '0');
register_shutdown_function(function(){
$err = error_get_last();
if ($err && isset($_POST['ajax'])) {
if (!headers_sent()) {
header('Content-Type: application/json');
http_response_code(500);
}
echo json_encode([
'success' => false,
'messages' => ['Server error: ' . ($err['message'] ?? 'unknown') . ' at ' . ($err['file'] ?? '?') . ':' . ($err['line'] ?? '?')],
'result' => null,
]);
}
});
function http_post_json(string $url, array $payload, int $timeout = 30): array {
// Build headers with optional Bearer token
$headers = [
'Content-Type: application/json',
'Accept: application/json'
];
global $N8N_AUTH_TOKEN;
if (!empty($N8N_AUTH_TOKEN)) {
$headers[] = 'Authorization: Bearer ' . $N8N_AUTH_TOKEN;
}
$ch = curl_init($url);
$json = json_encode($payload);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $json,
CURLOPT_TIMEOUT => $timeout,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = $errno ? curl_error($ch) : null;
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($errno) {
return ['_error' => "cURL error: $error", '_status' => $status, '_content_type' => $ctype];
}
$trimmed = is_string($response) ? trim($response) : '';
$decoded = null;
if ($trimmed !== '') {
$decoded = json_decode($trimmed, true);
}
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
// If non-JSON but 2xx, return raw for caller to interpret
if ($status >= 200 && $status < 300) {
return ['_status' => $status, '_content_type' => $ctype, '_raw' => $response];
}
return ['_error' => 'Invalid JSON response', '_status' => $status, '_content_type' => $ctype, '_raw' => $response];
}
if (!is_array($decoded)) {
$decoded = ['_value' => $decoded];
}
$decoded['_status'] = $status;
$decoded['_content_type'] = $ctype;
return $decoded;
}
function http_post_multipart(string $url, array $fields, string $filePath, string $fileName, string $mime = 'application/octet-stream', int $timeout = 30): array {
$headers = [
'Accept: application/json'
];
global $N8N_AUTH_TOKEN;
if (!empty($N8N_AUTH_TOKEN)) {
$headers[] = 'Authorization: Bearer ' . $N8N_AUTH_TOKEN;
}
$data = $fields;
$data['file'] = new CURLFile($filePath, $mime, $fileName);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $data,
CURLOPT_TIMEOUT => $timeout,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = $errno ? curl_error($ch) : null;
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($errno) {
return ['_error' => "cURL error: $error", '_status' => $status, '_content_type' => $ctype];
}
$trimmed = is_string($response) ? trim($response) : '';
$decoded = null;
if ($trimmed !== '') {
$decoded = json_decode($trimmed, true);
}
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
if ($status >= 200 && $status < 300) {
return ['_status' => $status, '_content_type' => $ctype, '_raw' => $response];
}
return ['_error' => 'Invalid JSON response', '_status' => $status, '_content_type' => $ctype, '_raw' => $response];
}
if (!is_array($decoded)) {
$decoded = ['_value' => $decoded];
}
$decoded['_status'] = $status;
$decoded['_content_type'] = $ctype;
return $decoded;
}
// Simple router for actions
$action = $_POST['action'] ?? '';
$messages = [];
$resultData = null;
// Detect AJAX requests via explicit flag
$isAjax = isset($_POST['ajax']);
if ($action === 'upload') {
// Validate file
if (!isset($_FILES['invoice']) || $_FILES['invoice']['error'] !== UPLOAD_ERR_OK) {
$messages[] = 'Please choose an invoice image file.';
} else {
$fileTmp = $_FILES['invoice']['tmp_name'];
$fileContents = @file_get_contents($fileTmp);
// Ensure filename and mime are safe strings
$fileName = (isset($_FILES['invoice']['name']) && $_FILES['invoice']['name']) ? (string)$_FILES['invoice']['name'] : 'invoice';
$mime = 'application/octet-stream';
if (function_exists('finfo_open')) {
$fi = @finfo_open(FILEINFO_MIME_TYPE);
if ($fi) {
$detected = @finfo_file($fi, $fileTmp);
if (is_string($detected) && $detected !== '') { $mime = $detected; }
@finfo_close($fi);
}
}
if ($mime === 'application/octet-stream' && isset($_FILES['invoice']['type']) && $_FILES['invoice']['type']) {
$mime = (string)$_FILES['invoice']['type'];
}
if ($fileContents === false) {
$messages[] = 'Unable to read uploaded file.';
} else {
$base64 = base64_encode($fileContents);
// Step 1: Call initial webhook to get resumeUrl
$startUrl = rtrim($N8N_BASE_URL, '/') . '/webhook/' . $WEBHOOK_PATH;
$startResp = http_post_json($startUrl, [ 'start' => true ]);
// If production webhook is not registered, try test webhook
if ((int)($startResp['_status'] ?? 0) === 404) {
$messages[] = 'Production webhook not active. Trying test webhook...';
$startUrlTest = rtrim($N8N_BASE_URL, '/') . '/webhook-test/' . $WEBHOOK_PATH;
$startRespTest = http_post_json($startUrlTest, [ 'start' => true ]);
// Prefer test response if it has resumeUrl
if (isset($startRespTest['resumeUrl'])) {
$startResp = $startRespTest;
} else {
// Keep both for diagnostics
$messages[] = 'Test webhook also failed.';
$messages[] = 'Prod response: ' . htmlspecialchars(json_encode($startResp));
$messages[] = 'Test response: ' . htmlspecialchars(json_encode($startRespTest));
}
}
if (!empty($startResp['_error'])) {
$messages[] = 'Start webhook error: ' . $startResp['_error'];
} elseif (!isset($startResp['resumeUrl'])) {
$messages[] = 'Start webhook did not return resumeUrl.';
$messages[] = 'Response: ' . htmlspecialchars(json_encode($startResp));
} else {
$resumeUrl = $startResp['resumeUrl'];
$startResumeUrl = $resumeUrl;
// Step 2: POST the image as base64 JSON to the resumeUrl (per n8n custom-ui.json)
$resumeResp = http_post_json($resumeUrl, [
'file' => $base64,
'filename' => ($fileName ?: 'invoice'),
'mime' => ($mime ?: 'application/octet-stream')
]);
// Treat HTTP errors explicitly
if ((int)($resumeResp['_status'] ?? 0) >= 400) {
$messages[] = 'Resume webhook HTTP error ' . (int)$resumeResp['_status'];
if (!empty($resumeResp['_raw'])) {
$messages[] = 'Raw response: ' . htmlspecialchars(substr((string)$resumeResp['_raw'], 0, 500));
}
} elseif (!empty($resumeResp['_error'])) {
$messages[] = 'Resume webhook error: ' . $resumeResp['_error'];
} else {
// Attempt to extract summary/products from common n8n shapes
$summary = $resumeResp['summary'] ?? '';
$products = $resumeResp['products'] ?? '';
$finalResumeUrl = $resumeResp['resumeUrl'] ?? null;
$rawForUi = $resumeResp;
if (!$summary && !$products && isset($resumeResp['_raw']) && is_string($resumeResp['_raw'])) {
$maybe = json_decode(trim($resumeResp['_raw']), true);
if (is_array($maybe)) {
if (isset($maybe[0]['json'])) {
$j = $maybe[0]['json'];
$summary = $j['summary'] ?? $summary;
$products = $j['products'] ?? $products;
$finalResumeUrl = $j['resumeUrl'] ?? $finalResumeUrl;
} elseif (isset($maybe['json'])) {
$j = $maybe['json'];
$summary = $j['summary'] ?? $summary;
$products = $j['products'] ?? $products;
$finalResumeUrl = $j['resumeUrl'] ?? $finalResumeUrl;
}
$rawForUi = $maybe;
}
}
$resultData = [
'summary' => $summary,
'products' => $products,
'startResumeUrl' => $startResumeUrl ?? null,
'finalResumeUrl' => $finalResumeUrl,
'raw' => $rawForUi,
];
if (empty($finalResumeUrl)) {
$messages[] = 'Warning: final resumeUrl missing in response.';
if (!empty($resumeResp['_raw'])) {
$messages[] = 'Note: showing raw response from resume webhook.';
}
}
}
}
}
}
} elseif ($action === 'finalize') {
$finalResumeUrl = $_POST['finalResumeUrl'] ?? '';
if (!$finalResumeUrl) {
$messages[] = 'Missing final resumeUrl.';
} else {
$finalResp = http_post_json($finalResumeUrl, [ 'confirm' => true ]);
if (!empty($finalResp['_error'])) {
$messages[] = 'Finalize webhook error: ' . $finalResp['_error'];
$resultData = [ 'finalizeRaw' => $finalResp ];
} else {
$resultData = [ 'finalizeRaw' => $finalResp ];
}
}
}
// If AJAX, return JSON response and exit before rendering HTML
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode([
'success' => empty($messages),
'messages' => $messages,
'result' => $resultData,
]);
exit;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Custom UI - Invoice Analyzer</title>
<style>
body { font-family: system-ui, Arial, sans-serif; margin: 2rem; color: #222; }
header { margin-bottom: 1.5rem; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
.row { display: flex; gap: 1rem; }
.row > .col { flex: 1; }
label { display: block; margin-bottom: 0.5rem; }
input[type=file] { margin-bottom: 1rem; }
button { padding: 0.5rem 1rem; border: 1px solid #888; background: #f5f5f5; border-radius: 6px; cursor: pointer; }
button:hover { background: #eee; }
pre { background: #f9f9f9; padding: 1rem; border-radius: 6px; overflow: auto; }
.messages { color: #b00020; }
.meta { color: #555; font-size: 0.9rem; }
</style>
</head>
<body>
<header>
<h1>Invoice Analyzer (n8n workflow)</h1>
<p class="meta">n8n base: <?php echo htmlspecialchars($N8N_BASE_URL); ?>, webhook path: <?php echo htmlspecialchars($WEBHOOK_PATH); ?></p>
</header>
<div class="card">
<h2>Upload Invoice Image</h2>
<form id="uploadForm" method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload">
<label for="invoice">Choose invoice image (PNG/JPG/PDF as image):</label>
<input type="file" name="invoice" id="invoice" accept="image/*,.png,.jpg,.jpeg">
<div>
<button id="analyzeBtn" type="submit">Analyze</button>
</div>
</form>
</div>
<div id="progressCard" class="card" style="display:none">
<h2>Progress</h2>
<p id="statusText"></p>
</div>
<div id="messagesCard" class="card messages" style="display:none">
<h2>Messages</h2>
<ul id="messagesList"></ul>
</div>
<div id="resultCard" class="card" style="display:none">
<h2>Result</h2>
<div class="row">
<div class="col">
<h3>Summary</h3>
<p id="summaryText"></p>
</div>
<div class="col">
<h3>Products</h3>
<p id="productsText"></p>
</div>
</div>
<div class="row">
<div class="col">
<h3>Start Resume URL</h3>
<p id="startResumeUrlText"></p>
</div>
<div class="col">
<h3>Final Resume URL</h3>
<p id="finalResumeUrlText"></p>
</div>
</div>
<form id="finalizeForm" style="display:none; margin-top: 1rem;">
<button type="submit">Finalize</button>
</form>
</div>
<div id="finalizeCard" class="card" style="display:none">
<h2>Finalize Response</h2>
<pre id="finalizeRaw"></pre>
</div>
<?php if (!empty($messages)): ?>
<div class="card messages">
<h2>Messages</h2>
<ul>
<?php foreach ($messages as $m): ?>
<li><?php echo htmlspecialchars($m); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($resultData && isset($resultData['summary'])): ?>
<div class="card">
<h2>Result</h2>
<div class="row">
<div class="col">
<h3>Summary</h3>
<p><?php echo nl2br(htmlspecialchars($resultData['summary'])); ?></p>
</div>
<div class="col">
<h3>Products</h3>
<p><?php echo nl2br(htmlspecialchars($resultData['products'])); ?></p>
</div>
</div>
<?php if (!empty($resultData['finalResumeUrl'])): ?>
<form method="post" style="margin-top: 1rem;">
<input type="hidden" name="action" value="finalize">
<input type="hidden" name="finalResumeUrl" value="<?php echo htmlspecialchars($resultData['finalResumeUrl']); ?>">
<button type="submit">Finalize</button>
</form>
<?php endif; ?>
<details style="margin-top: 1rem;">
<summary>Raw response</summary>
<pre><?php echo htmlspecialchars(json_encode($resultData['raw'], JSON_PRETTY_PRINT)); ?></pre>
</details>
</div>
<?php endif; ?>
<?php if ($resultData && isset($resultData['finalizeRaw'])): ?>
<div class="card">
<h2>Finalize Response</h2>
<pre><?php echo htmlspecialchars(json_encode($resultData['finalizeRaw'], JSON_PRETTY_PRINT)); ?></pre>
</div>
<?php endif; ?>
<footer class="meta">
<p>Tip: If your n8n instance is not running on localhost:5678, set N8N_BASE_URL in your environment or update the config variables at the top of this file.</p>
</footer>
</body>
</html>
<script>
(function(){
function initCore(){
const uploadForm = document.getElementById('uploadForm');
const messagesCard = document.getElementById('messagesCard');
const messagesList = document.getElementById('messagesList');
const progressCard = document.getElementById('progressCard');
const statusText = document.getElementById('statusText');
const resultCard = document.getElementById('resultCard');
const summaryText = document.getElementById('summaryText');
const productsText = document.getElementById('productsText');
const startResumeUrlText = document.getElementById('startResumeUrlText');
const finalResumeUrlText = document.getElementById('finalResumeUrlText');
const finalizeForm = document.getElementById('finalizeForm');
const finalizeCard = document.getElementById('finalizeCard');
const finalizeRaw = document.getElementById('finalizeRaw');
let finalResumeUrl = null;
function setLink(el, url){
if (!el) return;
if (url) {
const safe = document.createElement('a');
safe.href = url;
safe.target = '_blank';
safe.rel = 'noopener';
safe.textContent = url;
el.innerHTML = '';
el.appendChild(safe);
} else {
el.textContent = '';
}
}
function showMessages(msgs){
if (!messagesCard || !messagesList) return;
messagesList.innerHTML = '';
(msgs || []).forEach(m => {
const li = document.createElement('li');
li.textContent = m;
messagesList.appendChild(li);
});
messagesCard.style.display = (msgs && msgs.length) ? 'block' : 'none';
}
function showProgress(text){
if (!progressCard || !statusText) return;
statusText.textContent = text || '';
progressCard.style.display = text ? 'block' : 'none';
}
function clearUI(){
showMessages([]);
showProgress('');
if (resultCard) resultCard.style.display = 'none';
if (finalizeCard) finalizeCard.style.display = 'none';
if (finalizeForm) finalizeForm.style.display = 'none';
setLink(startResumeUrlText, '');
setLink(finalResumeUrlText, '');
}
if (uploadForm) {
uploadForm.addEventListener('submit', async function(e){
e.preventDefault();
clearUI();
showProgress('Starting webhook...');
const fd = new FormData(uploadForm);
fd.set('action','upload');
fd.set('ajax','1');
try {
const res = await fetch('index.php', { method:'POST', body: fd, headers: { 'Accept': 'application/json' } });
showProgress('Waiting for analysis...');
const status = res.status;
const text = await res.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch(parseErr) {
showProgress('');
showMessages([`HTTP ${status}: invalid JSON`, text ? `Raw: ${text.slice(0,500)}` : 'Empty response']);
return;
}
showProgress('');
if (!data) {
showMessages([`HTTP ${status}: Empty response`]);
return;
}
showMessages(data.messages || []);
if (data.result) {
if (summaryText) summaryText.textContent = data.result.summary || '';
if (productsText) productsText.textContent = data.result.products || '';
setLink(startResumeUrlText, data.result.startResumeUrl || '');
setLink(finalResumeUrlText, data.result.finalResumeUrl || '');
if (resultCard) resultCard.style.display = 'block';
finalResumeUrl = data.result.finalResumeUrl || null;
if (finalizeForm) finalizeForm.style.display = finalResumeUrl ? 'block' : 'none';
}
} catch(err){
showProgress('');
showMessages(['Network error: ' + err]);
}
});
}
if (finalizeForm) {
finalizeForm.addEventListener('submit', async function(e){
e.preventDefault();
if (!finalResumeUrl) {
showMessages(['Missing final resume URL.']);
return;
}
showProgress('Finalizing...');
const fd = new FormData();
fd.set('action','finalize');
fd.set('ajax','1');
fd.set('finalResumeUrl', finalResumeUrl);
try {
const res = await fetch('index.php', { method:'POST', body: fd, headers: { 'Accept': 'application/json' } });
const status = res.status;
const text = await res.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch(parseErr) {
showProgress('');
showMessages([`HTTP ${status}: invalid JSON`, text ? `Raw: ${text.slice(0,500)}` : 'Empty response']);
return;
}
showProgress('');
showMessages(data.messages || []);
if (finalizeCard && finalizeRaw) {
finalizeCard.style.display = 'block';
finalizeRaw.textContent = JSON.stringify((data.result && data.result.finalizeRaw) ? data.result.finalizeRaw : data, null, 2);
}
} catch(err){
showProgress('');
showMessages(['Network error: ' + err]);
}
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCore);
} else {
initCore();
}
})();
</script>