( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
<?php
// ProposalX: Custom multi-step wizard replacing n8n forms.
// Configure webhook endpoints below to match your workflow.
// Config (env-backed, with safe defaults)
$N8N_BASE_URL = getenv('N8N_BASE_URL') ?: 'https://thebrandai.app.n8n.cloud';
$N8N_AUTH_TOKEN = getenv('N8N_AUTH_TOKEN') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZDcwMDk1Mi1hNjhiLTRhMDMtODFkZC02NTY3OWRmOGE4YWEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzYxNTA3MzY2LCJleHAiOjE3NjQwMTgwMDB9.w-J43tTtQz_qeTf7CCLIeH9cqsbLKjPRnLyMp0Opn-M';
$PROPOSALX_TRIGGER = getenv('PROPOSALX_TRIGGER') ?: rtrim($N8N_BASE_URL, '/') . '/webhook/proposalx/trigger';
$PROPOSALX_ANSWER = getenv('PROPOSALX_ANSWER') ?: rtrim($N8N_BASE_URL, '/') . '/webhook/proposalx/answer';
$PROPOSALX_CONFIRM = getenv('PROPOSALX_CONFIRM') ?: rtrim($N8N_BASE_URL, '/') . '/webhook/proposalx/confirm';
function proxy_to_n8n($url, $authToken = null){
$method = $_SERVER['REQUEST_METHOD'];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_ENCODING, ''); // auto-decompress gzip
// Build pass-through headers with safe defaults
$hdrs = array();
$incoming = function_exists('getallheaders') ? getallheaders() : array();
foreach ($incoming as $k=>$v){
$lk = strtolower($k);
if ($lk === 'accept-encoding' || $lk === 'host') continue;
$hdrs[] = $k . ': ' . $v;
}
// Inject server-side Authorization if provided (override any incoming Authorization)
if ($authToken) {
$tmp = array();
foreach ($hdrs as $h){ if (stripos($h, 'authorization:') !== 0) $tmp[] = $h; }
$hdrs = $tmp;
$hdrs[] = 'Authorization: Bearer ' . $authToken;
}
// Forward body and ensure Content-Type for JSON
if (in_array($method, array('POST','PUT','PATCH'))){
$body = file_get_contents('php://input');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$hasCt = false;
foreach ($hdrs as $h){ if (stripos($h, 'Content-Type:') === 0) { $hasCt = true; break; } }
if (!$hasCt) { $hdrs[] = 'Content-Type: application/json'; }
}
if (!empty($hdrs)) curl_setopt($ch, CURLOPT_HTTPHEADER, $hdrs);
$resp = curl_exec($ch);
$err = curl_error($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
// If upstream unreachable or failed at transport level, return JSON error
if ($resp === false){
header('Content-Type: application/json');
http_response_code(502);
echo json_encode(array(
'error' => 'Proxy request failed',
'upstream' => $url,
'method' => $method,
'curl_error' => $err
));
exit;
}
if ($ctype) header('Content-Type: ' . $ctype);
http_response_code($status ?: 200);
echo $resp; exit;
}
if (isset($_GET['action'])){
$a = $_GET['action'];
$method = $_SERVER['REQUEST_METHOD'];
if (in_array($a, array('trigger','answer','confirm')) && $method !== 'POST'){
header('Content-Type: application/json');
http_response_code(405);
echo json_encode(array(
'error' => 'Method Not Allowed',
'message' => 'Use POST with JSON body for action=' . $a,
'allowed' => array('POST')
));
exit;
}
if ($a === 'trigger') proxy_to_n8n($PROPOSALX_TRIGGER, $N8N_AUTH_TOKEN);
if ($a === 'answer') proxy_to_n8n($PROPOSALX_ANSWER, $N8N_AUTH_TOKEN);
if ($a === 'confirm') proxy_to_n8n($PROPOSALX_CONFIRM, $N8N_AUTH_TOKEN);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ProposalX Wizard</title>
<style>
:root{--bg:#f7f9fc;--card:#ffffff;--border:#e6e6e6;--text:#0f172a;--muted:#607089;--accent:#1f7aec}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;color:var(--text)}
.wrap{max-width:920px;margin:24px auto;padding:0 16px}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;box-shadow:0 2px 6px rgba(0,0,0,.04);padding:16px}
h1{margin:0 0 8px;font-size:20px}
.small{color:var(--muted);font-size:12px}
.code{background:#eef2ff;border:1px solid #c7d2fe;border-radius:6px;padding:2px 6px;font-family:ui-monospace,Consolas,Monaco,monospace}
.row{display:flex;gap:10px;align-items:center;margin-top:8px}
.btn{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 14px;font-weight:600;cursor:pointer}
.btn.secondary{background:#eef2ff;color:#0f172a;border:1px solid #c7d2fe}
.btn:disabled{opacity:.6;cursor:not-allowed}
.inputs{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px}
.inputs .field{background:#fafafa;border:1px solid var(--border);border-radius:8px;padding:10px}
.field label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
.field input,.field textarea,.field select{width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px}
.q{margin-top:12px}
.q .label{font-weight:600;margin-bottom:6px}
.q textarea{width:100%;height:90px}
.list{margin-top:8px}
.item{display:flex;align-items:center;gap:8px;margin-top:6px}
.item .label{min-width:160px;color:#607089}
.item .value{flex:1}
.steps{display:flex;gap:6px;margin-top:12px}
.pill{padding:6px 10px;border:1px solid var(--border);border-radius:999px;font-size:12px;color:#607089}
.pill.active{background:#eef2ff;border-color:#c7d2fe;color:#0f172a;font-weight:600}
.section{display:none;margin-top:12px}
.section.active{display:block}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
pre{background:#0f172a;color:#fff;padding:10px;border-radius:8px;overflow:auto}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>ProposalX <span class="small">Webhook Wizard</span></h1>
<div class="row small">Webhook Base: <span class="code" id="whBase"><?php echo htmlspecialchars(rtrim($N8N_BASE_URL, '/')); ?></span></div>
<div class="steps"><span class="pill" id="p1">1. Trigger</span><span class="pill" id="p2">2. Answers</span><span class="pill" id="p3">3. Confirm</span></div>
<div class="section active" id="step1">
<div class="inputs">
<div class="field"><label>Proposal Request</label><textarea id="prompt" placeholder="e.g., Build a CRM for SMBs"></textarea></div>
<div class="field"><label>Depth</label><select id="depth"><option>3</option><option>4</option><option>5</option></select></div>
<div class="field"><label>Breadth</label><select id="breadth"><option>3</option><option>4</option><option>5</option></select></div>
</div>
<div class="row"><button class="btn" id="btnTrigger">Get Questions</button></div>
<div class="grid">
<div>
<div class="list" id="triggerOut"></div>
<div id="questionsContainer"></div>
<div class="row"><button class="btn secondary" id="btnBack1">Back</button><button class="btn" id="btnSubmitAnswers">Submit Answers</button></div>
</div>
<div>
<pre id="triggerRaw">(no response yet)</pre>
</div>
</div>
</div>
<div class="section" id="step2">
<div class="grid">
<div>
<div class="list" id="answersOut"></div>
<div class="row"><button class="btn secondary" id="btnRestart">Start Over</button><button class="btn" id="btnConfirm">Done</button></div>
</div>
<div>
<pre id="answersRaw">(no response yet)</pre>
</div>
</div>
</div>
<div class="section" id="step3">
<div class="grid">
<div>
<div class="list" id="finalList"></div>
</div>
<div>
<pre id="finalRaw">(no response yet)</pre>
</div>
</div>
</div>
</div>
</div>
<script>
function $(id){ return document.getElementById(id); }
function setStep(n){ ['step1','step2','step3'].forEach((id,i)=>{ const el=$(id); if(el) el.classList.toggle('active', i+1===n); }); ['p1','p2','p3'].forEach((id,i)=>{ const el=$(id); if(el) el.classList.toggle('active', i < n); }); }
function pretty(obj){ try{ return JSON.stringify(obj, null, 2); }catch(e){ return String(obj); } }
function firstJson(latest){ let obj = latest; try{ if(Array.isArray(latest)) obj = latest[0] || {}; if (obj.json) obj = obj.json; }catch(e){} return obj || {}; }
var state = { requestId:null, prompt:'', depth:3, breadth:3, questions:[], triggerResp:null, answersResp:null, notion:{title:'', description:'', url:''} };
function extractQuestions(resp){
const obj = firstJson(resp);
let q = obj.output && obj.output.questions ? obj.output.questions : (obj.questions || []);
if (!Array.isArray(q)) q = [];
if (!q.length && Array.isArray(resp)) {
q = resp.map(it=>{ const j=it.json||it; return j.question || j.label || null; }).filter(Boolean);
}
return q;
}
function extractRequestId(resp){
const obj = firstJson(resp); return obj.request_id || obj.requestId || null;
}
function renderQuestions(){
const c = $('questionsContainer'); c.innerHTML = '';
if (!state.questions.length){ c.innerHTML = '<div class="small">No questions returned.</div>'; return; }
state.questions.forEach((q,i)=>{
const wrap = document.createElement('div'); wrap.className='q';
const lab = document.createElement('div'); lab.className='label'; lab.textContent = (i+1) + '. ' + q;
const ta = document.createElement('textarea'); ta.id = 'answer_'+i; ta.placeholder = 'Your answer...';
wrap.appendChild(lab); wrap.appendChild(ta); c.appendChild(wrap);
});
}
function list(containerId, items){
const rl = $(containerId); rl.innerHTML='';
items.forEach(it=>{
const d = document.createElement('div'); d.className='item';
const l = document.createElement('div'); l.className='label'; l.textContent = it.label;
const v = document.createElement('div'); v.className='value'; v.textContent = it.value;
d.appendChild(l); d.appendChild(v); rl.appendChild(d);
});
}
$('btnTrigger').addEventListener('click', function(){
state.prompt = $('prompt').value || '';
state.depth = parseInt(($('depth').value||'3'),10);
state.breadth = parseInt(($('breadth').value||'3'),10);
const payload = {
'What proposal would you like to create?': state.prompt,
depth: state.depth,
breadth: state.breadth
};
fetch('proposal.php?action=trigger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(async r => { const txt = await r.text(); try { return JSON.parse(txt); } catch(e){ return { raw: txt, status: r.status, contentType: r.headers.get('Content-Type') }; } }).then(json=>{
state.triggerResp = json;
const q = extractQuestions(json);
state.questions = q;
state.requestId = extractRequestId(json);
list('triggerOut', [
{ label: 'Request ID', value: state.requestId || '(none)' },
{ label: 'Total Questions', value: String(q.length) },
]);
$('triggerRaw').textContent = pretty(json);
renderQuestions();
setStep(2);
}).catch(err=>{
$('triggerRaw').textContent = 'Error: ' + err;
});
});
$('btnBack1').addEventListener('click', function(){ setStep(1); });
$('btnSubmitAnswers').addEventListener('click', function(){
const answersMap = {};
state.questions.forEach((q,i)=>{ const a = (document.getElementById('answer_'+i).value||'').trim(); answersMap[q] = a; });
const payload = {
request_id: state.requestId,
prompt: state.prompt,
depth: state.depth,
breadth: state.breadth,
answers: answersMap
};
fetch('proposal.php?action=answer', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
}).then(async r => { const txt = await r.text(); try { return JSON.parse(txt); } catch(e){ return { raw: txt, status: r.status, contentType: r.headers.get('Content-Type') }; } }).then(json=>{
state.answersResp = json;
const obj = firstJson(json);
const title = (obj.output && obj.output.title) ? obj.output.title : (obj.title || '');
const desc = (obj.output && obj.output.description) ? obj.output.description : (obj.description || '');
const url = obj.url || (obj.notionUrl || '');
state.notion = { title: title, description: desc, url: url };
list('answersOut', [
{ label: 'Title', value: title || '(none)' },
{ label: 'Description', value: desc || '(none)' },
{ label: 'Notion URL', value: url || '(none)' },
]);
$('answersRaw').textContent = pretty(json);
list('finalList', [
{ label: 'Request ID', value: state.requestId || '(none)' },
{ label: 'Title', value: title || '(none)' },
{ label: 'Description', value: desc || '(none)' },
{ label: 'Notion URL', value: url || '(none)' },
]);
$('finalRaw').textContent = pretty(json);
setStep(3);
}).catch(err=>{ $('answersRaw').textContent = 'Error: ' + err; });
});
$('btnConfirm').addEventListener('click', function(){
const payload = { request_id: state.requestId, status: 'done' };
fetch('proposal.php?action=confirm', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
}).then(r=>r.text()).then(txt=>{ alert('Confirmation sent.'); }).catch(err=>{ alert('Error: ' + err); });
});
$('btnRestart').addEventListener('click', function(){ location.href = '/proposal.php'; });
</script>
</body>
</html>