Spaces:
Running
Running
| <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% ; | |
| height: 100% ; | |
| 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">▶</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">🛒</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">🔊</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">🎤</button> | |
| <input id="cinput" placeholder="Ask about products..." onkeydown="if(event.key==='Enter')sendChat()" /> | |
| <button class="chat-send" onclick="sendChat()">➤</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">📷</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%">⚡ 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> ⚡ 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">`:'🛒'} | |
| ${p.txHash?'<div class="chain-tag">☑ 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">`:'🛒'} | |
| ${p.txHash?'<div class="chain-tag">☑ 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">📷</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">✅ 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">✅ TEE-verified · 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')?'📦':'💰'} ${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'?'✓ Confirmed':'◯ '+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?'₦'+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> | |