Sofa Test

<!–
=============================================================
AI ROOM VISUALIZER — WordPress/WooCommerce Widget
Drop this into a Gutenberg “Custom HTML” block or
Elementor “HTML” widget on any WooCommerce product page.

HOW IT WORKS:
1. On load, it grabs the WooCommerce product featured image
automatically from the page (no client action needed).
2. Client uploads only their room photo.
3. Both images are sent to your n8n webhook as base64.
4. The AI-generated result is displayed on the same page.

SETUP:
– Replace YOUR_N8N_WEBHOOK_URL below with your actual URL.
=============================================================
–>

<style>
@import url(‘https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600&family=DM+Mono:wght@300;400&display=swap’);

:root {
–cream: #F5F0E8;
–warm-white: #FDFAF5;
–charcoal: #1C1C1E;
–slate: #3A3A3C;
–muted: #8A8A8E;
–accent: #C4955A;
–accent-light: #E8D5B5;
–accent-dark: #9A6F38;
–border: #E0D8CC;
–success: #4A7C59;
–error: #C0392B;
}

#ai-visualizer-root {
font-family: ‘Cormorant Garamond’, Georgia, serif;
background: var(–warm-white);
border: 1px solid var(–border);
max-width: 760px;
margin: 40px auto;
padding: 0;
overflow: hidden;
position: relative;
}

#ai-visualizer-root * {
box-sizing: border-box;
margin: 0;
padding: 0;
}

/* ── Header ── */
.viz-header {
background: var(–charcoal);
padding: 28px 36px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
}

.viz-header-text h2 {
font-size: 26px;
font-weight: 300;
letter-spacing: 0.06em;
color: var(–cream);
line-height: 1.1;
}

.viz-header-text p {
font-family: ‘DM Mono’, monospace;
font-size: 10px;
font-weight: 300;
letter-spacing: 0.15em;
color: var(–muted);
text-transform: uppercase;
margin-top: 6px;
}

.viz-badge {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.2em;
color: var(–accent);
border: 1px solid var(–accent-dark);
padding: 5px 10px;
text-transform: uppercase;
white-space: nowrap;
flex-shrink: 0;
}

/* ── Body ── */
.viz-body {
padding: 36px;
}

/* ── Product Preview Row ── */
.viz-product-row {
display: flex;
align-items: center;
gap: 16px;
background: var(–cream);
border: 1px solid var(–border);
padding: 14px 18px;
margin-bottom: 32px;
}

.viz-product-thumb {
width: 56px;
height: 56px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid var(–border);
background: var(–border);
}

.viz-product-thumb-placeholder {
width: 56px;
height: 56px;
flex-shrink: 0;
border: 1px solid var(–border);
background: var(–border);
display: flex;
align-items: center;
justify-content: center;
}

.viz-product-info {
flex: 1;
}

.viz-product-label {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(–muted);
margin-bottom: 4px;
}

.viz-product-name {
font-size: 15px;
font-weight: 600;
color: var(–charcoal);
letter-spacing: 0.02em;
}

.viz-product-status {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.15em;
color: var(–success);
text-transform: uppercase;
display: flex;
align-items: center;
gap: 5px;
margin-top: 3px;
}

.viz-product-status::before {
content: ”;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(–success);
flex-shrink: 0;
}

/* ── Upload Zone ── */
.viz-upload-label {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(–muted);
display: block;
margin-bottom: 10px;
}

.viz-dropzone {
border: 1px dashed var(–accent);
background: transparent;
padding: 40px 24px;
text-align: center;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
position: relative;
}

.viz-dropzone:hover,
.viz-dropzone.drag-over {
background: var(–accent-light);
border-color: var(–accent-dark);
}

.viz-dropzone input[type=”file”] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}

.viz-dropzone-icon {
width: 36px;
height: 36px;
margin: 0 auto 14px;
opacity: 0.4;
}

.viz-dropzone-text {
font-size: 15px;
font-weight: 300;
color: var(–slate);
letter-spacing: 0.04em;
}

.viz-dropzone-sub {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.15em;
color: var(–muted);
text-transform: uppercase;
margin-top: 6px;
}

/* ── Preview of uploaded room ── */
.viz-room-preview {
display: none;
margin-top: 16px;
position: relative;
}

