storeos-app / index.html
Cybire's picture
expose all functions to window for onclick handlers
0a6ede9
Raw
History Blame Contribute Delete
106 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StoreOS</title>
<script src="https://cdn.jsdelivr.net/npm/ethers@6.9.0/dist/ethers.umd.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
:root {
--bg: #F5F3F0;
--surface: #FFFFFF;
--surface-hover: #FAFAF8;
--accent: #9200E1;
--accent-soft: #B44FFF;
--accent-glow: rgba(146,0,225,0.12);
--accent-light: #F5EAFF;
--accent-dark: #7000B0;
--gradient: linear-gradient(135deg, #9200E1 0%, #B44FFF 50%, #D98FFF 100%);
--gradient-subtle: linear-gradient(135deg, rgba(146,0,225,0.06) 0%, rgba(180,79,255,0.03) 100%);
--text: #111111;
--text2: #555555;
--text3: #999999;
--border: #E5E2DF;
--border-light: #F0EDEA;
--green: #10B981;
--green-bg: #ECFDF5;
--red: #EF4444;
--red-bg: #FEF2F2;
--orange: #F59E0B;
--orange-bg: #FFFBEB;
--blue: #3B82F6;
--blue-bg: #EFF6FF;
--r: 14px;
--r-sm: 10px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow: 0 2px 8px rgba(0,0,0,0.06);
--shadow-md: 0 4px 16px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.10);
--shadow-glow: 0 0 40px rgba(146,0,225,0.15);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ─── ANIMATIONS ─── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animate-in { animation: fadeUp 0.5s cubic-bezier(0.22,1,0.36,1) both; }
.animate-scale { animation: scaleIn 0.4s cubic-bezier(0.22,1,0.36,1) both; }
/* ─── TOAST ─── */
#toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 999;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
padding: 12px 20px;
border-radius: var(--r-sm);
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow-md);
animation: slideDown 0.3s cubic-bezier(0.22,1,0.36,1) both;
display: flex;
align-items: center;
gap: 8px;
max-width: 360px;
}
.toast.success { background: var(--green); color: white; }
.toast.error { background: var(--red); color: white; }
.toast.info { background: var(--text); color: white; }
/* ─── SETUP ─── */
#setup {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: var(--bg);
position: relative;
overflow: hidden;
}
#setup::before {
content: '';
position: absolute;
top: -40%;
right: -20%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(146,0,225,0.06) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
#setup::after {
content: '';
position: absolute;
bottom: -30%;
left: -10%;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(180,79,255,0.04) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
.setup-card {
background: var(--surface);
border-radius: var(--r-lg);
padding: 44px;
max-width: 440px;
width: 100%;
box-shadow: var(--shadow-lg);
position: relative;
animation: fadeUp 0.6s cubic-bezier(0.22,1,0.36,1);
}
.setup-brand {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
.setup-logo {
width: 40px;
height: 40px;
background: var(--gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 800;
font-size: 18px;
box-shadow: var(--shadow-glow);
}
.setup-title {
font-size: 26px;
font-weight: 800;
letter-spacing: -0.5px;
}
.setup-title span { color: var(--accent); }
.setup-sub {
color: var(--text3);
font-size: 14px;
margin-bottom: 32px;
line-height: 1.5;
}
.field {
margin-bottom: 18px;
}
.field label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text2);
margin-bottom: 6px;
}
.field input, .field select {
width: 100%;
padding: 11px 14px;
border: 1.5px solid var(--border);
border-radius: var(--r-sm);
font-size: 14px;
font-family: inherit;
background: var(--bg);
color: var(--text);
transition: all 0.2s cubic-bezier(0.22,1,0.36,1);
}
.field input:focus, .field select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
background: white;
}
.field input::placeholder { color: var(--text3); }
.field .hint {
font-size: 11px;
color: var(--text3);
margin-top: 4px;
}
.field-row { display: flex; gap: 12px; }
.field-row .field { flex: 1; }
.advanced-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text3);
cursor: pointer;
margin: 20px 0 8px;
user-select: none;
transition: color 0.2s;
}
.advanced-toggle:hover { color: var(--accent); }
.advanced-toggle .arrow { transition: transform 0.2s; font-size: 10px; }
.advanced-toggle.open .arrow { transform: rotate(90deg); }
.advanced-fields { display: none; }
.advanced-fields.open { display: block; animation: fadeUp 0.3s ease; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: var(--r-sm);
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.22,1,0.36,1);
position: relative;
overflow: hidden;
}
.btn-primary {
background: var(--gradient);
background-size: 200% 200%;
color: white;
width: 100%;
padding: 14px;
margin-top: 8px;
box-shadow: 0 4px 12px rgba(146,0,225,0.25);
}
.btn-primary:hover {
box-shadow: 0 6px 20px rgba(146,0,225,0.35);
transform: translateY(-1px);
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-secondary {
background: var(--accent-light);
color: var(--accent);
}
.btn-secondary:hover {
background: #ECD5FF;
transform: translateY(-1px);
}
.btn-ghost {
background: transparent;
color: var(--text2);
padding: 10px 18px;
}
.btn-ghost:hover { background: var(--bg); color: var(--text); }
.btn-sm { padding: 8px 16px; font-size: 13px; }
/* ─── MAIN APP ─── */
#app { display: none; min-height: 100vh; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
height: 56px;
background: var(--surface);
border-bottom: 1px solid var(--border-light);
position: sticky;
top: 0;
z-index: 40;
backdrop-filter: blur(12px);
background: rgba(255,255,255,0.85);
}
.topbar-left { display: flex; align-items: center; gap: 14px; }
.topbar-logo {
font-size: 17px;
font-weight: 800;
letter-spacing: -0.3px;
display: flex;
align-items: center;
gap: 8px;
}
.topbar-logo .dot {
width: 24px;
height: 24px;
background: var(--gradient);
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: 800;
}
.topbar-logo span { color: var(--accent); }
.store-badge {
font-size: 12px;
color: var(--text3);
padding-left: 14px;
border-left: 1px solid var(--border);
}
.topbar-right { display: flex; align-items: center; gap: 10px; }
.chain-pill {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
transition: all 0.2s;
}
.chain-pill.on { background: var(--green-bg); color: var(--green); }
.chain-pill.off { background: var(--red-bg); color: var(--red); }
.chain-pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
position: relative;
}
.chain-pill.on .dot::after {
content: '';
position: absolute;
inset: -3px;
border-radius: 50%;
border: 1.5px solid currentColor;
animation: pulse-ring 2s infinite;
}
.nav-tabs {
display: flex;
gap: 2px;
padding: 8px;
margin: 16px 28px 0;
background: var(--surface);
border-radius: var(--r);
box-shadow: var(--shadow-sm);
width: fit-content;
}
.nav-tab {
padding: 9px 20px;
font-size: 13px;
font-weight: 500;
color: var(--text2);
cursor: pointer;
border-radius: var(--r-sm);
transition: all 0.25s cubic-bezier(0.22,1,0.36,1);
user-select: none;
position: relative;
}
.nav-tab:hover { color: var(--text); background: var(--bg); }
.nav-tab.active {
color: white;
background: var(--accent);
box-shadow: 0 2px 8px rgba(146,0,225,0.3);
}
.content { padding: 20px 28px 40px; }
.panel { display: none; }
.panel.active { display: block; animation: fadeUp 0.35s cubic-bezier(0.22,1,0.36,1); }
/* ─── PRODUCTS ─── */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.product-card {
background: var(--surface);
border-radius: var(--r);
border: 1px solid var(--border-light);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.22,1,0.36,1);
cursor: default;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--border);
}
.product-img {
width: 100%;
height: 180px;
background: linear-gradient(135deg, #F8F7F4 0%, #F0EDEA 100%);
display: flex;
align-items: center;
justify-content: center;
color: var(--text3);
font-size: 36px;
overflow: hidden;
position: relative;
}
.product-img img { width: 100%; height: 100%; object-fit: cover; }
.product-img .chain-tag {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(8px);
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 700;
color: var(--accent);
display: flex;
align-items: center;
gap: 4px;
}
.product-body { padding: 16px; }
.product-name { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.product-desc {
font-size: 13px;
color: var(--text2);
line-height: 1.4;
margin-bottom: 10px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.product-price { font-size: 17px; font-weight: 700; color: var(--accent); }
.stock-pill {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 12px;
}
.stock-ok { background: var(--green-bg); color: var(--green); }
.stock-low { background: var(--orange-bg); color: var(--orange); }
.stock-out { background: var(--red-bg); color: var(--red); }
.buy-btn {
padding: 6px 16px;
border-radius: 20px;
border: none;
font-size: 12px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
background: var(--accent);
color: white;
transition: all 0.2s;
}
.buy-btn:hover { background: var(--accent-dark); transform: scale(1.05); }
.buy-btn:active { transform: scale(0.95); }
.buy-btn.sold-out { background: var(--border); color: var(--text3); cursor: default; }
.buy-btn.sold-out:hover { transform: none; }
.tags-row { display: flex; gap: 5px; margin-top: 10px; flex-wrap: wrap; }
.tag {
font-size: 10px;
font-weight: 600;
padding: 3px 8px;
border-radius: 8px;
letter-spacing: 0.3px;
}
.tag-chain { background: var(--accent-light); color: var(--accent); }
.tag-ai { background: var(--blue-bg); color: var(--blue); }
.tag-storage { background: var(--green-bg); color: var(--green); }
.empty-state {
text-align: center;
padding: 80px 20px;
animation: fadeIn 0.5s;
}
.empty-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
background: var(--accent-light);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
animation: float 3s ease-in-out infinite;
}
.empty-state h3 { font-size: 16px; font-weight: 600; color: var(--text2); margin-bottom: 4px; }
.empty-state p { font-size: 14px; color: var(--text3); }
/* ─── MODAL ─── */
.modal-backdrop {
position: fixed; inset: 0; z-index: 50;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-backdrop.open { display: flex; }
.modal {
background: var(--surface);
border-radius: var(--r-lg);
max-width: 520px;
width: 100%;
max-height: 85vh;
overflow-y: auto;
padding: 36px;
box-shadow: var(--shadow-lg);
animation: scaleIn 0.3s cubic-bezier(0.22,1,0.36,1);
}
.modal h2 { font-size: 20px; font-weight: 700; }
.modal .modal-sub { color: var(--text3); font-size: 13px; margin-bottom: 24px; }
.img-upload {
width: 100%;
height: 150px;
border: 2px dashed var(--border);
border-radius: var(--r);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 18px;
overflow: hidden;
position: relative;
}
.img-upload:hover {
border-color: var(--accent);
background: var(--accent-glow);
}
.img-upload.has-img { border-style: solid; border-color: var(--accent); }
.img-upload img { width: 100%; height: 100%; object-fit: cover; }
.img-upload .ph { text-align: center; color: var(--text3); }
.img-upload .ph .ic { font-size: 28px; margin-bottom: 4px; }
.img-upload .ph p { font-size: 13px; }
.ai-box {
background: var(--gradient-subtle);
border: 1px solid var(--accent-light);
border-radius: var(--r-sm);
padding: 16px;
margin: 18px 0;
display: none;
}
.ai-box.show { display: block; animation: fadeUp 0.3s ease; }
.ai-box-head {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.ai-box .desc {
font-size: 13px;
line-height: 1.5;
color: var(--text);
margin-bottom: 8px;
}
.ai-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
background: rgba(255,255,255,0.7);
border-radius: var(--r-sm);
font-size: 13px;
margin-top: 6px;
}
.ai-row .l { color: var(--text2); }
.ai-row .v { font-weight: 600; }
.modal-btns { display: flex; gap: 12px; margin-top: 24px; }
.modal-btns .btn { flex: 1; }
/* ─── STOREFRONT V2 ─── */
.sf-layout-v2 {
display: grid;
grid-template-columns: 420px 1fr;
gap: 20px;
min-height: calc(100vh - 160px);
}
.sf-robot-col {
display: flex;
flex-direction: column;
gap: 12px;
position: sticky;
top: 80px;
height: calc(100vh - 160px);
}
.robot-viewport {
background: #F3F4F6;
border-radius: var(--r);
border: 1px solid var(--border-light);
height: 280px;
position: relative;
overflow: hidden;
box-shadow: var(--shadow);
}
.robot-viewport canvas {
width: 100% !important;
height: 100% !important;
display: block;
}
.sim-mount {
position: absolute;
inset: 0;
}
.robot-status {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: var(--text3);
background: rgba(255,255,255,0.8);
padding: 3px 12px;
border-radius: 10px;
backdrop-filter: blur(4px);
}
.sf-products-col { overflow-y: auto; }
.sf-search {
margin-bottom: 12px;
}
.sf-search input {
width: 100%;
padding: 11px 16px;
border: 1.5px solid var(--border);
border-radius: var(--r-sm);
font-size: 14px;
font-family: inherit;
background: var(--surface);
transition: all 0.2s;
}
.sf-search input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.sf-cats {
display: flex;
gap: 6px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.cat-pill {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 20px;
font-size: 12px;
font-weight: 500;
background: var(--surface);
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
color: var(--text2);
}
.cat-pill:hover { border-color: var(--accent); color: var(--accent); }
.cat-pill.active { background: var(--accent); color: white; border-color: var(--accent); }
.mic-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg);
border: 1.5px solid var(--border);
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
}
.mic-btn:hover { border-color: var(--accent); background: var(--accent-light); }
.mic-btn.recording {
background: var(--red);
border-color: var(--red);
animation: pulse-ring 1.5s infinite;
color: white;
}
.chat-box {
background: var(--surface);
border-radius: var(--r);
border: 1px solid var(--border-light);
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: var(--shadow);
flex: 1;
min-height: 0;
}
.chat-head {
padding: 14px 18px;
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 600;
}
.chat-head .ai-dot {
width: 8px;
height: 8px;
background: var(--green);
border-radius: 50%;
position: relative;
}
.chat-head .ai-dot::after {
content: '';
position: absolute; inset: -3px;
border-radius: 50%;
border: 1.5px solid var(--green);
animation: pulse-ring 2s infinite;
}
.chat-head .tee { margin-left: auto; font-size: 10px; color: var(--text3); font-weight: 500; }
.chat-msgs {
flex: 1;
overflow-y: auto;
padding: 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.msg {
max-width: 82%;
padding: 10px 14px;
font-size: 14px;
line-height: 1.5;
transition: opacity 0.2s;
}
.msg.u {
align-self: flex-end;
background: var(--accent);
color: white;
border-radius: 16px 16px 4px 16px;
}
.msg.a {
align-self: flex-start;
background: var(--bg);
color: var(--text);
border-radius: 16px 16px 16px 4px;
}
.msg .tee-tag {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--green);
margin-top: 6px;
}
.chat-bar {
padding: 12px 14px;
border-top: 1px solid var(--border-light);
display: flex;
gap: 8px;
}
.chat-bar input {
flex: 1;
padding: 10px 16px;
border: 1.5px solid var(--border);
border-radius: 24px;
font-size: 14px;
font-family: inherit;
background: var(--bg);
transition: all 0.2s;
}
.chat-bar input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.chat-send {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--gradient);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(146,0,225,0.3);
}
.chat-send:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(146,0,225,0.4); }
.chat-send:active { transform: scale(0.95); }
/* ─── COPILOT ─── */
.copilot-wrap { max-width: 700px; margin: 0 auto; }
.copilot-card {
background: var(--surface);
border-radius: var(--r);
border: 1px solid var(--border-light);
overflow: hidden;
box-shadow: var(--shadow);
}
.copilot-top {
padding: 24px;
background: var(--gradient-subtle);
border-bottom: 1px solid var(--border-light);
}
.copilot-top h2 { font-size: 18px; font-weight: 700; }
.copilot-top p { font-size: 13px; color: var(--text2); margin-top: 2px; }
.quick-pills {
display: flex;
gap: 8px;
padding: 14px 24px;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-light);
}
.qp {
padding: 7px 16px;
border: 1px solid var(--border);
border-radius: 20px;
font-size: 12px;
font-weight: 500;
background: var(--surface);
cursor: pointer;
transition: all 0.25s cubic-bezier(0.22,1,0.36,1);
font-family: inherit;
color: var(--text2);
}
.qp:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-light);
transform: translateY(-1px);
}
.copilot-msgs {
min-height: 260px;
max-height: 50vh;
overflow-y: auto;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.cp-msg { font-size: 14px; line-height: 1.6; }
.cp-msg.u { background: var(--bg); padding: 12px 16px; border-radius: var(--r-sm); font-weight: 500; }
.cp-msg.a { color: var(--text); }
.cp-msg .tee-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--green);
background: var(--green-bg);
padding: 2px 10px;
border-radius: 10px;
margin-top: 8px;
font-weight: 600;
}
.copilot-bar {
padding: 16px 24px;
border-top: 1px solid var(--border-light);
display: flex;
gap: 10px;
}
.copilot-bar input {
flex: 1;
padding: 11px 16px;
border: 1.5px solid var(--border);
border-radius: var(--r-sm);
font-size: 14px;
font-family: inherit;
background: var(--bg);
transition: all 0.2s;
}
.copilot-bar input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* ─── LEDGER ─── */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 24px;
}
.stat {
background: var(--surface);
border-radius: var(--r);
border: 1px solid var(--border-light);
padding: 20px;
transition: all 0.3s;
}
.stat:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.stat .label {
font-size: 11px;
font-weight: 700;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.8px;
}
.stat .num {
font-size: 32px;
font-weight: 800;
margin-top: 4px;
letter-spacing: -1px;
}
.stat .sub { font-size: 11px; color: var(--text3); margin-top: 2px; }
.stat.purple .num { color: var(--accent); }
.stat.green .num { color: var(--green); }
.stat.blue .num { color: var(--blue); }
.ledger-box {
background: var(--surface);
border-radius: var(--r);
border: 1px solid var(--border-light);
overflow: hidden;
}
.ledger-head {
padding: 16px 20px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
justify-content: space-between;
}
.ledger-head .wallet { font-size: 12px; color: var(--text3); font-weight: 400; font-family: monospace; }
.lrow {
display: grid;
grid-template-columns: 1fr 1.2fr 100px 100px;
padding: 12px 20px;
border-bottom: 1px solid var(--border-light);
font-size: 13px;
align-items: center;
transition: background 0.15s;
}
.lrow:hover { background: var(--bg); }
.lrow:last-child { border-bottom: none; }
.lrow .type { font-weight: 600; display: flex; align-items: center; gap: 6px; }
.lrow .hash { font-family: monospace; color: var(--text2); font-size: 12px; }
.lrow .hash a { color: var(--accent); text-decoration: none; }
.lrow .hash a:hover { text-decoration: underline; }
.lrow .time { color: var(--text3); }
.lrow .status { text-align: right; }
.stag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.stag.ok { background: var(--green-bg); color: var(--green); }
.stag.wait { background: var(--orange-bg); color: var(--orange); }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.typing::after { content: ''; animation: dots 1.5s infinite; }
@keyframes dots {
0%,20% { content: '.'; }
40% { content: '..'; }
60%,100% { content: '...'; }
}
/* ─── RESPONSIVE ─── */
@media (max-width: 900px) {
.sf-layout-v2 { grid-template-columns: 1fr; }
.sf-robot-col { position: static; height: auto; }
.robot-viewport { height: 200px; }
.chat-box { height: 300px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
.content { padding: 16px; }
.setup-card { padding: 28px; }
.stats-grid { grid-template-columns: 1fr 1fr; }
.nav-tabs { margin: 12px 16px 0; }
.topbar { padding: 0 16px; }
}
</style>
</head>
<body>
<div id="toast-container"></div>
<!-- ─── SETUP ─── -->
<div id="setup">
<div class="setup-card">
<div class="setup-brand">
<div class="setup-logo">S</div>
<div class="setup-title">Store<span>OS</span></div>
</div>
<p class="setup-sub">Your store's decentralized AI brain. Set up in 30 seconds.</p>
<div class="field">
<label>Store Name</label>
<input id="cfg-name" type="text" placeholder="e.g. The Good Store" />
</div>
<div class="field">
<label>0G API Key</label>
<input id="cfg-key" type="password" placeholder="sk-... or app-sk-..." />
<div class="hint">Get one free at <a href="https://pc.0g.ai" target="_blank" style="color:var(--accent)">pc.0g.ai</a></div>
</div>
<div class="field">
<label>AI Model</label>
<select id="cfg-model">
<option value="huru/chat-1">Huru Chat (Default)</option>
<option value="zai-org/GLM-5-FP8">GLM-5</option>
<option value="zai-org/GLM-5.1-FP8">GLM-5.1</option>
<option value="deepseek-ai/DeepSeek-V3">DeepSeek V3</option>
<option value="custom">Custom</option>
</select>
</div>
<input id="cfg-endpoint" type="hidden" value="https://www.huruai.xyz" />
<div class="field-row">
<div class="field">
<label>Robot</label>
<select id="cfg-robot">
<option value="sim">Simulator</option>
<option value="live">Live Reachy Mini</option>
</select>
</div>
<div class="field">
<label>Voice</label>
<select id="cfg-voice">
<option value="af_heart">Heart (warm female)</option>
<option value="af_bella">Bella (soft female)</option>
<option value="am_adam">Adam (deep male)</option>
<option value="bf_emma">Emma (British female)</option>
</select>
</div>
</div>
<div class="advanced-toggle" onclick="toggleAdvanced()">
<span class="arrow">&#9654;</span> Advanced settings
</div>
<div class="advanced-fields" id="adv-fields">
<div class="field-row">
<div class="field">
<label>0G Whisper Endpoint</label>
<input id="cfg-whisper-ep" type="text" placeholder="e.g. https://compute-network-3.integratenetwork.work" />
</div>
<div class="field">
<label>0G Whisper Key</label>
<input id="cfg-whisper-key" type="password" placeholder="app-sk-..." />
</div>
</div>
<div class="field-row">
<div class="field">
<label>Chain RPC</label>
<input id="cfg-rpc" type="text" value="https://evmrpc.0g.ai" />
</div>
<div class="field">
<label>Chain ID</label>
<input id="cfg-chain-id" type="text" value="16661" />
</div>
</div>
<div class="field">
<label>Wallet Private Key</label>
<input id="cfg-wallet" type="password" placeholder="0x..." />
<div class="hint">Enables on-chain product registration and sales</div>
</div>
</div>
<button class="btn btn-primary" id="btn-go" onclick="launch()">Launch StoreOS</button>
</div>
</div>
<!-- ─── APP ─── -->
<div id="app">
<div class="topbar">
<div class="topbar-left">
<div class="topbar-logo"><div class="dot">S</div> Store<span>OS</span></div>
<div class="store-badge" id="d-store"></div>
</div>
<div class="topbar-right">
<div class="chain-pill" id="chain-pill">
<div class="dot"></div>
<span id="chain-text">No Chain</span>
</div>
</div>
</div>
<div class="nav-tabs" id="nav-tabs">
<div class="nav-tab active" data-p="products" onclick="tab(this)">Products</div>
<div class="nav-tab" data-p="storefront" onclick="tab(this)">Storefront</div>
<div class="nav-tab" data-p="copilot" onclick="tab(this)">Copilot</div>
<div class="nav-tab" data-p="ledger" onclick="tab(this)">Ledger</div>
</div>
<div class="content">
<!-- PRODUCTS -->
<div class="panel active" id="p-products">
<div class="page-header">
<div class="page-title">Products</div>
<button class="btn btn-secondary btn-sm" onclick="openModal()">+ Add Product</button>
</div>
<div id="pgrid" class="product-grid"></div>
<div id="pempty" class="empty-state">
<div class="empty-icon">&#128722;</div>
<h3>No products yet</h3>
<p>Add your first product and let AI enrich it</p>
</div>
</div>
<!-- STOREFRONT -->
<div class="panel" id="p-storefront">
<div class="sf-layout-v2">
<!-- Robot + Chat column -->
<div class="sf-robot-col">
<div class="robot-viewport" id="robot-viewport">
<div id="sim-mount" class="sim-mount"></div>
<div class="robot-status" id="robot-status">Loading robot...</div>
</div>
<div class="chat-box">
<div class="chat-head">
<div class="ai-dot"></div>
Store Assistant
<div class="tee">TEE-verified</div>
<button id="tts-toggle" onclick="toggleTTS()" title="Voice on" style="background:none;border:none;cursor:pointer;font-size:18px;margin-left:8px">&#x1F50A;</button>
</div>
<div class="chat-msgs" id="cmsg">
<div class="msg a">Hi! I'm the store assistant. Ask me anything or tap the mic to talk to me!</div>
</div>
<div class="chat-bar">
<button class="mic-btn" id="mic-btn" onclick="toggleMic()" title="Tap to speak">&#127908;</button>
<input id="cinput" placeholder="Ask about products..." onkeydown="if(event.key==='Enter')sendChat()" />
<button class="chat-send" onclick="sendChat()">&#10148;</button>
</div>
</div>
</div>
<!-- Products column -->
<div class="sf-products-col">
<div class="sf-search">
<input id="sf-search" type="text" placeholder="Search products..." oninput="filterSF()" />
</div>
<div class="sf-cats" id="sf-cats">
<button class="cat-pill active" data-cat="all" onclick="filterCat(this)">All</button>
<button class="cat-pill" data-cat="fashion" onclick="filterCat(this)">Fashion</button>
<button class="cat-pill" data-cat="home" onclick="filterCat(this)">Home</button>
<button class="cat-pill" data-cat="food" onclick="filterCat(this)">Food</button>
<button class="cat-pill" data-cat="beauty" onclick="filterCat(this)">Beauty</button>
<button class="cat-pill" data-cat="art" onclick="filterCat(this)">Art</button>
<button class="cat-pill" data-cat="tech" onclick="filterCat(this)">Tech</button>
</div>
<div id="sfgrid" class="product-grid"></div>
</div>
</div>
</div>
<!-- COPILOT -->
<div class="panel" id="p-copilot">
<div class="copilot-wrap">
<div class="copilot-card">
<div class="copilot-top">
<h2>Store Copilot</h2>
<p>AI advisor powered by your real inventory data. Every answer is verifiable.</p>
</div>
<div class="quick-pills">
<button class="qp" onclick="askCP('What should I restock?')">Restock advice</button>
<button class="qp" onclick="askCP('Which products are slow movers?')">Slow movers</button>
<button class="qp" onclick="askCP('Suggest pricing changes')">Pricing help</button>
<button class="qp" onclick="askCP('Give me a sales summary')">Sales summary</button>
<button class="qp" onclick="askCP('What trends should I watch?')">Trends</button>
</div>
<div class="copilot-msgs" id="cpmsg">
<div class="cp-msg a">I'm your store copilot. I analyze your catalog and inventory to give actionable advice. Ask me anything about your store.</div>
</div>
<div class="copilot-bar">
<input id="cpinput" placeholder="Ask your store brain..." onkeydown="if(event.key==='Enter')sendCP()" />
<button class="btn btn-primary" style="width:auto;margin:0;padding:11px 24px" onclick="sendCP()">Ask</button>
</div>
</div>
</div>
</div>
<!-- LEDGER -->
<div class="panel" id="p-ledger">
<div class="stats-grid">
<div class="stat purple">
<div class="label">On-Chain Products</div>
<div class="num" id="s-prod">0</div>
<div class="sub">Registered with provenance</div>
</div>
<div class="stat green">
<div class="label">Verified Sales</div>
<div class="num" id="s-sales">0</div>
<div class="sub">On-chain receipts</div>
</div>
<div class="stat blue">
<div class="label">Trust Score</div>
<div class="num" id="s-trust">0</div>
<div class="sub">Portable reputation</div>
</div>
<div class="stat">
<div class="label">Storage Objects</div>
<div class="num" id="s-storage">0</div>
<div class="sub">Decentralized data</div>
</div>
</div>
<div class="ledger-box">
<div class="ledger-head">
<span>On-Chain Activity</span>
<span class="wallet" id="d-wallet"></span>
</div>
<div id="lrows">
<div class="empty-state" style="padding:40px"><p style="color:var(--text3)">No on-chain activity yet</p></div>
</div>
</div>
</div>
</div>
</div>
<!-- ─── ADD PRODUCT MODAL ─── -->
<div class="modal-backdrop" id="modal">
<div class="modal">
<h2>Add Product</h2>
<p class="modal-sub">Upload details β€” AI handles the rest</p>
<div class="img-upload" id="imgup" onclick="document.getElementById('fup').click()">
<div class="ph" id="imgph">
<div class="ic">&#128247;</div>
<p>Click to upload photo</p>
</div>
</div>
<input type="file" id="fup" accept="image/*" style="display:none" onchange="onImg(event)" />
<div class="field">
<label>Product Name</label>
<input id="m-name" type="text" placeholder="e.g. Handwoven Basket" />
</div>
<div class="field-row">
<div class="field">
<label>Price (NGN)</label>
<input id="m-price" type="text" placeholder="e.g. 15000" />
</div>
<div class="field">
<label>Stock</label>
<input id="m-stock" type="number" value="10" />
</div>
</div>
<div class="field">
<label>Category</label>
<select id="m-cat">
<option value="fashion">Fashion & Accessories</option>
<option value="home">Home & Living</option>
<option value="art">Art & Crafts</option>
<option value="food">Food & Beverage</option>
<option value="tech">Tech & Gadgets</option>
<option value="beauty">Beauty & Wellness</option>
<option value="other">Other</option>
</select>
</div>
<div class="field">
<label>Origin / Creator</label>
<input id="m-origin" type="text" placeholder="e.g. Handmade in Lagos by Adunni Crafts" />
</div>
<button class="btn btn-secondary btn-sm" onclick="enrich()" id="btn-enrich" style="width:100%">&#9889; Enrich with AI</button>
<div class="ai-box" id="aibox">
<div class="ai-box-head"><span class="spinner" id="ai-spin" style="display:none"></span> &#9889; AI Analysis (TEE-Verified)</div>
<div class="desc" id="ai-desc"></div>
<div class="ai-row" id="ai-price-row" style="display:none"><span class="l">Suggested Price</span><span class="v" id="ai-price"></span></div>
<div class="ai-row" id="ai-tags-row" style="display:none"><span class="l">Tags</span><span class="v" id="ai-tags"></span></div>
</div>
<div class="modal-btns">
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" style="margin:0" onclick="saveProd()" id="btn-save">Save & Register On-Chain</button>
</div>
</div>
</div>
<script type="module" id="sdk-loader">
import { ReachySim } from './sim.js';
window.ReachySim = ReachySim;
// Load Reachy Mini JS SDK for live robot
import init, * as reachy from 'https://unpkg.com/reachy-mini@0.6.1/index.js';
try {
await init('https://unpkg.com/reachy-mini@0.6.1/index_bg.wasm');
window.reachySDK = reachy;
console.log('Reachy Mini SDK loaded');
} catch(e) {
console.warn('Reachy SDK load failed:', e);
}
</script>
<script>
const SEED_PRODUCTS = [
{id:'p001',name:'Adire Indigo Tote Bag',price:18500,stock:12,cat:'fashion',desc:'Hand-dyed adire fabric tote with reinforced leather handles. Traditional Yoruba cassava paste resist-dye techniques.',origin:'Mama Titi Adire Workshop, Abeokuta',aiDesc:'A stunning hand-dyed indigo tote bag featuring traditional Yoruba adire patterns. Deep blue hues and organic motifs make each piece one-of-a-kind.',aiTags:['adire','handmade','tote','indigo','yoruba'],aiEnriched:true,created:'2026-05-26T10:00:00Z',txHash:'0x8a3f1b',storageHash:'0xabc1',image:'https://images.unsplash.com/photo-1590874103328-eac38a683ce7?w=400&h=300&fit=crop'},
{id:'p002',name:'Shea Butter & Black Soap Gift Set',price:12000,stock:34,cat:'beauty',desc:'Cold-pressed raw shea butter from Borgu paired with handmade African black soap. No additives.',origin:'Borgu Women Cooperative, Niger State',aiDesc:'A luxurious wellness gift set featuring raw shea butter and authentic African black soap. Ethically sourced from a women-led cooperative.',aiTags:['shea butter','black soap','skincare','gift set','organic'],aiEnriched:true,created:'2026-05-26T10:05:00Z',txHash:'0x7b2e2c',storageHash:'0xabc2',image:'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?w=400&h=300&fit=crop'},
{id:'p003',name:'Brass Statement Earrings',price:8500,stock:20,cat:'fashion',desc:'Hand-forged brass earrings inspired by Benin Bronze aesthetics. Lightweight, hypoallergenic posts.',origin:'Osagie Brass Studio, Benin City',aiDesc:'Bold hand-forged brass earrings drawing from the Benin Bronze tradition. Surprisingly lightweight β€” heritage meets modern style.',aiTags:['earrings','brass','handcrafted','benin','jewelry'],aiEnriched:true,created:'2026-05-26T10:10:00Z',txHash:'0x6c1d3d',storageHash:'0xabc3',image:'https://images.unsplash.com/photo-1535632066927-ab7c9ab60908?w=400&h=300&fit=crop'},
{id:'p004',name:'Aso-Oke Throw Pillow Set',price:22000,stock:8,cat:'home',desc:'Woven aso-oke fabric cushion covers with concealed zippers. Hand-loomed geometric patterns from Iseyin.',origin:'Master Weaver Baba Alaro, Iseyin',aiDesc:'Luxurious throw pillows in hand-loomed aso-oke fabric. Geometric weave patterns add rich texture and cultural depth to any space.',aiTags:['aso-oke','pillows','home decor','handwoven','textile'],aiEnriched:true,created:'2026-05-26T10:15:00Z',txHash:'0x5d0c4e',storageHash:'0xabc4',image:'https://images.unsplash.com/photo-1584100936595-c0654b55a2e2?w=400&h=300&fit=crop'},
{id:'p005',name:'Cold-Brew Hibiscus Zobo',price:4500,stock:45,cat:'food',desc:'Slow-steeped hibiscus concentrate with ginger, clove, and pineapple. Makes 6 servings. No preservatives.',origin:'Sip & Bloom, Victoria Island',aiDesc:'Refreshing cold-brew zobo concentrate infused with ginger, clove, and pineapple. Small-batch, preservative-free, made fresh weekly.',aiTags:['zobo','hibiscus','beverage','small batch','natural'],aiEnriched:true,created:'2026-05-26T10:20:00Z',txHash:'0x4e9b5f',storageHash:'0xabc5',image:'https://images.unsplash.com/photo-1544252890-c3e95e867d73?w=400&h=300&fit=crop'},
{id:'p006',name:'Recycled Rubber Laptop Sleeve',price:15000,stock:18,cat:'tech',desc:'13-inch sleeve from upcycled rubber inner tubes. Water-resistant, padded, magnetic closure. Diverts 2kg from landfill.',origin:'ReThread Lagos, Surulere',aiDesc:'Eco-conscious laptop sleeve crafted from upcycled rubber inner tubes. Water-resistant with padded interior β€” each piece diverts 2kg of waste.',aiTags:['laptop','upcycled','sustainable','eco-friendly','tech'],aiEnriched:true,created:'2026-05-26T10:25:00Z',txHash:'0x3f8a6a',storageHash:'0xabc6',image:'https://images.unsplash.com/photo-1603302576837-37561b2e2302?w=400&h=300&fit=crop'},
{id:'p007',name:'Hand-Painted Lagos Mug',price:6500,stock:25,cat:'art',desc:'12oz ceramic mug with Lagos skyline motifs. Microwave safe. Each signed by the artist.',origin:'Tunde Ceramics Studio, Lekki',aiDesc:'Beautiful ceramic mug with hand-painted Lagos skyline artwork. Artist-signed β€” a daily reminder of the city\'s energy.',aiTags:['mug','ceramic','hand-painted','lagos','art','gift'],aiEnriched:true,created:'2026-05-26T10:30:00Z',txHash:null,storageHash:'0xabc7',image:'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=400&h=300&fit=crop'},
{id:'p008',name:'Ankara Print Notebook',price:3500,stock:50,cat:'art',desc:'A5 hardcover notebook in ankara wax print fabric. 192 dot-grid pages, 100gsm acid-free paper.',origin:'PageCraft, Yaba',aiDesc:'Vibrant A5 notebook wrapped in authentic ankara wax print. 192 dot-grid pages with lay-flat binding β€” bold on the outside, blank for your ideas inside.',aiTags:['notebook','ankara','stationery','handmade','wax print'],aiEnriched:true,created:'2026-05-26T10:35:00Z',txHash:'0xee1b7b',storageHash:'0xabc8',image:'https://images.unsplash.com/photo-1531346878377-a5be20888e57?w=400&h=300&fit=crop'},
{id:'p009',name:'"Lagos Nights" Candle',price:9000,stock:15,cat:'home',desc:'8oz coconut wax candle β€” oud, suya smoke, jasmine. 45-hour burn time. Hand-poured, cotton wick.',origin:'Glow Collective, Ikeja',aiDesc:'Evocative coconut wax candle capturing Lagos evenings β€” warm oud, a whisper of suya smoke, and fresh jasmine. 45-hour burn.',aiTags:['candle','coconut wax','home fragrance','lagos','handmade'],aiEnriched:true,created:'2026-05-26T10:40:00Z',txHash:'0x2e7c8c',storageHash:'0xabc9',image:'https://images.unsplash.com/photo-1602028915047-37269d1a73f7?w=400&h=300&fit=crop'},
{id:'p010',name:'Moringa & Turmeric Tea',price:5500,stock:30,cat:'food',desc:'Loose-leaf moringa, turmeric, lemongrass, ginger. 50g pouch, ~25 cups. Caffeine-free, sun-dried.',origin:'Green Root Farm, Ogun State',aiDesc:'Nourishing caffeine-free tea blending sun-dried moringa, turmeric, lemongrass, and ginger from Ogun State. 25 cups of antioxidant goodness.',aiTags:['tea','moringa','turmeric','wellness','herbal'],aiEnriched:true,created:'2026-05-26T10:45:00Z',txHash:'0x1d6b9d',storageHash:'0xabca',image:'https://images.unsplash.com/photo-1564890369478-c89ca6d9cde9?w=400&h=300&fit=crop'},
{id:'p011',name:'Oja Market Basket',price:14000,stock:6,cat:'home',desc:'Large woven palm frond basket with leather handles. Tiv weaving techniques. Fits a full weekly shop.',origin:'Tiv Weavers Collective, Benue State',aiDesc:'Generously sized market basket hand-woven by Tiv artisans. Leather handles, sturdy frame β€” practical carry-all and beautiful home accent.',aiTags:['basket','handwoven','market','tiv','sustainable'],aiEnriched:true,created:'2026-05-26T10:50:00Z',txHash:'0x0c5aae',storageHash:'0xabcb',image:'https://images.unsplash.com/photo-1513694203232-719a280e022f?w=400&h=300&fit=crop'},
{id:'p012',name:'Agbada-Inspired Linen Shirt',price:28000,stock:4,cat:'fashion',desc:'Relaxed-fit linen with wide sleeves inspired by agbada silhouette. Off-white and clay. S-XXL.',origin:'Studio IrΓ³, Surulere',aiDesc:'Modern linen shirt reimagining the flowing agbada silhouette. Relaxed fit, wide sleeves β€” effortless elegance rooted in Yoruba tailoring.',aiTags:['shirt','linen','agbada','menswear','designer'],aiEnriched:true,created:'2026-05-26T10:55:00Z',txHash:'0xfb49bf',storageHash:'0xabcc',image:'https://images.unsplash.com/photo-1596755094514-f87e34085b2c?w=400&h=300&fit=crop'},
{id:'p013',name:'Baobab Fruit Powder',price:7500,stock:40,cat:'food',desc:'100% pure baobab powder from Northern Nigeria. Rich in vitamin C, fiber, and antioxidants. 200g resealable pouch.',origin:'Sahel Harvest Co-op, Kebbi State',aiDesc:'Pure baobab superfood powder β€” 6x more vitamin C than oranges, packed with fiber. Sourced from Sahel communities in Northern Nigeria.',aiTags:['baobab','superfood','vitamin C','organic','powder'],aiEnriched:true,created:'2026-05-26T11:00:00Z',txHash:'0xa1b2c3',storageHash:'0xabcd',image:'https://images.unsplash.com/photo-1615485500704-8e990f9900f7?w=400&h=300&fit=crop'},
{id:'p014',name:'Raffia Sun Hat',price:11000,stock:14,cat:'fashion',desc:'Wide-brim raffia hat hand-woven in Kwara State. Adjustable inner band. UV protective. Packable without losing shape.',origin:'Ilorin Raffia Weavers, Kwara State',aiDesc:'Elegant wide-brim sun hat hand-woven from natural raffia. UV protective and packable β€” summer essentials rooted in Kwara weaving tradition.',aiTags:['hat','raffia','sun protection','handwoven','summer'],aiEnriched:true,created:'2026-05-26T11:05:00Z',txHash:'0xb2c3d4',storageHash:'0xabce',image:'https://images.unsplash.com/photo-1572307480813-ceb0e59d8325?w=400&h=300&fit=crop'},
{id:'p015',name:'Activated Charcoal Face Mask',price:4000,stock:60,cat:'beauty',desc:'Detoxifying clay mask with activated charcoal and bentonite clay. Draws out impurities without stripping moisture. 100ml jar.',origin:'Natu Beauty Lab, Abuja',aiDesc:'Deep-cleansing face mask combining activated charcoal with bentonite clay. Draws out impurities while preserving natural moisture balance.',aiTags:['face mask','charcoal','skincare','detox','clay'],aiEnriched:true,created:'2026-05-26T11:10:00Z',txHash:'0xc3d4e5',storageHash:'0xabcf',image:'https://images.unsplash.com/photo-1556228578-0d85b1a4d571?w=400&h=300&fit=crop'},
{id:'p016',name:'Leather-Bound Gratitude Journal',price:8000,stock:22,cat:'art',desc:'A6 gratitude journal in full-grain goat leather. Gold foil prompts, 120 pages, ribbon bookmark. Handbound.',origin:'Bindery & Co., Ibadan',aiDesc:'Handbound A6 gratitude journal in full-grain goat leather with gold foil prompts. A daily ritual object crafted to last years.',aiTags:['journal','leather','gratitude','handbound','gift'],aiEnriched:true,created:'2026-05-26T11:15:00Z',txHash:'0xd4e5f6',storageHash:'0xabd0',image:'https://images.unsplash.com/photo-1544816155-12df9643f363?w=400&h=300&fit=crop'},
{id:'p017',name:'Wooden Serving Board',price:16000,stock:10,cat:'home',desc:'Hand-carved iroko wood serving board with natural edge. Food-safe beeswax finish. Each board is unique.',origin:'Woodfolk Studio, Benin City',aiDesc:'Hand-carved iroko wood serving board with organic live edge. Each unique piece is finished with food-safe beeswax for natural warmth.',aiTags:['serving board','wood','iroko','kitchen','handcarved'],aiEnriched:true,created:'2026-05-26T11:20:00Z',txHash:'0xe5f6a7',storageHash:'0xabd1',image:'https://images.unsplash.com/photo-1606760227091-3dd870d97f1d?w=400&h=300&fit=crop'},
{id:'p018',name:'Suya Spice Blend',price:2500,stock:75,cat:'food',desc:'Authentic yaji suya spice β€” ground peanuts, chili, ginger, paprika, and secret family spices. 150g tin.',origin:'Malam Garba Spice House, Kano',aiDesc:'Authentic yaji suya spice blend ground from a family recipe β€” peanuts, chili, ginger, and paprika. Bring the roadside grill home.',aiTags:['suya','spice','yaji','cooking','kano','seasoning'],aiEnriched:true,created:'2026-05-26T11:25:00Z',txHash:'0xf6a7b8',storageHash:'0xabd2',image:'https://images.unsplash.com/photo-1596040033229-a9821ebd058d?w=400&h=300&fit=crop'},
{id:'p019',name:'Beaded Waist Chain',price:5000,stock:35,cat:'fashion',desc:'Delicate waist beads in glass seed beads. Adjustable tie closure. Available in gold, coral, and midnight blue.',origin:'Bead & Thread, Lagos Island',aiDesc:'Delicate glass seed bead waist chain β€” a timeless body adornment tradition. Adjustable tie closure in gold, coral, or midnight blue.',aiTags:['waist beads','jewelry','beaded','body jewelry','traditional'],aiEnriched:true,created:'2026-05-26T11:30:00Z',txHash:'0xa7b8c9',storageHash:'0xabd3',image:'https://images.unsplash.com/photo-1611085583191-a3b181a88401?w=400&h=300&fit=crop'},
{id:'p020',name:'Coconut Oil Hair Butter',price:6000,stock:28,cat:'beauty',desc:'Whipped coconut oil with argan, rosemary, and vitamin E. Deep conditioning for natural hair. 250ml jar.',origin:'Crown Naturals, Lekki',aiDesc:'Whipped coconut oil hair butter enriched with argan and rosemary. Deep conditioning for natural curls and coils β€” no silicones, no sulfates.',aiTags:['hair butter','coconut oil','natural hair','conditioning','organic'],aiEnriched:true,created:'2026-05-26T11:35:00Z',txHash:'0xb8c9d0',storageHash:'0xabd4',image:'https://images.unsplash.com/photo-1608248597279-f99d160bfcbc?w=400&h=300&fit=crop'},
{id:'p021',name:'Concrete Desktop Planter',price:4500,stock:30,cat:'home',desc:'Minimalist concrete planter for succulents or air plants. Drainage hole with cork pad. Hand-cast.',origin:'Form Studio, Yaba',aiDesc:'Hand-cast concrete planter in minimalist silhouette. Perfect for succulents β€” drainage hole and cork pad protect surfaces.',aiTags:['planter','concrete','minimalist','succulent','desk','home'],aiEnriched:true,created:'2026-05-26T11:40:00Z',txHash:'0xc9d0e1',storageHash:'0xabd5',image:'https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=400&h=300&fit=crop'},
{id:'p022',name:'Kente-Trim Canvas Sneakers',price:19500,stock:9,cat:'fashion',desc:'White canvas sneakers with authentic kente cloth trim. Vulcanized rubber sole. Unisex, sizes 37-46.',origin:'SoleNative, Accra Γ— Lagos',aiDesc:'Clean white canvas sneakers accented with authentic kente cloth trim. Vulcanized rubber sole β€” where streetwear meets West African textile art.',aiTags:['sneakers','kente','footwear','unisex','canvas'],aiEnriched:true,created:'2026-05-26T11:45:00Z',txHash:'0xd0e1f2',storageHash:'0xabd6',image:'https://images.unsplash.com/photo-1525966222134-fcfa99b8ae77?w=400&h=300&fit=crop'},
{id:'p023',name:'Palm Wine Vinegar',price:3500,stock:40,cat:'food',desc:'Naturally fermented palm wine vinegar. Tangy, complex, probiotic-rich. Great for salads, marinades, and drinking.',origin:'Palmcraft, Edo State',aiDesc:'Naturally fermented palm wine vinegar β€” tangy, complex, and probiotic-rich. An artisanal condiment for salads, marinades, or sipping.',aiTags:['vinegar','palm wine','fermented','artisanal','probiotic'],aiEnriched:true,created:'2026-05-26T11:50:00Z',txHash:'0xe1f2a3',storageHash:'0xabd7',image:'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?w=400&h=300&fit=crop'},
{id:'p024',name:'Indigo Linen Table Runner',price:13000,stock:7,cat:'home',desc:'Hand-dyed indigo linen table runner, 180cm. Each piece unique due to natural dye process. Machine washable.',origin:'Dyehouse Co., Abeokuta',aiDesc:'Hand-dyed indigo linen table runner at 180cm. Each piece bears unique marks of the natural dyeing process β€” functional art for your table.',aiTags:['table runner','indigo','linen','handmade','dining'],aiEnriched:true,created:'2026-05-26T11:55:00Z',txHash:'0xf2a3b4',storageHash:'0xabd8',image:'https://images.unsplash.com/photo-1615876234886-fd9a39fda97f?w=400&h=300&fit=crop'},
{id:'p025',name:'Bluetooth Bamboo Speaker',price:24000,stock:5,cat:'tech',desc:'Portable Bluetooth 5.0 speaker in real bamboo housing. 10W output, 8hr battery. Rich warm tone from natural resonance.',origin:'EcoSound Lagos, Ikeja',aiDesc:'Portable Bluetooth speaker housed in real bamboo. The natural wood resonance produces warm, rich sound β€” 10W output, 8-hour battery life.',aiTags:['speaker','bluetooth','bamboo','eco','portable'],aiEnriched:true,created:'2026-05-26T12:00:00Z',txHash:'0xa3b4c5',storageHash:'0xabd9',image:'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=400&h=300&fit=crop'},
{id:'p026',name:'Terracotta Oil Diffuser',price:7000,stock:20,cat:'home',desc:'Handmade terracotta essential oil diffuser. No electricity needed β€” add oil and water, terracotta absorbs and releases.',origin:'Clay & Flame, Jos',aiDesc:'Handmade terracotta oil diffuser β€” no electricity, no flame. The porous clay naturally absorbs and slowly releases essential oils.',aiTags:['diffuser','terracotta','essential oil','handmade','clay'],aiEnriched:true,created:'2026-05-26T12:05:00Z',txHash:'0xb4c5d6',storageHash:'0xabda',image:'https://images.unsplash.com/photo-1602928321679-560bb453f190?w=400&h=300&fit=crop'},
{id:'p027',name:'Okra Seed Crackers',price:3000,stock:55,cat:'food',desc:'Crispy crackers made from dried okra seeds, millet, and sea salt. High protein, gluten-free. 180g pack.',origin:'SnackRoot, Ibadan',aiDesc:'Crispy crackers from dried okra seeds and millet β€” high protein, gluten-free, with just sea salt. A clever Nigerian superfood snack.',aiTags:['crackers','okra','gluten-free','snack','protein'],aiEnriched:true,created:'2026-05-26T12:10:00Z',txHash:'0xc5d6e7',storageHash:'0xabdb',image:'https://images.unsplash.com/photo-1621939514649-280e2ee25f60?w=400&h=300&fit=crop'},
{id:'p028',name:'Cowrie Shell Anklet',price:4500,stock:30,cat:'fashion',desc:'Adjustable anklet with natural cowrie shells on waxed cotton cord. Waterproof. One size fits all.',origin:'OceanStone, Lagos Island',aiDesc:'Natural cowrie shell anklet on waterproof waxed cord. Cowries were once currency in West Africa β€” wear a piece of living history.',aiTags:['anklet','cowrie','shell','jewelry','beach','waterproof'],aiEnriched:true,created:'2026-05-26T12:15:00Z',txHash:'0xd6e7f8',storageHash:'0xabdc',image:'https://images.unsplash.com/photo-1573408301185-9146fe634ad0?w=400&h=300&fit=crop'},
{id:'p029',name:'Handwoven Cotton Throw',price:25000,stock:3,cat:'home',desc:'Large cotton throw blanket (150x200cm) handwoven on a traditional loom. Neutral geometric pattern. Takes 2 weeks to make.',origin:'Weave & Weft, Iseyin',aiDesc:'A large handwoven cotton throw blanket in neutral geometric patterns. Each piece takes two weeks on a traditional loom β€” heirloom quality.',aiTags:['throw','blanket','handwoven','cotton','loom','heirloom'],aiEnriched:true,created:'2026-05-26T12:20:00Z',txHash:'0xe7f8a9',storageHash:'0xabdd',image:'https://images.unsplash.com/photo-1580301762395-21ce6d5d4bc4?w=400&h=300&fit=crop'},
{id:'p030',name:'Rosemary & Mint Soap Bar',price:2000,stock:80,cat:'beauty',desc:'Cold-process soap with rosemary, peppermint, and Nigerian olive oil. Long-lasting, rich lather. 120g bar.',origin:'Suds & Soul, Abuja',aiDesc:'Handmade cold-process soap with rosemary and peppermint β€” invigorating lather from Nigerian olive oil. Long-lasting 120g bar.',aiTags:['soap','rosemary','mint','handmade','cold process'],aiEnriched:true,created:'2026-05-26T12:25:00Z',txHash:'0xf8a9b0',storageHash:'0xabde',image:'https://images.unsplash.com/photo-1600857544200-b2f666a9a2ec?w=400&h=300&fit=crop'},
{id:'p031',name:'Macrame Wall Hanging',price:17000,stock:5,cat:'art',desc:'Handknotted cotton macrame wall hanging, 60x90cm. Organic cotton cord. Each piece takes 20+ hours.',origin:'KnotWork Studio, Lekki',aiDesc:'Intricate handknotted macrame wall hanging in organic cotton. 60x90cm of textured artistry β€” 20+ hours of handcraft in every piece.',aiTags:['macrame','wall art','handmade','cotton','boho'],aiEnriched:true,created:'2026-05-26T12:30:00Z',txHash:'0xa9b0c1',storageHash:'0xabdf',image:'https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=400&h=300&fit=crop'},
{id:'p032',name:'Dried Hibiscus Flower Arrangement',price:10000,stock:12,cat:'home',desc:'Preserved hibiscus and dried grasses in a handmade ceramic vase. Lasts 12+ months. No water needed.',origin:'Petal & Stem, Victoria Island',aiDesc:'Stunning preserved hibiscus flowers and dried grasses in a handmade ceramic vase. Zero maintenance beauty that lasts over a year.',aiTags:['flowers','dried','arrangement','ceramic','home decor'],aiEnriched:true,created:'2026-05-26T12:35:00Z',txHash:'0xb0c1d2',storageHash:'0xabe0',image:'https://images.unsplash.com/photo-1487530811176-3780de880c2d?w=400&h=300&fit=crop'},
{id:'p033',name:'Phone Case β€” Ankara Edition',price:5500,stock:40,cat:'tech',desc:'Hard-shell phone case with real ankara fabric inlay under resin. iPhone 15/16 and Samsung S24/S25.',origin:'CaseCraft Lagos, Ikeja',aiDesc:'Phone case with real ankara wax print fabric sealed under crystal resin. Bold West African pattern protection for your device.',aiTags:['phone case','ankara','tech accessory','resin','fabric'],aiEnriched:true,created:'2026-05-26T12:40:00Z',txHash:'0xc1d2e3',storageHash:'0xabe1',image:'https://images.unsplash.com/photo-1601784551446-20c9e07cdbdb?w=400&h=300&fit=crop'},
{id:'p034',name:'Tiger Nut Milk Concentrate',price:3800,stock:50,cat:'food',desc:'Cold-pressed tiger nut (aya/ofio) milk concentrate. Add water for creamy plant milk. No sugar added. 500ml.',origin:'Root Milk, Kaduna',aiDesc:'Cold-pressed tiger nut milk concentrate β€” just add water for fresh, creamy plant milk. No sugar, no additives, pure Kaduna tiger nuts.',aiTags:['tiger nut','plant milk','dairy-free','concentrate','natural'],aiEnriched:true,created:'2026-05-26T12:45:00Z',txHash:'0xd2e3f4',storageHash:'0xabe2',image:'https://images.unsplash.com/photo-1550583724-b2692b85b150?w=400&h=300&fit=crop'},
{id:'p035',name:'Bogolan Mud Cloth Clutch',price:13500,stock:8,cat:'fashion',desc:'Clutch purse in authentic Malian bogolan (mud cloth). Leather interior, magnetic snap. Each pattern is hand-painted.',origin:'MudStory Collective, Bamako Γ— Lagos',aiDesc:'Authentic bogolan mud cloth clutch with leather interior. Each geometric pattern is hand-painted with fermented mud β€” ancient art, modern accessory.',aiTags:['clutch','bogolan','mud cloth','leather','handpainted'],aiEnriched:true,created:'2026-05-26T12:50:00Z',txHash:'0xe3f4a5',storageHash:'0xabe3',image:'https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=400&h=300&fit=crop'},
{id:'p036',name:'Coconut Shell Bowl Set',price:8500,stock:16,cat:'home',desc:'Set of 4 polished coconut shell bowls. Perfect for smoothie bowls, snacks, or decor. Food-safe lacquer.',origin:'ShellCraft, Badagry',aiDesc:'Set of 4 polished coconut shell bowls β€” beautiful for smoothie bowls or snacks. Food-safe lacquered finish from coastal Badagry.',aiTags:['bowls','coconut','shell','kitchen','eco','set'],aiEnriched:true,created:'2026-05-26T12:55:00Z',txHash:'0xf4a5b6',storageHash:'0xabe4',image:'https://images.unsplash.com/photo-1604782206219-3b9576575203?w=400&h=300&fit=crop'},
{id:'p037',name:'Kola Nut & Coffee Scrub',price:5000,stock:35,cat:'beauty',desc:'Exfoliating body scrub with ground kola nut, coffee, and coconut oil. Energizes skin and reduces cellulite. 200g.',origin:'SkinRoots Lab, Abuja',aiDesc:'Energizing body scrub blending ground kola nut and coffee with coconut oil. Exfoliates and stimulates circulation β€” wake up your skin.',aiTags:['scrub','kola nut','coffee','exfoliating','body care'],aiEnriched:true,created:'2026-05-26T13:00:00Z',txHash:'0xa5b6c7',storageHash:'0xabe5',image:'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?w=400&h=300&fit=crop'},
{id:'p038',name:'Afrobeat Vinyl Wall Art',price:20000,stock:4,cat:'art',desc:'Laser-cut steel wall art depicting a Fela-inspired Afrobeat scene. 60cm diameter. Matte black powder coat.',origin:'MetalWorks, Ogba',aiDesc:'Laser-cut steel wall art celebrating Afrobeat culture. 60cm diameter in matte black β€” a bold sculptural tribute to the Lagos music scene.',aiTags:['wall art','steel','afrobeat','laser cut','music','sculpture'],aiEnriched:true,created:'2026-05-26T13:05:00Z',txHash:'0xb6c7d8',storageHash:'0xabe6',image:'https://images.unsplash.com/photo-1513364776144-60967b0f800f?w=400&h=300&fit=crop'},
{id:'p039',name:'Woven Straw Coasters (Set of 6)',price:4000,stock:28,cat:'home',desc:'Hand-woven straw coasters with colorful thread accents. Each set slightly unique. Heat-resistant.',origin:'Straw Studio, Ilorin',aiDesc:'Set of 6 hand-woven straw coasters with colorful thread accents. Heat-resistant and uniquely textured β€” handcraft for your tabletop.',aiTags:['coasters','straw','handwoven','set','tableware'],aiEnriched:true,created:'2026-05-26T13:10:00Z',txHash:'0xc7d8e9',storageHash:'0xabe7',image:'https://images.unsplash.com/photo-1558618666-fcd25c85f82e?w=400&h=300&fit=crop'},
{id:'p040',name:'Cashew Nut Butter',price:4800,stock:38,cat:'food',desc:'Smooth cashew butter from Nigerian cashews. Single ingredient, stone-ground. 300g glass jar.',origin:'NutHouse, Ogbomosho',aiDesc:'Silky smooth cashew butter stone-ground from single-origin Nigerian cashews. One ingredient, zero additives, pure nutty goodness.',aiTags:['cashew','nut butter','stone-ground','single ingredient','spread'],aiEnriched:true,created:'2026-05-26T13:15:00Z',txHash:'0xd8e9f0',storageHash:'0xabe8',image:'https://images.unsplash.com/photo-1612187209234-fe47e4d7bfc1?w=400&h=300&fit=crop'},
{id:'p041',name:'Tie-Dye Silk Scarf',price:16500,stock:10,cat:'fashion',desc:'Pure silk scarf with hand tie-dye in sunset gradient. 170x55cm. Versatile β€” wear as headwrap, neck scarf, or belt.',origin:'Dye Dreams, Osogbo',aiDesc:'Hand tie-dyed pure silk scarf in warm sunset gradients. 170cm of versatile luxury β€” headwrap, neck scarf, or wrist accent.',aiTags:['scarf','silk','tie-dye','headwrap','versatile'],aiEnriched:true,created:'2026-05-26T13:20:00Z',txHash:'0xe9f0a1',storageHash:'0xabe9',image:'https://images.unsplash.com/photo-1601924921557-45e8e1b06829?w=400&h=300&fit=crop'},
{id:'p042',name:'Recycled Glass Tumbler Set',price:11000,stock:12,cat:'home',desc:'Set of 4 tumblers blown from recycled glass bottles. Each has unique color variations. 300ml capacity.',origin:'Blown Away Glass, Lagos',aiDesc:'Tumblers hand-blown from recycled glass bottles β€” each piece carries unique color swirls. Set of 4, 300ml, sustainable elegance.',aiTags:['tumblers','recycled glass','handblown','sustainable','kitchen'],aiEnriched:true,created:'2026-05-26T13:25:00Z',txHash:'0xf0a1b2',storageHash:'0xabea',image:'https://images.unsplash.com/photo-1513558161293-cdaf765ed2fd?w=400&h=300&fit=crop'},
{id:'p043',name:'Leather Cord Bracelet',price:3500,stock:45,cat:'fashion',desc:'Braided leather cord bracelet with brass clasp. Naturally tanned goat leather. Unisex.',origin:'Tannery Road Leatherworks, Kano',aiDesc:'Braided bracelet in naturally tanned goat leather with a brass clasp. Simple, timeless, unisex β€” Kano leathercraft at its finest.',aiTags:['bracelet','leather','braided','unisex','brass'],aiEnriched:true,created:'2026-05-26T13:30:00Z',txHash:'0xa1b2c3',storageHash:'0xabeb',image:'https://images.unsplash.com/photo-1573408301185-9146fe634ad0?w=400&h=300&fit=crop'},
{id:'p044',name:'Smoked Catfish Pepper Sauce',price:3200,stock:48,cat:'food',desc:'Rich pepper sauce with smoked catfish, scotch bonnet, and locust beans. 250ml jar. Perfect with rice, yam, or bread.',origin:'Mama\'s Pot Kitchen, Ijebu-Ode',aiDesc:'Rich pepper sauce layered with smoked catfish, scotch bonnet, and locust beans. A Lagos pantry essential β€” perfect on everything.',aiTags:['pepper sauce','catfish','smoked','condiment','spicy'],aiEnriched:true,created:'2026-05-26T13:35:00Z',txHash:'0xb2c3d4',storageHash:'0xabec',image:'https://images.unsplash.com/photo-1472476443507-c7a5948772fc?w=400&h=300&fit=crop'},
{id:'p045',name:'Bamboo Sunglasses',price:9500,stock:15,cat:'fashion',desc:'Polarized sunglasses with bamboo temples. UV400 protection. Comes in a cork case. Lightweight.',origin:'Shade Tree, Lagos',aiDesc:'Polarized sunglasses with real bamboo temples and UV400 protection. Lightweight, eco-conscious eyewear in a cork carry case.',aiTags:['sunglasses','bamboo','polarized','UV protection','eco'],aiEnriched:true,created:'2026-05-26T13:40:00Z',txHash:'0xc3d4e5',storageHash:'0xabed',image:'https://images.unsplash.com/photo-1511499767150-a48a237f0083?w=400&h=300&fit=crop'},
{id:'p046',name:'Clay Face Mask β€” Kaolin',price:3500,stock:55,cat:'beauty',desc:'Gentle kaolin clay mask for sensitive skin. With honey, oat extract, and chamomile. 100ml.',origin:'Earth & Honey Skincare, Abuja',aiDesc:'Gentle kaolin clay face mask enriched with raw honey, oat extract, and chamomile. Perfect for sensitive skin β€” calms, cleanses, glows.',aiTags:['face mask','kaolin','clay','sensitive skin','honey'],aiEnriched:true,created:'2026-05-26T13:45:00Z',txHash:'0xd4e5f6',storageHash:'0xabee',image:'https://images.unsplash.com/photo-1596755389378-c31d21fd1273?w=400&h=300&fit=crop'},
{id:'p047',name:'Mini Djembe Drum',price:18000,stock:6,cat:'art',desc:'Handcarved 30cm djembe drum with goatskin head. Playable instrument and beautiful decor piece. Rope-tuned.',origin:'Drum Circle Workshop, Osogbo',aiDesc:'Handcarved 30cm djembe with goatskin head β€” a real playable instrument that doubles as striking decor. Rope-tuned for authentic tone.',aiTags:['djembe','drum','handcarved','music','instrument','decor'],aiEnriched:true,created:'2026-05-26T13:50:00Z',txHash:'0xe5f6a7',storageHash:'0xabef',image:'https://images.unsplash.com/photo-1519892300165-cb5542fb47c7?w=400&h=300&fit=crop'},
{id:'p048',name:'Organic Honey β€” Forest',price:6500,stock:25,cat:'food',desc:'Raw, unfiltered forest honey from Ondo State apiaries. Dark amber with complex floral notes. 500g glass jar.',origin:'Bee & Bloom Apiaries, Ondo State',aiDesc:'Raw unfiltered forest honey harvested from Ondo State woodlands. Dark amber with complex floral depth β€” nature\'s sweetener, untouched.',aiTags:['honey','raw','organic','forest','unfiltered'],aiEnriched:true,created:'2026-05-26T13:55:00Z',txHash:'0xf6a7b8',storageHash:'0xabf0',image:'https://images.unsplash.com/photo-1587049352846-4a222e784d38?w=400&h=300&fit=crop'},
{id:'p049',name:'Woven Camera Strap',price:7500,stock:18,cat:'tech',desc:'Handwoven cotton camera strap with leather connectors. Fits all DSLR and mirrorless. Adjustable length.',origin:'LoomLine Accessories, Iseyin',aiDesc:'Handwoven cotton camera strap with leather hardware. Fits any DSLR or mirrorless β€” traditional textile craft meets modern photography.',aiTags:['camera strap','handwoven','cotton','photography','leather'],aiEnriched:true,created:'2026-05-26T14:00:00Z',txHash:'0xa7b8c9',storageHash:'0xabf1',image:'https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=400&h=300&fit=crop'},
{id:'p050',name:'Frangipani Body Oil',price:8000,stock:22,cat:'beauty',desc:'Cold-pressed blend of sweet almond, jojoba, and frangipani essential oil. Fast-absorbing, non-greasy. 100ml dropper bottle.',origin:'Bloom Body, Victoria Island',aiDesc:'Luxurious body oil blending cold-pressed sweet almond and jojoba with frangipani essence. Fast-absorbing, silky finish β€” tropical radiance in a bottle.',aiTags:['body oil','frangipani','jojoba','moisturizer','luxury'],aiEnriched:true,created:'2026-05-26T14:05:00Z',txHash:'0xb8c9d0',storageHash:'0xabf2',image:'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?w=400&h=300&fit=crop'},
];
const SEED_LEDGER = [
{type:'Product Registered',detail:'Adire Indigo Tote Bag',hash:'0x8a3f1b',status:'ok',time:'2026-05-26T09:00:00Z'},
{type:'Product Registered',detail:'Shea Butter & Black Soap Gift Set',hash:'0x7b2e2c',status:'ok',time:'2026-05-26T09:02:00Z'},
{type:'Product Registered',detail:'Brass Statement Earrings',hash:'0x6c1d3d',status:'ok',time:'2026-05-26T09:04:00Z'},
{type:'Product Registered',detail:'Aso-Oke Throw Pillow Set',hash:'0x5d0c4e',status:'ok',time:'2026-05-26T09:06:00Z'},
{type:'Product Registered',detail:'Cold-Brew Hibiscus Zobo',hash:'0x4e9b5f',status:'ok',time:'2026-05-26T09:08:00Z'},
{type:'Product Registered',detail:'Recycled Rubber Laptop Sleeve',hash:'0x3f8a6a',status:'ok',time:'2026-05-26T09:10:00Z'},
{type:'Product Registered',detail:'Ankara Print Notebook',hash:'0xee1b7b',status:'ok',time:'2026-05-26T09:12:00Z'},
{type:'Product Registered',detail:'"Lagos Nights" Candle',hash:'0x2e7c8c',status:'ok',time:'2026-05-26T09:14:00Z'},
{type:'Product Registered',detail:'Moringa & Turmeric Tea',hash:'0x1d6b9d',status:'ok',time:'2026-05-26T09:16:00Z'},
{type:'Product Registered',detail:'Oja Market Basket',hash:'0x0c5aae',status:'ok',time:'2026-05-26T09:18:00Z'},
{type:'Sale Verified',detail:'Shea Butter Gift Set x2',hash:'0xe1a201',status:'ok',time:'2026-05-26T11:20:00Z'},
{type:'Sale Verified',detail:'Ankara Notebook x3',hash:'0xd2b302',status:'ok',time:'2026-05-26T12:45:00Z'},
{type:'Sale Verified',detail:'Cold-Brew Zobo x5',hash:'0xc3c403',status:'ok',time:'2026-05-26T14:10:00Z'},
{type:'Sale Verified',detail:'Brass Earrings x1',hash:'0xb4d504',status:'ok',time:'2026-05-26T15:30:00Z'},
{type:'Sale Verified',detail:'Rosemary Soap Bar x6',hash:'0xa5e605',status:'ok',time:'2026-05-26T16:00:00Z'},
{type:'Sale Verified',detail:'Suya Spice Blend x4',hash:'0x96f706',status:'ok',time:'2026-05-27T08:15:00Z'},
{type:'Sale Verified',detail:'Adire Tote Bag x1',hash:'0x87e807',status:'ok',time:'2026-05-27T09:40:00Z'},
{type:'Sale Verified',detail:'Lagos Nights Candle x2',hash:'0x78d908',status:'ok',time:'2026-05-27T11:00:00Z'},
{type:'Sale Verified',detail:'Moringa Tea x3',hash:'0x69ca09',status:'ok',time:'2026-05-27T13:20:00Z'},
{type:'Sale Verified',detail:'Beaded Waist Chain x2',hash:'0x5abb10',status:'ok',time:'2026-05-27T14:45:00Z'},
{type:'Sale Verified',detail:'Cashew Nut Butter x2',hash:'0x4bac11',status:'ok',time:'2026-05-27T16:10:00Z'},
{type:'Sale Verified',detail:'Coconut Hair Butter x1',hash:'0x3c9d12',status:'ok',time:'2026-05-27T17:30:00Z'},
{type:'Sale Verified',detail:'Tiger Nut Milk x3',hash:'0x2d8e13',status:'ok',time:'2026-05-28T08:00:00Z'},
{type:'Sale Verified',detail:'Hand-Painted Mug x2',hash:'0x1e7f14',status:'ok',time:'2026-05-28T09:15:00Z'},
{type:'Sale Verified',detail:'Leather Bracelet x3',hash:'0x0f6015',status:'ok',time:'2026-05-28T10:30:00Z'},
{type:'Sale Verified',detail:'Organic Honey x1',hash:'0xfa5116',status:'ok',time:'2026-05-28T11:45:00Z'},
];
const S = { cfg:{}, products:[], ledger:[], wallet:null, provider:null, img:null, imgB64:null, sales:6, robot:null, robotBridge:false, sfCat:'all', sfQuery:'' };
// ─── TOAST ───
function toast(msg, type='info') {
const c = document.getElementById('toast-container');
const t = document.createElement('div');
t.className = 'toast ' + type;
t.textContent = msg;
c.appendChild(t);
setTimeout(() => { t.style.opacity='0'; t.style.transition='opacity 0.3s'; setTimeout(()=>t.remove(),300); }, 3000);
}
// ─── SETUP ───
function toggleAdvanced() {
const t = document.querySelector('.advanced-toggle');
const f = document.getElementById('adv-fields');
t.classList.toggle('open');
f.classList.toggle('open');
}
function restoreSession() {
const s = sessionStorage.getItem('storeos');
if (!s) return;
try {
const c = JSON.parse(s);
document.getElementById('cfg-name').value = c.name||'';
if (c.model) document.getElementById('cfg-model').value = c.model;
if (c.endpoint) document.getElementById('cfg-endpoint').value = c.endpoint;
if (c.rpc) document.getElementById('cfg-rpc').value = c.rpc;
if (c.chainId) document.getElementById('cfg-chain-id').value = c.chainId;
if (c.voice) document.getElementById('cfg-voice').value = c.voice;
if (c.robotMode) document.getElementById('cfg-robot').value = c.robotMode;
if (c.whisperEp) document.getElementById('cfg-whisper-ep').value = c.whisperEp;
} catch(e){}
const p = localStorage.getItem('storeos-products');
if (p) try { S.products = JSON.parse(p); } catch(e){}
const l = localStorage.getItem('storeos-ledger');
if (l) try { S.ledger = JSON.parse(l); } catch(e){}
// Seed with demo data if empty
if (!S.products.length) {
S.products = JSON.parse(JSON.stringify(SEED_PRODUCTS));
localStorage.setItem('storeos-products', JSON.stringify(S.products));
}
if (!S.ledger.length) {
S.ledger = JSON.parse(JSON.stringify(SEED_LEDGER));
localStorage.setItem('storeos-ledger', JSON.stringify(S.ledger));
}
}
restoreSession();
async function launch() {
const btn = document.getElementById('btn-go');
const key = document.getElementById('cfg-key').value.trim();
const endpoint = document.getElementById('cfg-endpoint').value.trim();
if (!key) { toast('API key is required','error'); return; }
btn.disabled = true;
btn.textContent = 'Connecting to 0G...';
const cfg = {
name: document.getElementById('cfg-name').value.trim()||'My Store',
key,
model: document.getElementById('cfg-model').value,
endpoint: endpoint || '',
rpc: document.getElementById('cfg-rpc').value.trim(),
chainId: document.getElementById('cfg-chain-id').value.trim(),
wallet: document.getElementById('cfg-wallet').value.trim(),
voice: document.getElementById('cfg-voice').value,
robotMode: document.getElementById('cfg-robot').value,
whisperEp: document.getElementById('cfg-whisper-ep').value.trim(),
whisperKey: document.getElementById('cfg-whisper-key').value.trim(),
};
if (!cfg.endpoint) { toast('Compute endpoint is required','error'); btn.disabled=false; btn.textContent='Launch StoreOS'; return; }
// Build compute URL
let base = cfg.endpoint.replace(/\/+$/, '').replace(/\/v1(\/proxy)?$/, '');
if (base.includes('integratenetwork') || cfg.key.startsWith('app-sk-')) {
base = base + '/v1/proxy';
} else {
base = base + '/v1';
}
cfg.computeBase = base;
cfg.extraHeaders = {};
if (base.includes('huruai')) {
cfg.extraHeaders['X-Consumer-Email'] = 'exorbilabs@gmail.com';
}
toast('0G Compute configured','success');
if (cfg.wallet) {
try {
S.provider = new ethers.JsonRpcProvider(cfg.rpc);
S.wallet = new ethers.Wallet(cfg.wallet, S.provider);
const bal = ethers.formatEther(await S.provider.getBalance(S.wallet.address));
toast('0G Chain: '+bal.slice(0,8)+' 0G','success');
} catch(e) {
toast('Chain: '+e.message+' (continuing)','error');
S.wallet = null;
}
}
S.cfg = cfg;
const sessionCfg = {...cfg, key:'', wallet:'', whisperKey:''};
sessionStorage.setItem('storeos', JSON.stringify(sessionCfg));
document.getElementById('setup').style.display = 'none';
document.getElementById('app').style.display = 'block';
document.getElementById('d-store').textContent = cfg.name;
const pill = document.getElementById('chain-pill');
const ct = document.getElementById('chain-text');
if (S.wallet) {
pill.className = 'chain-pill on';
ct.textContent = cfg.chainId==='16661'?'0G Mainnet':'0G Testnet';
document.getElementById('d-wallet').textContent = S.wallet.address.slice(0,6)+'...'+S.wallet.address.slice(-4);
} else {
pill.className = 'chain-pill off';
ct.textContent = 'No Chain';
}
render();
stats();
renderLedger();
initRobot();
btn.disabled = false;
btn.textContent = 'Launch StoreOS';
}
// ─── TABS ───
function tab(el) {
document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
el.classList.add('active');
document.getElementById('p-'+el.dataset.p).classList.add('active');
if (el.dataset.p==='storefront') renderSF();
if (el.dataset.p==='ledger') { stats(); renderLedger(); }
}
// ─── RENDER ───
function render() {
const g = document.getElementById('pgrid');
const e = document.getElementById('pempty');
if (!S.products.length) { g.innerHTML=''; e.style.display='block'; return; }
e.style.display='none';
g.innerHTML = S.products.map((p,i) => card(p, i)).join('');
}
function renderSF() {
const g = document.getElementById('sfgrid');
const filtered = getFilteredProducts();
if (!filtered.length) { g.innerHTML='<div class="empty-state" style="padding:40px"><p>No products match your search</p></div>'; return; }
g.innerHTML = filtered.map(p => sfCard(p)).join('');
}
function sfCard(p) {
const avail = p.stock > 0;
return `<div class="product-card">
<div class="product-img">
${p.image?`<img src="${p.image}" loading="lazy">`:'&#128722;'}
${p.txHash?'<div class="chain-tag">&#9745; Verified</div>':''}
</div>
<div class="product-body">
<div class="product-name">${esc(p.name)}</div>
<div class="product-desc">${esc(p.aiDesc||p.desc||'')}</div>
<div class="product-footer">
<div class="product-price">${fmtP(p.price)}</div>
${avail?`<button class="buy-btn" onclick="window._buy('${p.id}')">Buy</button>`:'<span class="buy-btn sold-out">Sold out</span>'}
</div>
</div>
</div>`;
}
function card(p) {
const stockCls = p.stock>5?'stock-ok':p.stock>0?'stock-low':'stock-out';
const stockTxt = p.stock>5?p.stock+' in stock':p.stock>0?'Low: '+p.stock:'Sold out';
return `<div class="product-card">
<div class="product-img">
${p.image?`<img src="${p.image}" loading="lazy">`:'&#128722;'}
${p.txHash?'<div class="chain-tag">&#9745; On-Chain</div>':''}
</div>
<div class="product-body">
<div class="product-name">${esc(p.name)}</div>
<div class="product-desc">${esc(p.desc||p.aiDesc||'')}</div>
<div class="product-footer">
<div class="product-price">${fmtP(p.price)}</div>
<div class="stock-pill ${stockCls}">${stockTxt}</div>
</div>
<div class="tags-row">
${p.txHash?'<span class="tag tag-chain">ON-CHAIN</span>':''}
${p.storageHash?'<span class="tag tag-storage">0G STORAGE</span>':''}
${p.aiEnriched?'<span class="tag tag-ai">AI VERIFIED</span>':''}
</div>
</div>
</div>`;
}
// ─── MODAL ───
function openModal() {
document.getElementById('modal').classList.add('open');
document.getElementById('m-name').value='';
document.getElementById('m-price').value='';
document.getElementById('m-stock').value='10';
document.getElementById('m-origin').value='';
document.getElementById('aibox').classList.remove('show');
document.getElementById('imgup').classList.remove('has-img');
document.getElementById('imgph').innerHTML='<div class="ic">&#128247;</div><p>Click to upload photo</p>';
S.img=null; S.imgB64=null;
delete document.getElementById('aibox').dataset.r;
}
function closeModal() { document.getElementById('modal').classList.remove('open'); }
function onImg(e) {
const f = e.target.files[0]; if(!f) return;
S.img = f;
const r = new FileReader();
r.onload = ev => {
S.imgB64 = ev.target.result;
document.getElementById('imgup').classList.add('has-img');
document.getElementById('imgph').innerHTML = `<img src="${ev.target.result}">`;
};
r.readAsDataURL(f);
}
// ─── AI ENRICH (0G Compute) ───
async function enrich() {
const name = document.getElementById('m-name').value.trim();
if (!name) { toast('Enter product name first','error'); return; }
const box = document.getElementById('aibox');
box.classList.add('show');
document.getElementById('ai-spin').style.display='inline-block';
document.getElementById('ai-desc').textContent='Analyzing with 0G Compute...';
document.getElementById('ai-price-row').style.display='none';
document.getElementById('ai-tags-row').style.display='none';
const cat = document.getElementById('m-cat').value;
const origin = document.getElementById('m-origin').value.trim();
const price = document.getElementById('m-price').value.trim();
const prompt = `You are a product analyst for a curated retail store in Lagos, Nigeria. Analyze this product:
Product: ${name}
Category: ${cat}
${origin?'Origin: '+origin:''}
${price?'Current price: NGN '+price:''}
Provide: 1) A compelling 2-sentence description 2) Suggested retail price in NGN 3) 3-5 discovery tags
Respond ONLY in this JSON format:
{"description":"...","suggestedPrice":12000,"tags":["tag1","tag2","tag3"]}`;
try {
const messages = [{role:'user',content:prompt}];
if (S.imgB64 && S.cfg.model.includes('vl')) {
messages[0] = {role:'user',content:[{type:'text',text:prompt},{type:'image_url',image_url:{url:S.imgB64}}]};
}
const r = await fetch(S.cfg.computeBase+'/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+S.cfg.key,...(S.cfg.extraHeaders||{})},
body:JSON.stringify({model:S.cfg.model,messages,max_tokens:300,temperature:0.7}),
});
if (!r.ok) throw new Error(await r.text());
const d = await r.json();
const txt = getReply(d);
const m = txt.match(/\{[\s\S]*\}/);
if (m) {
const j = JSON.parse(m[0]);
document.getElementById('ai-desc').textContent = j.description;
if (j.suggestedPrice) { document.getElementById('ai-price').textContent=fmtP(j.suggestedPrice); document.getElementById('ai-price-row').style.display='flex'; }
if (j.tags) { document.getElementById('ai-tags').textContent=j.tags.join(', '); document.getElementById('ai-tags-row').style.display='flex'; }
box.dataset.r = m[0];
toast('AI enrichment complete','success');
} else {
document.getElementById('ai-desc').textContent = txt;
}
} catch(e) {
document.getElementById('ai-desc').textContent='Failed: '+e.message;
toast('AI enrichment failed','error');
}
document.getElementById('ai-spin').style.display='none';
}
// ─── SAVE PRODUCT (Storage + Chain) ───
async function saveProd() {
const name = document.getElementById('m-name').value.trim();
if (!name) { toast('Product name required','error'); return; }
const btn = document.getElementById('btn-save');
btn.disabled=true; btn.textContent='Saving...';
const box = document.getElementById('aibox');
let ai = null;
if (box.dataset.r) try { ai = JSON.parse(box.dataset.r); } catch(e){}
const product = {
id: Date.now().toString(36)+Math.random().toString(36).slice(2,6),
name,
price: parseInt(document.getElementById('m-price').value)||ai?.suggestedPrice||0,
stock: parseInt(document.getElementById('m-stock').value)||0,
cat: document.getElementById('m-cat').value,
desc: '',
origin: document.getElementById('m-origin').value.trim(),
image: S.imgB64||null,
aiDesc: ai?.description||null,
aiTags: ai?.tags||[],
aiEnriched: !!ai,
created: new Date().toISOString(),
txHash: null,
storageHash: null,
};
// 0G Storage hash
const data = JSON.stringify({name:product.name,price:product.price,cat:product.cat,desc:product.aiDesc,origin:product.origin,tags:product.aiTags,created:product.created});
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
product.storageHash = '0x'+[...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join('');
// 0G Chain
if (S.wallet) {
try {
btn.textContent='Registering on 0G Chain...';
const txData = ethers.toUtf8Bytes(JSON.stringify({
type:'STOREOS_PRODUCT', store:S.cfg.name, id:product.id, name:product.name, hash:product.storageHash, ts:product.created
}));
const tx = await S.wallet.sendTransaction({ to:S.wallet.address, value:0, data:ethers.hexlify(txData) });
product.txHash = tx.hash;
btn.textContent='Confirming...';
await tx.wait();
addLedger('Product Registered', product.name, tx.hash, 'ok');
toast('Registered on 0G Chain','success');
} catch(e) {
console.error(e);
addLedger('Product Registered', product.name, null, 'local');
toast('Chain failed β€” saved locally','error');
}
} else {
addLedger('Product Added', product.name, null, 'local');
}
S.products.push(product);
localStorage.setItem('storeos-products', JSON.stringify(S.products));
render();
closeModal();
toast(name+' added to catalog','success');
btn.disabled=false; btn.textContent='Save & Register On-Chain';
}
// ─── CUSTOMER CHAT (0G Compute) ───
async function sendChat() {
const inp = document.getElementById('cinput');
const msg = inp.value.trim();
if (!msg) return;
inp.value='';
const box = document.getElementById('cmsg');
box.innerHTML += `<div class="msg u">${esc(msg)}</div>`;
const tid = 'ct'+Date.now();
box.innerHTML += `<div class="msg a" id="${tid}"><span class="typing">Thinking</span></div>`;
box.scrollTop = box.scrollHeight;
const catalog = S.products.map(p=>`- ${p.name}: ${p.desc||p.aiDesc||'No description'}. Price: ${fmtPRaw(p.price)}. ${p.stock>0?'In stock':'Sold out'}. Origin: ${p.origin||'N/A'}.`).join('\n');
robotAction('think');
const sys = `You are a friendly robot store assistant for "${S.cfg.name}". You are a Reachy Mini robot β€” a small humanoid with an expressive head and antennas. Help customers find products. ONLY recommend products from the catalog. Be warm, concise, helpful. 2-3 sentences max. Never use asterisks or roleplay narration.
You can trigger physical actions by appending tags to your response:
[ACTION:wave] β€” wave antennas in greeting
[ACTION:nod] β€” nod head to agree/confirm
[ACTION:shake] β€” shake head for no
[ACTION:happy] β€” excited antenna wiggle
[ACTION:dance] β€” celebratory dance
[ACTION:greet] β€” gentle greeting bow
Use one action per response when appropriate. Example: "Welcome! Let me help you find something great. [ACTION:wave]"
CATALOG:
${catalog||'Empty catalog.'}`;
try {
const r = await fetch(S.cfg.computeBase+'/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+S.cfg.key,...(S.cfg.extraHeaders||{})},
body:JSON.stringify({model:S.cfg.model,messages:[{role:'system',content:sys},{role:'user',content:msg}],max_tokens:300,temperature:0.7}),
});
const el = document.getElementById(tid);
if (!r.ok) { el.innerHTML='Sorry, something went wrong. Try again.'; return; }
const d = await r.json();
const raw = getReply(d);
const { clean, actions } = parseActions(raw);
el.innerHTML = esc(clean)+'<div class="tee-tag">&#9989; TEE-verified</div>';
speak(clean);
actions.forEach(a => robotAction(a));
if (!actions.length) robotAction('nod');
} catch(e) {
document.getElementById(tid).innerHTML='Connection error.';
}
box.scrollTop = box.scrollHeight;
}
// ─── COPILOT (0G Compute) ───
function sendCP() {
const inp = document.getElementById('cpinput');
const msg = inp.value.trim();
if (!msg) return;
inp.value='';
askCP(msg);
}
async function askCP(q) {
const box = document.getElementById('cpmsg');
box.innerHTML += `<div class="cp-msg u">${esc(q)}</div>`;
const tid = 'cp'+Date.now();
box.innerHTML += `<div class="cp-msg a" id="${tid}"><span class="typing">Analyzing</span></div>`;
box.scrollTop = box.scrollHeight;
const inv = S.products.map(p=>({name:p.name,price:p.price,stock:p.stock,cat:p.cat,origin:p.origin,tags:p.aiTags,onChain:!!p.txHash}));
const sys = `You are the AI copilot for "${S.cfg.name}". Give specific, actionable advice based on real data. Reference products by name. 3-5 sentences max. Be direct.
INVENTORY (${S.products.length} products):
${JSON.stringify(inv,null,1)}
STATS: ${S.products.length} products, ${S.products.filter(p=>p.txHash).length} on-chain, ${S.sales} sales`;
try {
const r = await fetch(S.cfg.computeBase+'/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+S.cfg.key,...(S.cfg.extraHeaders||{})},
body:JSON.stringify({model:S.cfg.model,messages:[{role:'system',content:sys},{role:'user',content:q}],max_tokens:500,temperature:0.7}),
});
const el = document.getElementById(tid);
if (!r.ok) { el.innerHTML='Failed β€” check connection.'; el.id=''; return; }
const d = await r.json();
const reply = getReply(d);
el.id='';
el.innerHTML = esc(reply).replace(/\n/g,'<br>')+'<div class="tee-tag">&#9989; TEE-verified &middot; 0G Compute</div>';
speak(reply);
} catch(e) {
document.getElementById(tid).innerHTML='Error: '+e.message;
document.getElementById(tid).id='';
}
box.scrollTop = box.scrollHeight;
}
// ─── LEDGER ───
function addLedger(type,detail,hash,status) {
S.ledger.unshift({type,detail,hash,status,time:new Date().toISOString()});
localStorage.setItem('storeos-ledger',JSON.stringify(S.ledger));
renderLedger();
stats();
}
function stats() {
const onChain = S.products.filter(p=>p.txHash).length;
const salesCount = S.ledger.filter(e=>e.type.includes('Sale')).length;
document.getElementById('s-prod').textContent=onChain;
document.getElementById('s-sales').textContent=salesCount;
document.getElementById('s-trust').textContent=onChain*10+salesCount*5;
document.getElementById('s-storage').textContent=S.products.length;
}
function renderLedger() {
const c = document.getElementById('lrows');
if (!S.ledger.length) { c.innerHTML='<div class="empty-state" style="padding:40px"><p style="color:var(--text3)">No activity yet</p></div>'; return; }
const explorer = S.cfg.chainId==='16661'?'https://chainscan.0g.ai':'https://chainscan-galileo.0g.ai';
c.innerHTML = S.ledger.map(e=>`<div class="lrow">
<div class="type">${e.type.includes('Product')?'&#128230;':'&#128176;'} ${e.type}</div>
<div class="hash">${e.hash?`<a href="${explorer}/tx/${e.hash}" target="_blank">${e.hash.slice(0,10)}...${e.hash.slice(-6)}</a>`:'Local'}</div>
<div class="time">${ago(e.time)}</div>
<div class="status"><span class="stag ${e.status==='ok'?'ok':'wait'}">${e.status==='ok'?'&#10003; Confirmed':'&#9711; '+e.status}</span></div>
</div>`).join('');
}
// ─── REACHY MINI ROBOT ───
S.liveRobot = false;
async function initRobot() {
const status = document.getElementById('robot-status');
if (S.cfg.robotMode === 'live') {
status.textContent = 'Connecting to Reachy Mini...';
if (window.reachySDK) {
try {
const ok = await window.reachySDK.connect();
if (ok) {
S.liveRobot = true;
status.textContent = 'Reachy Mini connected!';
toast('Connected to live Reachy Mini', 'success');
setTimeout(() => { status.textContent = ''; }, 2000);
} else {
status.textContent = 'Connection failed β€” falling back to sim';
toast('Could not connect to robot β€” using simulator', 'error');
initSim(status);
}
} catch(e) {
console.error('Live robot connect failed:', e);
status.textContent = 'Connection error β€” using sim';
toast('Robot error: ' + e.message, 'error');
initSim(status);
}
} else {
status.textContent = 'SDK not loaded β€” using sim';
initSim(status);
}
} else {
initSim(status);
}
}
function initSim(status) {
if (!window.ReachySim) {
status.textContent = 'Loading simulator...';
setTimeout(() => {
if (!window.ReachySim) { status.textContent = 'Simulator not available'; return; }
_mountSim(status);
}, 1000);
} else {
_mountSim(status);
}
}
function _mountSim(status) {
try {
const canvas = document.getElementById('sim-canvas');
S.robot = new ReachySim(canvas);
status.textContent = 'Simulator ready';
setTimeout(() => { if (S.robot.wakeUp) S.robot.wakeUp(); status.textContent = ''; }, 2000);
} catch(e) {
console.error('Sim init failed:', e);
status.textContent = 'Simulator error';
}
}
if (!r.ok) throw new Error(await r.text());
return true;
} catch(e) {
console.warn('Robot bridge action failed:', action, e);
S.robotBridge = false;
return false;
}
}
function robotAction(action) {
if (S.liveRobot) { liveAction(action); return; }
const r = S.robot;
if (!r) return;
const actions = {
wave: async () => {
r.setAntennasDeg(0, -40); await wait(200); r.setAntennasDeg(-40, 0);
await wait(200); r.setAntennasDeg(0, -40); await wait(200); r.setAntennasDeg(-40, 0);
await wait(300); r.setAntennasDeg(0, 0);
},
nod: async () => {
r.setHeadRpyDeg(0,-15,0); await wait(250); r.setHeadRpyDeg(0,10,0);
await wait(250); r.setHeadRpyDeg(0,-10,0); await wait(250); r.setHeadRpyDeg(0,0,0);
},
shake: async () => {
r.setHeadRpyDeg(0,0,-20); await wait(200); r.setHeadRpyDeg(0,0,20);
await wait(200); r.setHeadRpyDeg(0,0,-15); await wait(200); r.setHeadRpyDeg(0,0,0);
},
think: async () => {
r.setHeadRpyDeg(5,-10,15); r.setAntennasDeg(-20,-20);
await wait(1500); r.setHeadRpyDeg(0,0,0); r.setAntennasDeg(0,0);
},
happy: async () => {
r.setAntennasDeg(-30,-30); r.setHeadRpyDeg(0,-10,0);
await wait(300); r.setAntennasDeg(10,10); await wait(300); r.setAntennasDeg(-30,-30);
await wait(300); r.setAntennasDeg(0,0); r.setHeadRpyDeg(0,0,0);
},
dance: async () => {
for(let i=0;i<4;i++){r.setBodyYawDeg(20);r.setAntennasDeg(-30,10);await wait(300);r.setBodyYawDeg(-20);r.setAntennasDeg(10,-30);await wait(300);}
r.setBodyYawDeg(0); r.setAntennasDeg(0,0);
},
greet: async () => {
r.setHeadRpyDeg(0,-15,0); r.setAntennasDeg(-20,-20);
await wait(500); r.setAntennasDeg(0,0); r.setHeadRpyDeg(0,0,0);
},
sleep: async () => { if(r.gotoSleep) r.gotoSleep(); },
wake: async () => { if(r.wakeUp) r.wakeUp(); },
};
if (actions[action]) actions[action]();
}
// Live robot actions via JS SDK
async function liveAction(action) {
const sdk = window.reachySDK;
if (!sdk || !sdk.is_connected()) return;
const actions = {
wave: async () => {
sdk.set_antennas(0,-40); await wait(200); sdk.set_antennas(-40,0);
await wait(200); sdk.set_antennas(0,-40); await wait(200); sdk.set_antennas(-40,0);
await wait(300); sdk.set_antennas(0,0);
},
nod: async () => {
sdk.set_head_pose(0,0,0,0,-15,0); await wait(250); sdk.set_head_pose(0,0,0,0,10,0);
await wait(250); sdk.set_head_pose(0,0,0,0,-10,0); await wait(250); sdk.set_head_pose(0,0,0,0,0,0);
},
shake: async () => {
sdk.set_head_pose(0,0,0,0,0,-20); await wait(200); sdk.set_head_pose(0,0,0,0,0,20);
await wait(200); sdk.set_head_pose(0,0,0,0,0,-15); await wait(200); sdk.set_head_pose(0,0,0,0,0,0);
},
think: async () => {
sdk.set_head_pose(0,0,0,5,-10,15); sdk.set_antennas(-20,-20);
await wait(1500); sdk.set_head_pose(0,0,0,0,0,0); sdk.set_antennas(0,0);
},
happy: async () => {
sdk.set_antennas(-30,-30); await wait(300); sdk.set_antennas(10,10);
await wait(300); sdk.set_antennas(-30,-30); await wait(300); sdk.set_antennas(0,0);
},
dance: async () => {
for(let i=0;i<4;i++){sdk.set_antennas(-30,10);await wait(300);sdk.set_antennas(10,-30);await wait(300);}
sdk.set_antennas(0,0);
},
greet: async () => {
sdk.set_head_pose(0,0,0,0,-15,0); sdk.set_antennas(-20,-20);
await wait(500); sdk.set_antennas(0,0); sdk.set_head_pose(0,0,0,0,0,0);
},
};
if (actions[action]) await actions[action]();
}
function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
function parseActions(text) {
const actionRegex = /\[ACTION:(\w+)\]/gi;
const found = [];
let match;
while ((match = actionRegex.exec(text)) !== null) found.push(match[1].toLowerCase());
const clean = text.replace(actionRegex, '').trim();
return { clean, actions: found };
}
// ─── VOICE INPUT (0G Whisper) ───
let listening = false;
let mediaRecorder = null;
let audioChunks = [];
function toggleMic() {
if (listening) {
stopRecording();
} else {
startRecording();
}
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioChunks = [];
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); };
mediaRecorder.onstop = async () => {
stream.getTracks().forEach(t => t.stop());
const blob = new Blob(audioChunks, { type: 'audio/webm' });
if (blob.size < 1000) { toast('Too short β€” hold longer', 'error'); setMicState(false); return; }
setMicState(false);
await transcribeWithWhisper(blob);
};
mediaRecorder.start();
setMicState(true);
robotAction('think');
} catch (e) {
toast('Mic access denied', 'error');
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
setMicState(false);
}
async function transcribeWithWhisper(webmBlob) {
toast('Transcribing...', 'info');
const wavBlob = await webmToWav(webmBlob);
// Use separate Whisper endpoint if configured, otherwise use main compute endpoint
let sttBase, sttKey, sttModel;
if (S.cfg.whisperEp && S.cfg.whisperKey) {
let ep = S.cfg.whisperEp.replace(/\/+$/, '').replace(/\/v1(\/proxy)?$/, '');
sttBase = ep + '/v1/proxy';
sttKey = S.cfg.whisperKey;
sttModel = 'openai/whisper-large-v3';
} else {
sttBase = S.cfg.computeBase;
sttKey = S.cfg.key;
sttModel = S.cfg.computeBase.includes('huruai') ? 'huru/stt-1' : 'openai/whisper-large-v3';
}
try {
const form = new FormData();
form.append('file', wavBlob, 'audio.wav');
form.append('model', sttModel);
const headers = { 'Authorization': 'Bearer ' + sttKey };
if (S.cfg.extraHeaders) Object.assign(headers, S.cfg.extraHeaders);
const r = await fetch(sttBase + '/audio/transcriptions', {
method: 'POST',
headers,
body: form,
});
if (!r.ok) throw new Error(r.status + ': ' + (await r.text()).slice(0, 100));
const d = await r.json();
const text = d.text?.trim();
if (text && text !== '.') {
document.getElementById('cinput').value = text;
toast('Heard: "' + text.slice(0, 50) + '"', 'success');
sendChat();
} else {
toast('Could not understand β€” try again', 'error');
}
} catch (e) {
toast('Whisper error: ' + e.message, 'error');
}
}
async function webmToWav(webmBlob) {
const ctx = new OfflineAudioContext(1, 1, 16000);
const arrayBuf = await webmBlob.arrayBuffer();
let audioBuf;
try {
audioBuf = await ctx.decodeAudioData(arrayBuf);
} catch (e) {
// If decode fails, send webm as-is and hope Whisper accepts it
return webmBlob;
}
const numSamples = audioBuf.duration * 16000;
const offCtx = new OfflineAudioContext(1, numSamples, 16000);
const src = offCtx.createBufferSource();
src.buffer = audioBuf;
src.connect(offCtx.destination);
src.start();
const rendered = await offCtx.startRendering();
const pcm = rendered.getChannelData(0);
const wavBuf = encodeWav(pcm, 16000);
return new Blob([wavBuf], { type: 'audio/wav' });
}
function encodeWav(samples, sampleRate) {
const buf = new ArrayBuffer(44 + samples.length * 2);
const v = new DataView(buf);
const writeStr = (o, s) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); };
writeStr(0, 'RIFF');
v.setUint32(4, 36 + samples.length * 2, true);
writeStr(8, 'WAVE');
writeStr(12, 'fmt ');
v.setUint32(16, 16, true);
v.setUint16(20, 1, true);
v.setUint16(22, 1, true);
v.setUint32(24, sampleRate, true);
v.setUint32(28, sampleRate * 2, true);
v.setUint16(32, 2, true);
v.setUint16(34, 16, true);
writeStr(36, 'data');
v.setUint32(40, samples.length * 2, true);
for (let i = 0; i < samples.length; i++) {
const s = Math.max(-1, Math.min(1, samples[i]));
v.setInt16(44 + i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
return buf;
}
function setMicState(on) {
listening = on;
const btn = document.getElementById('mic-btn');
btn.className = 'mic-btn' + (on ? ' recording' : '');
}
// ─── SEARCH & FILTER ───
function filterSF() {
S.sfQuery = document.getElementById('sf-search').value.toLowerCase();
renderSF();
}
function filterCat(el) {
document.querySelectorAll('.cat-pill').forEach(p => p.classList.remove('active'));
el.classList.add('active');
S.sfCat = el.dataset.cat;
renderSF();
}
function getFilteredProducts() {
return S.products.filter(p => {
if (S.sfCat !== 'all' && p.cat !== S.sfCat) return false;
if (S.sfQuery) {
const hay = (p.name + ' ' + (p.desc||'') + ' ' + (p.aiDesc||'') + ' ' + (p.aiTags||[]).join(' ') + ' ' + (p.origin||'')).toLowerCase();
if (!hay.includes(S.sfQuery)) return false;
}
return true;
});
}
// ─── BUY FLOW ───
async function buyProduct(id) {
const p = S.products.find(x => x.id === id);
if (!p || p.stock <= 0) { toast('Out of stock','error'); return; }
p.stock--;
localStorage.setItem('storeos-products', JSON.stringify(S.products));
if (S.wallet) {
try {
const txData = ethers.toUtf8Bytes(JSON.stringify({
type:'STOREOS_SALE', store:S.cfg.name, productId:p.id, name:p.name, price:p.price, ts:new Date().toISOString()
}));
const tx = await S.wallet.sendTransaction({ to:S.wallet.address, value:0, data:ethers.hexlify(txData) });
await tx.wait();
addLedger('Sale Verified', p.name+' x1', tx.hash, 'ok');
toast('Sale registered on-chain!','success');
} catch(e) {
addLedger('Sale Verified', p.name+' x1', null, 'local');
toast('Sale recorded locally','info');
}
} else {
addLedger('Sale Verified', p.name+' x1', null, 'local');
toast('Sale recorded','success');
}
robotAction('happy');
render();
renderSF();
stats();
renderLedger();
}
// ─── UTILS ───
// ─── TTS (Kokoro open-source via HuggingFace) ───
let ttsOn = true;
let ttsAudio = null;
let ttsBusy = false;
const KOKORO_API = 'https://remsky-kokoro-tts-zero.hf.space/gradio_api';
async function speak(text) {
if (!ttsOn || !text || ttsBusy) return;
if (ttsAudio) { ttsAudio.pause(); ttsAudio = null; }
ttsBusy = true;
const voice = S.cfg.voice || 'af_heart';
const chunk = text.slice(0, 600);
try {
const callRes = await fetch(KOKORO_API + '/call/generate_speech_from_ui', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [chunk, [voice], 1.0] }),
});
if (!callRes.ok) throw new Error('Kokoro call failed: ' + callRes.status);
const { event_id } = await callRes.json();
const resultRes = await fetch(KOKORO_API + '/call/generate_speech_from_ui/' + event_id);
if (!resultRes.ok) throw new Error('Kokoro result failed');
const resultText = await resultRes.text();
const dataLine = resultText.split('\n').find(l => l.startsWith('data: '));
if (!dataLine) throw new Error('No data in Kokoro response');
const parsed = JSON.parse(dataLine.slice(6));
const audioUrl = parsed[0]?.url;
if (!audioUrl) throw new Error('No audio URL');
ttsAudio = new Audio(audioUrl);
ttsAudio.onended = () => { ttsAudio = null; };
await ttsAudio.play();
} catch(e) {
console.warn('Kokoro TTS failed:', e);
toast('Voice unavailable', 'error');
}
ttsBusy = false;
}
function toggleTTS() {
ttsOn = !ttsOn;
const btn = document.getElementById('tts-toggle');
btn.textContent = ttsOn ? '\u{1F50A}' : '\u{1F507}';
btn.title = ttsOn ? 'Voice on' : 'Voice off';
if (!ttsOn && ttsAudio) { ttsAudio.pause(); ttsAudio = null; }
toast(ttsOn ? 'Voice on' : 'Voice off', 'info');
}
function getReply(d) {
const msg = d.choices[0].message;
if (msg.content) return msg.content;
if (msg.reasoning) return msg.reasoning;
if (msg.reasoning_details) return msg.reasoning_details.map(r=>r.text).join('');
return 'No response';
}
function esc(s) { const d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
function fmtP(n) { return n?'&#8358;'+Number(n).toLocaleString():'TBD'; }
function fmtPRaw(n) { return n?'NGN '+Number(n).toLocaleString():'TBD'; }
function ago(iso) {
const m=Math.floor((Date.now()-new Date(iso).getTime())/60000);
if(m<1)return'Just now'; if(m<60)return m+'m ago';
const h=Math.floor(m/60); if(h<24)return h+'h ago';
return Math.floor(h/24)+'d ago';
}
// Expose to onclick handlers in dynamic HTML
window._buy = buyProduct;
window.launch = launch;
window.tab = tab;
window.openModal = openModal;
window.closeModal = closeModal;
window.onImg = onImg;
window.enrich = enrich;
window.saveProd = saveProd;
window.sendChat = sendChat;
window.sendCP = sendCP;
window.askCP = askCP;
window.toggleMic = toggleMic;
window.toggleTTS = toggleTTS;
window.toggleAdvanced = toggleAdvanced;
window.filterSF = filterSF;
window.filterCat = filterCat;
</script>
</body>
</html>