Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Oga Mi β Good Online AI Assistant</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| /* βββ DESIGN TOKENS (Oga Mi & Good Online Design System) βββ */ | |
| :root { | |
| --forest: #1F3B2D; | |
| --forest-hover: #16291F; | |
| --forest-soft: #2E5840; | |
| --terracotta: #C84B23; | |
| --terracotta-soft: #E0744F; | |
| --saffron: #E8B547; | |
| --saffron-soft: #F2D080; | |
| --cream: #F5EFE3; | |
| --cream-deep: #ECE3CF; | |
| --paper: #FBF7EE; | |
| --ink: #15130F; | |
| --ink-soft: #2A2620; | |
| --stone-1: #EAE3D2; | |
| --stone-2: #D9D2C0; | |
| --stone-3: #B8AD96; | |
| --stone-4: #8A8170; | |
| --stone-5: #6B6354; | |
| --fg-1: var(--ink); | |
| --fg-2: var(--stone-5); | |
| --fg-3: var(--stone-4); | |
| --fg-on-dark: var(--cream); | |
| --bg-1: var(--cream); | |
| --bg-2: var(--paper); | |
| --bg-3: var(--cream-deep); | |
| --border-1: var(--stone-2); | |
| --border-2: var(--ink); | |
| --accent: var(--forest); | |
| --accent-hover: var(--forest-hover); | |
| --warn: var(--terracotta); | |
| --highlight: var(--saffron); | |
| --font-serif: 'Instrument Serif', Georgia, serif; | |
| --font-sans: 'Geist', system-ui, sans-serif; | |
| --font-mono: 'Geist Mono', 'Courier New', monospace; | |
| --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; | |
| --sp-6: 24px; --sp-8: 32px; --sp-12: 48px; --sp-16: 64px; | |
| --radius-sm: 2px; | |
| --radius-md: 4px; | |
| --radius-pill: 999px; | |
| --radius-card: 0px; | |
| --shadow-1: 0 1px 2px rgba(21,19,15,0.06); | |
| --shadow-2: 0 8px 24px -8px rgba(21,19,15,0.12); | |
| --shadow-3: 0 24px 64px -16px rgba(21,19,15,0.22); | |
| --ease-default: cubic-bezier(.2,.7,.2,1); | |
| --dur-quick: 120ms; | |
| --dur-state: 200ms; | |
| --dur-image: 300ms; | |
| --dur-panel: 400ms; | |
| --content-max: 1280px; | |
| } | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { height: 100%; } | |
| body { | |
| font-family: var(--font-sans); | |
| background: var(--cream); | |
| color: var(--fg-1); | |
| height: 100vh; | |
| height: 100dvh; /* mobile-safe β accounts for browser chrome */ | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| /* βββ KIOSK SHELL (3-column) βββ */ | |
| .ogm-shell { | |
| display: grid; | |
| grid-template-columns: 360px 1fr 320px; | |
| grid-template-rows: auto 1fr; | |
| height: 100vh; | |
| height: 100dvh; /* mobile-safe β accounts for browser chrome */ | |
| max-width: var(--content-max); | |
| margin: 0 auto; | |
| opacity: 0; | |
| animation: fadeIn 0.6s var(--ease-default) 0.1s forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(4px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* βββ TOPBAR βββ */ | |
| .ogm-topbar { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px var(--sp-6); | |
| background: var(--paper); | |
| border-bottom: 1px solid var(--border-1); | |
| z-index: 10; | |
| } | |
| .ogm-brand { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-3); | |
| } | |
| .ogm-brand-name { | |
| font-family: var(--font-serif); | |
| font-size: 22px; | |
| font-style: italic; | |
| color: var(--fg-1); | |
| letter-spacing: -0.01em; | |
| } | |
| .ogm-brand-tag { | |
| font-family: var(--font-sans); | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| color: var(--fg-2); | |
| padding: 3px 10px; | |
| border: 1px solid var(--border-1); | |
| border-radius: var(--radius-sm); | |
| } | |
| .ogm-topbar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-4); | |
| } | |
| .ogm-services { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-2); | |
| } | |
| .ogm-chip { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-family: var(--font-sans); | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| padding: 3px 8px; | |
| border-radius: var(--radius-pill); | |
| opacity: 0.35; | |
| transition: opacity var(--dur-state); | |
| } | |
| .ogm-chip.active { opacity: 1; } | |
| .ogm-chip.compute { background: rgba(31,59,45,0.1); color: var(--forest); } | |
| .ogm-chip.storage { background: rgba(232,181,71,0.12); color: #8A6A1A; } | |
| .ogm-chip.chain { background: rgba(200,75,35,0.1); color: var(--terracotta); } | |
| .ogm-chip::before { | |
| content: ''; | |
| width: 5px; | |
| height: 5px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| } | |
| #status { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-family: var(--font-sans); | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: var(--fg-3); | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| #status::before { | |
| content: ''; | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--stone-3); | |
| transition: all var(--dur-state); | |
| } | |
| #status.connected { color: var(--forest); } | |
| #status.connected::before { | |
| background: var(--forest); | |
| box-shadow: 0 0 6px rgba(31,59,45,0.4); | |
| animation: pulse 2.4s ease-in-out infinite; | |
| } | |
| #status.error { color: var(--terracotta); } | |
| #status.error::before { background: var(--terracotta); } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.4; transform: scale(0.8); } | |
| } | |
| /* βββ STAGE (robot / 3D sim β left column) βββ */ | |
| .ogm-stage { | |
| background: var(--forest); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .ogm-stage::after { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: radial-gradient(ellipse at 50% 40%, rgba(232,181,71,0.08) 0%, transparent 60%); | |
| pointer-events: none; | |
| } | |
| #sim-container { | |
| flex: 1; | |
| min-height: 0; | |
| } | |
| .ogm-motion-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 10px 14px; | |
| background: rgba(0,0,0,0.15); | |
| overflow-x: auto; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .ogm-motion-label { | |
| font-family: var(--font-sans); | |
| font-size: 9px; | |
| font-weight: 500; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--stone-3); | |
| margin-right: 6px; | |
| flex-shrink: 0; | |
| } | |
| .ogm-motion-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 5px 10px; | |
| font-family: var(--font-sans); | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: var(--cream); | |
| background: rgba(255,255,255,0.08); | |
| border: 1px solid rgba(255,255,255,0.12); | |
| border-radius: var(--radius-pill); | |
| cursor: pointer; | |
| transition: all var(--dur-state) var(--ease-default); | |
| white-space: nowrap; | |
| } | |
| .ogm-motion-btn:hover { | |
| background: rgba(255,255,255,0.16); | |
| border-color: rgba(255,255,255,0.25); | |
| } | |
| .ogm-motion-btn:active { transform: scale(0.97); } | |
| .ogm-motion-btn svg { width: 12px; height: 12px; opacity: 0.7; } | |
| /* βββ CONVERSATION (center column) βββ */ | |
| .ogm-conversation { | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-1); | |
| border-left: 1px solid var(--border-1); | |
| border-right: 1px solid var(--border-1); | |
| overflow: hidden; | |
| } | |
| .ogm-conv-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px 20px; | |
| border-bottom: 1px solid var(--border-1); | |
| background: var(--paper); | |
| } | |
| .ogm-conv-header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-2); | |
| } | |
| .ogm-conv-header-left h2 { | |
| font-family: var(--font-serif); | |
| font-style: italic; | |
| font-size: 18px; | |
| font-weight: 400; | |
| color: var(--fg-1); | |
| } | |
| #customer-badge { | |
| font-family: var(--font-sans); | |
| font-size: 10px; | |
| font-weight: 500; | |
| padding: 2px 8px; | |
| border-radius: var(--radius-pill); | |
| background: rgba(31,59,45,0.08); | |
| color: var(--forest); | |
| display: none; | |
| } | |
| #receipt-badge { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| padding: 2px 8px; | |
| border-radius: var(--radius-sm); | |
| background: rgba(200,75,35,0.08); | |
| color: var(--terracotta); | |
| display: none; | |
| cursor: pointer; | |
| text-decoration: none; | |
| } | |
| #receipt-badge:hover { background: rgba(200,75,35,0.14); } | |
| /* ββ Messages ββ */ | |
| #transcript { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--stone-2) transparent; | |
| } | |
| #transcript::-webkit-scrollbar { width: 4px; } | |
| #transcript::-webkit-scrollbar-thumb { background: var(--stone-2); border-radius: 4px; } | |
| .msg { | |
| max-width: 85%; | |
| padding: 10px 16px; | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| line-height: 1.55; | |
| word-break: break-word; | |
| animation: msgIn 0.35s var(--ease-default) both; | |
| } | |
| @keyframes msgIn { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .msg.user { | |
| align-self: flex-end; | |
| background: var(--forest); | |
| color: var(--cream); | |
| border-radius: var(--radius-pill); | |
| border-bottom-right-radius: var(--radius-sm); | |
| } | |
| .msg.bot { | |
| align-self: flex-start; | |
| background: var(--paper); | |
| color: var(--fg-1); | |
| border: 1px solid var(--border-1); | |
| border-radius: var(--radius-pill); | |
| border-bottom-left-radius: var(--radius-sm); | |
| } | |
| .msg.system { | |
| align-self: center; | |
| background: transparent; | |
| color: var(--fg-3); | |
| font-size: 12px; | |
| padding: 4px 0; | |
| } | |
| .msg.chain-receipt { | |
| align-self: center; | |
| background: var(--ink); | |
| color: var(--cream); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 8px 14px; | |
| border-radius: var(--radius-sm); | |
| border: none; | |
| } | |
| .msg.chain-receipt::before { | |
| content: 'β '; | |
| } | |
| .msg.storage-event { | |
| align-self: center; | |
| background: rgba(232,181,71,0.08); | |
| border: 1px solid rgba(232,181,71,0.2); | |
| color: #8A6A1A; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 6px 12px; | |
| border-radius: var(--radius-sm); | |
| } | |
| /* βββ INPUT βββ */ | |
| .ogm-input-area { | |
| padding: 14px 20px; | |
| border-top: 1px solid var(--border-1); | |
| background: var(--paper); | |
| } | |
| .ogm-input-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--bg-1); | |
| border: 1px solid var(--border-1); | |
| border-radius: var(--radius-pill); | |
| padding: 4px 4px 4px 18px; | |
| transition: border-color var(--dur-state), box-shadow var(--dur-state); | |
| } | |
| .ogm-input-row:focus-within { | |
| border-color: var(--forest); | |
| box-shadow: 0 0 0 3px rgba(31,59,45,0.08); | |
| } | |
| #chat-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--fg-1); | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| padding: 8px 0; | |
| min-width: 0; | |
| } | |
| #chat-input::placeholder { color: var(--fg-3); } | |
| #mic-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| border: none; | |
| background: var(--bg-3); | |
| color: var(--fg-2); | |
| cursor: pointer; | |
| transition: all var(--dur-state); | |
| flex-shrink: 0; | |
| } | |
| #mic-btn:hover { background: var(--stone-1); color: var(--fg-1); } | |
| .ogm-rec-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| background: var(--terracotta); | |
| border-radius: var(--radius-pill); | |
| padding: 6px 6px 6px 16px; | |
| animation: recIn 0.3s var(--ease-default); | |
| } | |
| @keyframes recIn { | |
| from { opacity: 0; transform: scale(0.97); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| .ogm-rec-info { display: flex; align-items: center; gap: 8px; } | |
| .ogm-rec-dot { | |
| width: 8px; height: 8px; border-radius: 50%; background: var(--cream); | |
| animation: recBlink 1s ease-in-out infinite; | |
| } | |
| @keyframes recBlink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } | |
| .ogm-rec-timer { | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--cream); | |
| } | |
| #rec-waveform { flex: 1; height: 32px; min-width: 0; opacity: 0.7; } | |
| .ogm-rec-stop { | |
| display: flex; align-items: center; justify-content: center; | |
| width: 38px; height: 38px; border-radius: 50%; border: none; | |
| background: var(--cream); color: var(--terracotta); cursor: pointer; | |
| transition: all var(--dur-quick); flex-shrink: 0; | |
| } | |
| .ogm-rec-stop:hover { transform: scale(1.06); } | |
| #send-btn { | |
| display: flex; align-items: center; justify-content: center; | |
| width: 38px; height: 38px; border-radius: 50%; border: none; | |
| background: var(--forest); color: var(--cream); cursor: pointer; | |
| transition: all var(--dur-quick); flex-shrink: 0; | |
| } | |
| #send-btn:hover { background: var(--forest-hover); transform: scale(1.04); } | |
| #send-btn:active { transform: scale(0.97); } | |
| #send-btn:disabled { opacity: 0.3; cursor: default; transform: none; } | |
| #send-btn svg { width: 16px; height: 16px; } | |
| .ogm-powered { | |
| padding: 6px 0 0; | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--fg-3); | |
| text-align: center; | |
| display: none; | |
| } | |
| .ogm-powered.visible { display: block; } | |
| /* βββ SIDE PANEL (right column) βββ */ | |
| .ogm-side { | |
| background: var(--paper); | |
| padding: var(--sp-6); | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--sp-6); | |
| } | |
| .ogm-side-eyebrow { | |
| font-family: var(--font-sans); | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| color: var(--fg-3); | |
| } | |
| .ogm-side-title { | |
| font-family: var(--font-serif); | |
| font-style: italic; | |
| font-size: 24px; | |
| line-height: 28px; | |
| color: var(--fg-1); | |
| margin-top: var(--sp-2); | |
| } | |
| .ogm-step { | |
| display: flex; | |
| gap: var(--sp-3); | |
| } | |
| .ogm-step-num { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: var(--fg-3); | |
| flex-shrink: 0; | |
| width: 20px; | |
| text-align: right; | |
| } | |
| .ogm-step-body h4 { | |
| font-family: var(--font-sans); | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--fg-1); | |
| margin-bottom: 2px; | |
| } | |
| .ogm-step-body p { | |
| font-family: var(--font-sans); | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: var(--fg-2); | |
| } | |
| /* βββ SETUP OVERLAY βββ */ | |
| #setup { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 1000; | |
| display: flex; | |
| background: rgba(21,19,15,0.5); | |
| backdrop-filter: blur(20px); | |
| animation: overlayIn 0.4s var(--ease-default); | |
| overflow-y: auto; | |
| padding: var(--sp-6) var(--sp-4); | |
| } | |
| #setup.hidden { display: none; } | |
| @keyframes overlayIn { from { opacity: 0; } to { opacity: 1; } } | |
| .setup-card { | |
| width: 100%; | |
| max-width: 460px; | |
| min-width: 0; /* allow shrink below select option intrinsic width on narrow screens */ | |
| margin: auto; /* centers when shorter than viewport, scrolls when taller */ | |
| background: var(--paper); | |
| border-radius: var(--radius-md); | |
| padding: 40px 36px 32px; | |
| animation: cardIn 0.5s var(--ease-default) 0.15s both; | |
| box-shadow: var(--shadow-3); | |
| } | |
| @keyframes cardIn { | |
| from { opacity: 0; transform: translateY(16px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .setup-card h2 { | |
| font-family: var(--font-serif); | |
| font-style: italic; | |
| font-size: 36px; | |
| font-weight: 400; | |
| color: var(--fg-1); | |
| margin-bottom: 4px; | |
| } | |
| .setup-card .setup-subtitle { | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| color: var(--fg-2); | |
| line-height: 1.5; | |
| margin-bottom: 28px; | |
| } | |
| .setup-card .setup-subtitle a { | |
| color: var(--forest); | |
| text-decoration: none; | |
| border-bottom: 1px solid transparent; | |
| } | |
| .setup-card .setup-subtitle a:hover { border-bottom-color: var(--forest); } | |
| .field-group { margin-bottom: 16px; } | |
| .field-group label { | |
| display: block; | |
| font-family: var(--font-sans); | |
| font-size: 11px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| color: var(--fg-3); | |
| margin-bottom: 6px; | |
| } | |
| .field-group input, | |
| .field-group select { | |
| width: 100%; | |
| padding: 11px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| color: var(--fg-1); | |
| background: var(--bg-1); | |
| border: 1px solid var(--border-1); | |
| border-radius: var(--radius-sm); | |
| outline: none; | |
| transition: border-color var(--dur-state), box-shadow var(--dur-state); | |
| -webkit-appearance: none; | |
| } | |
| .field-group input:focus, .field-group select:focus { | |
| border-color: var(--forest); | |
| box-shadow: 0 0 0 3px rgba(31,59,45,0.08); | |
| } | |
| .field-group input::placeholder { color: var(--fg-3); font-family: var(--font-mono); } | |
| .field-group .field-hint { font-family: var(--font-sans); font-size: 11px; color: var(--fg-3); margin-top: 4px; } | |
| .field-group .field-hint a { color: var(--forest); text-decoration: none; } | |
| .field-group select { | |
| cursor: pointer; | |
| min-width: 0; | |
| text-overflow: ellipsis; | |
| background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%238A8170' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; | |
| background-position: right 12px center; | |
| padding-right: 34px; | |
| } | |
| .advanced-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-family: var(--font-sans); | |
| font-size: 11px; | |
| color: var(--fg-3); | |
| cursor: pointer; | |
| margin: 12px 0 10px; | |
| padding: 0; | |
| border: none; | |
| background: none; | |
| transition: color var(--dur-state); | |
| } | |
| .advanced-toggle:hover { color: var(--fg-2); } | |
| .advanced-toggle svg { width: 12px; height: 12px; transition: transform var(--dur-state); } | |
| .advanced-toggle.open svg { transform: rotate(90deg); } | |
| .advanced-fields { display: none; } | |
| .advanced-fields.open { display: block; } | |
| .setup-start { | |
| width: 100%; | |
| padding: 14px; | |
| margin-top: 24px; | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--cream); | |
| background: var(--forest); | |
| border: none; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: background var(--dur-state); | |
| } | |
| .setup-start:hover { background: var(--forest-hover); } | |
| .setup-start:active { transform: scale(0.99); } | |
| #cfg-err { | |
| color: var(--terracotta); | |
| font-family: var(--font-sans); | |
| font-size: 12px; | |
| margin-top: 8px; | |
| display: none; | |
| } | |
| #remoteVideo { display: none; } | |
| /* βββ RESPONSIVE βββ */ | |
| @media (max-width: 1100px) { | |
| .ogm-shell { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 220px 1fr; | |
| } | |
| .ogm-stage { min-height: 0; } | |
| .ogm-conversation { border-left: none; border-right: none; border-top: 1px solid var(--border-1); } | |
| .ogm-side { display: none; } | |
| .ogm-services { display: none; } | |
| } | |
| @media (max-width: 480px) { | |
| #setup { padding: var(--sp-3); } | |
| .setup-card { padding: 28px 20px 24px; } | |
| .setup-card h2 { font-size: 30px; } | |
| .setup-card .setup-subtitle { margin-bottom: 20px; } | |
| .ogm-shell { grid-template-rows: auto 180px 1fr; } | |
| .ogm-topbar { padding: 10px var(--sp-4); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- βββ SETUP βββ --> | |
| <div id="setup"> | |
| <div class="setup-card"> | |
| <h2>Oga Mi</h2> | |
| <p class="setup-subtitle">AI Shopping Assistant for <a href="https://good.online" target="_blank">Good Online</a>. Paste your 0G API key to start. Get one from <a href="https://pc.0g.ai" target="_blank">pc.0g.ai</a></p> | |
| <div class="field-group"> | |
| <label>Your Name</label> | |
| <input id="cfg-name" type="text" placeholder="What should Oga Mi call you?" spellcheck="false" /> | |
| </div> | |
| <div class="field-group"> | |
| <label>Chat API Key</label> | |
| <input id="cfg-chat-key" type="password" placeholder="app-sk-..." spellcheck="false" /> | |
| <div class="field-hint"><a href="https://pc.0g.ai" target="_blank">pc.0g.ai</a> β Advanced mode β fund provider β API Reference β mint key</div> | |
| </div> | |
| <div class="field-group"> | |
| <label>Chat Endpoint</label> | |
| <select id="cfg-chat-url"> | |
| <option value="https://compute-network-1.integratenetwork.work/v1/proxy/chat/completions">compute-network-1 (GLM-5, DeepSeek, Qwen)</option> | |
| <option value="https://compute-network-2.integratenetwork.work/v1/proxy/chat/completions">compute-network-2</option> | |
| <option value="https://compute-network-3.integratenetwork.work/v1/proxy/chat/completions">compute-network-3</option> | |
| </select> | |
| </div> | |
| <div class="field-group"> | |
| <label>Model</label> | |
| <select id="cfg-model"> | |
| <option value="zai-org/GLM-5-FP8">GLM-5 FP8 (recommended)</option> | |
| <option value="deepseek/deepseek-chat-v3-0324">DeepSeek Chat v3</option> | |
| <option value="qwen3.6-plus">Qwen 3.6 Plus</option> | |
| </select> | |
| </div> | |
| <div class="field-group"> | |
| <label>Robot</label> | |
| <select id="cfg-robot"> | |
| <option value="sim">3D Simulator</option> | |
| <option value="live">Live Robot (WebRTC)</option> | |
| </select> | |
| </div> | |
| <button class="advanced-toggle" type="button" onclick="this.classList.toggle('open'); document.getElementById('adv-fields').classList.toggle('open');"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M9 6l6 6-6 6"/></svg> | |
| Voice, Chain, TEE | |
| </button> | |
| <div id="adv-fields" class="advanced-fields"> | |
| <div class="field-group"> | |
| <label>Whisper API Key (enables voice input)</label> | |
| <input id="cfg-stt-key" type="password" placeholder="app-sk-... (different key for whisper provider)" /> | |
| <div class="field-hint">Fund <code>openai/whisper-large-v3</code> separately, then mint its key</div> | |
| </div> | |
| <div class="field-group"> | |
| <label>Whisper Endpoint</label> | |
| <select id="cfg-stt-url"> | |
| <option value="https://compute-network-16.integratenetwork.work/v1/proxy/audio/transcriptions">compute-network-16 (Whisper)</option> | |
| </select> | |
| </div> | |
| <div class="field-group"> | |
| <label>0G Chain Private Key (optional)</label> | |
| <input id="cfg-chain-key" type="password" placeholder="0x... (testnet private key)" /> | |
| <div class="field-hint">Enables on-chain receipts. Free tokens at <a href="https://faucet.0g.ai" target="_blank">faucet.0g.ai</a></div> | |
| </div> | |
| </div> | |
| <div id="cfg-err"></div> | |
| <button class="setup-start" onclick="startApp()">Start Oga Mi</button> | |
| </div> | |
| </div> | |
| <!-- βββ KIOSK SHELL βββ --> | |
| <div class="ogm-shell"> | |
| <!-- Topbar --> | |
| <div class="ogm-topbar"> | |
| <div class="ogm-brand"> | |
| <svg width="28" height="28" viewBox="0 0 96 96" fill="none"> | |
| <rect x="14" y="30" width="68" height="56" rx="6" fill="#1F3B2D"/> | |
| <circle cx="36" cy="58" r="6" fill="#F5EFE3"/> | |
| <circle cx="60" cy="58" r="6" fill="#F5EFE3"/> | |
| <line x1="34" y1="30" x2="34" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/> | |
| <line x1="62" y1="30" x2="62" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/> | |
| <circle cx="34" cy="12" r="3.5" fill="#C84B23"/> | |
| <circle cx="62" cy="12" r="3.5" fill="#E8B547"/> | |
| </svg> | |
| <span class="ogm-brand-name">Oga Mi</span> | |
| <span class="ogm-brand-tag">Good Online</span> | |
| </div> | |
| <div class="ogm-topbar-right"> | |
| <div class="ogm-services"> | |
| <span class="ogm-chip compute" id="chip-compute">Compute</span> | |
| <span class="ogm-chip storage" id="chip-storage">Storage</span> | |
| <span class="ogm-chip chain" id="chip-chain">Chain</span> | |
| </div> | |
| <div id="status">offline</div> | |
| </div> | |
| </div> | |
| <!-- Stage (robot / sim) --> | |
| <div class="ogm-stage"> | |
| <div id="sim-container"></div> | |
| <div class="ogm-motion-bar"> | |
| <span class="ogm-motion-label">Express</span> | |
| <button class="ogm-motion-btn" onclick="doWave()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 11V4a2 2 0 0 1 4 0v3m0 0V3a2 2 0 0 1 4 0v4m0 0V5a2 2 0 0 1 4 0v7"/></svg>Wave</button> | |
| <button class="ogm-motion-btn" onclick="doNod()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14m0-14l-4 4m4-4l4 4"/></svg>Nod</button> | |
| <button class="ogm-motion-btn" onclick="doShake()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M5 12l4-4m-4 4l4 4m10-4l-4-4m4 4l-4 4"/></svg>Shake</button> | |
| <button class="ogm-motion-btn" onclick="doDance()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm12-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>Dance</button> | |
| <button class="ogm-motion-btn" onclick="doExcited()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>Excited</button> | |
| <button class="ogm-motion-btn" onclick="doThinking()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Think</button> | |
| </div> | |
| </div> | |
| <!-- Conversation --> | |
| <div class="ogm-conversation"> | |
| <div class="ogm-conv-header"> | |
| <div class="ogm-conv-header-left"> | |
| <h2>Conversation</h2> | |
| <span id="customer-badge"></span> | |
| </div> | |
| <a id="receipt-badge" href="#" target="_blank"></a> | |
| </div> | |
| <div id="transcript"></div> | |
| <div class="ogm-input-area"> | |
| <div class="ogm-input-row" id="input-default"> | |
| <input id="chat-input" placeholder="Ask Oga Mi about Good products..." autocomplete="off" /> | |
| <button id="mic-btn" title="Tap to record"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> | |
| <rect x="9" y="2" width="6" height="11" rx="3"/> | |
| <path d="M5 10a7 7 0 0 0 14 0"/> | |
| <line x1="12" y1="17" x2="12" y2="21"/> | |
| </svg> | |
| </button> | |
| <button id="send-btn" onclick="sendMessage()"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 12h14m-6-6 6 6-6 6"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="ogm-rec-row" id="input-recording" style="display:none;"> | |
| <div class="ogm-rec-info"> | |
| <span class="ogm-rec-dot"></span> | |
| <span class="ogm-rec-timer" id="rec-timer">0:00</span> | |
| </div> | |
| <canvas id="rec-waveform" height="32"></canvas> | |
| <button class="ogm-rec-stop" id="rec-stop-btn" title="Send recording"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 12h14m-6-6 6 6-6 6"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div id="powered-by" class="ogm-powered"></div> | |
| </div> | |
| </div> | |
| <!-- Side panel --> | |
| <div class="ogm-side"> | |
| <div> | |
| <div class="ogm-side-eyebrow">Behind the kiosk Β· 0G stack</div> | |
| <div class="ogm-side-title">Speech to recommendation to receipt.</div> | |
| </div> | |
| <div class="ogm-step"> | |
| <span class="ogm-step-num">01</span> | |
| <div class="ogm-step-body"> | |
| <h4>Listen</h4> | |
| <p>Whisper-large-v3 transcribes speech on 0G Compute. Verifiable via TEE.</p> | |
| </div> | |
| </div> | |
| <div class="ogm-step"> | |
| <span class="ogm-step-num">02</span> | |
| <div class="ogm-step-body"> | |
| <h4>Remember</h4> | |
| <p>0G Storage fetches your customer profile and past interactions.</p> | |
| </div> | |
| </div> | |
| <div class="ogm-step"> | |
| <span class="ogm-step-num">03</span> | |
| <div class="ogm-step-body"> | |
| <h4>Recommend</h4> | |
| <p>GLM-5 on 0G Compute recommends from the Good Online catalog.</p> | |
| </div> | |
| </div> | |
| <div class="ogm-step"> | |
| <span class="ogm-step-num">04</span> | |
| <div class="ogm-step-body"> | |
| <h4>Log on-chain</h4> | |
| <p>Interaction receipt minted to 0G Chain. Verifiable, immutable.</p> | |
| </div> | |
| </div> | |
| <div style="margin-top: auto; padding-top: var(--sp-6); border-top: 1px solid var(--border-1);"> | |
| <div class="ogm-side-eyebrow">Good House Β· Ikeja GRA</div> | |
| <p style="font-family: var(--font-sans); font-size: 12px; color: var(--fg-2); margin-top: 4px; line-height: 1.5;">19B Sasegbon Street, Ikeja GRA, Lagos. Come say hello to Oga Mi.</p> | |
| <p style="font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); margin-top: 8px;">good.online Β· support@goodthingco.xyz</p> | |
| </div> | |
| </div> | |
| </div> | |
| <video id="remoteVideo" autoplay playsinline></video> | |
| <!-- 3D Sim --> | |
| <script type="module"> | |
| import { ReachySim } from './sim.js'; | |
| window.ReachySim = ReachySim; | |
| </script> | |
| <!-- SDK --> | |
| <script type="module"> | |
| import { ReachyMini } from 'https://cdn.jsdelivr.net/gh/pollen-robotics/reachy_mini@v1.7.1/js/reachy-mini.js'; | |
| window.ReachyMini = ReachyMini; | |
| </script> | |
| <!-- ethers.js for 0G Chain --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.4/ethers.umd.min.js"></script> | |
| <script> | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GOOD ONLINE KNOWLEDGE BASE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const GOOD_CATALOG = ` | |
| ## GOOD ONLINE PRODUCT CATALOG (good.online) | |
| "Made by Nigerians. Curated for you." β A marketplace connecting you with Nigerian creators committed to quality and positive impact. Delivers within Lagos only. | |
| ### FEATURED PRODUCTS | |
| - Coconut Rice (430g) by Chumbys β N5,400 [Cook it Yourself] | |
| - Glow Oil by Arami β N13,600 [Beauty] | |
| - Whole Cashew Jar by The M & W Nuts Company β N11,875 [Snacks] | |
| - Gym Socks Box by Smileys Africa β N17,500 [Fashion] | |
| - Miniature Trio by Dala Craft β N54,000 [Home & Living] | |
| - Sugar Wax Kit by Sugar Wax By Audrey β N17,000 [Beauty] | |
| - Dudu Ball Chain by ITURA Jewelry β N40,000 [Fashion/Jewelry] | |
| - Mini Scented Candle Gift Set by Jacque Lagos β N70,000 [Gifts/Home] | |
| - Plantain Clusters by Good Originals β N5,520 [Snacks] | |
| ### CATEGORIES | |
| Fashion, Beauty, Home & Living, Wellness, Gifts, Leisure, Good Originals, Snacks, Drinks, Cook it Yourself, Worklife, For Moms, For Women | |
| ### CURATED LISTS (with starting prices) | |
| - "For Everyone Carrying This Economy on Their Back" β from N3,000 | |
| - "A Good Place to Start" β from N500 (great for budget shoppers!) | |
| - "Something to Add to Your Skincare Routine" β from N4,000 | |
| - "Some Thoughtful Gifts for Everyone" β from N13,000 | |
| - "The Faster Way to Familiar Dishes" β from N2,250 | |
| - "Everyday Comforts That Make Life Feel Lighter" β from N7,500 | |
| - "For the Kids We're All Soft For" β from N5,160 | |
| - "Cyn Ugwu's Picks to Living a Good Life" β from N4,200 | |
| - "Travel Essentials That Actually Come in Handy" β from N4,500 | |
| ### TOP CREATORS (50+) | |
| Fashion: Smileys Africa, TETE AFRICA, Garmisland, Asabithebrand, AZACH, Claic Design, Captain Atelier, Marte Egele, Sarima By Ama, TwentySix, Crivel, OhMyCowries, Floy NG, Rade Label, 1403 Luxury, Lano Lagos, THESHOEBLOCC, Outlash, Vinc & Eliz, ITURA Jewelry, KunbiWorks | |
| Beauty: Arami, Tamed Lux, Infused Organics, ESTEVAN PERFUMERY, Sugar Wax By Audrey, Sublime Skin by Jumai, Msmetics, Viektors Bath and Body, TGIN, Onoh Naturals | |
| Home & Living: Jacque Lagos, Wallinggs Interior, Silkluxe Hub, Oeuvre Designs, Alore Lifestyle, Ediye Home Scents, Iluna Candles & Co, Hingees, The DA Brand, Scent L'avie | |
| Wellness: Soft Mum Era, Enjoca, Olile Enterprises, Clove and Nectar, Just Journals | |
| Snacks & Food: Chumbys, The M & W Nuts Company, Good Originals | |
| Gifts: The Kulture Company, Not Just Pulp, FCAL (FC Accessories), The Ajala Game Store | |
| Leisure: Atito, The Ajala Game Store | |
| ### STORE INFO | |
| - Website: good.online | |
| - Good House: 19B Sasegbon Street, Ikeja GRA, Lagos | |
| - Good Village: Plot 33 Block 15, Admiralty Way, Lekki Phase 1, Lagos | |
| - Contact: +2348142511154 / support@goodthingco.xyz | |
| - Delivery: Lagos only | |
| - Gift vouchers available at good.online | |
| `; | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // OGA MI SYSTEM PROMPT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildSystemPrompt(customerName, customerHistory) { | |
| let prompt = `You are Oga Mi, a warm and friendly AI shopping assistant robot at Good House (19B Sasegbon St, Ikeja GRA, Lagos) β the physical home of Good Online (good.online), Nigeria's curated marketplace for local creators. | |
| PERSONALITY: | |
| - Warm and welcoming, like a helpful market seller who genuinely cares | |
| - Mix English with light Pidgin/Yoruba naturally ("Oga mi!", "E kaaro!", "Na good choice!", "Ah, this one sweet well well!") | |
| - Enthusiastic about Nigerian creators and their craft | |
| - Never pushy β recommend, don't hard-sell | |
| - Keep responses to 2-4 sentences. Recommend 1-2 products per response, not a long list | |
| - Use Naira prices (write as N5,400 not β¦5,400) | |
| - Always finish your sentences β never leave a response incomplete | |
| ${GOOD_CATALOG} | |
| WHEN RECOMMENDING: | |
| - Always mention the price in Naira | |
| - Always name the creator ("made by [Creator]") | |
| - Suggest from curated lists when the question matches | |
| - Budget shoppers: start with "A Good Place to Start" (from N500) | |
| - Gift seekers: "Some Thoughtful Gifts for Everyone" (from N13,000) | |
| - Skincare: "Something to Add to Your Skincare Routine" (from N4,000) | |
| - If you don't know a specific product detail, say so honestly and suggest visiting good.online | |
| PHYSICAL ACTIONS (include ONE at END of message when natural): | |
| [ACTION:wave] - greeting customers | |
| [ACTION:nod] - confirming a recommendation or agreement | |
| [ACTION:shake] - gentle disagreement or "hmm, not quite" | |
| [ACTION:dance] - excitement about a great product or happy moment | |
| [ACTION:excited] - enthusiasm when suggesting something perfect | |
| [ACTION:thinking] - pondering what to recommend | |
| [ACTION:sleep] - saying goodbye | |
| Use actions when they fit β don't force them into every message.`; | |
| if (customerName) { | |
| prompt += `\n\nCUSTOMER: The person talking to you is named "${customerName}". Use their name naturally in conversation (not every message).`; | |
| } | |
| if (customerHistory && customerHistory.length > 0) { | |
| prompt += `\n\nPREVIOUS INTERACTIONS:\n${customerHistory.map(h => `- ${h}`).join('\n')}\nReference past interactions naturally when relevant ("I remember you liked..." or "Last time you asked about...").`; | |
| } | |
| return prompt; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CONFIG & STATE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let CFG = {}; | |
| let robot = null; | |
| let isLive = false; | |
| let chainProvider = null; | |
| let chainWallet = null; | |
| let receiptContract = null; | |
| let customerMemory = {}; | |
| let interactionCount = 0; | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SETUP | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| (function restoreSettings() { | |
| const saved = sessionStorage.getItem('ogami-cfg'); | |
| if (!saved) return; | |
| try { | |
| const s = JSON.parse(saved); | |
| if (s.name) document.getElementById('cfg-name').value = s.name; | |
| if (s.chatKey) document.getElementById('cfg-chat-key').value = s.chatKey; | |
| if (s.model) { | |
| const sel = document.getElementById('cfg-model'); | |
| for (let i = 0; i < sel.options.length; i++) { | |
| if (sel.options[i].value === s.model) { sel.selectedIndex = i; break; } | |
| } | |
| } | |
| if (s.chatUrl) { | |
| const urlSel = document.getElementById('cfg-chat-url'); | |
| for (let i = 0; i < urlSel.options.length; i++) { | |
| if (urlSel.options[i].value === s.chatUrl) { urlSel.selectedIndex = i; break; } | |
| } | |
| } | |
| if (s.mode) document.getElementById('cfg-robot').value = s.mode; | |
| if (s.sttKey) document.getElementById('cfg-stt-key').value = s.sttKey; | |
| if (s.chainKey) document.getElementById('cfg-chain-key').value = s.chainKey; | |
| } catch (_) {} | |
| })(); | |
| try { | |
| const mem = localStorage.getItem('ogami-customers'); | |
| if (mem) customerMemory = JSON.parse(mem); | |
| } catch (_) {} | |
| function saveCustomerMemory() { | |
| localStorage.setItem('ogami-customers', JSON.stringify(customerMemory)); | |
| document.getElementById('chip-storage').classList.add('active'); | |
| } | |
| window.startApp = async function () { | |
| const chatKey = document.getElementById('cfg-chat-key').value.trim(); | |
| if (!chatKey) return showErr('Chat API key is required (app-sk-...).'); | |
| const model = document.getElementById('cfg-model').value; | |
| const customerName = document.getElementById('cfg-name').value.trim() || 'Friend'; | |
| const sttKey = document.getElementById('cfg-stt-key')?.value.trim() || ''; | |
| const chainKey = document.getElementById('cfg-chain-key')?.value.trim() || ''; | |
| CFG = { | |
| chatKey, | |
| chatUrl: document.getElementById('cfg-chat-url').value, | |
| sttKey, | |
| sttUrl: document.getElementById('cfg-stt-url')?.value || 'https://compute-network-16.integratenetwork.work/v1/proxy/audio/transcriptions', | |
| model, | |
| customerName, | |
| chainKey, | |
| mode: document.getElementById('cfg-robot').value, | |
| }; | |
| setStatus('verifying...', false); | |
| try { | |
| const r = await fetch(CFG.chatUrl, { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${CFG.chatKey}`, 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model: CFG.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 3 }), | |
| }); | |
| if (!r.ok) throw new Error(`${r.status}: ${(await r.text()).slice(0, 120)}`); | |
| } catch (e) { | |
| return showErr('Key verification failed β ' + e.message); | |
| } | |
| sessionStorage.setItem('ogami-cfg', JSON.stringify(CFG)); | |
| document.getElementById('setup').classList.add('hidden'); | |
| document.getElementById('chip-compute').classList.add('active'); | |
| if (!customerMemory[CFG.customerName]) { | |
| customerMemory[CFG.customerName] = { visits: 0, interactions: [], productsAsked: [] }; | |
| } | |
| customerMemory[CFG.customerName].visits++; | |
| customerMemory[CFG.customerName].lastVisit = new Date().toISOString(); | |
| saveCustomerMemory(); | |
| const custBadge = document.getElementById('customer-badge'); | |
| custBadge.textContent = CFG.customerName; | |
| custBadge.style.display = ''; | |
| const history = customerMemory[CFG.customerName].interactions.slice(-5); | |
| conversationHistory.length = 0; | |
| conversationHistory.push({ role: 'system', content: buildSystemPrompt(CFG.customerName, history) }); | |
| await initRobot(); | |
| const visits = customerMemory[CFG.customerName].visits; | |
| if (visits > 1) { | |
| addMsg('system', `0G Storage: Returning customer detected (visit #${visits})`); | |
| document.getElementById('chip-storage').classList.add('active'); | |
| } | |
| setStatus('connected', true); | |
| const pb = document.getElementById('powered-by'); | |
| pb.textContent = `0G Compute Β· ${CFG.model}`; | |
| pb.classList.add('visible'); | |
| if (CFG.chainKey) initChain(); | |
| const welcomePrompt = visits > 1 | |
| ? `The customer "${CFG.customerName}" just returned (visit #${visits}). Welcome them back warmly and ask what they're looking for today.` | |
| : `A new customer named "${CFG.customerName}" just walked up. Greet them warmly, introduce yourself as Oga Mi, and ask how you can help them shop Good Online today.`; | |
| sending = true; | |
| document.getElementById('send-btn').disabled = true; | |
| animateThinking(); | |
| try { | |
| const raw = await askAI(welcomePrompt, { isInternal: true }); | |
| const actionMatch = raw.match(/\[ACTION:(wave|nod|shake|dance|sleep|wake|excited|thinking)\]/i); | |
| const cleanReply = raw.replace(/\[ACTION:\w+\]/gi, '').trim(); | |
| addMsg('bot', cleanReply); | |
| animateSpeaking(); | |
| const speakDone = speak(cleanReply); | |
| if (actionMatch) fireAction(actionMatch[1].toLowerCase()); | |
| else doWave(); | |
| await speakDone; | |
| animateIdle(); | |
| } catch (e) { | |
| addMsg('bot', `Welcome to Good Online! I'm Oga Mi, your shopping assistant. How can I help you today, ${CFG.customerName}?`); | |
| doWave(); | |
| } | |
| sending = false; | |
| document.getElementById('send-btn').disabled = false; | |
| }; | |
| function showErr(msg) { | |
| const el = document.getElementById('cfg-err'); | |
| el.textContent = msg; el.style.display = 'block'; | |
| setStatus('error'); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 0G CHAIN | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const RECEIPT_ABI = [ | |
| 'function logInteraction(string customerName, string query, string recommendation, uint256 timestamp) external', | |
| 'function getInteractionCount() external view returns (uint256)', | |
| 'event InteractionLogged(uint256 indexed id, string customerName, string query, uint256 timestamp)' | |
| ]; | |
| async function initChain() { | |
| try { | |
| chainProvider = new ethers.JsonRpcProvider('https://evmrpc-testnet.0g.ai'); | |
| chainWallet = new ethers.Wallet(CFG.chainKey, chainProvider); | |
| const balance = await chainProvider.getBalance(chainWallet.address); | |
| if (balance > 0n) { | |
| document.getElementById('chip-chain').classList.add('active'); | |
| addMsg('system', `0G Chain: ${chainWallet.address.slice(0, 8)}...${chainWallet.address.slice(-4)}`); | |
| } else { | |
| addMsg('system', '0G Chain: No balance β faucet.0g.ai'); | |
| } | |
| } catch (e) { | |
| addMsg('system', '0G Chain: ' + e.message); | |
| } | |
| } | |
| async function logOnChain(customerName, query, recommendation) { | |
| if (!chainWallet) return null; | |
| try { | |
| const data = ethers.AbiCoder.defaultAbiCoder().encode( | |
| ['string', 'string', 'string', 'uint256'], | |
| [customerName, query.slice(0, 100), recommendation.slice(0, 200), Math.floor(Date.now() / 1000)] | |
| ); | |
| const tx = await chainWallet.sendTransaction({ to: chainWallet.address, value: 0n, data }); | |
| const receipt = await tx.wait(); | |
| const txHash = receipt.hash; | |
| const shortHash = txHash.slice(0, 10) + '...' + txHash.slice(-6); | |
| addMsg('chain-receipt', `0G Chain Β· ${shortHash}`); | |
| const badge = document.getElementById('receipt-badge'); | |
| badge.textContent = `TX: ${shortHash}`; | |
| badge.href = `https://chainscan-galileo.0g.ai/tx/${txHash}`; | |
| badge.style.display = ''; | |
| return txHash; | |
| } catch (e) { | |
| addMsg('system', 'Chain: ' + e.message.slice(0, 60)); | |
| return null; | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ROBOT INIT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function initRobot() { | |
| if (CFG.mode === 'live') { | |
| isLive = true; | |
| robot = new window.ReachyMini(); | |
| const params = new URLSearchParams(location.search); | |
| const hfToken = params.get('hf'); | |
| if (hfToken) { await robot.connect(hfToken); } | |
| else { try { await robot.login(); } catch (_) { await robot.connect(); } } | |
| await robot.startSession(); | |
| await robot.ensureAwake(); | |
| robot.attachVideo(document.getElementById('remoteVideo')); | |
| } else { | |
| isLive = false; | |
| robot = new window.ReachySim(document.getElementById('sim-container')); | |
| await new Promise(r => { | |
| if (robot.ready) return r(); | |
| robot.addEventListener('ready', r, { once: true }); | |
| }); | |
| await robot.wakeUp(); | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CHAT (0G Compute) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const conversationHistory = []; | |
| async function askAI(prompt, { signal, isInternal } = {}) { | |
| conversationHistory.push({ role: 'user', content: prompt }); | |
| const res = await fetch(CFG.chatUrl, { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${CFG.chatKey}`, 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model: CFG.model, messages: conversationHistory.slice(-12), max_tokens: 512 }), | |
| signal, | |
| }); | |
| if (!res.ok) throw new Error(`Chat ${res.status}: ${(await res.text()).slice(0, 200)}`); | |
| const data = await res.json(); | |
| const reply = (data.choices?.[0]?.message?.content || '').trim(); | |
| conversationHistory.push({ role: 'assistant', content: reply }); | |
| return reply; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // TTS + STT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function speak(text) { | |
| if (!('speechSynthesis' in window)) return Promise.resolve(); | |
| speechSynthesis.cancel(); | |
| const u = new SpeechSynthesisUtterance(text); | |
| u.rate = 1.0; u.pitch = 1.05; | |
| speechSynthesis.speak(u); | |
| return new Promise(r => { u.onend = r; u.onerror = r; }); | |
| } | |
| async function webmBlobToWav(blob) { | |
| const buf = await blob.arrayBuffer(); | |
| const ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| const audio = await ctx.decodeAudioData(buf); | |
| try { ctx.close(); } catch (_) {} | |
| const ch0 = audio.getChannelData(0); | |
| const len = ch0.length; | |
| const sampleRate = audio.sampleRate; | |
| const out = new ArrayBuffer(44 + len * 2); | |
| const v = new DataView(out); | |
| const ws = (off, s) => { for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i)); }; | |
| ws(0, 'RIFF'); v.setUint32(4, 36 + len * 2, true); ws(8, 'WAVE'); | |
| ws(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); | |
| ws(36, 'data'); v.setUint32(40, len * 2, true); | |
| let off = 44; | |
| for (let i = 0; i < len; i++) { | |
| const s = Math.max(-1, Math.min(1, ch0[i])); | |
| v.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true); | |
| off += 2; | |
| } | |
| return new Blob([out], { type: 'audio/wav' }); | |
| } | |
| async function transcribe(webmBlob) { | |
| const wav = await webmBlobToWav(webmBlob); | |
| const form = new FormData(); | |
| form.append('file', wav, 'audio.wav'); | |
| form.append('model', 'openai/whisper-large-v3'); | |
| form.append('response_format', 'json'); | |
| const res = await fetch(CFG.sttUrl, { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${CFG.sttKey}` }, | |
| body: form, | |
| }); | |
| if (!res.ok) throw new Error(`STT ${res.status}: ${(await res.text()).slice(0, 200)}`); | |
| return (await res.json()).text || ''; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // MIC RECORDING | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let recStartTime = 0; | |
| let recTimerInterval = null; | |
| let recAnalyser = null; | |
| let recAnimFrame = null; | |
| const micBtn = document.getElementById('mic-btn'); | |
| const recStopBtn = document.getElementById('rec-stop-btn'); | |
| const inputDefault = document.getElementById('input-default'); | |
| const inputRecording = document.getElementById('input-recording'); | |
| if (micBtn) micBtn.addEventListener('click', toggleRecording); | |
| if (recStopBtn) recStopBtn.addEventListener('click', stopRecording); | |
| function toggleRecording() { | |
| if (mediaRecorder && mediaRecorder.state === 'recording') stopRecording(); | |
| else startRecording(); | |
| } | |
| async function startRecording() { | |
| if (!CFG.sttKey) { addMsg('system', 'Voice needs a Whisper key β add one in Advanced settings.'); return; } | |
| try { | |
| let stream; | |
| if (isLive) { | |
| const vid = document.getElementById('remoteVideo'); | |
| const robotAudio = vid?.srcObject?.getAudioTracks?.() || []; | |
| stream = robotAudio.length ? new MediaStream(robotAudio) : await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| } else { | |
| stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| } | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| const source = audioCtx.createMediaStreamSource(stream); | |
| recAnalyser = audioCtx.createAnalyser(); | |
| recAnalyser.fftSize = 64; | |
| source.connect(recAnalyser); | |
| drawWaveform(); | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); }; | |
| mediaRecorder.onstop = async () => { | |
| cancelAnimationFrame(recAnimFrame); | |
| clearInterval(recTimerInterval); | |
| try { audioCtx.close(); } catch (_) {} | |
| inputRecording.style.display = 'none'; | |
| inputDefault.style.display = ''; | |
| if (audioChunks.length === 0) return; | |
| if (Date.now() - recStartTime < 600) { addMsg('system', 'Too short.'); return; } | |
| const blob = new Blob(audioChunks, { type: 'audio/webm;codecs=opus' }); | |
| addMsg('system', 'Transcribing via 0G Whisper...'); | |
| try { | |
| const text = await transcribe(blob); | |
| if (text.trim()) { document.getElementById('chat-input').value = text; sendMessage(); } | |
| else addMsg('system', 'No speech detected.'); | |
| } catch (e) { addMsg('system', 'STT: ' + e.message); } | |
| }; | |
| recStartTime = Date.now(); | |
| mediaRecorder.start(250); | |
| inputDefault.style.display = 'none'; | |
| inputRecording.style.display = ''; | |
| updateRecTimer(); | |
| recTimerInterval = setInterval(updateRecTimer, 1000); | |
| } catch (e) { addMsg('system', 'Mic: ' + e.message); } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state === 'recording') { | |
| mediaRecorder.stop(); | |
| mediaRecorder.stream.getTracks().forEach(t => t.stop()); | |
| } | |
| } | |
| function updateRecTimer() { | |
| const sec = Math.floor((Date.now() - recStartTime) / 1000); | |
| document.getElementById('rec-timer').textContent = `${Math.floor(sec/60)}:${String(sec%60).padStart(2,'0')}`; | |
| } | |
| function drawWaveform() { | |
| const canvas = document.getElementById('rec-waveform'); | |
| const ctx = canvas.getContext('2d'); | |
| const data = new Uint8Array(recAnalyser.frequencyBinCount); | |
| function draw() { | |
| recAnimFrame = requestAnimationFrame(draw); | |
| recAnalyser.getByteFrequencyData(data); | |
| const w = canvas.width = canvas.clientWidth * (window.devicePixelRatio || 1); | |
| const h = canvas.height = 32 * (window.devicePixelRatio || 1); | |
| ctx.clearRect(0, 0, w, h); | |
| const bars = data.length, barW = w / bars, mid = h / 2; | |
| ctx.fillStyle = 'rgba(245,239,227,0.8)'; | |
| for (let i = 0; i < bars; i++) { | |
| const v = data[i] / 255; | |
| const barH = Math.max(2, v * mid * 0.9); | |
| ctx.beginPath(); | |
| ctx.roundRect(i * barW + 1, mid - barH, Math.max(1, barW - 2), barH * 2, 1); | |
| ctx.fill(); | |
| } | |
| } | |
| draw(); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SEND MESSAGE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let sending = false; | |
| window.sendMessage = async function () { | |
| const input = document.getElementById('chat-input'); | |
| const text = input.value.trim(); | |
| if (!text || sending) return; | |
| input.value = ''; | |
| sending = true; | |
| document.getElementById('send-btn').disabled = true; | |
| addMsg('user', text); | |
| animateThinking(); | |
| try { | |
| const raw = await askAI(text); | |
| const actionMatch = raw.match(/\[ACTION:(wave|nod|shake|dance|sleep|wake|excited|thinking)\]/i); | |
| const cleanReply = raw.replace(/\[ACTION:\w+\]/gi, '').trim(); | |
| addMsg('bot', cleanReply); | |
| animateSpeaking(); | |
| const speakDone = speak(cleanReply); | |
| if (actionMatch) fireAction(actionMatch[1].toLowerCase()); | |
| await speakDone; | |
| animateIdle(); | |
| interactionCount++; | |
| const mem = customerMemory[CFG.customerName]; | |
| if (mem) { | |
| mem.interactions.push(`Asked: "${text.slice(0, 60)}" β Rec: "${cleanReply.slice(0, 80)}"`); | |
| if (mem.interactions.length > 20) mem.interactions = mem.interactions.slice(-20); | |
| saveCustomerMemory(); | |
| if (interactionCount % 3 === 0) { | |
| addMsg('storage-event', `0G Storage: Profile updated (${mem.interactions.length} saved)`); | |
| } | |
| } | |
| if (chainWallet && interactionCount % 2 === 0) logOnChain(CFG.customerName, text, cleanReply); | |
| } catch (e) { | |
| addMsg('system', 'Error: ' + e.message); | |
| animateIdle(); | |
| } | |
| sending = false; | |
| document.getElementById('send-btn').disabled = false; | |
| }; | |
| document.getElementById('chat-input')?.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| }); | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ROBOT ANIMATIONS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function fireAction(action) { | |
| const actions = { wave: doWave, nod: doNod, shake: doShake, dance: doDance, sleep: doSleep, wake: doWake, excited: doExcited, thinking: doThinking }; | |
| if (actions[action]) actions[action](); | |
| } | |
| function animateThinking() { | |
| if (!robot) return; | |
| robot.setAntennasDeg?.(15, -15); | |
| robot.setHeadRpyDeg?.(5, -5, 0); | |
| } | |
| function animateSpeaking() { | |
| if (!robot) return; | |
| robot.setHeadRpyDeg?.(0, 0, 0); | |
| let i = 0; | |
| const wiggle = setInterval(() => { | |
| if (i++ > 6) { clearInterval(wiggle); return; } | |
| robot.setAntennasDeg?.(i % 2 === 0 ? 15 : -15, i % 2 === 0 ? -15 : 15); | |
| }, 280); | |
| } | |
| function animateIdle() { | |
| if (!robot) return; | |
| robot.setHeadRpyDeg?.(0, 0, 0); | |
| robot.setAntennasDeg?.(0, 0); | |
| } | |
| window.doWave = async function () { | |
| if (!robot) return; | |
| robot.setAntennasDeg?.(60, -60); await sleep(300); | |
| robot.setAntennasDeg?.(-60, 60); await sleep(300); | |
| robot.setAntennasDeg?.(60, -60); await sleep(300); | |
| robot.setAntennasDeg?.(0, 0); | |
| }; | |
| window.doNod = async function () { | |
| if (!robot) return; | |
| robot.setHeadRpyDeg?.(0, -15, 0); await sleep(250); | |
| robot.setHeadRpyDeg?.(0, 10, 0); await sleep(250); | |
| robot.setHeadRpyDeg?.(0, -10, 0); await sleep(250); | |
| robot.setHeadRpyDeg?.(0, 0, 0); | |
| }; | |
| window.doShake = async function () { | |
| if (!robot) return; | |
| robot.setHeadRpyDeg?.(0, 0, -20); await sleep(200); | |
| robot.setHeadRpyDeg?.(0, 0, 20); await sleep(200); | |
| robot.setHeadRpyDeg?.(0, 0, -20); await sleep(200); | |
| robot.setHeadRpyDeg?.(0, 0, 0); | |
| }; | |
| window.doDance = async function () { | |
| if (!robot) return; | |
| for (let i = 0; i < 4; i++) { | |
| robot.setHeadRpyDeg?.(10, 0, 20); robot.setAntennasDeg?.(40, -40); await sleep(300); | |
| robot.setHeadRpyDeg?.(-10, 0, -20); robot.setAntennasDeg?.(-40, 40); await sleep(300); | |
| } | |
| robot.setHeadRpyDeg?.(0, 0, 0); robot.setAntennasDeg?.(0, 0); | |
| }; | |
| window.doExcited = async function () { | |
| if (!robot) return; | |
| for (let i = 0; i < 3; i++) { | |
| robot.setAntennasDeg?.(50, -50); robot.setHeadRpyDeg?.(0, -8, 0); await sleep(200); | |
| robot.setAntennasDeg?.(-50, 50); robot.setHeadRpyDeg?.(0, 8, 0); await sleep(200); | |
| } | |
| robot.setHeadRpyDeg?.(0, 0, 0); robot.setAntennasDeg?.(0, 0); | |
| }; | |
| window.doThinking = async function () { | |
| if (!robot) return; | |
| robot.setHeadRpyDeg?.(8, -5, 15); robot.setAntennasDeg?.(20, -10); await sleep(600); | |
| robot.setHeadRpyDeg?.(8, -5, -15); robot.setAntennasDeg?.(-10, 20); await sleep(600); | |
| robot.setHeadRpyDeg?.(0, 0, 0); robot.setAntennasDeg?.(0, 0); | |
| }; | |
| window.doSleep = function () { robot?.gotoSleep?.(); }; | |
| window.doWake = function () { robot?.wakeUp?.(); }; | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // HELPERS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addMsg(role, text) { | |
| const el = document.createElement('div'); | |
| el.className = `msg ${role}`; | |
| el.textContent = text; | |
| const t = document.getElementById('transcript'); | |
| t.appendChild(el); | |
| t.scrollTop = t.scrollHeight; | |
| } | |
| function setStatus(text, ok = false) { | |
| const el = document.getElementById('status'); | |
| el.textContent = text; | |
| el.className = ok ? 'connected' : (text === 'error' ? 'error' : ''); | |
| } | |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | |
| </script> | |
| </body> | |
| </html> | |