Spaces:
Running
Running
harystyleseze commited on
Commit Β·
dcc955a
0
Parent(s):
Oga Market: Day 2 hybrid shopper + creator app for Good Online
Browse files- .gitignore +8 -0
- AGENTS.md +69 -0
- CLAUDE.md +70 -0
- README.md +81 -0
- contract/ListingNFT.sol +93 -0
- contract/README.md +45 -0
- contract/deploy.js +111 -0
- contract/package.json +14 -0
- creator.html +468 -0
- index.html +72 -0
- shared.js +233 -0
- shopper.html +440 -0
- styles.css +843 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.DS_Store
|
| 3 |
+
*.log
|
| 4 |
+
contract/.env
|
| 5 |
+
contract/node_modules/
|
| 6 |
+
contract/ListingNFT.abi.json
|
| 7 |
+
contract/ListingNFT.bytecode
|
| 8 |
+
node_modules/
|
AGENTS.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md β Oga Market
|
| 2 |
+
|
| 3 |
+
Portable agent guidance for the Oga Market codebase.
|
| 4 |
+
|
| 5 |
+
## What this is
|
| 6 |
+
|
| 7 |
+
**Day 2** entry for the 0G Onsite Lagos Hackathon. A web-only commerce app for [Good Online](https://good.online) with two surfaces:
|
| 8 |
+
|
| 9 |
+
1. **Shopper** (`shopper.html`) β AI shopping assistant (voice + text), wishlist on 0G Storage, on-chain shopping receipts
|
| 10 |
+
2. **Creator** (`creator.html`) β AI listing generator (vision + LLM), mints listings as iNFTs (ERC-7857-style) on 0G Chain
|
| 11 |
+
|
| 12 |
+
Sibling project to **Day 1 Oga Mi** at `/Users/mac/OG/ogami` (which is the robot version).
|
| 13 |
+
|
| 14 |
+
## File map
|
| 15 |
+
|
| 16 |
+
| File | Purpose |
|
| 17 |
+
|------|---------|
|
| 18 |
+
| `index.html` | Landing page with mode selector |
|
| 19 |
+
| `shopper.html` | Shopper experience (chat, voice, wishlist) |
|
| 20 |
+
| `creator.html` | Creator studio (upload photo, generate listing, mint iNFT) |
|
| 21 |
+
| `shared.js` | Catalog, system prompts, 0G helpers (askAI, askVision, transcribe, OG_CHAIN config, OG_STORAGE) |
|
| 22 |
+
| `styles.css` | All CSS β design tokens from `colors_and_type.css` reference |
|
| 23 |
+
| `contract/ListingNFT.sol` | Solidity source β ERC-7857-inspired |
|
| 24 |
+
| `contract/deploy.js` | One-step compile + deploy via solc-js + ethers |
|
| 25 |
+
| `contract/package.json` | Just `solc`, `ethers`, `dotenv` |
|
| 26 |
+
|
| 27 |
+
## 0G integration
|
| 28 |
+
|
| 29 |
+
**Direct mode endpoints** (browser-CORS-friendly, NOT Router API):
|
| 30 |
+
- Chat: `compute-network-1.integratenetwork.work/v1/proxy/chat/completions`
|
| 31 |
+
- Whisper: `compute-network-16.integratenetwork.work/v1/proxy/audio/transcriptions`
|
| 32 |
+
|
| 33 |
+
Each provider needs a separate `app-sk-...` key minted from **Advanced mode** on pc.0g.ai.
|
| 34 |
+
|
| 35 |
+
**Chain**: 0G testnet (Galileo), Chain ID 16602, RPC `https://evmrpc-testnet.0g.ai`.
|
| 36 |
+
|
| 37 |
+
## Gotchas (copy-paste from Day 1 / hackathon docs)
|
| 38 |
+
|
| 39 |
+
1. **CORS** β Router API (`router-api.0g.ai`) does NOT support browser preflight. Use Direct endpoints.
|
| 40 |
+
2. **Key per provider** β Chat key won't authorize Whisper. Mint separate `app-sk-...` for each.
|
| 41 |
+
3. **Whisper format** β Browser MediaRecorder gives webm/opus. 0G Whisper rejects raw webm. `webmBlobToWav()` in shared.js converts to PCM WAV first.
|
| 42 |
+
4. **Vision** β May fail on `zai-org/GLM-5-FP8` if that provider's not vision-enabled. The creator flow catches this and falls back to a text stub. To use real vision, deposit to `qwen/qwen3-vl-30b-a3b-instruct` and update `CFG.visionModel` in `creator.html`.
|
| 43 |
+
5. **iNFT contract** β Must be deployed once via `contract/deploy.js`, address pasted into `shared.js`. If empty, creator mode falls back to plain calldata transactions (still on-chain but not real iNFTs).
|
| 44 |
+
|
| 45 |
+
## Editing conventions
|
| 46 |
+
|
| 47 |
+
- **Single-file pages**: each HTML file is self-contained, scripts inline. Only `shared.js` and `styles.css` are imported.
|
| 48 |
+
- **No build step**: serve with `python3 -m http.server`.
|
| 49 |
+
- **Design tokens**: in `:root` of `styles.css`. Change colors there.
|
| 50 |
+
- **System prompts**: in `shared.js` (`buildShopperPrompt`, `buildCreatorPrompt`). Edit there.
|
| 51 |
+
- **Catalog**: in `shared.js` (`window.GOOD_CATALOG` string).
|
| 52 |
+
|
| 53 |
+
## Day 1 vs Day 2
|
| 54 |
+
|
| 55 |
+
| | Day 1 (Oga Mi) | Day 2 (Oga Market) |
|
| 56 |
+
|---|---|---|
|
| 57 |
+
| Surface | Robot + 3D sim | Web only, no robot |
|
| 58 |
+
| Audience | In-store at Good House | Customers + creators online |
|
| 59 |
+
| Hardware | Reachy Mini (WebRTC) | None |
|
| 60 |
+
| Chain | Plain interaction calldata | iNFTs (ERC-7857-inspired) |
|
| 61 |
+
| Vision | None | Photo β AI listing |
|
| 62 |
+
| Catalog | Read-only (recommends) | Read + Write (creators add new) |
|
| 63 |
+
|
| 64 |
+
## References
|
| 65 |
+
|
| 66 |
+
- 0G docs: https://docs.0g.ai/ai-context
|
| 67 |
+
- Hackathon repo: https://github.com/0gfoundation/reachy-mini-hackathon
|
| 68 |
+
- Good Online: https://good.online
|
| 69 |
+
- ERC-7857 spec: https://eips.ethereum.org/EIPS/eip-7857 (we use a minimal version β no TEE/ZKP since listings are public)
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md β Oga Market
|
| 2 |
+
|
| 3 |
+
@AGENTS.md
|
| 4 |
+
|
| 5 |
+
## Claude Code workflow
|
| 6 |
+
|
| 7 |
+
1. Edit `shopper.html`, `creator.html`, or `shared.js`
|
| 8 |
+
2. Serve: `python3 -m http.server 8766`
|
| 9 |
+
3. Test: http://localhost:8766 β refresh after edits
|
| 10 |
+
4. Both modes work without hardware
|
| 11 |
+
|
| 12 |
+
## Quick nav
|
| 13 |
+
|
| 14 |
+
| Feature | Where |
|
| 15 |
+
|---------|-------|
|
| 16 |
+
| CSS tokens | `:root` block in `styles.css` |
|
| 17 |
+
| Mode selector | `index.html` |
|
| 18 |
+
| Shopper chat loop | `shopper.html` `sendMessage()` |
|
| 19 |
+
| Shopper voice | `shopper.html` mic-btn click handler |
|
| 20 |
+
| Wishlist | `shopper.html` `addToWishlist()`, `renderWishlist()` |
|
| 21 |
+
| Creator upload | `creator.html` `file-input` change handler |
|
| 22 |
+
| Creator AI listing gen | `creator.html` `generateListing()` |
|
| 23 |
+
| Creator iNFT mint | `creator.html` `mintListing()` |
|
| 24 |
+
| 0G Compute helpers | `shared.js` `askAI()`, `askVision()`, `transcribe()` |
|
| 25 |
+
| 0G Chain config | `shared.js` `OG_CHAIN`, `LISTING_NFT_ABI` |
|
| 26 |
+
| System prompts | `shared.js` `buildShopperPrompt()`, `buildCreatorPrompt()` |
|
| 27 |
+
| Catalog | `shared.js` `GOOD_CATALOG` |
|
| 28 |
+
| Contract source | `contract/ListingNFT.sol` |
|
| 29 |
+
| Contract deploy | `contract/deploy.js` |
|
| 30 |
+
|
| 31 |
+
## Smoke test
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
# Test chat key
|
| 35 |
+
curl -sS https://compute-network-1.integratenetwork.work/v1/proxy/chat/completions \
|
| 36 |
+
-H "Authorization: Bearer YOUR_APP_SK" \
|
| 37 |
+
-H "Content-Type: application/json" \
|
| 38 |
+
-d '{"model":"zai-org/GLM-5-FP8","messages":[{"role":"user","content":"hi"}],"max_tokens":5}'
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
200 + JSON = key works.
|
| 42 |
+
|
| 43 |
+
## Deploy iNFT contract
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
cd contract
|
| 47 |
+
echo "PRIVATE_KEY=0x..." > .env
|
| 48 |
+
npm install
|
| 49 |
+
node deploy.js
|
| 50 |
+
# Copy printed address into shared.js OG_CHAIN.listingNFT
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Adding features
|
| 54 |
+
|
| 55 |
+
**New shopper feature** β edit `shopper.html`. State lives in module-scope `let` vars (CFG, customerMemory, wishlist).
|
| 56 |
+
|
| 57 |
+
**New creator field** β add input to `creator.html` form, include in `generateListing()` prompt, persist to `creatorVault[CFG.name].listings[*]`.
|
| 58 |
+
|
| 59 |
+
**New on-chain action** β extend `LISTING_NFT_ABI` in `shared.js`, add a function to `ListingNFT.sol`, redeploy contract, update address.
|
| 60 |
+
|
| 61 |
+
## Publishing
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
git add -A
|
| 65 |
+
git commit -m "your message"
|
| 66 |
+
git push hf main # HuggingFace Space
|
| 67 |
+
git push origin main # GitHub
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
HF Space lives at https://devharystyles-oga-market.static.hf.space/
|
README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Oga Market
|
| 3 |
+
emoji: ποΈ
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- 0g-hackathon
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Oga Market β Good Online on the 0G Stack
|
| 13 |
+
|
| 14 |
+
Day 2 entry for the **0G Onsite Lagos Hackathon (Commerce x AI)**.
|
| 15 |
+
|
| 16 |
+
Two surfaces on one app, both powered by 0G:
|
| 17 |
+
|
| 18 |
+
- **Shopper mode** β AI shopping assistant (chat + voice) that knows the entire Good Online catalog, remembers what you like, and writes on-chain receipts for every interaction.
|
| 19 |
+
- **Creator mode** β Upload a product photo β AI writes the listing (title, story, category, price feedback) β mint it as an **iNFT (ERC-7857-inspired)** on 0G Chain so the creator owns their listing as an on-chain asset.
|
| 20 |
+
|
| 21 |
+
## Live
|
| 22 |
+
|
| 23 |
+
- HuggingFace Space: https://devharystyles-oga-market.static.hf.space/ *(after deploy)*
|
| 24 |
+
- GitHub: https://github.com/harystyleseze/oga-market *(after push)*
|
| 25 |
+
|
| 26 |
+
## 0G integration
|
| 27 |
+
|
| 28 |
+
| Service | Used by | What |
|
| 29 |
+
|---------|---------|------|
|
| 30 |
+
| Compute (Chat) | Both | GLM-5 FP8 for conversation + listing copy |
|
| 31 |
+
| Compute (Whisper) | Shopper | Voice product search |
|
| 32 |
+
| Compute (Vision) | Creator | Analyse uploaded product photos |
|
| 33 |
+
| Storage | Both | Customer profiles, wishlists, minted listings (localStorage proxy) |
|
| 34 |
+
| Chain | Both | Shopper receipts + creator iNFTs (ERC-7857-style) on 0G testnet |
|
| 35 |
+
|
| 36 |
+
## Run locally
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
cd /Users/mac/OG/oga-market
|
| 40 |
+
python3 -m http.server 8766
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Open http://localhost:8766 β choose Shopper or Creator.
|
| 44 |
+
|
| 45 |
+
## Deploy the iNFT contract
|
| 46 |
+
|
| 47 |
+
The creator mode needs a deployed `ListingNFT` contract on 0G testnet. One-time setup:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
cd contract
|
| 51 |
+
echo "PRIVATE_KEY=0xYOUR_TESTNET_KEY" > .env
|
| 52 |
+
npm install
|
| 53 |
+
node deploy.js
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Paste the printed address into `shared.js` β `window.OG_CHAIN.listingNFT`.
|
| 57 |
+
|
| 58 |
+
Get free testnet 0G at https://faucet.0g.ai
|
| 59 |
+
|
| 60 |
+
## Files
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
oga-market/
|
| 64 |
+
βββ index.html Mode selector (Shopper or Creator)
|
| 65 |
+
βββ shopper.html AI shopping assistant
|
| 66 |
+
βββ creator.html AI listing generator + iNFT minter
|
| 67 |
+
βββ shared.js Catalog + 0G helpers (Compute, Chain, Storage)
|
| 68 |
+
βββ styles.css Good Online design system (cream/forest/saffron)
|
| 69 |
+
βββ contract/
|
| 70 |
+
β βββ ListingNFT.sol ERC-7857-inspired contract source
|
| 71 |
+
β βββ deploy.js One-step compile + deploy
|
| 72 |
+
β βββ package.json
|
| 73 |
+
β βββ README.md
|
| 74 |
+
βββ CLAUDE.md
|
| 75 |
+
βββ AGENTS.md
|
| 76 |
+
βββ README.md
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Built for
|
| 80 |
+
|
| 81 |
+
0G Onsite Lagos Β· Day 2 Β· Commerce x AI Β· 2026-05-28
|
contract/ListingNFT.sol
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// SPDX-License-Identifier: MIT
|
| 2 |
+
pragma solidity ^0.8.20;
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* @title ListingNFT
|
| 6 |
+
* @notice A minimal ERC-7857-inspired iNFT for Good Online product listings.
|
| 7 |
+
* Each listing is minted by a creator with on-chain metadata description
|
| 8 |
+
* and a content hash. Ownership is transferable.
|
| 9 |
+
*
|
| 10 |
+
* This is a simplified version of ERC-7857 (Intelligent NFTs) that
|
| 11 |
+
* skips the TEE/ZKP verification layer required for full encrypted
|
| 12 |
+
* metadata transfer. Listings here are public by design (it's a
|
| 13 |
+
* marketplace), so we just need: ownership + content commitment.
|
| 14 |
+
*/
|
| 15 |
+
contract ListingNFT {
|
| 16 |
+
struct IntelligentData {
|
| 17 |
+
string dataDescription;
|
| 18 |
+
bytes32 dataHash;
|
| 19 |
+
address creator;
|
| 20 |
+
uint256 timestamp;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
string public constant name = "Good Online Listing";
|
| 24 |
+
string public constant symbol = "GOOD-LIST";
|
| 25 |
+
|
| 26 |
+
uint256 private _nextTokenId = 1;
|
| 27 |
+
mapping(uint256 => address) private _owners;
|
| 28 |
+
mapping(uint256 => IntelligentData) private _data;
|
| 29 |
+
mapping(address => uint256) private _balances;
|
| 30 |
+
|
| 31 |
+
event ListingMinted(
|
| 32 |
+
uint256 indexed tokenId,
|
| 33 |
+
address indexed creator,
|
| 34 |
+
string dataDescription,
|
| 35 |
+
bytes32 dataHash,
|
| 36 |
+
uint256 timestamp
|
| 37 |
+
);
|
| 38 |
+
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
|
| 39 |
+
|
| 40 |
+
/// @notice Mint a new listing as an iNFT.
|
| 41 |
+
/// @param dataDescription JSON string describing the listing (title, category, price, etc.)
|
| 42 |
+
/// @param dataHash keccak256 commitment to the full content (image + text)
|
| 43 |
+
/// @return tokenId of the new listing
|
| 44 |
+
function mint(string memory dataDescription, bytes32 dataHash) external returns (uint256) {
|
| 45 |
+
uint256 tokenId = _nextTokenId++;
|
| 46 |
+
_owners[tokenId] = msg.sender;
|
| 47 |
+
_balances[msg.sender] += 1;
|
| 48 |
+
_data[tokenId] = IntelligentData({
|
| 49 |
+
dataDescription: dataDescription,
|
| 50 |
+
dataHash: dataHash,
|
| 51 |
+
creator: msg.sender,
|
| 52 |
+
timestamp: block.timestamp
|
| 53 |
+
});
|
| 54 |
+
emit ListingMinted(tokenId, msg.sender, dataDescription, dataHash, block.timestamp);
|
| 55 |
+
emit Transfer(address(0), msg.sender, tokenId);
|
| 56 |
+
return tokenId;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/// @notice Get the listing data for a token.
|
| 60 |
+
function getData(uint256 tokenId) external view returns (
|
| 61 |
+
string memory dataDescription,
|
| 62 |
+
bytes32 dataHash,
|
| 63 |
+
address creator,
|
| 64 |
+
uint256 timestamp
|
| 65 |
+
) {
|
| 66 |
+
IntelligentData memory d = _data[tokenId];
|
| 67 |
+
return (d.dataDescription, d.dataHash, d.creator, d.timestamp);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function totalSupply() external view returns (uint256) {
|
| 71 |
+
return _nextTokenId - 1;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function ownerOf(uint256 tokenId) external view returns (address) {
|
| 75 |
+
require(_owners[tokenId] != address(0), "Token does not exist");
|
| 76 |
+
return _owners[tokenId];
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function balanceOf(address owner) external view returns (uint256) {
|
| 80 |
+
return _balances[owner];
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/// @notice Transfer ownership of a listing.
|
| 84 |
+
function transferFrom(address from, address to, uint256 tokenId) external {
|
| 85 |
+
require(_owners[tokenId] == from, "Not owner");
|
| 86 |
+
require(from == msg.sender, "Only owner can transfer");
|
| 87 |
+
require(to != address(0), "Cannot transfer to zero");
|
| 88 |
+
_balances[from] -= 1;
|
| 89 |
+
_balances[to] += 1;
|
| 90 |
+
_owners[tokenId] = to;
|
| 91 |
+
emit Transfer(from, to, tokenId);
|
| 92 |
+
}
|
| 93 |
+
}
|
contract/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ListingNFT β Contract Deploy
|
| 2 |
+
|
| 3 |
+
Minimal ERC-7857-inspired iNFT contract for Good Online product listings. Creators mint listings as on-chain owned tokens.
|
| 4 |
+
|
| 5 |
+
## Deploy in 3 commands
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
cd contract
|
| 9 |
+
echo "PRIVATE_KEY=0xYOUR_TESTNET_PRIVATE_KEY" > .env
|
| 10 |
+
npm install
|
| 11 |
+
node deploy.js
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
The script compiles `ListingNFT.sol` with `solc-js`, deploys to **0G testnet** (Chain ID 16602), and prints the deployed address.
|
| 15 |
+
|
| 16 |
+
## After deploy
|
| 17 |
+
|
| 18 |
+
Copy the printed address and paste it into `../shared.js`:
|
| 19 |
+
|
| 20 |
+
```js
|
| 21 |
+
window.OG_CHAIN = {
|
| 22 |
+
rpc: 'https://evmrpc-testnet.0g.ai',
|
| 23 |
+
chainId: 16602,
|
| 24 |
+
explorerTx: (h) => `https://chainscan-galileo.0g.ai/tx/${h}`,
|
| 25 |
+
listingNFT: '0xYOUR_DEPLOYED_ADDRESS', // β paste here
|
| 26 |
+
};
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Get testnet 0G
|
| 30 |
+
|
| 31 |
+
Free from https://faucet.0g.ai (0.1 0G/day, more than enough for deploy)
|
| 32 |
+
|
| 33 |
+
## Contract API
|
| 34 |
+
|
| 35 |
+
```solidity
|
| 36 |
+
function mint(string dataDescription, bytes32 dataHash) external returns (uint256 tokenId);
|
| 37 |
+
function getData(uint256 tokenId) external view returns (string, bytes32, address, uint256);
|
| 38 |
+
function ownerOf(uint256 tokenId) external view returns (address);
|
| 39 |
+
function transferFrom(address from, address to, uint256 tokenId) external;
|
| 40 |
+
function totalSupply() external view returns (uint256);
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Events:
|
| 44 |
+
- `ListingMinted(uint256 indexed tokenId, address indexed creator, string dataDescription, bytes32 dataHash, uint256 timestamp)`
|
| 45 |
+
- `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` (ERC-721 compatible)
|
contract/deploy.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// deploy.js β compile and deploy ListingNFT.sol to 0G testnet
|
| 2 |
+
//
|
| 3 |
+
// Usage:
|
| 4 |
+
// cd contract
|
| 5 |
+
// echo "PRIVATE_KEY=0x..." > .env
|
| 6 |
+
// npm install
|
| 7 |
+
// node deploy.js
|
| 8 |
+
//
|
| 9 |
+
// Prints the deployed address β paste into ../shared.js OG_CHAIN.listingNFT
|
| 10 |
+
|
| 11 |
+
import { readFileSync, writeFileSync } from 'fs';
|
| 12 |
+
import { fileURLToPath } from 'url';
|
| 13 |
+
import { dirname, resolve } from 'path';
|
| 14 |
+
import { ethers } from 'ethers';
|
| 15 |
+
import solc from 'solc';
|
| 16 |
+
import 'dotenv/config';
|
| 17 |
+
|
| 18 |
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
| 19 |
+
|
| 20 |
+
const OG_RPC = 'https://evmrpc-testnet.0g.ai';
|
| 21 |
+
const OG_CHAIN_ID = 16602;
|
| 22 |
+
|
| 23 |
+
function compile() {
|
| 24 |
+
console.log('Compiling ListingNFT.solβ¦');
|
| 25 |
+
const source = readFileSync(resolve(__dirname, 'ListingNFT.sol'), 'utf8');
|
| 26 |
+
|
| 27 |
+
const input = {
|
| 28 |
+
language: 'Solidity',
|
| 29 |
+
sources: { 'ListingNFT.sol': { content: source } },
|
| 30 |
+
settings: {
|
| 31 |
+
outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object'] } },
|
| 32 |
+
optimizer: { enabled: true, runs: 200 },
|
| 33 |
+
},
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const output = JSON.parse(solc.compile(JSON.stringify(input)));
|
| 37 |
+
|
| 38 |
+
if (output.errors) {
|
| 39 |
+
for (const err of output.errors) {
|
| 40 |
+
if (err.severity === 'error') {
|
| 41 |
+
console.error(err.formattedMessage);
|
| 42 |
+
process.exit(1);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const contract = output.contracts['ListingNFT.sol']['ListingNFT'];
|
| 48 |
+
const abi = contract.abi;
|
| 49 |
+
const bytecode = contract.evm.bytecode.object;
|
| 50 |
+
|
| 51 |
+
// Save artifacts for the frontend
|
| 52 |
+
writeFileSync(resolve(__dirname, 'ListingNFT.abi.json'), JSON.stringify(abi, null, 2));
|
| 53 |
+
writeFileSync(resolve(__dirname, 'ListingNFT.bytecode'), bytecode);
|
| 54 |
+
console.log('Compiled. ABI + bytecode written to contract/.');
|
| 55 |
+
|
| 56 |
+
return { abi, bytecode };
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
async function deploy() {
|
| 60 |
+
const privateKey = process.env.PRIVATE_KEY;
|
| 61 |
+
if (!privateKey) {
|
| 62 |
+
console.error('Missing PRIVATE_KEY in contract/.env');
|
| 63 |
+
console.error('Create one: echo "PRIVATE_KEY=0x..." > .env');
|
| 64 |
+
process.exit(1);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const { abi, bytecode } = compile();
|
| 68 |
+
|
| 69 |
+
console.log('Connecting to 0G testnetβ¦');
|
| 70 |
+
const provider = new ethers.JsonRpcProvider(OG_RPC);
|
| 71 |
+
const wallet = new ethers.Wallet(privateKey, provider);
|
| 72 |
+
|
| 73 |
+
const network = await provider.getNetwork();
|
| 74 |
+
if (Number(network.chainId) !== OG_CHAIN_ID) {
|
| 75 |
+
console.warn(`Warning: chainId is ${network.chainId}, expected ${OG_CHAIN_ID}`);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const balance = await provider.getBalance(wallet.address);
|
| 79 |
+
console.log(`Wallet: ${wallet.address}`);
|
| 80 |
+
console.log(`Balance: ${ethers.formatEther(balance)} 0G`);
|
| 81 |
+
|
| 82 |
+
if (balance === 0n) {
|
| 83 |
+
console.error('Wallet has 0 balance. Get testnet tokens at https://faucet.0g.ai');
|
| 84 |
+
process.exit(1);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
console.log('Deploying ListingNFTβ¦');
|
| 88 |
+
const factory = new ethers.ContractFactory(abi, '0x' + bytecode, wallet);
|
| 89 |
+
const contract = await factory.deploy();
|
| 90 |
+
console.log(`Tx submitted: ${contract.deploymentTransaction().hash}`);
|
| 91 |
+
console.log('Waiting for confirmationβ¦');
|
| 92 |
+
await contract.waitForDeployment();
|
| 93 |
+
const addr = await contract.getAddress();
|
| 94 |
+
|
| 95 |
+
console.log('');
|
| 96 |
+
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββ');
|
| 97 |
+
console.log('β Deployed!');
|
| 98 |
+
console.log('');
|
| 99 |
+
console.log(`Address: ${addr}`);
|
| 100 |
+
console.log(`Explorer: https://chainscan-galileo.0g.ai/address/${addr}`);
|
| 101 |
+
console.log('');
|
| 102 |
+
console.log('Next step:');
|
| 103 |
+
console.log(' Edit ../shared.js and set:');
|
| 104 |
+
console.log(` OG_CHAIN.listingNFT = '${addr}'`);
|
| 105 |
+
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββ');
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
deploy().catch(e => {
|
| 109 |
+
console.error('Deploy failed:', e);
|
| 110 |
+
process.exit(1);
|
| 111 |
+
});
|
contract/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "oga-market-contract",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "ListingNFT (ERC-7857-inspired) iNFT contract for Oga Market",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"deploy": "node deploy.js"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"dotenv": "^16.4.5",
|
| 11 |
+
"ethers": "^6.13.4",
|
| 12 |
+
"solc": "^0.8.20"
|
| 13 |
+
}
|
| 14 |
+
}
|
creator.html
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Oga Market β Creator Studio</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
|
| 11 |
+
<!-- βββ SETUP βββ -->
|
| 12 |
+
<div id="setup" class="om-setup">
|
| 13 |
+
<div class="om-setup-card">
|
| 14 |
+
<h2>Creator studio.</h2>
|
| 15 |
+
<p class="sub">List your product on Good Online β AI writes the copy, you own it on-chain as an iNFT.</p>
|
| 16 |
+
|
| 17 |
+
<div class="om-field">
|
| 18 |
+
<label>Creator name</label>
|
| 19 |
+
<input id="cfg-name" type="text" placeholder="Your brand name (e.g. Arami, JacquΓ© Lagos)" spellcheck="false" />
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="om-field">
|
| 23 |
+
<label>0G Chat API Key</label>
|
| 24 |
+
<input id="cfg-chat-key" type="password" placeholder="app-sk-..." spellcheck="false" />
|
| 25 |
+
<div class="om-field-hint">Used for vision + copywriting via 0G Compute</div>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="om-field">
|
| 29 |
+
<label>0G Chain Private Key</label>
|
| 30 |
+
<input id="cfg-chain-key" type="password" placeholder="0x... (required to mint iNFTs)" />
|
| 31 |
+
<div class="om-field-hint">Free testnet tokens at <a href="https://faucet.0g.ai" target="_blank">faucet.0g.ai</a></div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div id="cfg-err" class="om-err"></div>
|
| 35 |
+
<button class="om-cta" onclick="startCreator()">Enter studio</button>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<!-- βββ TOPBAR βββ -->
|
| 40 |
+
<div class="om-topbar">
|
| 41 |
+
<div class="om-brand">
|
| 42 |
+
<svg width="24" height="24" viewBox="0 0 96 96" fill="none">
|
| 43 |
+
<rect x="14" y="30" width="68" height="56" rx="6" fill="#1F3B2D"/>
|
| 44 |
+
<circle cx="36" cy="58" r="6" fill="#F5EFE3"/>
|
| 45 |
+
<circle cx="60" cy="58" r="6" fill="#F5EFE3"/>
|
| 46 |
+
<line x1="34" y1="30" x2="34" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 47 |
+
<line x1="62" y1="30" x2="62" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 48 |
+
</svg>
|
| 49 |
+
<a href="index.html">Oga Market</a>
|
| 50 |
+
<span class="om-brand-tag">Creator</span>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="om-topbar-actions">
|
| 53 |
+
<span id="creator-badge" style="font-size: 13px; color: var(--fg-2);"></span>
|
| 54 |
+
<div class="om-services">
|
| 55 |
+
<span class="om-chip compute" id="chip-compute">Compute</span>
|
| 56 |
+
<span class="om-chip storage" id="chip-storage">Storage</span>
|
| 57 |
+
<span class="om-chip chain" id="chip-chain">Chain</span>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<!-- βββ APP βββ -->
|
| 63 |
+
<main class="om-app">
|
| 64 |
+
<div class="om-app-grid-creator">
|
| 65 |
+
|
| 66 |
+
<!-- LEFT: Upload form -->
|
| 67 |
+
<section class="om-upload">
|
| 68 |
+
<h3>1. Upload your product</h3>
|
| 69 |
+
|
| 70 |
+
<label for="file-input" class="om-dropzone" id="dropzone">
|
| 71 |
+
<div id="dropzone-empty">
|
| 72 |
+
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color: var(--fg-3);">
|
| 73 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
| 74 |
+
<polyline points="17 8 12 3 7 8"/>
|
| 75 |
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
| 76 |
+
</svg>
|
| 77 |
+
<p style="margin-top: 12px;">Click to upload photo<br><small style="color:var(--fg-3);">PNG, JPG, WebP Β· max 4MB</small></p>
|
| 78 |
+
</div>
|
| 79 |
+
<img id="dropzone-img" style="display:none;" />
|
| 80 |
+
</label>
|
| 81 |
+
<input id="file-input" type="file" accept="image/*" style="display:none;" />
|
| 82 |
+
|
| 83 |
+
<div class="om-field">
|
| 84 |
+
<label>Product name (working title)</label>
|
| 85 |
+
<input id="prod-name" type="text" placeholder="e.g. Hibiscus body oil" />
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div class="om-field">
|
| 89 |
+
<label>Price (NGN)</label>
|
| 90 |
+
<input id="prod-price" type="number" placeholder="13600" />
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<button class="om-cta" id="generate-btn" onclick="generateListing()" disabled>
|
| 94 |
+
Generate listing with AI
|
| 95 |
+
</button>
|
| 96 |
+
<div id="gen-status" style="font-size: 12px; color: var(--fg-3); text-align: center; min-height: 16px;"></div>
|
| 97 |
+
</section>
|
| 98 |
+
|
| 99 |
+
<!-- RIGHT: Preview + mint -->
|
| 100 |
+
<section class="om-listing-preview">
|
| 101 |
+
<h3>2. Preview & mint</h3>
|
| 102 |
+
<div id="listing-empty" class="om-listing-preview-empty">Upload a photo and click Generate to see the AI listing here.</div>
|
| 103 |
+
<div id="listing-card" class="om-listing-card" style="display:none;">
|
| 104 |
+
<span class="om-listing-eyebrow" id="lp-eyebrow">Category</span>
|
| 105 |
+
<h4 class="om-listing-title" id="lp-title"></h4>
|
| 106 |
+
<p class="om-listing-tagline" id="lp-tagline"></p>
|
| 107 |
+
<p class="om-listing-desc" id="lp-desc"></p>
|
| 108 |
+
<div class="om-listing-meta" id="lp-meta"></div>
|
| 109 |
+
<div class="om-listing-price" id="lp-price"></div>
|
| 110 |
+
<div class="om-listing-feedback" id="lp-feedback"></div>
|
| 111 |
+
<div class="om-listing-actions">
|
| 112 |
+
<button class="om-cta" id="mint-btn" onclick="mintListing()">Mint as iNFT on 0G Chain</button>
|
| 113 |
+
<button class="om-cta om-cta-secondary" onclick="regenerate()">Regenerate</button>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<div id="mint-result" style="margin-top: 16px;"></div>
|
| 118 |
+
|
| 119 |
+
<div style="margin-top: var(--sp-8); padding-top: var(--sp-6); border-top: 1px solid var(--border-1);">
|
| 120 |
+
<span class="om-side-eyebrow">Your minted listings Β· on 0G Chain</span>
|
| 121 |
+
<div id="minted-grid" class="om-minted-grid">
|
| 122 |
+
<div style="font-size: 12px; color: var(--fg-3); padding: 12px 0;">None yet.</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</section>
|
| 126 |
+
|
| 127 |
+
</div>
|
| 128 |
+
</main>
|
| 129 |
+
|
| 130 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.4/ethers.umd.min.js"></script>
|
| 131 |
+
<script src="shared.js"></script>
|
| 132 |
+
<script>
|
| 133 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
// STATE
|
| 135 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
let CFG = {};
|
| 137 |
+
let chainProvider = null, chainWallet = null, listingContract = null;
|
| 138 |
+
let currentImage = null; // data URL of uploaded photo
|
| 139 |
+
let currentListing = null; // generated listing JSON
|
| 140 |
+
|
| 141 |
+
let creatorVault = window.OG_STORAGE.load('omk-creator') || {};
|
| 142 |
+
|
| 143 |
+
// Restore previous session
|
| 144 |
+
(function () {
|
| 145 |
+
const saved = sessionStorage.getItem('omk-creator-cfg');
|
| 146 |
+
if (!saved) return;
|
| 147 |
+
try {
|
| 148 |
+
const s = JSON.parse(saved);
|
| 149 |
+
if (s.name) document.getElementById('cfg-name').value = s.name;
|
| 150 |
+
if (s.chatKey) document.getElementById('cfg-chat-key').value = s.chatKey;
|
| 151 |
+
if (s.chainKey) document.getElementById('cfg-chain-key').value = s.chainKey;
|
| 152 |
+
} catch (_) {}
|
| 153 |
+
})();
|
| 154 |
+
|
| 155 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
+
// START
|
| 157 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
+
window.startCreator = async function () {
|
| 159 |
+
const name = document.getElementById('cfg-name').value.trim();
|
| 160 |
+
const chatKey = document.getElementById('cfg-chat-key').value.trim();
|
| 161 |
+
const chainKey = document.getElementById('cfg-chain-key').value.trim();
|
| 162 |
+
if (!name) return showErr('Creator name is required.');
|
| 163 |
+
if (!chatKey) return showErr('0G Chat API key is required.');
|
| 164 |
+
if (!chainKey) return showErr('0G Chain key is required to mint iNFTs.');
|
| 165 |
+
|
| 166 |
+
CFG = { name, chatKey, chainKey, model: 'zai-org/GLM-5-FP8', visionModel: 'zai-org/GLM-5-FP8' };
|
| 167 |
+
|
| 168 |
+
// Verify chat key
|
| 169 |
+
try {
|
| 170 |
+
await window.askAI(chatKey, CFG.model, [{ role: 'user', content: 'hi' }], { maxTokens: 3 });
|
| 171 |
+
} catch (e) {
|
| 172 |
+
return showErr('Chat key failed: ' + e.message);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Init chain
|
| 176 |
+
try {
|
| 177 |
+
chainProvider = new ethers.JsonRpcProvider(window.OG_CHAIN.rpc);
|
| 178 |
+
chainWallet = new ethers.Wallet(chainKey, chainProvider);
|
| 179 |
+
const balance = await chainProvider.getBalance(chainWallet.address);
|
| 180 |
+
if (balance === 0n) {
|
| 181 |
+
return showErr('Chain wallet has 0 balance. Visit faucet.0g.ai for testnet tokens.');
|
| 182 |
+
}
|
| 183 |
+
if (window.OG_CHAIN.listingNFT) {
|
| 184 |
+
listingContract = new ethers.Contract(window.OG_CHAIN.listingNFT, window.LISTING_NFT_ABI, chainWallet);
|
| 185 |
+
}
|
| 186 |
+
} catch (e) {
|
| 187 |
+
return showErr('Chain setup failed: ' + e.message);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
sessionStorage.setItem('omk-creator-cfg', JSON.stringify(CFG));
|
| 191 |
+
document.getElementById('setup').classList.add('hidden');
|
| 192 |
+
document.getElementById('chip-compute').classList.add('active');
|
| 193 |
+
document.getElementById('chip-chain').classList.add('active');
|
| 194 |
+
document.getElementById('creator-badge').textContent = name + ' Β· ' + chainWallet.address.slice(0, 6) + 'β¦' + chainWallet.address.slice(-4);
|
| 195 |
+
|
| 196 |
+
// Load creator's minted listings
|
| 197 |
+
if (!creatorVault[name]) creatorVault[name] = { listings: [] };
|
| 198 |
+
renderMinted();
|
| 199 |
+
if (creatorVault[name].listings.length > 0) {
|
| 200 |
+
document.getElementById('chip-storage').classList.add('active');
|
| 201 |
+
}
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
function showErr(msg) {
|
| 205 |
+
const el = document.getElementById('cfg-err');
|
| 206 |
+
el.textContent = msg;
|
| 207 |
+
el.style.display = 'block';
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 211 |
+
// IMAGE UPLOAD
|
| 212 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
+
const fileInput = document.getElementById('file-input');
|
| 214 |
+
const dropzone = document.getElementById('dropzone');
|
| 215 |
+
const dropzoneImg = document.getElementById('dropzone-img');
|
| 216 |
+
const dropzoneEmpty = document.getElementById('dropzone-empty');
|
| 217 |
+
|
| 218 |
+
fileInput.addEventListener('change', (e) => {
|
| 219 |
+
const f = e.target.files[0];
|
| 220 |
+
if (!f) return;
|
| 221 |
+
if (f.size > 4 * 1024 * 1024) { alert('Max 4MB please.'); return; }
|
| 222 |
+
const reader = new FileReader();
|
| 223 |
+
reader.onload = (ev) => {
|
| 224 |
+
currentImage = ev.target.result;
|
| 225 |
+
dropzoneImg.src = currentImage;
|
| 226 |
+
dropzoneImg.style.display = 'block';
|
| 227 |
+
dropzoneEmpty.style.display = 'none';
|
| 228 |
+
dropzone.classList.add('has-image');
|
| 229 |
+
document.getElementById('generate-btn').disabled = false;
|
| 230 |
+
};
|
| 231 |
+
reader.readAsDataURL(f);
|
| 232 |
+
});
|
| 233 |
+
|
| 234 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 235 |
+
// GENERATE LISTING
|
| 236 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
+
let generating = false;
|
| 238 |
+
|
| 239 |
+
window.generateListing = async function () {
|
| 240 |
+
if (generating) return;
|
| 241 |
+
if (!currentImage) { alert('Upload a photo first.'); return; }
|
| 242 |
+
const name = document.getElementById('prod-name').value.trim();
|
| 243 |
+
const price = document.getElementById('prod-price').value.trim();
|
| 244 |
+
if (!name) { alert('Working name is required.'); return; }
|
| 245 |
+
if (!price) { alert('Price is required.'); return; }
|
| 246 |
+
|
| 247 |
+
generating = true;
|
| 248 |
+
const btn = document.getElementById('generate-btn');
|
| 249 |
+
const status = document.getElementById('gen-status');
|
| 250 |
+
btn.disabled = true;
|
| 251 |
+
btn.textContent = 'Generatingβ¦';
|
| 252 |
+
|
| 253 |
+
try {
|
| 254 |
+
// 1. Vision: analyse the photo
|
| 255 |
+
status.textContent = 'Analysing photo via 0G Compute (Vision)β¦';
|
| 256 |
+
let visionDesc;
|
| 257 |
+
try {
|
| 258 |
+
visionDesc = await window.askVision(CFG.chatKey, CFG.visionModel, currentImage,
|
| 259 |
+
'Describe this product photo in 2 sentences. Focus on: what the item is, colour, texture, mood. Keep it factual.');
|
| 260 |
+
} catch (e) {
|
| 261 |
+
// Vision may not be available on the chat provider β fall back to a stub
|
| 262 |
+
console.warn('Vision failed, falling back:', e);
|
| 263 |
+
visionDesc = `Product photo for "${name}".`;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// 2. LLM: generate the listing
|
| 267 |
+
status.textContent = 'Writing listing with GLM-5 on 0G Computeβ¦';
|
| 268 |
+
const prompt = window.buildCreatorPrompt(visionDesc, name, Number(price), CFG.name);
|
| 269 |
+
const raw = await window.askAI(CFG.chatKey, CFG.model, [
|
| 270 |
+
{ role: 'system', content: 'You are a Nigerian e-commerce copywriter. Always respond with valid JSON only.' },
|
| 271 |
+
{ role: 'user', content: prompt },
|
| 272 |
+
], { maxTokens: 600 });
|
| 273 |
+
|
| 274 |
+
// Extract JSON
|
| 275 |
+
let json;
|
| 276 |
+
try {
|
| 277 |
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
| 278 |
+
json = JSON.parse(jsonMatch ? jsonMatch[0] : raw);
|
| 279 |
+
} catch (e) {
|
| 280 |
+
throw new Error('AI returned invalid JSON. Try again.');
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
currentListing = {
|
| 284 |
+
...json,
|
| 285 |
+
creator: CFG.name,
|
| 286 |
+
price: Number(price),
|
| 287 |
+
workingName: name,
|
| 288 |
+
image: currentImage,
|
| 289 |
+
visionDesc,
|
| 290 |
+
createdAt: Date.now(),
|
| 291 |
+
};
|
| 292 |
+
renderListing(currentListing);
|
| 293 |
+
status.textContent = '';
|
| 294 |
+
} catch (e) {
|
| 295 |
+
status.textContent = 'Error: ' + e.message;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
generating = false;
|
| 299 |
+
btn.textContent = 'Generate listing with AI';
|
| 300 |
+
btn.disabled = false;
|
| 301 |
+
};
|
| 302 |
+
|
| 303 |
+
window.regenerate = () => generateListing();
|
| 304 |
+
|
| 305 |
+
function renderListing(L) {
|
| 306 |
+
document.getElementById('listing-empty').style.display = 'none';
|
| 307 |
+
document.getElementById('listing-card').style.display = '';
|
| 308 |
+
document.getElementById('lp-eyebrow').textContent = (L.category || 'Uncategorised').toUpperCase();
|
| 309 |
+
document.getElementById('lp-title').textContent = L.title || '';
|
| 310 |
+
document.getElementById('lp-tagline').textContent = L.tagline || '';
|
| 311 |
+
document.getElementById('lp-desc').textContent = L.description || '';
|
| 312 |
+
const meta = document.getElementById('lp-meta');
|
| 313 |
+
meta.innerHTML = '';
|
| 314 |
+
if (L.category) {
|
| 315 |
+
const pill = document.createElement('span');
|
| 316 |
+
pill.className = 'pill';
|
| 317 |
+
pill.textContent = L.category;
|
| 318 |
+
meta.appendChild(pill);
|
| 319 |
+
}
|
| 320 |
+
if (L.curatedList && L.curatedList !== 'null' && L.curatedList) {
|
| 321 |
+
const pill = document.createElement('span');
|
| 322 |
+
pill.className = 'pill';
|
| 323 |
+
pill.textContent = 'List: ' + L.curatedList;
|
| 324 |
+
meta.appendChild(pill);
|
| 325 |
+
}
|
| 326 |
+
document.getElementById('lp-price').textContent = 'N' + L.price.toLocaleString();
|
| 327 |
+
document.getElementById('lp-feedback').textContent = L.priceFeedback || '';
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 331 |
+
// MINT iNFT
|
| 332 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 333 |
+
window.mintListing = async function () {
|
| 334 |
+
if (!currentListing) return;
|
| 335 |
+
if (!chainWallet) { alert('Chain not connected.'); return; }
|
| 336 |
+
const mintBtn = document.getElementById('mint-btn');
|
| 337 |
+
mintBtn.disabled = true;
|
| 338 |
+
mintBtn.textContent = 'Mintingβ¦';
|
| 339 |
+
|
| 340 |
+
const resultEl = document.getElementById('mint-result');
|
| 341 |
+
resultEl.innerHTML = '';
|
| 342 |
+
|
| 343 |
+
try {
|
| 344 |
+
// Build dataDescription (compact JSON)
|
| 345 |
+
const dataDescription = JSON.stringify({
|
| 346 |
+
title: currentListing.title,
|
| 347 |
+
tagline: currentListing.tagline,
|
| 348 |
+
description: currentListing.description,
|
| 349 |
+
category: currentListing.category,
|
| 350 |
+
curatedList: currentListing.curatedList,
|
| 351 |
+
creator: currentListing.creator,
|
| 352 |
+
price: currentListing.price,
|
| 353 |
+
});
|
| 354 |
+
// dataHash = keccak256(image bytes + listing text)
|
| 355 |
+
const imageHash = ethers.keccak256(ethers.toUtf8Bytes(currentListing.image.slice(0, 4000)));
|
| 356 |
+
const dataHash = ethers.keccak256(ethers.toUtf8Bytes(dataDescription + imageHash));
|
| 357 |
+
|
| 358 |
+
let txHash, tokenId;
|
| 359 |
+
|
| 360 |
+
if (listingContract) {
|
| 361 |
+
// Use deployed contract
|
| 362 |
+
const tx = await listingContract.mint(dataDescription, dataHash);
|
| 363 |
+
const receipt = await tx.wait();
|
| 364 |
+
txHash = receipt.hash;
|
| 365 |
+
// Try to parse the ListingMinted event for tokenId
|
| 366 |
+
const event = receipt.logs.find(l => {
|
| 367 |
+
try { return listingContract.interface.parseLog(l)?.name === 'ListingMinted'; } catch (_) { return false; }
|
| 368 |
+
});
|
| 369 |
+
if (event) {
|
| 370 |
+
tokenId = listingContract.interface.parseLog(event).args.tokenId.toString();
|
| 371 |
+
}
|
| 372 |
+
} else {
|
| 373 |
+
// Fallback: simple calldata transaction if contract not deployed yet
|
| 374 |
+
const data = ethers.AbiCoder.defaultAbiCoder().encode(
|
| 375 |
+
['string', 'bytes32', 'address'],
|
| 376 |
+
[dataDescription, dataHash, chainWallet.address]
|
| 377 |
+
);
|
| 378 |
+
const tx = await chainWallet.sendTransaction({ to: chainWallet.address, value: 0n, data });
|
| 379 |
+
const receipt = await tx.wait();
|
| 380 |
+
txHash = receipt.hash;
|
| 381 |
+
tokenId = 'pending-deploy';
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
// Save to creator vault
|
| 385 |
+
const record = {
|
| 386 |
+
tokenId,
|
| 387 |
+
txHash,
|
| 388 |
+
title: currentListing.title,
|
| 389 |
+
price: currentListing.price,
|
| 390 |
+
category: currentListing.category,
|
| 391 |
+
mintedAt: Date.now(),
|
| 392 |
+
};
|
| 393 |
+
creatorVault[CFG.name].listings.unshift(record);
|
| 394 |
+
window.OG_STORAGE.save('omk-creator', creatorVault);
|
| 395 |
+
document.getElementById('chip-storage').classList.add('active');
|
| 396 |
+
|
| 397 |
+
// Show result
|
| 398 |
+
resultEl.innerHTML = '';
|
| 399 |
+
const success = document.createElement('div');
|
| 400 |
+
success.style.cssText = 'background: rgba(31,59,45,0.08); border-left: 3px solid var(--forest); padding: 12px 16px;';
|
| 401 |
+
const title = document.createElement('div');
|
| 402 |
+
title.style.cssText = 'font-family: var(--font-serif); font-style: italic; font-size: 18px; color: var(--forest);';
|
| 403 |
+
title.textContent = listingContract ? `Minted as iNFT #${tokenId}` : 'Logged on-chain (deploy iNFT contract for full ERC-7857)';
|
| 404 |
+
success.appendChild(title);
|
| 405 |
+
const link = document.createElement('a');
|
| 406 |
+
link.href = window.OG_CHAIN.explorerTx(txHash);
|
| 407 |
+
link.target = '_blank';
|
| 408 |
+
link.style.cssText = 'font-family: var(--font-mono); font-size: 11px; color: var(--fg-2); display: block; margin-top: 4px;';
|
| 409 |
+
link.textContent = window.shortHash(txHash) + ' β';
|
| 410 |
+
success.appendChild(link);
|
| 411 |
+
resultEl.appendChild(success);
|
| 412 |
+
renderMinted();
|
| 413 |
+
} catch (e) {
|
| 414 |
+
resultEl.innerHTML = '';
|
| 415 |
+
const err = document.createElement('div');
|
| 416 |
+
err.style.cssText = 'color: var(--terracotta); font-size: 13px;';
|
| 417 |
+
err.textContent = 'Mint failed: ' + e.message;
|
| 418 |
+
resultEl.appendChild(err);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
mintBtn.disabled = false;
|
| 422 |
+
mintBtn.textContent = 'Mint another';
|
| 423 |
+
};
|
| 424 |
+
|
| 425 |
+
function renderMinted() {
|
| 426 |
+
const grid = document.getElementById('minted-grid');
|
| 427 |
+
const list = creatorVault[CFG.name]?.listings || [];
|
| 428 |
+
grid.innerHTML = '';
|
| 429 |
+
if (list.length === 0) {
|
| 430 |
+
const empty = document.createElement('div');
|
| 431 |
+
empty.style.cssText = 'font-size: 12px; color: var(--fg-3); padding: 12px 0;';
|
| 432 |
+
empty.textContent = 'None yet.';
|
| 433 |
+
grid.appendChild(empty);
|
| 434 |
+
return;
|
| 435 |
+
}
|
| 436 |
+
for (const item of list) {
|
| 437 |
+
const a = document.createElement('a');
|
| 438 |
+
a.className = 'om-minted-card';
|
| 439 |
+
a.href = window.OG_CHAIN.explorerTx(item.txHash);
|
| 440 |
+
a.target = '_blank';
|
| 441 |
+
|
| 442 |
+
const id = document.createElement('div');
|
| 443 |
+
id.className = 'id';
|
| 444 |
+
id.textContent = '#' + item.tokenId;
|
| 445 |
+
a.appendChild(id);
|
| 446 |
+
|
| 447 |
+
const n = document.createElement('div');
|
| 448 |
+
n.className = 'name';
|
| 449 |
+
n.textContent = item.title;
|
| 450 |
+
a.appendChild(n);
|
| 451 |
+
|
| 452 |
+
const p = document.createElement('div');
|
| 453 |
+
p.style.cssText = 'font-family: var(--font-mono); font-size: 11px; color: var(--ink);';
|
| 454 |
+
p.textContent = 'N' + item.price.toLocaleString();
|
| 455 |
+
a.appendChild(p);
|
| 456 |
+
|
| 457 |
+
const tx = document.createElement('div');
|
| 458 |
+
tx.className = 'tx';
|
| 459 |
+
tx.textContent = window.shortHash(item.txHash);
|
| 460 |
+
a.appendChild(tx);
|
| 461 |
+
|
| 462 |
+
grid.appendChild(a);
|
| 463 |
+
}
|
| 464 |
+
}
|
| 465 |
+
</script>
|
| 466 |
+
|
| 467 |
+
</body>
|
| 468 |
+
</html>
|
index.html
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Oga Market β Good Online on 0G</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
|
| 11 |
+
<div class="om-topbar">
|
| 12 |
+
<div class="om-brand">
|
| 13 |
+
<svg width="28" height="28" viewBox="0 0 96 96" fill="none">
|
| 14 |
+
<rect x="14" y="30" width="68" height="56" rx="6" fill="#1F3B2D"/>
|
| 15 |
+
<circle cx="36" cy="58" r="6" fill="#F5EFE3"/>
|
| 16 |
+
<circle cx="60" cy="58" r="6" fill="#F5EFE3"/>
|
| 17 |
+
<line x1="34" y1="30" x2="34" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 18 |
+
<line x1="62" y1="30" x2="62" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 19 |
+
<circle cx="34" cy="12" r="3.5" fill="#C84B23"/>
|
| 20 |
+
<circle cx="62" cy="12" r="3.5" fill="#E8B547"/>
|
| 21 |
+
</svg>
|
| 22 |
+
<a href="/">Oga Market</a>
|
| 23 |
+
<span class="om-brand-tag">Good Online Β· 0G</span>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="om-topbar-actions">
|
| 26 |
+
<a href="https://good.online" target="_blank" style="font-size: 13px; color: var(--fg-2);">good.online β</a>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<main class="om-landing">
|
| 31 |
+
<div class="om-landing-eyebrow">Commerce Γ AI Β· 0G Onsite Lagos Β· Day 2</div>
|
| 32 |
+
<h1>Two sides of the same market.</h1>
|
| 33 |
+
<p class="sub">For shoppers who want a friendly assistant who knows every creator. For sellers who need their listings to feel as good as their craft. Both powered by 0G Compute, Storage, and Chain.</p>
|
| 34 |
+
|
| 35 |
+
<div class="om-modes">
|
| 36 |
+
<a href="shopper.html" class="om-mode">
|
| 37 |
+
<div class="om-mode-icon">
|
| 38 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 39 |
+
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
|
| 40 |
+
<line x1="3" y1="6" x2="21" y2="6"/>
|
| 41 |
+
<path d="M16 10a4 4 0 0 1-8 0"/>
|
| 42 |
+
</svg>
|
| 43 |
+
</div>
|
| 44 |
+
<h2 class="om-mode-title">I'm shopping.</h2>
|
| 45 |
+
<p class="om-mode-desc">Ask Oga Mi what to get for your wife, your mum, yourself. Voice or text. He remembers what you like across visits and saves your wishlist on 0G Storage. Every recommendation receipted on-chain.</p>
|
| 46 |
+
<span class="om-mode-cta">Open shopper mode β</span>
|
| 47 |
+
</a>
|
| 48 |
+
|
| 49 |
+
<a href="creator.html" class="om-mode">
|
| 50 |
+
<div class="om-mode-icon">
|
| 51 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 52 |
+
<path d="M12 19l7-7 3 3-7 7-3-3z"/>
|
| 53 |
+
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
|
| 54 |
+
<path d="M2 2l7.586 7.586"/>
|
| 55 |
+
<circle cx="11" cy="11" r="2"/>
|
| 56 |
+
</svg>
|
| 57 |
+
</div>
|
| 58 |
+
<h2 class="om-mode-title">I'm a creator.</h2>
|
| 59 |
+
<p class="om-mode-desc">Upload a photo of what you made. AI writes you a listing β title, story, category, price feedback. Mint it as an iNFT on 0G Chain so you own the listing as an on-chain asset.</p>
|
| 60 |
+
<span class="om-mode-cta">Open creator studio β</span>
|
| 61 |
+
</a>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="om-stack-row">
|
| 65 |
+
<span class="om-stack-item"><strong>0G Compute</strong> Β· GLM-5 + Whisper + Vision</span>
|
| 66 |
+
<span class="om-stack-item"><strong>0G Storage</strong> Β· profiles & listings</span>
|
| 67 |
+
<span class="om-stack-item"><strong>0G Chain</strong> Β· iNFTs (ERC-7857) & receipts</span>
|
| 68 |
+
</div>
|
| 69 |
+
</main>
|
| 70 |
+
|
| 71 |
+
</body>
|
| 72 |
+
</html>
|
shared.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// shared.js β common catalog, system prompts, and 0G helpers for Oga Market
|
| 2 |
+
|
| 3 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
// GOOD ONLINE PRODUCT CATALOG (lifted from Oga Mi)
|
| 5 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
window.GOOD_CATALOG = `
|
| 7 |
+
## GOOD ONLINE PRODUCT CATALOG (good.online)
|
| 8 |
+
"Made by Nigerians. Curated for you." β A marketplace connecting you with Nigerian creators committed to quality and positive impact. Delivers within Lagos only.
|
| 9 |
+
|
| 10 |
+
### FEATURED PRODUCTS
|
| 11 |
+
- Coconut Rice (430g) by Chumbys β N5,400 [Cook it Yourself]
|
| 12 |
+
- Glow Oil by Arami β N13,600 [Beauty]
|
| 13 |
+
- Whole Cashew Jar by The M & W Nuts Company β N11,875 [Snacks]
|
| 14 |
+
- Gym Socks Box by Smileys Africa β N17,500 [Fashion]
|
| 15 |
+
- Miniature Trio by Dala Craft β N54,000 [Home & Living]
|
| 16 |
+
- Sugar Wax Kit by Sugar Wax By Audrey β N17,000 [Beauty]
|
| 17 |
+
- Dudu Ball Chain by ITURA Jewelry β N40,000 [Fashion/Jewelry]
|
| 18 |
+
- Mini Scented Candle Gift Set by Jacque Lagos β N70,000 [Gifts/Home]
|
| 19 |
+
- Plantain Clusters by Good Originals β N5,520 [Snacks]
|
| 20 |
+
|
| 21 |
+
### CATEGORIES
|
| 22 |
+
Fashion, Beauty, Home & Living, Wellness, Gifts, Leisure, Good Originals, Snacks, Drinks, Cook it Yourself, Worklife, For Moms, For Women
|
| 23 |
+
|
| 24 |
+
### CURATED LISTS (with starting prices)
|
| 25 |
+
- "For Everyone Carrying This Economy on Their Back" β from N3,000
|
| 26 |
+
- "A Good Place to Start" β from N500
|
| 27 |
+
- "Something to Add to Your Skincare Routine" β from N4,000
|
| 28 |
+
- "Some Thoughtful Gifts for Everyone" β from N13,000
|
| 29 |
+
- "The Faster Way to Familiar Dishes" β from N2,250
|
| 30 |
+
- "Everyday Comforts That Make Life Feel Lighter" β from N7,500
|
| 31 |
+
- "For the Kids We're All Soft For" β from N5,160
|
| 32 |
+
- "Cyn Ugwu's Picks to Living a Good Life" β from N4,200
|
| 33 |
+
- "Travel Essentials That Actually Come in Handy" β from N4,500
|
| 34 |
+
|
| 35 |
+
### TOP CREATORS (50+)
|
| 36 |
+
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
|
| 37 |
+
Beauty: Arami, Tamed Lux, Infused Organics, ESTEVAN PERFUMERY, Sugar Wax By Audrey, Sublime Skin by Jumai, Msmetics, Viektors Bath and Body, TGIN, Onoh Naturals
|
| 38 |
+
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
|
| 39 |
+
Wellness: Soft Mum Era, Enjoca, Olile Enterprises, Clove and Nectar, Just Journals
|
| 40 |
+
Snacks & Food: Chumbys, The M & W Nuts Company, Good Originals
|
| 41 |
+
Gifts: The Kulture Company, Not Just Pulp, FCAL (FC Accessories), The Ajala Game Store
|
| 42 |
+
Leisure: Atito, The Ajala Game Store
|
| 43 |
+
|
| 44 |
+
### STORE INFO
|
| 45 |
+
- Website: good.online
|
| 46 |
+
- Good House: 19B Sasegbon Street, Ikeja GRA, Lagos
|
| 47 |
+
- Good Village: Plot 33 Block 15, Admiralty Way, Lekki Phase 1, Lagos
|
| 48 |
+
- Contact: +2348142511154 / support@goodthingco.xyz
|
| 49 |
+
- Delivery: Lagos only
|
| 50 |
+
- Gift vouchers available at good.online
|
| 51 |
+
`;
|
| 52 |
+
|
| 53 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 54 |
+
// SYSTEM PROMPTS
|
| 55 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
|
| 57 |
+
window.buildShopperPrompt = function (customerName, history, wishlist) {
|
| 58 |
+
let prompt = `You are Oga Mi, a warm AI shopping assistant for Good Online (good.online), Nigeria's curated marketplace.
|
| 59 |
+
|
| 60 |
+
PERSONALITY:
|
| 61 |
+
- Warm, like a helpful Lagos market seller who genuinely cares
|
| 62 |
+
- Mix English with light Pidgin/Yoruba naturally ("Oga mi!", "Na good choice!", "E kaaro!")
|
| 63 |
+
- Enthusiastic about Nigerian creators
|
| 64 |
+
- Keep responses to 2-4 sentences. Recommend 1-2 products, not a long list
|
| 65 |
+
- Always finish your sentences
|
| 66 |
+
- Use Naira prices (write as N5,400)
|
| 67 |
+
|
| 68 |
+
${window.GOOD_CATALOG}
|
| 69 |
+
|
| 70 |
+
WHEN RECOMMENDING:
|
| 71 |
+
- Always mention price + creator
|
| 72 |
+
- Budget: "A Good Place to Start" (from N500)
|
| 73 |
+
- Gifts: "Some Thoughtful Gifts for Everyone" (from N13,000)
|
| 74 |
+
- Skincare: "Something to Add to Your Skincare Routine" (from N4,000)
|
| 75 |
+
|
| 76 |
+
When you recommend a specific product, end your message with [PRODUCT:Product Name|Creator|Price] so the UI can show an add-to-wishlist button. Example: [PRODUCT:Glow Oil|Arami|13600]. Only include this tag for products from the catalog above.`;
|
| 77 |
+
|
| 78 |
+
if (customerName) {
|
| 79 |
+
prompt += `\n\nCUSTOMER: ${customerName}. Use their name naturally.`;
|
| 80 |
+
}
|
| 81 |
+
if (wishlist && wishlist.length > 0) {
|
| 82 |
+
prompt += `\n\nWISHLIST: ${wishlist.map(w => w.name).join(', ')}. Reference these naturally if relevant.`;
|
| 83 |
+
}
|
| 84 |
+
if (history && history.length > 0) {
|
| 85 |
+
prompt += `\n\nPREVIOUS INTERACTIONS:\n${history.map(h => `- ${h}`).join('\n')}`;
|
| 86 |
+
}
|
| 87 |
+
return prompt;
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
window.buildCreatorPrompt = function (visionDescription, productName, price, creator) {
|
| 91 |
+
return `You are a copywriter for Good Online (good.online), a Nigerian curated marketplace. A creator is listing a new product. Generate a beautiful, warm, Nigerian-style product listing.
|
| 92 |
+
|
| 93 |
+
PRODUCT INFO:
|
| 94 |
+
- Name from creator: "${productName}"
|
| 95 |
+
- Creator: "${creator}"
|
| 96 |
+
- Price: N${Number(price).toLocaleString()}
|
| 97 |
+
- Vision analysis of the photo: "${visionDescription}"
|
| 98 |
+
|
| 99 |
+
Reference catalog (for style + category matching):
|
| 100 |
+
${window.GOOD_CATALOG}
|
| 101 |
+
|
| 102 |
+
OUTPUT REQUIREMENTS β respond ONLY with valid JSON, no other text:
|
| 103 |
+
{
|
| 104 |
+
"title": "A polished product name (max 6 words, sentence case)",
|
| 105 |
+
"tagline": "A 1-sentence editorial hook in warm, Nigerian voice (max 14 words)",
|
| 106 |
+
"description": "A 2-3 sentence description that tells the product's story, mentions the creator, and feels handmade/curated. Sentence case. No emoji.",
|
| 107 |
+
"category": "One category from this list: Fashion, Beauty, Home & Living, Wellness, Gifts, Leisure, Snacks, Drinks, Cook it Yourself, Worklife, For Moms, For Women",
|
| 108 |
+
"curatedList": "One of these curated lists if relevant, or null: A Good Place to Start | Some Thoughtful Gifts for Everyone | Something to Add to Your Skincare Routine | Everyday Comforts That Make Life Feel Lighter | For the Kids We're All Soft For | Travel Essentials That Actually Come in Handy",
|
| 109 |
+
"priceFeedback": "1 sentence: is this price aligned with similar products on Good? Be honest."
|
| 110 |
+
}`;
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
+
// 0G COMPUTE HELPERS (Direct mode, browser-CORS-friendly)
|
| 115 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
|
| 117 |
+
window.OG_ENDPOINTS = {
|
| 118 |
+
chat: 'https://compute-network-1.integratenetwork.work/v1/proxy/chat/completions',
|
| 119 |
+
whisper: 'https://compute-network-16.integratenetwork.work/v1/proxy/audio/transcriptions',
|
| 120 |
+
// Vision provider may live on a different compute-network β verify on pc.0g.ai
|
| 121 |
+
vision: 'https://compute-network-1.integratenetwork.work/v1/proxy/chat/completions',
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
window.askAI = async function (chatKey, model, messages, { maxTokens = 512 } = {}) {
|
| 125 |
+
const res = await fetch(window.OG_ENDPOINTS.chat, {
|
| 126 |
+
method: 'POST',
|
| 127 |
+
headers: { 'Authorization': `Bearer ${chatKey}`, 'Content-Type': 'application/json' },
|
| 128 |
+
body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
|
| 129 |
+
});
|
| 130 |
+
if (!res.ok) throw new Error(`Chat ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
| 131 |
+
const data = await res.json();
|
| 132 |
+
return (data.choices?.[0]?.message?.content || '').trim();
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
window.askVision = async function (chatKey, model, imageDataUrl, instruction, { maxTokens = 256 } = {}) {
|
| 136 |
+
// OpenAI-compatible vision message format
|
| 137 |
+
const res = await fetch(window.OG_ENDPOINTS.vision, {
|
| 138 |
+
method: 'POST',
|
| 139 |
+
headers: { 'Authorization': `Bearer ${chatKey}`, 'Content-Type': 'application/json' },
|
| 140 |
+
body: JSON.stringify({
|
| 141 |
+
model,
|
| 142 |
+
messages: [{
|
| 143 |
+
role: 'user',
|
| 144 |
+
content: [
|
| 145 |
+
{ type: 'text', text: instruction },
|
| 146 |
+
{ type: 'image_url', image_url: { url: imageDataUrl } },
|
| 147 |
+
],
|
| 148 |
+
}],
|
| 149 |
+
max_tokens: maxTokens,
|
| 150 |
+
}),
|
| 151 |
+
});
|
| 152 |
+
if (!res.ok) throw new Error(`Vision ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
| 153 |
+
const data = await res.json();
|
| 154 |
+
return (data.choices?.[0]?.message?.content || '').trim();
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
// Convert browser webm/opus audio to PCM WAV (0G Whisper rejects raw webm)
|
| 158 |
+
window.webmBlobToWav = async function (blob) {
|
| 159 |
+
const buf = await blob.arrayBuffer();
|
| 160 |
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
| 161 |
+
const audio = await ctx.decodeAudioData(buf);
|
| 162 |
+
try { ctx.close(); } catch (_) {}
|
| 163 |
+
const ch0 = audio.getChannelData(0);
|
| 164 |
+
const len = ch0.length;
|
| 165 |
+
const sampleRate = audio.sampleRate;
|
| 166 |
+
const out = new ArrayBuffer(44 + len * 2);
|
| 167 |
+
const v = new DataView(out);
|
| 168 |
+
const ws = (off, s) => { for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i)); };
|
| 169 |
+
ws(0, 'RIFF'); v.setUint32(4, 36 + len * 2, true); ws(8, 'WAVE');
|
| 170 |
+
ws(12, 'fmt '); v.setUint32(16, 16, true);
|
| 171 |
+
v.setUint16(20, 1, true); v.setUint16(22, 1, true);
|
| 172 |
+
v.setUint32(24, sampleRate, true); v.setUint32(28, sampleRate * 2, true);
|
| 173 |
+
v.setUint16(32, 2, true); v.setUint16(34, 16, true);
|
| 174 |
+
ws(36, 'data'); v.setUint32(40, len * 2, true);
|
| 175 |
+
let off = 44;
|
| 176 |
+
for (let i = 0; i < len; i++) {
|
| 177 |
+
const s = Math.max(-1, Math.min(1, ch0[i]));
|
| 178 |
+
v.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
| 179 |
+
off += 2;
|
| 180 |
+
}
|
| 181 |
+
return new Blob([out], { type: 'audio/wav' });
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
window.transcribe = async function (sttKey, webmBlob) {
|
| 185 |
+
const wav = await window.webmBlobToWav(webmBlob);
|
| 186 |
+
const form = new FormData();
|
| 187 |
+
form.append('file', wav, 'audio.wav');
|
| 188 |
+
form.append('model', 'openai/whisper-large-v3');
|
| 189 |
+
form.append('response_format', 'json');
|
| 190 |
+
const res = await fetch(window.OG_ENDPOINTS.whisper, {
|
| 191 |
+
method: 'POST',
|
| 192 |
+
headers: { 'Authorization': `Bearer ${sttKey}` },
|
| 193 |
+
body: form,
|
| 194 |
+
});
|
| 195 |
+
if (!res.ok) throw new Error(`STT ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
| 196 |
+
return (await res.json()).text || '';
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 200 |
+
// 0G CHAIN HELPERS
|
| 201 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 202 |
+
window.OG_CHAIN = {
|
| 203 |
+
rpc: 'https://evmrpc-testnet.0g.ai',
|
| 204 |
+
chainId: 16602,
|
| 205 |
+
explorerTx: (h) => `https://chainscan-galileo.0g.ai/tx/${h}`,
|
| 206 |
+
// Deployed ListingNFT contract address β paste after running contract/deploy.js
|
| 207 |
+
listingNFT: '',
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
window.LISTING_NFT_ABI = [
|
| 211 |
+
'function mint(string dataDescription, bytes32 dataHash) external returns (uint256)',
|
| 212 |
+
'function getData(uint256 tokenId) external view returns (string dataDescription, bytes32 dataHash, address creator, uint256 timestamp)',
|
| 213 |
+
'function totalSupply() external view returns (uint256)',
|
| 214 |
+
'function ownerOf(uint256 tokenId) external view returns (address)',
|
| 215 |
+
'function transferFrom(address from, address to, uint256 tokenId) external',
|
| 216 |
+
'event ListingMinted(uint256 indexed tokenId, address indexed creator, string dataDescription, bytes32 dataHash, uint256 timestamp)',
|
| 217 |
+
];
|
| 218 |
+
|
| 219 |
+
window.shortHash = function (h) {
|
| 220 |
+
return h.slice(0, 10) + 'β¦' + h.slice(-6);
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 224 |
+
// LOCAL STORAGE HELPERS (0G Storage proxy)
|
| 225 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
+
window.OG_STORAGE = {
|
| 227 |
+
load(key) {
|
| 228 |
+
try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (_) { return null; }
|
| 229 |
+
},
|
| 230 |
+
save(key, value) {
|
| 231 |
+
localStorage.setItem(key, JSON.stringify(value));
|
| 232 |
+
},
|
| 233 |
+
};
|
shopper.html
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Oga Market β Shopper</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
|
| 11 |
+
<!-- βββ SETUP βββ -->
|
| 12 |
+
<div id="setup" class="om-setup">
|
| 13 |
+
<div class="om-setup-card">
|
| 14 |
+
<h2>Welcome, oga.</h2>
|
| 15 |
+
<p class="sub">Tell me your name and paste your 0G API key. I'll remember what you like.</p>
|
| 16 |
+
|
| 17 |
+
<div class="om-field">
|
| 18 |
+
<label>Your name</label>
|
| 19 |
+
<input id="cfg-name" type="text" placeholder="What should I call you?" spellcheck="false" />
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="om-field">
|
| 23 |
+
<label>0G Chat API Key</label>
|
| 24 |
+
<input id="cfg-chat-key" type="password" placeholder="app-sk-..." spellcheck="false" />
|
| 25 |
+
<div class="om-field-hint"><a href="https://pc.0g.ai" target="_blank">pc.0g.ai</a> β Advanced mode β fund <code>zai-org/GLM-5-FP8</code> β mint key</div>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="om-field">
|
| 29 |
+
<label>Whisper API Key (optional β enables voice)</label>
|
| 30 |
+
<input id="cfg-stt-key" type="password" placeholder="app-sk-... (different key)" />
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="om-field">
|
| 34 |
+
<label>0G Chain Private Key (optional β on-chain receipts)</label>
|
| 35 |
+
<input id="cfg-chain-key" type="password" placeholder="0x..." />
|
| 36 |
+
<div class="om-field-hint">Free testnet tokens: <a href="https://faucet.0g.ai" target="_blank">faucet.0g.ai</a></div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div id="cfg-err" class="om-err"></div>
|
| 40 |
+
<button class="om-cta" onclick="startShopper()">Start shopping</button>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- βββ TOPBAR βββ -->
|
| 45 |
+
<div class="om-topbar">
|
| 46 |
+
<div class="om-brand">
|
| 47 |
+
<svg width="24" height="24" viewBox="0 0 96 96" fill="none">
|
| 48 |
+
<rect x="14" y="30" width="68" height="56" rx="6" fill="#1F3B2D"/>
|
| 49 |
+
<circle cx="36" cy="58" r="6" fill="#F5EFE3"/>
|
| 50 |
+
<circle cx="60" cy="58" r="6" fill="#F5EFE3"/>
|
| 51 |
+
<line x1="34" y1="30" x2="34" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 52 |
+
<line x1="62" y1="30" x2="62" y2="14" stroke="#15130F" stroke-width="3" stroke-linecap="round"/>
|
| 53 |
+
</svg>
|
| 54 |
+
<a href="index.html">Oga Market</a>
|
| 55 |
+
<span class="om-brand-tag">Shopper</span>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="om-topbar-actions">
|
| 58 |
+
<div class="om-services">
|
| 59 |
+
<span class="om-chip compute" id="chip-compute">Compute</span>
|
| 60 |
+
<span class="om-chip storage" id="chip-storage">Storage</span>
|
| 61 |
+
<span class="om-chip chain" id="chip-chain">Chain</span>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- βββ APP βββ -->
|
| 67 |
+
<main class="om-app">
|
| 68 |
+
<div class="om-app-grid-shopper">
|
| 69 |
+
|
| 70 |
+
<div class="om-conv">
|
| 71 |
+
<div class="om-conv-head">
|
| 72 |
+
<h3>Chat with Oga Mi</h3>
|
| 73 |
+
<span id="customer-badge"></span>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div id="transcript"></div>
|
| 77 |
+
|
| 78 |
+
<div class="om-input-area">
|
| 79 |
+
<div class="om-input-row">
|
| 80 |
+
<input id="chat-input" placeholder="What are you looking for today?" autocomplete="off" />
|
| 81 |
+
<button class="om-mic" id="mic-btn" title="Tap to record">
|
| 82 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 83 |
+
<rect x="9" y="2" width="6" height="11" rx="3"/>
|
| 84 |
+
<path d="M5 10a7 7 0 0 0 14 0"/>
|
| 85 |
+
<line x1="12" y1="17" x2="12" y2="21"/>
|
| 86 |
+
</svg>
|
| 87 |
+
</button>
|
| 88 |
+
<button class="om-send" onclick="sendMessage()">
|
| 89 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 90 |
+
<path d="M5 12h14m-6-6 6 6-6 6"/>
|
| 91 |
+
</svg>
|
| 92 |
+
</button>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<aside class="om-side">
|
| 98 |
+
<div>
|
| 99 |
+
<span class="om-side-eyebrow">Your wishlist Β· saved to 0G Storage</span>
|
| 100 |
+
<h4>What you're eyeing</h4>
|
| 101 |
+
</div>
|
| 102 |
+
<div id="wishlist" class="om-wishlist">
|
| 103 |
+
<div class="om-wishlist-empty">Nothing yet β ask Oga Mi for ideas.</div>
|
| 104 |
+
</div>
|
| 105 |
+
<div style="margin-top:auto; padding-top: 16px; border-top: 1px solid var(--border-1);">
|
| 106 |
+
<span class="om-side-eyebrow">Good House Β· Ikeja GRA</span>
|
| 107 |
+
<p style="font-size: 12px; color: var(--fg-2); margin-top: 4px; line-height: 1.5;">19B Sasegbon Street, Ikeja GRA. Stop by to meet the real Oga Mi (he's a robot!).</p>
|
| 108 |
+
</div>
|
| 109 |
+
</aside>
|
| 110 |
+
</div>
|
| 111 |
+
</main>
|
| 112 |
+
|
| 113 |
+
<!-- ethers.js for 0G Chain -->
|
| 114 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.4/ethers.umd.min.js"></script>
|
| 115 |
+
<script src="shared.js"></script>
|
| 116 |
+
<script>
|
| 117 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 118 |
+
// STATE
|
| 119 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 120 |
+
let CFG = {};
|
| 121 |
+
let conversationHistory = [];
|
| 122 |
+
let interactionCount = 0;
|
| 123 |
+
let chainProvider = null, chainWallet = null;
|
| 124 |
+
|
| 125 |
+
// Customer memory (0G Storage proxy)
|
| 126 |
+
let customerMemory = window.OG_STORAGE.load('omk-customers') || {};
|
| 127 |
+
let currentCustomer = null;
|
| 128 |
+
let wishlist = [];
|
| 129 |
+
|
| 130 |
+
function saveMemory() {
|
| 131 |
+
window.OG_STORAGE.save('omk-customers', customerMemory);
|
| 132 |
+
document.getElementById('chip-storage').classList.add('active');
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
// RESTORE
|
| 137 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 138 |
+
(function () {
|
| 139 |
+
const saved = sessionStorage.getItem('omk-shopper-cfg');
|
| 140 |
+
if (!saved) return;
|
| 141 |
+
try {
|
| 142 |
+
const s = JSON.parse(saved);
|
| 143 |
+
if (s.name) document.getElementById('cfg-name').value = s.name;
|
| 144 |
+
if (s.chatKey) document.getElementById('cfg-chat-key').value = s.chatKey;
|
| 145 |
+
if (s.sttKey) document.getElementById('cfg-stt-key').value = s.sttKey;
|
| 146 |
+
if (s.chainKey) document.getElementById('cfg-chain-key').value = s.chainKey;
|
| 147 |
+
} catch (_) {}
|
| 148 |
+
})();
|
| 149 |
+
|
| 150 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 151 |
+
// START
|
| 152 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 153 |
+
window.startShopper = async function () {
|
| 154 |
+
const chatKey = document.getElementById('cfg-chat-key').value.trim();
|
| 155 |
+
if (!chatKey) return showErr('Chat API key is required.');
|
| 156 |
+
const name = document.getElementById('cfg-name').value.trim() || 'Friend';
|
| 157 |
+
const sttKey = document.getElementById('cfg-stt-key').value.trim();
|
| 158 |
+
const chainKey = document.getElementById('cfg-chain-key').value.trim();
|
| 159 |
+
|
| 160 |
+
CFG = { chatKey, sttKey, chainKey, name, model: 'zai-org/GLM-5-FP8' };
|
| 161 |
+
|
| 162 |
+
// Verify chat key
|
| 163 |
+
try {
|
| 164 |
+
await window.askAI(chatKey, CFG.model, [{ role: 'user', content: 'hi' }], { maxTokens: 3 });
|
| 165 |
+
} catch (e) {
|
| 166 |
+
return showErr('Key verification failed: ' + e.message);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
sessionStorage.setItem('omk-shopper-cfg', JSON.stringify(CFG));
|
| 170 |
+
document.getElementById('setup').classList.add('hidden');
|
| 171 |
+
document.getElementById('chip-compute').classList.add('active');
|
| 172 |
+
|
| 173 |
+
currentCustomer = name;
|
| 174 |
+
if (!customerMemory[name]) {
|
| 175 |
+
customerMemory[name] = { visits: 0, interactions: [], wishlist: [] };
|
| 176 |
+
}
|
| 177 |
+
customerMemory[name].visits++;
|
| 178 |
+
customerMemory[name].lastVisit = new Date().toISOString();
|
| 179 |
+
wishlist = customerMemory[name].wishlist || [];
|
| 180 |
+
saveMemory();
|
| 181 |
+
renderWishlist();
|
| 182 |
+
|
| 183 |
+
const badge = document.getElementById('customer-badge');
|
| 184 |
+
badge.textContent = name;
|
| 185 |
+
badge.style.display = '';
|
| 186 |
+
|
| 187 |
+
const history = customerMemory[name].interactions.slice(-5);
|
| 188 |
+
conversationHistory = [{
|
| 189 |
+
role: 'system',
|
| 190 |
+
content: window.buildShopperPrompt(name, history, wishlist),
|
| 191 |
+
}];
|
| 192 |
+
|
| 193 |
+
const visits = customerMemory[name].visits;
|
| 194 |
+
if (visits > 1) {
|
| 195 |
+
addSystem(`0G Storage: Returning customer detected (visit #${visits})`);
|
| 196 |
+
document.getElementById('chip-storage').classList.add('active');
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if (chainKey) initChain();
|
| 200 |
+
|
| 201 |
+
// Welcome
|
| 202 |
+
const welcomePrompt = visits > 1
|
| 203 |
+
? `${name} just came back (visit #${visits}). Welcome them warmly in 1-2 sentences and ask what they're looking for.`
|
| 204 |
+
: `${name} just walked in for the first time. Greet warmly in 1-2 sentences. Ask what they need.`;
|
| 205 |
+
|
| 206 |
+
try {
|
| 207 |
+
const reply = await window.askAI(chatKey, CFG.model, [
|
| 208 |
+
...conversationHistory,
|
| 209 |
+
{ role: 'user', content: welcomePrompt },
|
| 210 |
+
], { maxTokens: 100 });
|
| 211 |
+
addBot(reply);
|
| 212 |
+
} catch (e) {
|
| 213 |
+
addBot(`Welcome to Good Online, ${name}! How can I help you today?`);
|
| 214 |
+
}
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
function showErr(msg) {
|
| 218 |
+
const el = document.getElementById('cfg-err');
|
| 219 |
+
el.textContent = msg;
|
| 220 |
+
el.style.display = 'block';
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 224 |
+
// CHAIN
|
| 225 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
+
async function initChain() {
|
| 227 |
+
try {
|
| 228 |
+
chainProvider = new ethers.JsonRpcProvider(window.OG_CHAIN.rpc);
|
| 229 |
+
chainWallet = new ethers.Wallet(CFG.chainKey, chainProvider);
|
| 230 |
+
const balance = await chainProvider.getBalance(chainWallet.address);
|
| 231 |
+
if (balance > 0n) {
|
| 232 |
+
document.getElementById('chip-chain').classList.add('active');
|
| 233 |
+
addSystem(`0G Chain: ${chainWallet.address.slice(0, 8)}β¦${chainWallet.address.slice(-4)}`);
|
| 234 |
+
} else {
|
| 235 |
+
addSystem('0G Chain: no balance β visit faucet.0g.ai');
|
| 236 |
+
}
|
| 237 |
+
} catch (e) {
|
| 238 |
+
addSystem('0G Chain: ' + e.message);
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
async function logShopOnChain(query, recommendation) {
|
| 243 |
+
if (!chainWallet) return;
|
| 244 |
+
try {
|
| 245 |
+
const data = ethers.AbiCoder.defaultAbiCoder().encode(
|
| 246 |
+
['string', 'string', 'string', 'uint256'],
|
| 247 |
+
[CFG.name, query.slice(0, 100), recommendation.slice(0, 200), Math.floor(Date.now() / 1000)]
|
| 248 |
+
);
|
| 249 |
+
const tx = await chainWallet.sendTransaction({ to: chainWallet.address, value: 0n, data });
|
| 250 |
+
const receipt = await tx.wait();
|
| 251 |
+
addReceipt(receipt.hash);
|
| 252 |
+
} catch (e) {
|
| 253 |
+
addSystem('Chain: ' + e.message.slice(0, 80));
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
+
// CHAT
|
| 259 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 260 |
+
let sending = false;
|
| 261 |
+
|
| 262 |
+
window.sendMessage = async function () {
|
| 263 |
+
const input = document.getElementById('chat-input');
|
| 264 |
+
const text = input.value.trim();
|
| 265 |
+
if (!text || sending) return;
|
| 266 |
+
input.value = '';
|
| 267 |
+
sending = true;
|
| 268 |
+
|
| 269 |
+
addUser(text);
|
| 270 |
+
conversationHistory.push({ role: 'user', content: text });
|
| 271 |
+
|
| 272 |
+
try {
|
| 273 |
+
const raw = await window.askAI(CFG.chatKey, CFG.model, conversationHistory.slice(-12));
|
| 274 |
+
// Parse [PRODUCT:name|creator|price] tags
|
| 275 |
+
const productMatch = raw.match(/\[PRODUCT:([^|]+)\|([^|]+)\|(\d+)\]/i);
|
| 276 |
+
const clean = raw.replace(/\[PRODUCT:[^\]]+\]/gi, '').trim();
|
| 277 |
+
addBot(clean);
|
| 278 |
+
if (productMatch) {
|
| 279 |
+
addProductCard(productMatch[1].trim(), productMatch[2].trim(), Number(productMatch[3]));
|
| 280 |
+
}
|
| 281 |
+
conversationHistory.push({ role: 'assistant', content: raw });
|
| 282 |
+
|
| 283 |
+
// Save to memory
|
| 284 |
+
interactionCount++;
|
| 285 |
+
if (currentCustomer) {
|
| 286 |
+
customerMemory[currentCustomer].interactions.push(`Asked: "${text.slice(0, 60)}" β Said: "${clean.slice(0, 80)}"`);
|
| 287 |
+
if (customerMemory[currentCustomer].interactions.length > 20) {
|
| 288 |
+
customerMemory[currentCustomer].interactions = customerMemory[currentCustomer].interactions.slice(-20);
|
| 289 |
+
}
|
| 290 |
+
saveMemory();
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// On-chain receipt every 2nd interaction
|
| 294 |
+
if (chainWallet && interactionCount % 2 === 0) {
|
| 295 |
+
logShopOnChain(text, clean);
|
| 296 |
+
}
|
| 297 |
+
} catch (e) {
|
| 298 |
+
addSystem('Error: ' + e.message);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
sending = false;
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
document.getElementById('chat-input').addEventListener('keydown', e => {
|
| 305 |
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
| 306 |
+
});
|
| 307 |
+
|
| 308 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 309 |
+
// VOICE
|
| 310 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 311 |
+
let mediaRecorder = null, audioChunks = [];
|
| 312 |
+
const micBtn = document.getElementById('mic-btn');
|
| 313 |
+
|
| 314 |
+
micBtn.addEventListener('click', async () => {
|
| 315 |
+
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
| 316 |
+
mediaRecorder.stop();
|
| 317 |
+
return;
|
| 318 |
+
}
|
| 319 |
+
if (!CFG.sttKey) { addSystem('Voice needs a Whisper key β add one in setup.'); return; }
|
| 320 |
+
try {
|
| 321 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 322 |
+
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
|
| 323 |
+
audioChunks = [];
|
| 324 |
+
micBtn.style.background = 'var(--terracotta)';
|
| 325 |
+
micBtn.style.color = 'var(--cream)';
|
| 326 |
+
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
| 327 |
+
mediaRecorder.onstop = async () => {
|
| 328 |
+
stream.getTracks().forEach(t => t.stop());
|
| 329 |
+
micBtn.style.background = '';
|
| 330 |
+
micBtn.style.color = '';
|
| 331 |
+
if (audioChunks.length === 0) return;
|
| 332 |
+
const blob = new Blob(audioChunks, { type: 'audio/webm;codecs=opus' });
|
| 333 |
+
addSystem('Transcribing via 0G Whisperβ¦');
|
| 334 |
+
try {
|
| 335 |
+
const txt = await window.transcribe(CFG.sttKey, blob);
|
| 336 |
+
if (txt.trim()) {
|
| 337 |
+
document.getElementById('chat-input').value = txt;
|
| 338 |
+
sendMessage();
|
| 339 |
+
} else { addSystem('No speech detected.'); }
|
| 340 |
+
} catch (e) { addSystem('STT: ' + e.message); }
|
| 341 |
+
};
|
| 342 |
+
mediaRecorder.start(250);
|
| 343 |
+
} catch (e) {
|
| 344 |
+
addSystem('Mic: ' + e.message);
|
| 345 |
+
}
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 349 |
+
// WISHLIST
|
| 350 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 351 |
+
function addToWishlist(name, creator, price) {
|
| 352 |
+
if (wishlist.find(w => w.name === name)) return;
|
| 353 |
+
wishlist.push({ name, creator, price, added: Date.now() });
|
| 354 |
+
if (currentCustomer) {
|
| 355 |
+
customerMemory[currentCustomer].wishlist = wishlist;
|
| 356 |
+
saveMemory();
|
| 357 |
+
}
|
| 358 |
+
renderWishlist();
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
function removeFromWishlist(name) {
|
| 362 |
+
wishlist = wishlist.filter(w => w.name !== name);
|
| 363 |
+
if (currentCustomer) {
|
| 364 |
+
customerMemory[currentCustomer].wishlist = wishlist;
|
| 365 |
+
saveMemory();
|
| 366 |
+
}
|
| 367 |
+
renderWishlist();
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
function renderWishlist() {
|
| 371 |
+
const el = document.getElementById('wishlist');
|
| 372 |
+
if (wishlist.length === 0) {
|
| 373 |
+
el.innerHTML = '<div class="om-wishlist-empty">Nothing yet β ask Oga Mi for ideas.</div>';
|
| 374 |
+
return;
|
| 375 |
+
}
|
| 376 |
+
el.innerHTML = wishlist.map(w => `
|
| 377 |
+
<div class="om-wishlist-item">
|
| 378 |
+
<span class="name">${escapeHtml(w.name)}<br><small style="color:var(--fg-3);font-size:10px;">${escapeHtml(w.creator)}</small></span>
|
| 379 |
+
<span class="price">N${w.price.toLocaleString()}</span>
|
| 380 |
+
<button class="remove" onclick="removeFromWishlist('${escapeHtml(w.name).replace(/'/g, "\\'")}')">Γ</button>
|
| 381 |
+
</div>
|
| 382 |
+
`).join('');
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
window.removeFromWishlist = removeFromWishlist;
|
| 386 |
+
|
| 387 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 388 |
+
// RENDER HELPERS
|
| 389 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 390 |
+
function addUser(text) { addMsg('user', text); }
|
| 391 |
+
function addBot(text) { addMsg('bot', text); }
|
| 392 |
+
function addSystem(text) { addMsg('system', text); }
|
| 393 |
+
|
| 394 |
+
function addMsg(role, text) {
|
| 395 |
+
const t = document.getElementById('transcript');
|
| 396 |
+
const el = document.createElement('div');
|
| 397 |
+
el.className = `msg ${role}`;
|
| 398 |
+
el.textContent = text;
|
| 399 |
+
t.appendChild(el);
|
| 400 |
+
t.scrollTop = t.scrollHeight;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
function addReceipt(txHash) {
|
| 404 |
+
const t = document.getElementById('transcript');
|
| 405 |
+
const el = document.createElement('div');
|
| 406 |
+
el.className = 'msg receipt';
|
| 407 |
+
el.innerHTML = `<a href="${window.OG_CHAIN.explorerTx(txHash)}" target="_blank" style="color:inherit;text-decoration:none;">0G Chain Β· ${window.shortHash(txHash)}</a>`;
|
| 408 |
+
t.appendChild(el);
|
| 409 |
+
t.scrollTop = t.scrollHeight;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function addProductCard(name, creator, price) {
|
| 413 |
+
const t = document.getElementById('transcript');
|
| 414 |
+
const el = document.createElement('div');
|
| 415 |
+
el.className = 'msg product-card';
|
| 416 |
+
el.innerHTML = `
|
| 417 |
+
<div class="pc-img">${escapeHtml(name.slice(0,1))}</div>
|
| 418 |
+
<div class="pc-body">
|
| 419 |
+
<div class="pc-title">${escapeHtml(name)}</div>
|
| 420 |
+
<div class="pc-creator">by ${escapeHtml(creator)}</div>
|
| 421 |
+
<div class="pc-price">N${price.toLocaleString()}</div>
|
| 422 |
+
<button class="pc-add">+ Add to wishlist</button>
|
| 423 |
+
</div>
|
| 424 |
+
`;
|
| 425 |
+
el.querySelector('.pc-add').addEventListener('click', () => {
|
| 426 |
+
addToWishlist(name, creator, price);
|
| 427 |
+
el.querySelector('.pc-add').textContent = 'β Saved';
|
| 428 |
+
el.querySelector('.pc-add').disabled = true;
|
| 429 |
+
});
|
| 430 |
+
t.appendChild(el);
|
| 431 |
+
t.scrollTop = t.scrollHeight;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
function escapeHtml(s) {
|
| 435 |
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 436 |
+
}
|
| 437 |
+
</script>
|
| 438 |
+
|
| 439 |
+
</body>
|
| 440 |
+
</html>
|
styles.css
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Oga Market β styles using the Good Online + Oga Mi design system */
|
| 2 |
+
|
| 3 |
+
@import url('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');
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--forest: #1F3B2D;
|
| 7 |
+
--forest-hover: #16291F;
|
| 8 |
+
--forest-soft: #2E5840;
|
| 9 |
+
--terracotta: #C84B23;
|
| 10 |
+
--terracotta-soft: #E0744F;
|
| 11 |
+
--saffron: #E8B547;
|
| 12 |
+
--saffron-soft: #F2D080;
|
| 13 |
+
--cream: #F5EFE3;
|
| 14 |
+
--cream-deep: #ECE3CF;
|
| 15 |
+
--paper: #FBF7EE;
|
| 16 |
+
--ink: #15130F;
|
| 17 |
+
--ink-soft: #2A2620;
|
| 18 |
+
--stone-1: #EAE3D2;
|
| 19 |
+
--stone-2: #D9D2C0;
|
| 20 |
+
--stone-3: #B8AD96;
|
| 21 |
+
--stone-4: #8A8170;
|
| 22 |
+
--stone-5: #6B6354;
|
| 23 |
+
--fg-1: var(--ink);
|
| 24 |
+
--fg-2: var(--stone-5);
|
| 25 |
+
--fg-3: var(--stone-4);
|
| 26 |
+
--bg-1: var(--cream);
|
| 27 |
+
--bg-2: var(--paper);
|
| 28 |
+
--bg-3: var(--cream-deep);
|
| 29 |
+
--border-1: var(--stone-2);
|
| 30 |
+
--accent: var(--forest);
|
| 31 |
+
--accent-hover: var(--forest-hover);
|
| 32 |
+
--warn: var(--terracotta);
|
| 33 |
+
--highlight: var(--saffron);
|
| 34 |
+
--font-serif: 'Instrument Serif', Georgia, serif;
|
| 35 |
+
--font-sans: 'Geist', system-ui, sans-serif;
|
| 36 |
+
--font-mono: 'Geist Mono', 'Courier New', monospace;
|
| 37 |
+
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
| 38 |
+
--sp-6: 24px; --sp-8: 32px; --sp-12: 48px; --sp-16: 64px;
|
| 39 |
+
--radius-sm: 2px;
|
| 40 |
+
--radius-md: 4px;
|
| 41 |
+
--radius-pill: 999px;
|
| 42 |
+
--shadow-1: 0 1px 2px rgba(21,19,15,0.06);
|
| 43 |
+
--shadow-2: 0 8px 24px -8px rgba(21,19,15,0.12);
|
| 44 |
+
--shadow-3: 0 24px 64px -16px rgba(21,19,15,0.22);
|
| 45 |
+
--ease: cubic-bezier(.2,.7,.2,1);
|
| 46 |
+
--dur-quick: 120ms;
|
| 47 |
+
--dur-state: 200ms;
|
| 48 |
+
--dur-panel: 400ms;
|
| 49 |
+
--content-max: 1280px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 53 |
+
|
| 54 |
+
body {
|
| 55 |
+
font-family: var(--font-sans);
|
| 56 |
+
background: var(--cream);
|
| 57 |
+
color: var(--fg-1);
|
| 58 |
+
-webkit-font-smoothing: antialiased;
|
| 59 |
+
min-height: 100vh;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
a { color: var(--forest); text-decoration: none; }
|
| 63 |
+
a:hover { text-decoration: underline; }
|
| 64 |
+
|
| 65 |
+
/* βββ TOPBAR βββ */
|
| 66 |
+
.om-topbar {
|
| 67 |
+
display: flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
justify-content: space-between;
|
| 70 |
+
padding: 14px var(--sp-6);
|
| 71 |
+
background: var(--paper);
|
| 72 |
+
border-bottom: 1px solid var(--border-1);
|
| 73 |
+
position: sticky;
|
| 74 |
+
top: 0;
|
| 75 |
+
z-index: 50;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.om-brand {
|
| 79 |
+
display: flex;
|
| 80 |
+
align-items: center;
|
| 81 |
+
gap: var(--sp-3);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.om-brand a {
|
| 85 |
+
font-family: var(--font-serif);
|
| 86 |
+
font-style: italic;
|
| 87 |
+
font-size: 22px;
|
| 88 |
+
color: var(--fg-1);
|
| 89 |
+
text-decoration: none;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.om-brand-tag {
|
| 93 |
+
font-family: var(--font-sans);
|
| 94 |
+
font-size: 10px;
|
| 95 |
+
font-weight: 500;
|
| 96 |
+
letter-spacing: 0.16em;
|
| 97 |
+
text-transform: uppercase;
|
| 98 |
+
color: var(--fg-2);
|
| 99 |
+
padding: 3px 10px;
|
| 100 |
+
border: 1px solid var(--border-1);
|
| 101 |
+
border-radius: var(--radius-sm);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.om-topbar-actions {
|
| 105 |
+
display: flex;
|
| 106 |
+
align-items: center;
|
| 107 |
+
gap: var(--sp-3);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.om-services {
|
| 111 |
+
display: flex;
|
| 112 |
+
gap: var(--sp-2);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.om-chip {
|
| 116 |
+
display: flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
gap: 4px;
|
| 119 |
+
font-family: var(--font-sans);
|
| 120 |
+
font-size: 10px;
|
| 121 |
+
font-weight: 500;
|
| 122 |
+
letter-spacing: 0.12em;
|
| 123 |
+
text-transform: uppercase;
|
| 124 |
+
padding: 3px 8px;
|
| 125 |
+
border-radius: var(--radius-pill);
|
| 126 |
+
opacity: 0.35;
|
| 127 |
+
transition: opacity var(--dur-state);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.om-chip.active { opacity: 1; }
|
| 131 |
+
.om-chip.compute { background: rgba(31,59,45,0.1); color: var(--forest); }
|
| 132 |
+
.om-chip.storage { background: rgba(232,181,71,0.12); color: #8A6A1A; }
|
| 133 |
+
.om-chip.chain { background: rgba(200,75,35,0.1); color: var(--terracotta); }
|
| 134 |
+
|
| 135 |
+
.om-chip::before {
|
| 136 |
+
content: '';
|
| 137 |
+
width: 5px; height: 5px;
|
| 138 |
+
border-radius: 50%;
|
| 139 |
+
background: currentColor;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/* βββ LANDING (mode selector) βββ */
|
| 143 |
+
.om-landing {
|
| 144 |
+
max-width: 1100px;
|
| 145 |
+
margin: 0 auto;
|
| 146 |
+
padding: var(--sp-16) var(--sp-6) var(--sp-8);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.om-landing-eyebrow {
|
| 150 |
+
font-family: var(--font-sans);
|
| 151 |
+
font-size: 12px;
|
| 152 |
+
font-weight: 500;
|
| 153 |
+
letter-spacing: 0.18em;
|
| 154 |
+
text-transform: uppercase;
|
| 155 |
+
color: var(--fg-2);
|
| 156 |
+
text-align: center;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.om-landing h1 {
|
| 160 |
+
font-family: var(--font-serif);
|
| 161 |
+
font-style: italic;
|
| 162 |
+
font-size: 84px;
|
| 163 |
+
line-height: 88px;
|
| 164 |
+
font-weight: 400;
|
| 165 |
+
letter-spacing: -0.02em;
|
| 166 |
+
text-align: center;
|
| 167 |
+
margin-top: var(--sp-4);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.om-landing .sub {
|
| 171 |
+
font-family: var(--font-sans);
|
| 172 |
+
font-size: 17px;
|
| 173 |
+
line-height: 26px;
|
| 174 |
+
color: var(--fg-2);
|
| 175 |
+
max-width: 600px;
|
| 176 |
+
margin: var(--sp-4) auto 0;
|
| 177 |
+
text-align: center;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.om-modes {
|
| 181 |
+
display: grid;
|
| 182 |
+
grid-template-columns: 1fr 1fr;
|
| 183 |
+
gap: var(--sp-6);
|
| 184 |
+
margin-top: var(--sp-12);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.om-mode {
|
| 188 |
+
background: var(--paper);
|
| 189 |
+
border: 1px solid var(--border-1);
|
| 190 |
+
padding: var(--sp-8);
|
| 191 |
+
text-decoration: none;
|
| 192 |
+
color: var(--fg-1);
|
| 193 |
+
display: flex;
|
| 194 |
+
flex-direction: column;
|
| 195 |
+
gap: var(--sp-3);
|
| 196 |
+
transition: all var(--dur-state) var(--ease);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.om-mode:hover {
|
| 200 |
+
background: var(--bg-3);
|
| 201 |
+
transform: translateY(-2px);
|
| 202 |
+
box-shadow: var(--shadow-2);
|
| 203 |
+
text-decoration: none;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.om-mode-icon {
|
| 207 |
+
width: 56px; height: 56px;
|
| 208 |
+
background: var(--forest);
|
| 209 |
+
color: var(--cream);
|
| 210 |
+
display: flex;
|
| 211 |
+
align-items: center;
|
| 212 |
+
justify-content: center;
|
| 213 |
+
margin-bottom: var(--sp-3);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.om-mode-icon svg { width: 28px; height: 28px; }
|
| 217 |
+
|
| 218 |
+
.om-mode-title {
|
| 219 |
+
font-family: var(--font-serif);
|
| 220 |
+
font-style: italic;
|
| 221 |
+
font-size: 36px;
|
| 222 |
+
line-height: 40px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.om-mode-desc {
|
| 226 |
+
font-family: var(--font-sans);
|
| 227 |
+
font-size: 14px;
|
| 228 |
+
line-height: 22px;
|
| 229 |
+
color: var(--fg-2);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.om-mode-cta {
|
| 233 |
+
font-family: var(--font-sans);
|
| 234 |
+
font-size: 13px;
|
| 235 |
+
font-weight: 500;
|
| 236 |
+
color: var(--forest);
|
| 237 |
+
margin-top: auto;
|
| 238 |
+
padding-top: var(--sp-3);
|
| 239 |
+
display: flex;
|
| 240 |
+
align-items: center;
|
| 241 |
+
gap: 6px;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.om-stack-row {
|
| 245 |
+
display: flex;
|
| 246 |
+
justify-content: center;
|
| 247 |
+
gap: var(--sp-4);
|
| 248 |
+
margin-top: var(--sp-12);
|
| 249 |
+
padding-top: var(--sp-6);
|
| 250 |
+
border-top: 1px solid var(--border-1);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.om-stack-item {
|
| 254 |
+
font-family: var(--font-mono);
|
| 255 |
+
font-size: 11px;
|
| 256 |
+
color: var(--fg-3);
|
| 257 |
+
letter-spacing: 0.04em;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.om-stack-item strong {
|
| 261 |
+
color: var(--forest);
|
| 262 |
+
font-weight: 500;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/* βββ APP SHELL (shopper + creator) βββ */
|
| 266 |
+
.om-app {
|
| 267 |
+
max-width: var(--content-max);
|
| 268 |
+
margin: 0 auto;
|
| 269 |
+
padding: var(--sp-6);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.om-app-grid-shopper {
|
| 273 |
+
display: grid;
|
| 274 |
+
grid-template-columns: 1fr 340px;
|
| 275 |
+
gap: var(--sp-6);
|
| 276 |
+
min-height: calc(100vh - 110px);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.om-app-grid-creator {
|
| 280 |
+
display: grid;
|
| 281 |
+
grid-template-columns: 1fr 1fr;
|
| 282 |
+
gap: var(--sp-6);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* βββ SETUP CARD βββ */
|
| 286 |
+
.om-setup {
|
| 287 |
+
position: fixed;
|
| 288 |
+
inset: 0;
|
| 289 |
+
background: rgba(21,19,15,0.5);
|
| 290 |
+
backdrop-filter: blur(16px);
|
| 291 |
+
display: flex;
|
| 292 |
+
align-items: center;
|
| 293 |
+
justify-content: center;
|
| 294 |
+
z-index: 1000;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.om-setup.hidden { display: none; }
|
| 298 |
+
|
| 299 |
+
.om-setup-card {
|
| 300 |
+
width: 90%;
|
| 301 |
+
max-width: 460px;
|
| 302 |
+
background: var(--paper);
|
| 303 |
+
border-radius: var(--radius-md);
|
| 304 |
+
padding: 36px;
|
| 305 |
+
box-shadow: var(--shadow-3);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.om-setup-card h2 {
|
| 309 |
+
font-family: var(--font-serif);
|
| 310 |
+
font-style: italic;
|
| 311 |
+
font-size: 32px;
|
| 312 |
+
font-weight: 400;
|
| 313 |
+
margin-bottom: 4px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.om-setup-card .sub {
|
| 317 |
+
font-size: 13px;
|
| 318 |
+
color: var(--fg-2);
|
| 319 |
+
line-height: 1.5;
|
| 320 |
+
margin-bottom: var(--sp-6);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.om-field { margin-bottom: var(--sp-4); }
|
| 324 |
+
|
| 325 |
+
.om-field label {
|
| 326 |
+
display: block;
|
| 327 |
+
font-size: 11px;
|
| 328 |
+
font-weight: 500;
|
| 329 |
+
letter-spacing: 0.12em;
|
| 330 |
+
text-transform: uppercase;
|
| 331 |
+
color: var(--fg-3);
|
| 332 |
+
margin-bottom: 6px;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.om-field input, .om-field select, .om-field textarea {
|
| 336 |
+
width: 100%;
|
| 337 |
+
padding: 11px 14px;
|
| 338 |
+
font-family: var(--font-mono);
|
| 339 |
+
font-size: 13px;
|
| 340 |
+
color: var(--fg-1);
|
| 341 |
+
background: var(--bg-1);
|
| 342 |
+
border: 1px solid var(--border-1);
|
| 343 |
+
border-radius: var(--radius-sm);
|
| 344 |
+
outline: none;
|
| 345 |
+
-webkit-appearance: none;
|
| 346 |
+
transition: border-color var(--dur-state), box-shadow var(--dur-state);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.om-field input:focus, .om-field select:focus, .om-field textarea:focus {
|
| 350 |
+
border-color: var(--forest);
|
| 351 |
+
box-shadow: 0 0 0 3px rgba(31,59,45,0.08);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.om-field-hint {
|
| 355 |
+
font-size: 11px;
|
| 356 |
+
color: var(--fg-3);
|
| 357 |
+
margin-top: 4px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.om-field-hint a { color: var(--forest); }
|
| 361 |
+
|
| 362 |
+
.om-cta {
|
| 363 |
+
width: 100%;
|
| 364 |
+
padding: 13px;
|
| 365 |
+
font-family: var(--font-sans);
|
| 366 |
+
font-size: 14px;
|
| 367 |
+
font-weight: 600;
|
| 368 |
+
color: var(--cream);
|
| 369 |
+
background: var(--forest);
|
| 370 |
+
border: none;
|
| 371 |
+
border-radius: var(--radius-sm);
|
| 372 |
+
cursor: pointer;
|
| 373 |
+
transition: background var(--dur-state);
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.om-cta:hover { background: var(--forest-hover); }
|
| 377 |
+
.om-cta:disabled { opacity: 0.5; cursor: default; }
|
| 378 |
+
|
| 379 |
+
.om-cta-secondary {
|
| 380 |
+
background: transparent;
|
| 381 |
+
color: var(--ink);
|
| 382 |
+
border: 1px solid var(--ink);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.om-cta-secondary:hover { background: var(--ink); color: var(--cream); }
|
| 386 |
+
|
| 387 |
+
.om-err {
|
| 388 |
+
color: var(--terracotta);
|
| 389 |
+
font-size: 12px;
|
| 390 |
+
margin-top: 8px;
|
| 391 |
+
display: none;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* βββ CONVERSATION (shopper) βββ */
|
| 395 |
+
.om-conv {
|
| 396 |
+
display: flex;
|
| 397 |
+
flex-direction: column;
|
| 398 |
+
background: var(--paper);
|
| 399 |
+
border: 1px solid var(--border-1);
|
| 400 |
+
min-height: 0;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.om-conv-head {
|
| 404 |
+
padding: 14px 20px;
|
| 405 |
+
border-bottom: 1px solid var(--border-1);
|
| 406 |
+
display: flex;
|
| 407 |
+
align-items: center;
|
| 408 |
+
justify-content: space-between;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.om-conv-head h3 {
|
| 412 |
+
font-family: var(--font-serif);
|
| 413 |
+
font-style: italic;
|
| 414 |
+
font-size: 18px;
|
| 415 |
+
font-weight: 400;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
#customer-badge {
|
| 419 |
+
font-size: 10px;
|
| 420 |
+
font-family: var(--font-mono);
|
| 421 |
+
padding: 2px 8px;
|
| 422 |
+
border-radius: var(--radius-pill);
|
| 423 |
+
background: rgba(31,59,45,0.08);
|
| 424 |
+
color: var(--forest);
|
| 425 |
+
display: none;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
#transcript {
|
| 429 |
+
flex: 1;
|
| 430 |
+
overflow-y: auto;
|
| 431 |
+
padding: 20px;
|
| 432 |
+
display: flex;
|
| 433 |
+
flex-direction: column;
|
| 434 |
+
gap: var(--sp-3);
|
| 435 |
+
max-height: 60vh;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.msg {
|
| 439 |
+
max-width: 88%;
|
| 440 |
+
padding: 10px 16px;
|
| 441 |
+
font-size: 14px;
|
| 442 |
+
line-height: 1.55;
|
| 443 |
+
border-radius: var(--radius-pill);
|
| 444 |
+
animation: msgIn 0.35s var(--ease) both;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
@keyframes msgIn {
|
| 448 |
+
from { opacity: 0; transform: translateY(6px); }
|
| 449 |
+
to { opacity: 1; transform: translateY(0); }
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.msg.user {
|
| 453 |
+
align-self: flex-end;
|
| 454 |
+
background: var(--forest);
|
| 455 |
+
color: var(--cream);
|
| 456 |
+
border-bottom-right-radius: var(--radius-sm);
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.msg.bot {
|
| 460 |
+
align-self: flex-start;
|
| 461 |
+
background: var(--bg-1);
|
| 462 |
+
border: 1px solid var(--border-1);
|
| 463 |
+
border-bottom-left-radius: var(--radius-sm);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.msg.system {
|
| 467 |
+
align-self: center;
|
| 468 |
+
background: transparent;
|
| 469 |
+
color: var(--fg-3);
|
| 470 |
+
font-size: 12px;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.msg.receipt {
|
| 474 |
+
align-self: center;
|
| 475 |
+
background: var(--ink);
|
| 476 |
+
color: var(--cream);
|
| 477 |
+
font-family: var(--font-mono);
|
| 478 |
+
font-size: 11px;
|
| 479 |
+
padding: 8px 14px;
|
| 480 |
+
border-radius: var(--radius-sm);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.msg.receipt::before { content: 'β '; }
|
| 484 |
+
|
| 485 |
+
.msg.product-card {
|
| 486 |
+
align-self: flex-start;
|
| 487 |
+
width: 88%;
|
| 488 |
+
max-width: 88%;
|
| 489 |
+
padding: 0;
|
| 490 |
+
background: var(--bg-1);
|
| 491 |
+
border: 1px solid var(--border-1);
|
| 492 |
+
border-radius: var(--radius-sm);
|
| 493 |
+
border-bottom-left-radius: var(--radius-sm);
|
| 494 |
+
display: grid;
|
| 495 |
+
grid-template-columns: 80px 1fr;
|
| 496 |
+
gap: 0;
|
| 497 |
+
overflow: hidden;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.msg.product-card .pc-img {
|
| 501 |
+
background: linear-gradient(135deg, var(--saffron-soft), var(--saffron));
|
| 502 |
+
display: flex;
|
| 503 |
+
align-items: center;
|
| 504 |
+
justify-content: center;
|
| 505 |
+
font-family: var(--font-serif);
|
| 506 |
+
font-style: italic;
|
| 507 |
+
font-size: 32px;
|
| 508 |
+
color: var(--cream);
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.msg.product-card .pc-body {
|
| 512 |
+
padding: 10px 14px;
|
| 513 |
+
display: flex;
|
| 514 |
+
flex-direction: column;
|
| 515 |
+
gap: 4px;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
.msg.product-card .pc-title {
|
| 519 |
+
font-family: var(--font-serif);
|
| 520 |
+
font-size: 16px;
|
| 521 |
+
line-height: 20px;
|
| 522 |
+
color: var(--fg-1);
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.msg.product-card .pc-creator {
|
| 526 |
+
font-size: 11px;
|
| 527 |
+
color: var(--fg-2);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.msg.product-card .pc-price {
|
| 531 |
+
font-family: var(--font-mono);
|
| 532 |
+
font-size: 13px;
|
| 533 |
+
font-weight: 500;
|
| 534 |
+
color: var(--ink);
|
| 535 |
+
margin-top: 2px;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.msg.product-card .pc-add {
|
| 539 |
+
margin-top: 4px;
|
| 540 |
+
align-self: flex-start;
|
| 541 |
+
font-size: 11px;
|
| 542 |
+
padding: 4px 10px;
|
| 543 |
+
background: var(--forest);
|
| 544 |
+
color: var(--cream);
|
| 545 |
+
border: none;
|
| 546 |
+
border-radius: var(--radius-pill);
|
| 547 |
+
cursor: pointer;
|
| 548 |
+
font-family: var(--font-sans);
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
/* βββ INPUT βββ */
|
| 552 |
+
.om-input-area {
|
| 553 |
+
padding: 14px 20px;
|
| 554 |
+
border-top: 1px solid var(--border-1);
|
| 555 |
+
background: var(--bg-3);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.om-input-row {
|
| 559 |
+
display: flex;
|
| 560 |
+
align-items: center;
|
| 561 |
+
gap: 8px;
|
| 562 |
+
background: var(--bg-1);
|
| 563 |
+
border: 1px solid var(--border-1);
|
| 564 |
+
border-radius: var(--radius-pill);
|
| 565 |
+
padding: 4px 4px 4px 18px;
|
| 566 |
+
transition: border-color var(--dur-state);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.om-input-row:focus-within { border-color: var(--forest); }
|
| 570 |
+
|
| 571 |
+
#chat-input {
|
| 572 |
+
flex: 1;
|
| 573 |
+
background: transparent;
|
| 574 |
+
border: none;
|
| 575 |
+
outline: none;
|
| 576 |
+
font-family: var(--font-sans);
|
| 577 |
+
font-size: 14px;
|
| 578 |
+
padding: 8px 0;
|
| 579 |
+
min-width: 0;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.om-mic, .om-send {
|
| 583 |
+
display: flex;
|
| 584 |
+
align-items: center;
|
| 585 |
+
justify-content: center;
|
| 586 |
+
width: 36px;
|
| 587 |
+
height: 36px;
|
| 588 |
+
border-radius: 50%;
|
| 589 |
+
border: none;
|
| 590 |
+
cursor: pointer;
|
| 591 |
+
flex-shrink: 0;
|
| 592 |
+
transition: transform var(--dur-quick);
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.om-mic { background: var(--bg-3); color: var(--fg-2); }
|
| 596 |
+
.om-mic:hover { background: var(--stone-1); color: var(--fg-1); }
|
| 597 |
+
.om-send { background: var(--forest); color: var(--cream); }
|
| 598 |
+
.om-send:hover { background: var(--forest-hover); transform: scale(1.04); }
|
| 599 |
+
.om-send svg { width: 16px; height: 16px; }
|
| 600 |
+
|
| 601 |
+
/* βββ SIDEBAR (wishlist) βββ */
|
| 602 |
+
.om-side {
|
| 603 |
+
background: var(--paper);
|
| 604 |
+
border: 1px solid var(--border-1);
|
| 605 |
+
padding: var(--sp-4);
|
| 606 |
+
display: flex;
|
| 607 |
+
flex-direction: column;
|
| 608 |
+
gap: var(--sp-4);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.om-side h4 {
|
| 612 |
+
font-family: var(--font-serif);
|
| 613 |
+
font-style: italic;
|
| 614 |
+
font-size: 20px;
|
| 615 |
+
font-weight: 400;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.om-side-eyebrow {
|
| 619 |
+
font-size: 10px;
|
| 620 |
+
font-weight: 500;
|
| 621 |
+
letter-spacing: 0.16em;
|
| 622 |
+
text-transform: uppercase;
|
| 623 |
+
color: var(--fg-3);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.om-wishlist {
|
| 627 |
+
display: flex;
|
| 628 |
+
flex-direction: column;
|
| 629 |
+
gap: 8px;
|
| 630 |
+
margin-top: 8px;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.om-wishlist-empty {
|
| 634 |
+
font-size: 12px;
|
| 635 |
+
color: var(--fg-3);
|
| 636 |
+
padding: 16px 0;
|
| 637 |
+
text-align: center;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.om-wishlist-item {
|
| 641 |
+
display: flex;
|
| 642 |
+
justify-content: space-between;
|
| 643 |
+
align-items: baseline;
|
| 644 |
+
padding: 8px 10px;
|
| 645 |
+
background: var(--bg-1);
|
| 646 |
+
border: 1px solid var(--border-1);
|
| 647 |
+
gap: 8px;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.om-wishlist-item .name {
|
| 651 |
+
font-size: 13px;
|
| 652 |
+
font-family: var(--font-serif);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.om-wishlist-item .price {
|
| 656 |
+
font-family: var(--font-mono);
|
| 657 |
+
font-size: 12px;
|
| 658 |
+
color: var(--forest);
|
| 659 |
+
white-space: nowrap;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
.om-wishlist-item .remove {
|
| 663 |
+
border: none;
|
| 664 |
+
background: transparent;
|
| 665 |
+
color: var(--fg-3);
|
| 666 |
+
cursor: pointer;
|
| 667 |
+
font-size: 14px;
|
| 668 |
+
padding: 0 4px;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
/* βββ CREATOR MODE βββ */
|
| 672 |
+
.om-upload {
|
| 673 |
+
display: flex;
|
| 674 |
+
flex-direction: column;
|
| 675 |
+
gap: var(--sp-4);
|
| 676 |
+
background: var(--paper);
|
| 677 |
+
border: 1px solid var(--border-1);
|
| 678 |
+
padding: var(--sp-6);
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.om-upload h3 {
|
| 682 |
+
font-family: var(--font-serif);
|
| 683 |
+
font-style: italic;
|
| 684 |
+
font-size: 28px;
|
| 685 |
+
font-weight: 400;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.om-dropzone {
|
| 689 |
+
border: 2px dashed var(--stone-3);
|
| 690 |
+
padding: var(--sp-8);
|
| 691 |
+
text-align: center;
|
| 692 |
+
cursor: pointer;
|
| 693 |
+
background: var(--bg-1);
|
| 694 |
+
transition: all var(--dur-state) var(--ease);
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.om-dropzone:hover {
|
| 698 |
+
border-color: var(--forest);
|
| 699 |
+
background: var(--bg-3);
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.om-dropzone.has-image {
|
| 703 |
+
padding: 0;
|
| 704 |
+
border-style: solid;
|
| 705 |
+
border-color: var(--border-1);
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
.om-dropzone img {
|
| 709 |
+
display: block;
|
| 710 |
+
width: 100%;
|
| 711 |
+
max-height: 320px;
|
| 712 |
+
object-fit: cover;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.om-dropzone p {
|
| 716 |
+
font-size: 13px;
|
| 717 |
+
color: var(--fg-2);
|
| 718 |
+
margin-top: 6px;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.om-listing-preview {
|
| 722 |
+
background: var(--paper);
|
| 723 |
+
border: 1px solid var(--border-1);
|
| 724 |
+
padding: var(--sp-6);
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.om-listing-preview h3 {
|
| 728 |
+
font-family: var(--font-serif);
|
| 729 |
+
font-style: italic;
|
| 730 |
+
font-size: 28px;
|
| 731 |
+
font-weight: 400;
|
| 732 |
+
margin-bottom: var(--sp-4);
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.om-listing-preview-empty {
|
| 736 |
+
text-align: center;
|
| 737 |
+
color: var(--fg-3);
|
| 738 |
+
padding: var(--sp-12) 0;
|
| 739 |
+
font-size: 14px;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.om-listing-card {
|
| 743 |
+
border: 1px solid var(--border-1);
|
| 744 |
+
background: var(--bg-1);
|
| 745 |
+
padding: var(--sp-4);
|
| 746 |
+
display: flex;
|
| 747 |
+
flex-direction: column;
|
| 748 |
+
gap: var(--sp-3);
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.om-listing-eyebrow {
|
| 752 |
+
font-size: 10px;
|
| 753 |
+
font-weight: 500;
|
| 754 |
+
letter-spacing: 0.16em;
|
| 755 |
+
text-transform: uppercase;
|
| 756 |
+
color: var(--fg-3);
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.om-listing-title {
|
| 760 |
+
font-family: var(--font-serif);
|
| 761 |
+
font-size: 28px;
|
| 762 |
+
line-height: 32px;
|
| 763 |
+
font-weight: 400;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.om-listing-tagline {
|
| 767 |
+
font-family: var(--font-serif);
|
| 768 |
+
font-style: italic;
|
| 769 |
+
font-size: 18px;
|
| 770 |
+
color: var(--forest);
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.om-listing-desc {
|
| 774 |
+
font-size: 14px;
|
| 775 |
+
line-height: 1.55;
|
| 776 |
+
color: var(--fg-1);
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.om-listing-meta {
|
| 780 |
+
display: flex;
|
| 781 |
+
flex-wrap: wrap;
|
| 782 |
+
gap: 8px;
|
| 783 |
+
margin-top: 4px;
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
.om-listing-meta .pill {
|
| 787 |
+
font-size: 11px;
|
| 788 |
+
padding: 4px 10px;
|
| 789 |
+
border-radius: var(--radius-pill);
|
| 790 |
+
background: var(--bg-3);
|
| 791 |
+
color: var(--fg-2);
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.om-listing-price {
|
| 795 |
+
font-family: var(--font-mono);
|
| 796 |
+
font-size: 24px;
|
| 797 |
+
font-weight: 500;
|
| 798 |
+
color: var(--ink);
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
.om-listing-feedback {
|
| 802 |
+
background: var(--bg-3);
|
| 803 |
+
padding: 10px 14px;
|
| 804 |
+
font-size: 12px;
|
| 805 |
+
color: var(--fg-2);
|
| 806 |
+
border-left: 3px solid var(--saffron);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.om-listing-actions {
|
| 810 |
+
display: flex;
|
| 811 |
+
gap: 8px;
|
| 812 |
+
margin-top: var(--sp-3);
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
/* βββ MINTED LISTINGS GRID βββ */
|
| 816 |
+
.om-minted-grid {
|
| 817 |
+
display: grid;
|
| 818 |
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
| 819 |
+
gap: var(--sp-3);
|
| 820 |
+
margin-top: var(--sp-3);
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.om-minted-card {
|
| 824 |
+
background: var(--bg-1);
|
| 825 |
+
border: 1px solid var(--border-1);
|
| 826 |
+
padding: var(--sp-3);
|
| 827 |
+
text-decoration: none;
|
| 828 |
+
color: var(--fg-1);
|
| 829 |
+
display: flex;
|
| 830 |
+
flex-direction: column;
|
| 831 |
+
gap: 4px;
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
.om-minted-card:hover { background: var(--bg-3); text-decoration: none; }
|
| 835 |
+
.om-minted-card .id { font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); }
|
| 836 |
+
.om-minted-card .name { font-family: var(--font-serif); font-size: 15px; }
|
| 837 |
+
.om-minted-card .tx { font-family: var(--font-mono); font-size: 10px; color: var(--forest); }
|
| 838 |
+
|
| 839 |
+
@media (max-width: 900px) {
|
| 840 |
+
.om-app-grid-shopper, .om-app-grid-creator { grid-template-columns: 1fr; }
|
| 841 |
+
.om-modes { grid-template-columns: 1fr; }
|
| 842 |
+
.om-landing h1 { font-size: 56px; line-height: 60px; }
|
| 843 |
+
}
|