2026-03-18 18:06:44 +00:00
import { Router } from "express" ;
const router = Router ( ) ;
router . get ( "/ui" , ( _req , res ) = > {
res . setHeader ( "Content-Type" , "text/html" ) ;
res . send ( ` <!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > Timmy — Lightning AI Agent < / title >
< style >
: root {
-- bg : # 0 a0a0f ;
-- surface : # 13131 a ;
-- border : # 2 a2a3a ;
-- accent : # f7931a ;
-- accent2 : # 7 b61ff ;
-- text : # e8e8f0 ;
-- muted : # 6 b6b80 ;
-- green : # 00 d4aa ;
-- red : # ff4d6d ;
}
* { box - sizing : border - box ; margin : 0 ; padding : 0 ; }
body {
background : var ( -- bg ) ;
color : var ( -- text ) ;
font - family : 'SF Mono' , 'Fira Code' , monospace ;
min - height : 100vh ;
display : flex ;
flex - direction : column ;
align - items : center ;
padding : 40px 20 px ;
}
header {
text - align : center ;
margin - bottom : 40px ;
}
header h1 {
font - size : 2.2rem ;
font - family : system - ui , sans - serif ;
font - weight : 700 ;
letter - spacing : - 1 px ;
}
header h1 span { color : var ( -- accent ) ; }
header p {
color : var ( -- muted ) ;
margin - top : 8px ;
font - family : system - ui , sans - serif ;
font - size : 0.95rem ;
}
. badge {
display : inline - block ;
background : # 1 a1a2e ;
border : 1px solid var ( -- accent2 ) ;
color : var ( -- accent2 ) ;
font - size : 0.7rem ;
padding : 2px 8 px ;
border - radius : 4px ;
margin - left : 8px ;
vertical - align : middle ;
letter - spacing : 1px ;
}
. card {
background : var ( -- surface ) ;
border : 1px solid var ( -- border ) ;
border - radius : 12px ;
padding : 28px ;
width : 100 % ;
max - width : 640px ;
margin - bottom : 16px ;
}
. card h2 {
font - family : system - ui , sans - serif ;
font - size : 1rem ;
font - weight : 600 ;
margin - bottom : 16px ;
color : var ( -- muted ) ;
text - transform : uppercase ;
letter - spacing : 1px ;
}
textarea {
width : 100 % ;
background : var ( -- bg ) ;
border : 1px solid var ( -- border ) ;
border - radius : 8px ;
color : var ( -- text ) ;
font - family : system - ui , sans - serif ;
font - size : 1rem ;
padding : 14px ;
resize : vertical ;
min - height : 100px ;
outline : none ;
transition : border - color 0.2 s ;
}
textarea :focus { border - color : var ( -- accent2 ) ; }
. char - count {
text - align : right ;
font - size : 0.75rem ;
color : var ( -- muted ) ;
margin - top : 6px ;
}
. char - count . warn { color : var ( -- accent ) ; }
. char - count . over { color : var ( -- red ) ; }
button {
background : var ( -- accent ) ;
color : # 000 ;
border : none ;
border - radius : 8px ;
padding : 12px 24 px ;
font - family : system - ui , sans - serif ;
font - size : 0.95rem ;
font - weight : 700 ;
cursor : pointer ;
transition : opacity 0.2 s , transform 0.1 s ;
margin - top : 16px ;
width : 100 % ;
}
button :hover { opacity : 0.9 ; transform : translateY ( - 1 px ) ; }
button :disabled { opacity : 0.4 ; cursor : not - allowed ; transform : none ; }
button . secondary {
background : transparent ;
border : 1px solid var ( -- accent ) ;
color : var ( -- accent ) ;
}
button . pay - btn {
background : var ( -- green ) ;
color : # 000 ;
}
. pipeline {
display : flex ;
align - items : center ;
gap : 8px ;
margin - bottom : 24px ;
flex - wrap : wrap ;
}
. step {
display : flex ;
align - items : center ;
gap : 8px ;
font - size : 0.78rem ;
color : var ( -- muted ) ;
font - family : system - ui , sans - serif ;
}
. step . dot {
width : 10px ; height : 10px ;
border - radius : 50 % ;
background : var ( -- border ) ;
flex - shrink : 0 ;
transition : background 0.4 s ;
}
. step . active . dot { background : var ( -- accent ) ; box - shadow : 0 0 8 px var ( -- accent ) ; }
. step . done . dot { background : var ( -- green ) ; }
. step . rejected . dot { background : var ( -- red ) ; }
. step . active { color : var ( -- text ) ; }
. step . done { color : var ( -- green ) ; }
. step . rejected { color : var ( -- red ) ; }
. arrow { color : var ( -- border ) ; font - size : 0.8rem ; }
. invoice - box {
background : var ( -- bg ) ;
border : 1px solid var ( -- border ) ;
border - radius : 8px ;
padding : 16px ;
margin - top : 12px ;
}
. invoice - box . label {
font - size : 0.72rem ;
color : var ( -- muted ) ;
text - transform : uppercase ;
letter - spacing : 1px ;
margin - bottom : 6px ;
}
. invoice - box . amount {
font - size : 1.8rem ;
font - weight : 700 ;
color : var ( -- accent ) ;
font - family : system - ui , sans - serif ;
}
. invoice - box . amount span {
font - size : 1rem ;
color : var ( -- muted ) ;
font - weight : 400 ;
}
. invoice - box . payment - request {
margin - top : 10px ;
font - size : 0.68rem ;
color : var ( -- muted ) ;
word - break : break - all ;
line - height : 1.4 ;
border - top : 1px solid var ( -- border ) ;
padding - top : 10px ;
}
. invoice - box . hash - line {
margin - top : 8px ;
font - size : 0.72rem ;
color : var ( -- accent2 ) ;
}
. result - box {
background : var ( -- bg ) ;
border : 1px solid var ( -- green ) ;
border - radius : 8px ;
padding : 20px ;
margin - top : 12px ;
line - height : 1.7 ;
font - family : system - ui , sans - serif ;
font - size : 0.95rem ;
white - space : pre - wrap ;
}
. rejected - box {
background : var ( -- bg ) ;
border : 1px solid var ( -- red ) ;
border - radius : 8px ;
padding : 20px ;
margin - top : 12px ;
font - family : system - ui , sans - serif ;
font - size : 0.95rem ;
color : var ( -- red ) ;
}
. status - line {
display : flex ;
align - items : center ;
gap : 10px ;
font - size : 0.85rem ;
color : var ( -- muted ) ;
margin - top : 12px ;
font - family : system - ui , sans - serif ;
}
. spinner {
width : 14px ; height : 14px ;
border : 2px solid var ( -- border ) ;
border - top - color : var ( -- accent ) ;
border - radius : 50 % ;
animation : spin 0.8 s linear infinite ;
flex - shrink : 0 ;
}
@keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. hidden { display : none ! important ; }
. error - msg {
color : var ( -- red ) ;
font - family : system - ui , sans - serif ;
font - size : 0.88rem ;
margin - top : 10px ;
}
. mode - tag {
display : inline - block ;
font - size : 0.68rem ;
padding : 2px 6 px ;
border - radius : 4px ;
margin - left : 6px ;
vertical - align : middle ;
}
. mode - stub { background : # 1 a1a2e ; color : var ( -- accent2 ) ; border : 1px solid var ( -- accent2 ) ; }
. reset - btn {
background : transparent ;
border : 1px solid var ( -- border ) ;
color : var ( -- muted ) ;
margin - top : 8px ;
}
< / style >
< / head >
< body >
< header >
< h1 > < span > ⚡ < / span > Timmy < span class = "badge" > STUB MODE < / span > < / h1 >
< p > Lightning - gated AI agent — visual payment flow demo < / p >
< / header >
< div class = "card" id = "pipeline-card" >
< div class = "pipeline" >
< div class = "step" id = "s-request" > < div class = "dot" > < / div > Request < / div >
< div class = "arrow" > → < / div >
< div class = "step" id = "s-eval" > < div class = "dot" > < / div > Eval fee < / div >
< div class = "arrow" > → < / div >
< div class = "step" id = "s-judge" > < div class = "dot" > < / div > Judge < / div >
< div class = "arrow" > → < / div >
< div class = "step" id = "s-work" > < div class = "dot" > < / div > Work fee < / div >
< div class = "arrow" > → < / div >
< div class = "step" id = "s-result" > < div class = "dot" > < / div > Result < / div >
< / div >
< / div >
<!-- Step 1: Enter request -->
< div class = "card" id = "card-input" >
< h2 > Your request < / h2 >
< textarea id = "request-input" placeholder = "Ask Timmy anything — e.g. 'Explain how Lightning Network payment channels work'" maxlength = "500" > < / textarea >
< div class = "char-count" id = "char-count" > 0 / 500 < / div >
< div id = "input-error" class = "error-msg hidden" > < / div >
< button id = "submit-btn" onclick = "createJob()" > Create job & amp ; get eval invoice → < / button >
< / div >
<!-- Step 2: Pay eval invoice -->
< div class = "card hidden" id = "card-eval" >
< h2 > Step 1 — Pay eval invoice < span class = "mode-tag mode-stub" > stub < / span > < / h2 >
< div class = "invoice-box" >
< div class = "label" > Amount due < / div >
< div class = "amount" id = "eval-amount" > — < span > sats < / span > < / div >
< div class = "payment-request" id = "eval-pr" > < / div >
< div class = "hash-line" > hash : < span id = "eval-hash" > < / span > < / div >
< / div >
< p style = "font-family:system-ui,sans-serif;font-size:0.82rem;color:var(--muted);margin-top:14px;" >
In production this would be a scannable BOLT11 invoice . In stub mode , click below to simulate payment instantly .
< / p >
< button class = "pay-btn" id = "pay-eval-btn" onclick = "payEval()" > ⚡ Simulate eval payment < / button >
< div class = "status-line hidden" id = "eval-polling" > < div class = "spinner" > < / div > Waiting for eval AI … < / div >
< / div >
<!-- Step 3: Rejected -->
< div class = "card hidden" id = "card-rejected" >
< h2 > Request rejected < / h2 >
< div class = "rejected-box" id = "rejected-reason" > < / div >
< button class = "reset-btn" onclick = "reset()" > ← Start over < / button >
< / div >
<!-- Step 4: Pay work invoice -->
< div class = "card hidden" id = "card-work" >
< h2 > Step 2 — Pay work invoice < span class = "mode-tag mode-stub" > stub < / span > < / h2 >
< div class = "invoice-box" >
< div class = "label" > Amount due < / div >
< div class = "amount" id = "work-amount" > — < span > sats < / span > < / div >
< div class = "payment-request" id = "work-pr" > < / div >
< div class = "hash-line" > hash : < span id = "work-hash" > < / span > < / div >
< / div >
< p style = "font-family:system-ui,sans-serif;font-size:0.82rem;color:var(--muted);margin-top:14px;" >
Work fee is priced by request length . Click to simulate payment and Timmy will start working .
< / p >
< button class = "pay-btn" id = "pay-work-btn" onclick = "payWork()" > ⚡ Simulate work payment < / button >
< div class = "status-line hidden" id = "work-polling" > < div class = "spinner" > < / div > Timmy is working … < / div >
< / div >
<!-- Step 5: Result -->
< div class = "card hidden" id = "card-result" >
< h2 > ✓ Complete < / h2 >
< div class = "result-box" id = "result-text" > < / div >
< button class = "reset-btn" onclick = "reset()" > ← New job < / button >
< / div >
< script >
const BASE = window . location . origin ;
let jobId = null ;
let evalHash = null ;
let workHash = null ;
let pollTimer = null ;
const $ = id = > document . getElementById ( id ) ;
const show = id = > $ ( id ) . classList . remove ( 'hidden' ) ;
const hide = id = > $ ( id ) . classList . add ( 'hidden' ) ;
function setStep ( name ) {
const steps = [ 's-request' , 's-eval' , 's-judge' , 's-work' , 's-result' ] ;
const map = {
request : 0 , awaiting_eval_payment : 1 , evaluating : 2 ,
awaiting_work_payment : 3 , executing : 3 , complete : 4 ,
rejected : 2
} ;
const idx = map [ name ] ? ? - 1 ;
steps . forEach ( ( s , i ) = > {
const el = $ ( s ) ;
el . classList . remove ( 'active' , 'done' , 'rejected' ) ;
if ( i < idx ) el . classList . add ( 'done' ) ;
else if ( i === idx ) {
el . classList . add ( name === 'rejected' && i === 2 ? 'rejected' : 'active' ) ;
}
} ) ;
}
$ ( 'request-input' ) . addEventListener ( 'input' , ( ) = > {
const len = $ ( 'request-input' ) . value . length ;
const el = $ ( 'char-count' ) ;
el . textContent = len + ' / 500' ;
el . className = 'char-count' + ( len > 450 ? ( len >= 500 ? ' over' : ' warn' ) : '' ) ;
} ) ;
async function createJob() {
const req = $ ( 'request-input' ) . value . trim ( ) ;
hide ( 'input-error' ) ;
if ( ! req ) { showErr ( 'input-error' , 'Please enter a request.' ) ; return ; }
$ ( 'submit-btn' ) . disabled = true ;
$ ( 'submit-btn' ) . textContent = 'Creating job…' ;
try {
const r = await fetch ( BASE + '/api/jobs' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { request : req } )
} ) ;
const data = await r . json ( ) ;
if ( ! r . ok ) { showErr ( 'input-error' , data . error || 'Failed to create job' ) ; $ ( 'submit-btn' ) . disabled = false ; $ ( 'submit-btn' ) . textContent = 'Create job & get eval invoice →' ; return ; }
jobId = data . jobId ;
evalHash = data . evalInvoice . paymentHash ;
$ ( 'eval-amount' ) . innerHTML = data . evalInvoice . amountSats + '<span> sats</span>' ;
$ ( 'eval-pr' ) . textContent = data . evalInvoice . paymentRequest ;
$ ( 'eval-hash' ) . textContent = evalHash ;
hide ( 'card-input' ) ;
show ( 'card-eval' ) ;
setStep ( 'awaiting_eval_payment' ) ;
} catch ( e ) {
showErr ( 'input-error' , 'Network error: ' + e . message ) ;
$ ( 'submit-btn' ) . disabled = false ;
$ ( 'submit-btn' ) . textContent = 'Create job & get eval invoice →' ;
}
}
async function payEval() {
$ ( 'pay-eval-btn' ) . disabled = true ;
$ ( 'pay-eval-btn' ) . textContent = 'Paying…' ;
try {
const r = await fetch ( BASE + '/api/dev/stub/pay/' + evalHash , { method : 'POST' } ) ;
if ( ! r . ok ) { alert ( 'Stub pay failed' ) ; $ ( 'pay-eval-btn' ) . disabled = false ; return ; }
show ( 'eval-polling' ) ;
setStep ( 'evaluating' ) ;
pollJob ( 'eval' ) ;
} catch ( e ) {
alert ( 'Error: ' + e . message ) ;
$ ( 'pay-eval-btn' ) . disabled = false ;
}
}
async function payWork() {
$ ( 'pay-work-btn' ) . disabled = true ;
$ ( 'pay-work-btn' ) . textContent = 'Paying…' ;
try {
const r = await fetch ( BASE + '/api/dev/stub/pay/' + workHash , { method : 'POST' } ) ;
if ( ! r . ok ) { alert ( 'Stub pay failed' ) ; $ ( 'pay-work-btn' ) . disabled = false ; return ; }
show ( 'work-polling' ) ;
setStep ( 'executing' ) ;
pollJob ( 'work' ) ;
} catch ( e ) {
alert ( 'Error: ' + e . message ) ;
$ ( 'pay-work-btn' ) . disabled = false ;
}
}
function pollJob ( phase ) {
clearInterval ( pollTimer ) ;
pollTimer = setInterval ( async ( ) = > {
try {
const r = await fetch ( BASE + '/api/jobs/' + jobId ) ;
2026-03-18 21:00:43 +00:00
if ( ! r . ok ) return ; // keep polling on transient errors
2026-03-18 18:06:44 +00:00
const data = await r . json ( ) ;
2026-03-18 21:00:43 +00:00
const s = data . state ;
// Failed is terminal regardless of phase
if ( s === 'failed' ) {
clearInterval ( pollTimer ) ;
hide ( 'card-eval' ) ;
hide ( 'card-work' ) ;
$ ( 'rejected-reason' ) . textContent = 'Error: ' + ( data . errorMessage || 'Something went wrong. Try again.' ) ;
show ( 'card-rejected' ) ;
setStep ( 'rejected' ) ;
return ;
}
2026-03-18 18:06:44 +00:00
if ( phase === 'eval' ) {
2026-03-18 21:00:43 +00:00
// evaluating = AI running in background, keep waiting
if ( s === 'evaluating' ) return ;
2026-03-18 18:06:44 +00:00
if ( s === 'rejected' ) {
clearInterval ( pollTimer ) ;
hide ( 'card-eval' ) ;
2026-03-18 21:00:43 +00:00
$ ( 'rejected-reason' ) . textContent = data . reason || 'Request was rejected.' ;
2026-03-18 18:06:44 +00:00
show ( 'card-rejected' ) ;
setStep ( 'rejected' ) ;
} else if ( s === 'awaiting_work_payment' ) {
clearInterval ( pollTimer ) ;
workHash = data . workInvoice . paymentHash ;
$ ( 'work-amount' ) . innerHTML = data . workInvoice . amountSats + '<span> sats</span>' ;
$ ( 'work-pr' ) . textContent = data . workInvoice . paymentRequest ;
$ ( 'work-hash' ) . textContent = workHash ;
hide ( 'card-eval' ) ;
show ( 'card-work' ) ;
setStep ( 'awaiting_work_payment' ) ;
}
} else if ( phase === 'work' ) {
2026-03-18 21:00:43 +00:00
// executing = AI running in background, keep waiting
if ( s === 'executing' ) return ;
2026-03-18 18:06:44 +00:00
if ( s === 'complete' ) {
clearInterval ( pollTimer ) ;
hide ( 'card-work' ) ;
$ ( 'result-text' ) . textContent = data . result ;
show ( 'card-result' ) ;
setStep ( 'complete' ) ;
}
}
2026-03-18 21:00:43 +00:00
} catch ( e ) { /* keep polling through network errors */ }
2026-03-18 18:06:44 +00:00
} , 1500 ) ;
}
function reset() {
clearInterval ( pollTimer ) ;
jobId = evalHash = workHash = null ;
$ ( 'request-input' ) . value = '' ;
$ ( 'char-count' ) . textContent = '0 / 500' ;
$ ( 'submit-btn' ) . disabled = false ;
$ ( 'submit-btn' ) . textContent = 'Create job & get eval invoice →' ;
$ ( 'pay-eval-btn' ) . disabled = false ;
$ ( 'pay-eval-btn' ) . textContent = '⚡ Simulate eval payment' ;
$ ( 'pay-work-btn' ) . disabled = false ;
$ ( 'pay-work-btn' ) . textContent = '⚡ Simulate work payment' ;
hide ( 'eval-polling' ) ; hide ( 'work-polling' ) ;
hide ( 'card-eval' ) ; hide ( 'card-rejected' ) ; hide ( 'card-work' ) ; hide ( 'card-result' ) ;
show ( 'card-input' ) ;
document . querySelectorAll ( '.step' ) . forEach ( el = > el . classList . remove ( 'active' , 'done' , 'rejected' ) ) ;
}
function showErr ( id , msg ) { $ ( id ) . textContent = msg ; show ( id ) ; }
setStep ( 'request' ) ;
$ ( 's-request' ) . classList . add ( 'active' ) ;
< / script >
< / body >
< / html > ` );
} ) ;
export default router ;