shkor.com/account: чат с агентом (канал портала, опрос 5 с, отдельный лимит)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
123e300fec
commit
b0502af842
2 changed files with 43 additions and 2 deletions
|
|
@ -10,7 +10,7 @@
|
|||
POST /api/account/login JSON email -> magic link by email (only for existing orders)
|
||||
GET /api/account/session?k=|t= -> session cookie (30 days) -> redirect /account/
|
||||
GET /api/account/me cabinet: order status, workspace, level ladder, project, products, earnings
|
||||
POST /api/account/{agreement,project,product,product-live,logout}
|
||||
POST /api/account/{agreement,project,product,product-live,chat,logout}
|
||||
|
||||
Env:
|
||||
SHKOR_DB sqlite path (default /var/lib/shkor-subscribe/subs.db)
|
||||
|
|
@ -479,6 +479,8 @@ def acc_action(email, action, d, ip):
|
|||
elif action == "product":
|
||||
body.update(name=clean(d, "name", 120), price_usd=d.get("price_usd"), interval=clean(d, "interval", 12),
|
||||
description=clean(d, "description", 300))
|
||||
elif action == "chat":
|
||||
body.update(text=str(d.get("text") or "").strip()[:8000])
|
||||
elif action == "product-live":
|
||||
body.update(product_id=d.get("product_id"), note=clean(d, "note", 1000))
|
||||
else:
|
||||
|
|
@ -592,7 +594,7 @@ class H(BaseHTTPRequestHandler):
|
|||
email = acc_email(self.sid())
|
||||
if not email:
|
||||
return self.reply(401, {"error": "Please sign in."})
|
||||
if limited("acc", self.ip(), 120):
|
||||
if limited("chat" if action == "chat" else "acc", self.ip(), 1500 if action == "chat" else 120):
|
||||
return self.reply(429, {"error": "Too many requests — slow down a little."})
|
||||
return self.reply(*acc_action(email, action, d, self.ip()))
|
||||
if path in ("/api/autopilot/start", "/api/autopilot/checkout"):
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@
|
|||
.ok-msg{color:var(--acid);font-size:14px;min-height:20px;margin-top:10px}
|
||||
.muted{color:var(--fg-3);font-size:14px}
|
||||
.topbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
|
||||
.chat{display:flex;flex-direction:column;gap:10px;max-height:420px;overflow-y:auto;margin-top:14px;padding:14px;border-radius:14px;background:rgba(0,0,0,.25);box-shadow:inset 0 0 0 1px var(--line-2)}
|
||||
.msg{max-width:82%;padding:10px 14px;border-radius:14px;font-size:15px;line-height:1.5;white-space:pre-wrap;word-wrap:break-word;color:var(--fg)}
|
||||
.msg.me{align-self:flex-end;background:rgba(198,255,77,.14);box-shadow:inset 0 0 0 1px rgba(198,255,77,.3)}
|
||||
.msg.ag{align-self:flex-start;background:rgba(255,255,255,.05);box-shadow:inset 0 0 0 1px var(--line-2)}
|
||||
.msg small{display:block;margin-top:4px;font:11px var(--mono);color:var(--fg-3)}
|
||||
.chat-in{display:flex;gap:10px;margin-top:12px}.chat-in textarea{flex:1;min-height:50px;max-height:160px;resize:vertical;padding:12px 14px;border:0;border-radius:12px;color:var(--fg);font:15px/1.5 var(--body);background:rgba(255,255,255,.035);box-shadow:inset 0 0 0 1px var(--line-2)}
|
||||
.chat-in textarea:focus{outline:none;box-shadow:inset 0 0 0 1px var(--acid)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -117,6 +124,10 @@
|
|||
}
|
||||
// workspace
|
||||
if (d.panel) h += '<div class="panel"><h2>Your workspace</h2><p>Claude Code (your own Claude account) and OpenCode, both connected to the lab MCP.</p><div class="creds">Panel: <b><a href="' + esc(d.panel.url) + '" target="_blank" rel="noopener">' + esc(d.panel.url) + '</a></b><br>Login: <b>' + esc(d.panel.user) + '</b> · Password: <b>' + esc(d.panel.password) + '</b></div></div>';
|
||||
// chat with the agent (same channel as the portal chat; the agent answers within ~1 min)
|
||||
h += '<div class="panel"><h2>Chat with your agent</h2><p>Your Simbiot agent runs on your server around the clock. Ask it anything or give it a task — it usually replies within a minute.</p>'
|
||||
+ '<div class="chat" id="chat"><p class="muted">Loading…</p></div>'
|
||||
+ '<form class="chat-in" id="cf"><textarea id="ct" maxlength="8000" placeholder="Message your agent… (Enter to send, Shift+Enter for a new line)"></textarea><button class="btn" type="submit"><span>Send</span></button></form><p class="form-err" id="cerr"></p></div>';
|
||||
// earnings
|
||||
var e = c.earnings || {};
|
||||
h += '<div class="panel"><h2>Earnings <span class="pill ' + (e.available > 0 ? 'ok' : 'wip') + '">' + money(e.available) + ' available</span></h2><p>Sales through the cooperative’s payments. The studio share is withheld automatically at each payment.</p>'
|
||||
|
|
@ -177,6 +188,28 @@
|
|||
bind();
|
||||
}
|
||||
|
||||
var chatSig = '';
|
||||
function drawChat(r) {
|
||||
var box = document.getElementById('chat'); if (!box) return;
|
||||
var msgs = (r && r.messages) || [], sig = msgs.length + ':' + (msgs.length ? msgs[msgs.length - 1].id : '');
|
||||
if (sig === chatSig) return; chatSig = sig;
|
||||
var atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40;
|
||||
box.innerHTML = msgs.length ? msgs.map(function (m) {
|
||||
var t = new Date(m.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return '<div class="msg ' + (m.from === 'agent' ? 'ag' : 'me') + '">' + esc(m.text) + '<small>' + (m.from === 'agent' ? 'Agent' : 'You') + ' · ' + t + (m.from !== 'agent' && !m.seen ? ' · sent' : '') + '</small></div>';
|
||||
}).join('') : '<p class="muted">No messages yet — say hi to your agent.</p>';
|
||||
if (atBottom || msgs.length && msgs[msgs.length - 1].from !== 'agent') box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
function chat(text) {
|
||||
return api('/api/account/chat', text ? { text: text } : {}).then(function (r) {
|
||||
if (r.status === 401) return loginView('Your session expired — sign in again.');
|
||||
var err = document.getElementById('cerr');
|
||||
if (!r.ok) { if (err) err.textContent = r.j.error || 'Chat is unavailable right now.'; return; }
|
||||
if (err) err.textContent = ''; drawChat(r.j);
|
||||
});
|
||||
}
|
||||
setInterval(function () { if (!document.hidden && document.getElementById('chat')) chat(''); }, 5000);
|
||||
|
||||
function act(path, body, form) {
|
||||
var err = form.querySelector('.form-err'), btn = form.querySelector('button[type=submit]');
|
||||
err.textContent = ''; btn.disabled = true;
|
||||
|
|
@ -200,6 +233,12 @@
|
|||
var fr = document.getElementById('fr');
|
||||
if (fr) fr.addEventListener('submit', function (e) { e.preventDefault(); act('product', { name: document.getElementById('rn').value.trim(), price_usd: Number(document.getElementById('rp').value), interval: document.getElementById('ri').value }, fr); });
|
||||
document.querySelectorAll('.fl').forEach(function (f) { f.addEventListener('submit', function (e) { e.preventDefault(); act('product-live', { product_id: Number(f.dataset.id), note: f.querySelector('.fl-note').value.trim() }, f); }); });
|
||||
var cf = document.getElementById('cf'), ct = document.getElementById('ct');
|
||||
if (cf) {
|
||||
chatSig = ''; chat('');
|
||||
cf.addEventListener('submit', function (e) { e.preventDefault(); var t = ct.value.trim(); if (!t) return; ct.value = ''; chat(t); });
|
||||
ct.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); cf.requestSubmit(); } });
|
||||
}
|
||||
document.querySelectorAll('[data-copy]').forEach(function (b) { b.addEventListener('click', function () { navigator.clipboard && navigator.clipboard.writeText(b.dataset.copy); b.textContent = 'Copied'; }); });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue