[claude] Add real-time cost ticker for Workshop interactions (#68) #82
@@ -15,7 +15,10 @@ export type DebateEvent =
|
||||
| { type: "debate:argument"; jobId: string; agent: "Beta-A" | "Beta-B"; position: "accept" | "reject"; argument: string }
|
||||
| { type: "debate:verdict"; jobId: string; accepted: boolean; reason: string };
|
||||
|
||||
export type BusEvent = JobEvent | SessionEvent | DebateEvent;
|
||||
export type CostEvent =
|
||||
| { type: "cost:update"; jobId: string; sats: number; phase: "eval" | "work" | "session"; isFinal: boolean };
|
||||
|
||||
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent;
|
||||
|
||||
class EventBus extends EventEmitter {
|
||||
emit(event: "bus", data: BusEvent): boolean;
|
||||
|
||||
@@ -247,6 +247,16 @@ function translateEvent(ev: BusEvent): object | null {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Real-time cost ticker (#68) ───────────────────────────────────────────
|
||||
case "cost:update":
|
||||
return {
|
||||
type: "cost_update",
|
||||
jobId: ev.jobId,
|
||||
sats: ev.sats,
|
||||
phase: ev.phase,
|
||||
isFinal: ev.isFinal,
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -205,6 +205,8 @@ async function runEvalInBackground(
|
||||
// to avoid economic DoS where pool is reserved before the user ever pays.
|
||||
|
||||
eventBus.publish({ type: "job:state", jobId, state: "awaiting_work_payment" });
|
||||
// Emit estimated cost so the UI ticker can show ~N sats before payment
|
||||
eventBus.publish({ type: "cost:update", jobId, sats: invoiceSats, phase: "work", isFinal: false });
|
||||
} else {
|
||||
await db
|
||||
.update(jobs)
|
||||
@@ -307,6 +309,10 @@ async function runWorkInBackground(
|
||||
refundState,
|
||||
});
|
||||
eventBus.publish({ type: "job:completed", jobId, result: workResult.result });
|
||||
// Emit final actual cost for the UI cost ticker
|
||||
if (!isFree && actualAmountSats > 0) {
|
||||
eventBus.publish({ type: "cost:update", jobId, sats: actualAmountSats, phase: "work", isFinal: true });
|
||||
}
|
||||
|
||||
// Credit the generosity pool from paid interactions
|
||||
if (!isFree && workAmountSats > 0) {
|
||||
|
||||
@@ -465,6 +465,11 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Emit real-time cost update for the UI cost ticker (#68)
|
||||
if (finalState === "complete" && debitedSats > 0) {
|
||||
eventBus.publish({ type: "cost:update", jobId: requestId, sats: debitedSats, phase: "session", isFinal: true });
|
||||
}
|
||||
|
||||
// ── Trust scoring ────────────────────────────────────────────────────────
|
||||
if (session.nostrPubkey) {
|
||||
if (finalState === "complete") {
|
||||
|
||||
@@ -132,6 +132,54 @@ function _scheduleCostPreview(text) {
|
||||
_estimateTimer = setTimeout(() => _fetchEstimate(text), 300);
|
||||
}
|
||||
|
||||
// ── Live cost ticker ──────────────────────────────────────────────────────────
|
||||
// Shown in the top-right HUD during active paid interactions.
|
||||
// Updated via WebSocket `cost_update` messages from the backend.
|
||||
|
||||
let $costTicker = null;
|
||||
let _tickerHideTimer = null;
|
||||
|
||||
function _ensureCostTicker() {
|
||||
if ($costTicker) return $costTicker;
|
||||
$costTicker = document.getElementById('timmy-cost-ticker');
|
||||
if (!$costTicker) {
|
||||
$costTicker = document.createElement('div');
|
||||
$costTicker.id = 'timmy-cost-ticker';
|
||||
$costTicker.style.cssText = [
|
||||
'position:fixed;top:36px;right:16px',
|
||||
'font-size:11px;font-family:"Courier New",monospace',
|
||||
'color:#ffcc44;text-shadow:0 0 6px #aa8822',
|
||||
'letter-spacing:1px',
|
||||
'pointer-events:none;z-index:10',
|
||||
'transition:opacity .4s;opacity:0',
|
||||
].join(';');
|
||||
document.body.appendChild($costTicker);
|
||||
}
|
||||
return $costTicker;
|
||||
}
|
||||
|
||||
export function showCostTicker(sats) {
|
||||
clearTimeout(_tickerHideTimer);
|
||||
const el = _ensureCostTicker();
|
||||
el.textContent = `⚡ ~${sats} sats`;
|
||||
el.style.opacity = '1';
|
||||
}
|
||||
|
||||
export function updateCostTicker(sats, isFinal = false) {
|
||||
clearTimeout(_tickerHideTimer);
|
||||
const el = _ensureCostTicker();
|
||||
el.textContent = isFinal ? `⚡ ${sats} sats charged` : `⚡ ~${sats} sats`;
|
||||
el.style.opacity = '1';
|
||||
if (isFinal) {
|
||||
_tickerHideTimer = setTimeout(hideCostTicker, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
export function hideCostTicker() {
|
||||
if (!$costTicker) return;
|
||||
$costTicker.style.opacity = '0';
|
||||
}
|
||||
|
||||
// ── Input bar ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function initUI() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './agents.js';
|
||||
import { appendSystemMessage, appendDebateMessage } from './ui.js';
|
||||
import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js';
|
||||
import { sentiment } from './edge-worker-client.js';
|
||||
import { setLabelState } from './hud-labels.js';
|
||||
|
||||
@@ -140,6 +140,17 @@ function handleMessage(msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cost_update': {
|
||||
// Real-time cost ticker (#68): show estimated cost when job starts,
|
||||
// update to final charged amount when job completes.
|
||||
if (msg.isFinal) {
|
||||
updateCostTicker(msg.sats, true);
|
||||
} else {
|
||||
showCostTicker(msg.sats);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agent_count':
|
||||
case 'visitor_count':
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user