.viz-room-preview img {
width: 100%;
max-height: 220px;
object-fit: cover;
display: block;
border: 1px solid var(–border);
}

.viz-room-preview-clear {
position: absolute;
top: 8px;
right: 8px;
background: var(–charcoal);
color: var(–cream);
border: none;
width: 26px;
height: 26px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
font-family: ‘DM Mono’, monospace;
line-height: 1;
}

/* ── Submit Button ── */
.viz-submit {
margin-top: 28px;
width: 100%;
background: var(–charcoal);
color: var(–cream);
border: none;
padding: 17px 24px;
font-family: ‘DM Mono’, monospace;
font-size: 11px;
letter-spacing: 0.25em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}

.viz-submit:hover:not(:disabled) {
background: var(–accent-dark);
}

.viz-submit:disabled {
opacity: 0.45;
cursor: not-allowed;
}

/* ── Status / Loading ── */
.viz-status {
display: none;
margin-top: 24px;
padding: 14px 18px;
font-family: ‘DM Mono’, monospace;
font-size: 10px;
letter-spacing: 0.15em;
text-transform: uppercase;
border-left: 3px solid var(–accent);
color: var(–slate);
background: var(–cream);
align-items: center;
gap: 12px;
}

.viz-status.active { display: flex; }

.viz-status.error {
border-left-color: var(–error);
color: var(–error);
background: #FDF2F0;
}

/* Spinner */
.viz-spinner {
width: 14px;
height: 14px;
border: 2px solid var(–accent-light);
border-top-color: var(–accent);
border-radius: 50%;
animation: viz-spin 0.8s linear infinite;
flex-shrink: 0;
}

@keyframes viz-spin {
to { transform: rotate(360deg); }
}

/* ── Result ── */
.viz-result {
display: none;
margin-top: 32px;
animation: viz-fadeup 0.5s ease forwards;
}

@keyframes viz-fadeup {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}

.viz-result-label {
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(–muted);
margin-bottom: 10px;
}

.viz-result img {
width: 100%;
display: block;
border: 1px solid var(–border);
}

.viz-result-actions {
display: flex;
gap: 10px;
margin-top: 14px;
}

.viz-btn-outline {
flex: 1;
background: transparent;
border: 1px solid var(–charcoal);
color: var(–charcoal);
padding: 11px 16px;
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
text-decoration: none;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}

.viz-btn-outline:hover {
background: var(–charcoal);
color: var(–cream);
}

/* ── Divider ── */
.viz-divider {
height: 1px;
background: var(–border);
margin: 28px 0;
}

/* ── Footer note ── */
.viz-footer {
padding: 14px 36px;
border-top: 1px solid var(–border);
background: var(–cream);
font-family: ‘DM Mono’, monospace;
font-size: 9px;
letter-spacing: 0.12em;
color: var(–muted);
text-transform: uppercase;
display: flex;
gap: 24px;
}
</style>

<div id=”ai-visualizer-root”>

<!– Header –>
<div class=”viz-header”>
<div class=”viz-header-text”>
<h2>Visualise In Your Space</h2>
<p>AI-powered room rendering · Results in ~60 seconds</p>
</div>
<div class=”viz-badge”>Beta</div>
</div>

<!– Body –>
<div class=”viz-body”>

<!– Auto-loaded product –>
<div class=”viz-product-row”>
<img id=”viz-product-img” class=”viz-product-thumb” src=”” alt=”” style=”display:none;” />
<div id=”viz-product-placeholder” class=”viz-product-thumb-placeholder”>
<svg width=”20″ height=”20″ viewBox=”0 0 24 24″ fill=”none” stroke=”#aaa” stroke-width=”1.5″><rect x=”3″ y=”3″ width=”18″ height=”18″ rx=”1″/><path d=”M3 9h18M9 21V9″/></svg>
</div>
<div class=”viz-product-info”>
<div class=”viz-product-label”>Selected product</div>
<div class=”viz-product-name” id=”viz-product-name”>Loading product…</div>
<div class=”viz-product-status” id=”viz-product-status”>Auto-detected from page</div>
</div>
</div>

