sharktide/tools / rewards_editor.py
sharktide's picture
download
raw
12.2 kB
#!/usr/bin/env python3
"""
Rewards Editor - Local GUI for editing rewards.json
Run: python tools/rewards_editor.py
Opens at http://localhost:8765
"""
import json
import os
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
REWARDS_FILE = Path(__file__).resolve().parent.parent / "assets" / "rewards.json"
HTML_PAGE = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rewards Editor</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f0f13;color:#e0e0e0;padding:1.5rem}
h1{font-size:1.4rem;margin-bottom:1rem;color:#fff}
.toolbar{display:flex;gap:.5rem;margin-bottom:1rem;flex-wrap:wrap}
.toolbar button{padding:.5rem 1rem;border-radius:8px;border:1px solid #333;background:#1a1a2e;color:#e0e0e0;cursor:pointer;font-size:.85rem;font-weight:600;transition:all .15s}
.toolbar button:hover{background:#2a2a4e;border-color:#555}
.toolbar button.primary{background:#10b981;border-color:#10b981;color:#fff}
.toolbar button.primary:hover{background:#059669}
.toolbar button.danger{background:#dc2626;border-color:#dc2626;color:#fff}
.toolbar button.danger:hover{background:#b91c1c}
.toolbar .count{color:#888;font-size:.85rem;align-self:center;margin-left:auto}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:1rem}
.card{background:#1a1a2e;border:1px solid #2a2a3e;border-radius:10px;padding:1rem;display:flex;flex-direction:column;gap:.6rem;transition:border-color .15s}
.card:hover{border-color:#444}
.card-header{display:flex;align-items:center;gap:.5rem}
.card-icon{font-size:1.5rem;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:#0f0f13;border-radius:8px;border:1px solid #333}
.card-title{flex:1}
.card-title input{width:100%;background:transparent;border:none;color:#fff;font-size:.95rem;font-weight:600;outline:none}
.card-title input:focus{border-bottom:1px solid #10b981}
.card-tier{font-size:.7rem;font-weight:700;padding:.2rem .5rem;border-radius:6px;background:rgba(26,107,255,.1);border:1px solid rgba(26,107,255,.2);color:#1a6bff}
.field{display:flex;flex-direction:column;gap:.2rem}
.field label{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#888}
.field input,.field select,.field textarea{background:#0f0f13;border:1px solid #2a2a3e;border-radius:6px;padding:.45rem .6rem;color:#e0e0e0;font-size:.82rem;outline:none;font-family:inherit}
.field input:focus,.field select:focus,.field textarea:focus{border-color:#10b981}
.field textarea{min-height:60px;resize:vertical}
.row{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}
.row-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.5rem}
.card-actions{display:flex;gap:.4rem;margin-top:.3rem}
.card-actions button{flex:1;padding:.4rem;border-radius:6px;border:1px solid #333;background:#0f0f13;color:#e0e0e0;cursor:pointer;font-size:.78rem;font-weight:500;transition:all .12s}
.card-actions button:hover{background:#2a2a4e}
.card-actions button.del{color:#dc2626;border-color:rgba(220,38,38,.3)}
.card-actions button.del:hover{background:rgba(220,38,38,.1)}
.toast{position:fixed;bottom:1.5rem;right:1.5rem;padding:.7rem 1.2rem;border-radius:8px;font-size:.85rem;font-weight:600;z-index:9999;animation:slideIn .3s ease}
.toast.ok{background:#10b981;color:#fff}
.toast.err{background:#dc2626;color:#fff}
@keyframes slideIn{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
.preview{margin-top:1.5rem;background:#0f0f13;border:1px solid #2a2a3e;border-radius:10px;padding:1rem;max-height:400px;overflow:auto}
.preview pre{font-family:'Fira Code',monospace;font-size:.75rem;color:#aaa;white-space:pre-wrap}
</style>
</head>
<body>
<h1>Rewards Editor</h1>
<div class="toolbar">
<button class="primary" onclick="saveRewards()">Save</button>
<button onclick="addReward()">+ Add Reward</button>
<button onclick="loadRewards()">Reload</button>
<button onclick="togglePreview()">Preview JSON</button>
<span class="count" id="count"></span>
</div>
<div class="grid" id="grid"></div>
<div class="preview" id="preview" style="display:none"><pre id="previewJson"></pre></div>
<script>
let rewards=[];
const CATEGORIES=['spend','diversity','volume','tokens','streak','special'];
const REWARD_TYPES=['percentage_discount','fixed_price','bonus_credits'];
const CRITERIA_TYPES=['total_spend_usd','total_requests','total_tokens','unique_models_tried','days_visited','consecutive_days','multi_criteria'];
function toast(msg,ok=true){const t=document.createElement('div');t.className='toast '+(ok?'ok':'err');t.textContent=msg;document.body.appendChild(t);setTimeout(()=>t.remove(),2500)}
async function loadRewards(){
try{const r=await fetch('/api/rewards');rewards=await r.json();render()}catch(e){toast('Failed to load: '+e.message,false)}
}
async function saveRewards(){
try{await fetch('/api/rewards',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(rewards)});toast('Saved!')}catch(e){toast('Failed to save: '+e.message,false)}
}
function addReward(){
rewards.push({id:'new_reward_'+Date.now(),name:'New Reward',description:'Description',icon:'⭐',tier:1,category:'spend',chain:'spend',criteria:{type:'total_spend_usd',threshold:10},reward:{type:'percentage_discount',model_id:'model-name',discount_percent:5,label:'5% off model'}});
render();toast('Added new reward')
}
function deleteReward(i){if(confirm('Delete this reward?')){rewards.splice(i,1);render();toast('Deleted')}}
function moveReward(i,dir){
const j=i+dir;if(j<0||j>=rewards.length)return;
[rewards[i],rewards[j]]=[rewards[j],rewards[i]];render()
}
function updateField(i,key,val){
if(key.includes('.')){const p=key.split('.');rewards[i][p[0]][p[1]]=val}
else rewards[i][key]=val;
}
function render(){
const g=document.getElementById('grid');g.innerHTML='';
document.getElementById('count').textContent=rewards.length+' rewards';
rewards.forEach((r,i)=>{
const d=document.createElement('div');d.className='card';
d.innerHTML=`
<div class="card-header">
<div class="card-icon"><input value="${r.icon||''}" onchange="updateField(${i},'icon',this.value);this.parentElement.childNodes[0].textContent=this.value" style="width:36px;text-align:center;font-size:1.3rem;background:transparent;border:none;color:#fff" /></div>
<div class="card-title"><input value="${esc(r.name)}" onchange="updateField(${i},'name',this.value)"/></div>
<span class="card-tier">T${r.tier||1}</span>
</div>
<div class="field"><label>Description</label><input value="${esc(r.description)}" onchange="updateField(${i},'description',this.value)"/></div>
<div class="row">
<div class="field"><label>Category</label><select onchange="updateField(${i},'category',this.value);updateField(${i},'chain',this.value)">${CATEGORIES.map(c=>`<option value="${c}" ${r.category===c?'selected':''}>${c}</option>`).join('')}</select></div>
<div class="field"><label>Tier</label><input type="number" min="1" value="${r.tier||1}" onchange="updateField(${i},'tier',parseInt(this.value))"/></div>
</div>
<div style="border-top:1px solid #2a2a3e;padding-top:.5rem;margin-top:.2rem">
<div style="font-size:.7rem;font-weight:600;color:#888;text-transform:uppercase;margin-bottom:.4rem">Criteria</div>
<div class="row">
<div class="field"><label>Type</label><select onchange="updateField(${i},'criteria.type',this.value)">${CRITERIA_TYPES.map(c=>`<option value="${c}" ${r.criteria?.type===c?'selected':''}>${c}</option>`).join('')}</select></div>
<div class="field"><label>Threshold</label><input type="number" value="${r.criteria?.threshold||0}" onchange="updateField(${i},'criteria.threshold',parseFloat(this.value))"/></div>
</div>
</div>
<div style="border-top:1px solid #2a2a3e;padding-top:.5rem;margin-top:.2rem">
<div style="font-size:.7rem;font-weight:600;color:#888;text-transform:uppercase;margin-bottom:.4rem">Reward</div>
<div class="row">
<div class="field"><label>Type</label><select onchange="updateField(${i},'reward.type',this.value)">${REWARD_TYPES.map(c=>`<option value="${c}" ${r.reward?.type===c?'selected':''}>${c}</option>`).join('')}</select></div>
<div class="field"><label>Model ID</label><input value="${esc(r.reward?.model_id||'')}" onchange="updateField(${i},'reward.model_id',this.value)"/></div>
</div>
<div class="row" style="margin-top:.4rem">
<div class="field"><label>Discount %</label><input type="number" min="0" max="100" value="${r.reward?.discount_percent||0}" onchange="updateField(${i},'reward.discount_percent',parseFloat(this.value))"/></div>
<div class="field"><label>Fixed Price (per M)</label><input type="number" step="0.01" value="${r.reward?.price_per_million_tokens||0}" onchange="updateField(${i},'reward.price_per_million_tokens',parseFloat(this.value))"/></div>
</div>
<div class="field" style="margin-top:.4rem"><label>Label</label><input value="${esc(r.reward?.label||'')}" onchange="updateField(${i},'reward.label',this.value)"/></div>
</div>
<div class="card-actions">
<button onclick="moveReward(${i},-1)">Up</button>
<button onclick="moveReward(${i},1)">Down</button>
<button class="del" onclick="deleteReward(${i})">Delete</button>
</div>`;
g.appendChild(d);
});
}
function esc(s){return String(s||'').replace(/"/g,'&quot;').replace(/</g,'&lt;')}
function togglePreview(){
const p=document.getElementById('preview');
if(p.style.display==='none'){p.style.display='block';document.getElementById('previewJson').textContent=JSON.stringify(rewards,null,2)}
else p.style.display='none';
}
loadRewards();
</script>
</body>
</html>"""
class RewardsHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/" or self.path == "/index.html":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML_PAGE.encode("utf-8"))
elif self.path == "/api/rewards":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
try:
data = json.loads(REWARDS_FILE.read_text(encoding="utf-8"))
except Exception:
data = []
self.wfile.write(json.dumps(data, indent=2).encode("utf-8"))
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/api/rewards":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
data = json.loads(body)
REWARDS_FILE.parent.mkdir(parents=True, exist_ok=True)
REWARDS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":true}')
except Exception as e:
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": str(e)}).encode("utf-8"))
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
print(f"[rewards-editor] {args[0]}")
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
print(f"Rewards Editor running at http://localhost:{port}")
print(f"Editing: {REWARDS_FILE}")
server = HTTPServer(("127.0.0.1", port), RewardsHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
server.server_close()
if __name__ == "__main__":
main()

Xet Storage Details

Size:
12.2 kB
·
Xet hash:
ed323b88964bc68a6df03ac1b7549dcdf40049d312f321768b369a29b3b4cdff

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.