<!– Room upload –>
<label class=”viz-upload-label”>Your room photo</label>
<div class=”viz-dropzone” id=”viz-dropzone”>
<input type=”file” id=”viz-room-input” accept=”image/*” />
<svg class=”viz-dropzone-icon” viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”1.5″>
<path d=”M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4″/>
<polyline points=”17 8 12 3 7 8″/>
<line x1=”12″ y1=”3″ x2=”12″ y2=”15″/>
</svg>
<div class=”viz-dropzone-text”>Drop your room photo here</div>
<div class=”viz-dropzone-sub”>or click to browse · JPG, PNG, WEBP</div>
</div>

<!– Room preview –>
<div class=”viz-room-preview” id=”viz-room-preview”>
<img id=”viz-room-img-preview” src=”” alt=”Room preview” />
<button class=”viz-room-preview-clear” id=”viz-clear-room” title=”Remove”>✕</button>
</div>

<!– Submit –>
<button class=”viz-submit” id=”viz-submit” disabled>
<svg width=”14″ height=”14″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″><polygon points=”13 2 3 14 12 14 11 22 21 10 12 10 13 2″/></svg>
Generate Visualisation
</button>

<!– Status –>
<div class=”viz-status” id=”viz-status”>
<div class=”viz-spinner” id=”viz-spinner”></div>
<span id=”viz-status-text”>Sending to AI…</span>
</div>

<!– Result –>
<div class=”viz-result” id=”viz-result”>
<div class=”viz-divider”></div>
<div class=”viz-result-label”>Your room — visualised</div>
<img id=”viz-result-img” src=”” alt=”AI visualisation result” />
<div class=”viz-result-actions”>
<a id=”viz-download-btn” class=”viz-btn-outline” download=”visualisation.jpg”>
<svg width=”11″ height=”11″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″><path d=”M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4″/><polyline points=”7 10 12 15 17 10″/><line x1=”12″ y1=”15″ x2=”12″ y2=”3″/></svg>
Download
</a>
<button class=”viz-btn-outline” id=”viz-retry-btn”>
<svg width=”11″ height=”11″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″><polyline points=”23 4 23 10 17 10″/><path d=”M20.49 15a9 9 0 1 1-2.12-9.36L23 10″/></svg>
Try Another Room
</button>
</div>
</div>

</div>

<!– Footer –>
<div class=”viz-footer”>
<span>🔒 Your photos are not stored</span>
<span>⚡ Powered by AI</span>
<span>~60 sec processing</span>
</div>

</div>

<script>
(function () {

// ─────────────────────────────────────────────
// CONFIG — replace with your n8n webhook URL
// ─────────────────────────────────────────────
var WEBHOOK_URL = ‘YOUR_N8N_WEBHOOK_URL’;

// ─────────────────────────────────────────────
// STATE
// ─────────────────────────────────────────────
var productImageBase64 = null;
var productImageUrl = null;
var roomImageBase64 = null;
var roomFileName = ‘room.jpg’;

// ─────────────────────────────────────────────
// ELEMENT REFS
// ─────────────────────────────────────────────
var $productImg = document.getElementById(‘viz-product-img’);
var $productPlaceholder = document.getElementById(‘viz-product-placeholder’);
var $productName = document.getElementById(‘viz-product-name’);
var $productStatus = document.getElementById(‘viz-product-status’);
var $dropzone = document.getElementById(‘viz-dropzone’);
var $roomInput = document.getElementById(‘viz-room-input’);
var $roomPreview = document.getElementById(‘viz-room-preview’);
var $roomImgPreview = document.getElementById(‘viz-room-img-preview’);
var $clearRoom = document.getElementById(‘viz-clear-room’);
var $submit = document.getElementById(‘viz-submit’);
var $status = document.getElementById(‘viz-status’);
var $statusText = document.getElementById(‘viz-status-text’);
var $spinner = document.getElementById(‘viz-spinner’);
var $result = document.getElementById(‘viz-result’);
var $resultImg = document.getElementById(‘viz-result-img’);
var $downloadBtn = document.getElementById(‘viz-download-btn’);
var $retryBtn = document.getElementById(‘viz-retry-btn’);

// ─────────────────────────────────────────────
// AUTO-DETECT WOOCOMMERCE PRODUCT IMAGE
// ─────────────────────────────────────────────
function detectProductImage() {
var imgEl = null;

// Priority 1 — WooCommerce featured image container
var selectors = [
‘.woocommerce-product-gallery__image img’,
‘.woocommerce-product-gallery img’,
‘.product-images img’,
‘.wp-post-image’,
‘img.attachment-woocommerce_single’,
‘img.attachment-shop_single’,
‘.product .woocommerce-main-image img’,
‘.product img.wp-post-image’
];

for (var i = 0; i < selectors.length; i++) {
imgEl = document.querySelector(selectors[i]);
if (imgEl && imgEl.src) break;
}

// Priority 2 — og:image meta tag
if (!imgEl) {
var ogImg = document.querySelector(‘meta[property=”og:image”]’);
if (ogImg && ogImg.content) {
productImageUrl = ogImg.content;
fetchImageAsBase64(productImageUrl, onProductImageReady);
showProductName();
return;
}
}

if (imgEl && imgEl.src) {
productImageUrl = imgEl.src;
// Show thumb immediately
$productImg.src = productImageUrl;
$productImg.style.display = ‘block’;
$productPlaceholder.style.display = ‘none’;
// Convert to base64
fetchImageAsBase64(productImageUrl, onProductImageReady);
} else {
$productName.textContent = ‘No product image found’;
$productStatus.style.color = ‘#C0392B’;
$productStatus.textContent = ‘Please add a featured image to this product’;
}

showProductName();
}

function showProductName() {
// Try to get product name from page
var titleEl = document.querySelector(
‘.product_title, h1.entry-title, .woocommerce-loop-product__title, h1.product_title’
);
if (titleEl) {
$productName.textContent = titleEl.textContent.trim();
} else {
$productName.textContent = document.title.split(‘–’)[0].split(‘|’)[0].trim();
}
}

function fetchImageAsBase64(url, callback) {
// Use a canvas to convert cross-origin image to base64
var img = new Image();
img.crossOrigin = ‘Anonymous’;
img.onload = function () {
var canvas = document.createElement(‘canvas’);
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext(‘2d’);
ctx.drawImage(img, 0, 0);
try {
var b64 = canvas.toDataURL(‘image/jpeg’, 0.9).split(‘,’)[1];
callback(b64);
} catch (e) {
// CORS blocked — send URL instead, handle in n8n
callback(null);
}
};
img.onerror = function () { callback(null); };
img.src = url + (url.indexOf(‘?’) > -1 ? ‘&’ : ‘?’) + ‘_t=’ + Date.now();
}

function onProductImageReady(b64) {
productImageBase64 = b64;
// Show thumb if not already shown
if ($productImg.style.display === ‘none’ && productImageUrl) {
$productImg.src = productImageUrl;
$productImg.style.display = ‘block’;
$productPlaceholder.style.display = ‘none’;
}
checkReady();
}

// ─────────────────────────────────────────────
// ROOM IMAGE UPLOAD
// ─────────────────────────────────────────────
$roomInput.addEventListener(‘change’, function () {
var file = this.files[0];
if (!file) return;
roomFileName = file.name;
var reader = new FileReader();
reader.onload = function (e) {
var dataUrl = e.target.result;
roomImageBase64 = dataUrl.split(‘,’)[1];
$roomImgPreview.src = dataUrl;
$roomPreview.style.display = ‘block’;
checkReady();
};
reader.readAsDataURL(file);
});

// Drag & drop
$dropzone.addEventListener(‘dragover’, function (e) {
e.preventDefault();
this.classList.add(‘drag-over’);
});
$dropzone.addEventListener(‘dragleave’, function () {
this.classList.remove(‘drag-over’);
});
$dropzone.addEventListener(‘drop’, function (e) {
e.preventDefault();
this.classList.remove(‘drag-over’);
var file = e.dataTransfer.files[0];
if (file && file.type.startsWith(‘image/’)) {
$roomInput.files = e.dataTransfer.files;
$roomInput.dispatchEvent(new Event(‘change’));
}
});

$clearRoom.addEventListener(‘click’, function () {
roomImageBase64 = null;
$roomInput.value = ”;
$roomImgPreview.src = ”;
$roomPreview.style.display = ‘none’;
checkReady();
});

// ─────────────────────────────────────────────
// ENABLE SUBMIT WHEN BOTH IMAGES READY
// ─────────────────────────────────────────────
function checkReady() {
// Product: we need either base64 or at least the URL
var hasProduct = productImageBase64 || productImageUrl;
var hasRoom = !!roomImageBase64;
$submit.disabled = !(hasProduct && hasRoom);
}

// ─────────────────────────────────────────────
// SUBMIT
// ─────────────────────────────────────────────
$submit.addEventListener(‘click’, function () {
if ($submit.disabled) return;
sendToWebhook();
});

function setStatus(msg, isError) {
$status.className = ‘viz-status active’ + (isError ? ‘ error’ : ”);
$statusText.textContent = msg;
$spinner.style.display = isError ? ‘none’ : ‘block’;
}

function sendToWebhook() {
$submit.disabled = true;
$result.style.display = ‘none’;
setStatus(‘Sending images to AI…’);

var payload = {
product_image_url: productImageUrl || null,
product_image_base64: productImageBase64 || null,
room_image_base64: roomImageBase64,
product_name: $productName.textContent,
page_url: window.location.href
};

fetch(WEBHOOK_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload)
})
.then(function (res) {
if (!res.ok) throw new Error(‘Server error: ‘ + res.status);
return res.json();
})
.then(function (data) {
// n8n should return: { result_url: “https://…” }
// Adjust the key below to match your n8n output field name
var imageUrl = data.result_url || data.output_url || data.image_url || data.url;
if (!imageUrl) throw new Error(‘No image URL in response’);
showResult(imageUrl);
})
.catch(function (err) {
setStatus(‘Something went wrong. Please try again.’, true);
$submit.disabled = false;
console.error(‘[AI Visualizer]’, err);
});
}

function showResult(url) {
$status.className = ‘viz-status’;
$resultImg.src = url;
$downloadBtn.href = url;
$result.style.display = ‘block’;
$submit.disabled = false;
// Scroll to result smoothly
setTimeout(function () {
$result.scrollIntoView({ behavior: ‘smooth’, block: ‘start’ });
}, 100);
}

// ─────────────────────────────────────────────
// RETRY
// ─────────────────────────────────────────────
$retryBtn.addEventListener(‘click’, function () {
roomImageBase64 = null;
$roomInput.value = ”;
$roomImgPreview.src = ”;
$roomPreview.style.display = ‘none’;
$result.style.display = ‘none’;
$status.className = ‘viz-status’;
checkReady();
$dropzone.scrollIntoView({ behavior: ‘smooth’, block: ‘center’ });
});

// ─────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────
detectProductImage();

})();
</script>

Categories

Reviews

There are no reviews yet.

Be the first to review “Sofa Test”

Your email address will not be published. Required fields are marked *

1. Booking & Payments

  • Pet owners must complete all bookings through Purrfect Match.
  • Payments must be made in full at the time of booking to secure the sitter.
  • Any attempt to bypass the platform may result in account suspension.
  • To ensure service quality we will hold the payment until completion of service by the sitter.

2. Cancellations & Refunds

  • Cancellations made 24 hours before the start of the booking date are eligible for a full refund.
  • Cancellations within 12 hours of booking start time may incur a 50% cancellation fee.
  • Refunds will be processed within 7 working days.

3. Pet Owner Responsibilities

  • Provide accurate information about your pet’s health, behavior, and special needs.
  • Ensure all vaccinations are up to date.
  • Pet owner is obligated to cover transportation costs
  • Respect the agreed-upon drop-off & pick-up schedule.

4. Liability Disclaimer

  • Purrfect Match is not liable for any accidents, injuries, or damages caused by pets during a booking.
  • Pet owners must ensure their pet is safe, non-aggressive, and fit for the selected service.

5. Payments and Bookings

  • Booking confirmation is subject to sitter availability and will be confirmed via email within 24 hours of submitting proof of payment.
  • You are kindly requested to make all bookings at least 24 hours in advance of your expected service date.

6. Cancellations and refunds:

  • • The pet owner is responsible for covering transportation costs.

Reviews

There are no reviews yet.

Be the first to review “Sofa Test”

Your email address will not be published. Required fields are marked *