2026-10-08 13:29:53 +02:00
<!doctype html>
< html lang = "it" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > LlmRun</ title >
< style >
/* Token di design: vedi docs/MVP.md → "Design (UI/UX)" */
: root {
color-scheme : light dark ;
--bg : #FFFFFF ; --surface : #FFFFFF ; --fg : #0F172A ; --muted : #F1F5F9 ; --muted-fg : #475569 ;
--border : #CBD5E1 ; --accent : #15803D ; --on-accent : #FFFFFF ; --accent-soft : #DCFCE7 ; --err : #B91C1C ;
--shadow : 0 1 px 2 px rgba ( 15 , 23 , 42 , .06 ), 0 8 px 24 px rgba ( 15 , 23 , 42 , .08 );
--font : 'Segoe UI Variable Text' , 'Segoe UI' , system-ui , sans-serif ;
--mono : 'Cascadia Code' , Consolas , monospace ;
}
@ media ( prefers-color-scheme : dark ) {
: root { --bg : #0F172A ; --surface : #1E293B ; --fg : #F8FAFC ; --muted : #1E293B ; --muted-fg : #94A3B8 ;
--border : #334155 ; --accent : #22C55E ; --on-accent : #0F172A ; --accent-soft : #14301F ; --err : #F87171 ;
--shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , .3 ), 0 8 px 24 px rgba ( 0 , 0 , 0 , .35 ); }
}
* { box-sizing : border-box ; }
html , body { height : 100 % ; margin : 0 ; }
2026-10-08 13:49:28 +02:00
body { display : flex ; font : 15.5 px / 1.65 var ( -- font ); background : var ( -- bg ); color : var ( -- fg ); overflow : hidden ; }
2026-10-08 13:29:53 +02:00
button , select , textarea , input { font : inherit ; color : inherit ; }
button { cursor : pointer ; }
: focus-visible { outline : 2 px solid var ( -- accent ); outline-offset : 2 px ; }
svg { width : 18 px ; height : 18 px ; fill : none ; stroke : currentColor ; stroke-width : 2 ; stroke-linecap : round ; stroke-linejoin : round ; flex : none ; }
2026-10-08 13:49:28 +02:00
/* barra laterale */
# side { width : 272 px ; flex : none ; display : flex ; flex-direction : column ; background : var ( -- muted ); border-right : 1 px solid var ( -- border ); transition : margin-left .2 s ; }
# side . closed { margin-left : -272 px ; }
. side-top { padding : 16 px 14 px 10 px ; display : flex ; flex-direction : column ; gap : 14 px ; }
. brand { display : flex ; align-items : center ; gap : 10 px ; font-weight : 600 ; letter-spacing : -.01 em ; padding : 0 4 px ; }
2026-10-08 13:29:53 +02:00
. logo { width : 28 px ; height : 28 px ; border-radius : 8 px ; background : var ( -- accent ); color : var ( -- on - accent ); display : grid ; place-items : center ; }
. logo svg { width : 16 px ; height : 16 px ; fill : currentColor ; stroke : none ; }
. ghost { display : inline-flex ; align-items : center ; gap : 8 px ; height : 36 px ; padding : 0 14 px ; border-radius : 10 px ; border : 1 px solid var ( -- border ); background : transparent ; transition : background .15 s , border-color .15 s ; }
2026-10-08 13:49:28 +02:00
. ghost : hover { background : var ( -- bg ); }
# reset { justify-content : center ; background : var ( -- bg ); }
# reset : hover { border-color : var ( -- accent ); }
. side-h { padding : 8 px 18 px 4 px ; font-size : 12 px ; font-weight : 600 ; letter-spacing : .05 em ; text-transform : uppercase ; color : var ( -- muted - fg ); }
# list { flex : 1 ; overflow-y : auto ; padding : 4 px 8 px 12 px ; }
. item { position : relative ; border-radius : 10 px ; padding : 8 px 38 px 8 px 12 px ; transition : background .15 s ; }
. item : hover { background : var ( -- bg ); }
. item . active { background : var ( -- bg ); box-shadow : inset 3 px 0 0 var ( -- accent ); }
. item . open { position : absolute ; inset : 0 ; width : 100 % ; border : 0 ; background : transparent ; border-radius : 10 px ; }
. item . t , . item . d { position : relative ; display : block ; pointer-events : none ; }
. item . t { font-size : 14 px ; line-height : 1.35 ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; }
. item . d { font-size : 12 px ; color : var ( -- muted - fg ); }
. item . x { position : absolute ; top : 50 % ; right : 6 px ; transform : translateY ( -50 % ); width : 26 px ; height : 26 px ; display : grid ; place-items : center ; border : 0 ; border-radius : 7 px ; background : transparent ; color : var ( -- muted - fg ); opacity : 0 ; }
. item : hover . x , . item : focus-within . x { opacity : 1 ; }
. item . x : hover { background : var ( -- muted ); color : var ( -- err ); }
. item . x svg { width : 14 px ; height : 14 px ; }
. none { padding : 10 px 14 px ; font-size : 13 px ; color : var ( -- muted - fg ); }
# main { flex : 1 ; min-width : 0 ; display : flex ; flex-direction : column ; position : relative ; }
# toggle { position : absolute ; top : 12 px ; left : 12 px ; z-index : 2 ; width : 36 px ; height : 36 px ; padding : 0 ; justify-content : center ; }
2026-10-08 13:29:53 +02:00
/* conversazione */
# scroll { flex : 1 ; overflow-y : auto ; scroll-behavior : smooth ; }
# chat { max-width : 780 px ; margin : 0 auto ; padding : 28 px 20 px 12 px ; }
. msg { display : flex ; gap : 14 px ; margin-bottom : 26 px ; animation : in .18 s ease-out ; }
@ keyframes in { from { opacity : 0 ; transform : translateY ( 6 px ); } }
. msg . user { justify-content : flex-end ; }
. bubble { max-width : 85 % ; padding : 10 px 16 px ; white-space : pre-wrap ; overflow-wrap : anywhere ; background : var ( -- muted ); border : 1 px solid var ( -- border ); border-radius : 18 px 18 px 4 px 18 px ; }
. avatar { width : 30 px ; height : 30 px ; border-radius : 50 % ; background : var ( -- accent ); color : var ( -- on - accent ); display : grid ; place-items : center ; flex : none ; margin-top : 2 px ; }
. avatar svg { width : 16 px ; height : 16 px ; fill : currentColor ; stroke : none ; }
. body { min-width : 0 ; flex : 1 ; position : relative ; }
. who { font-size : 12 px ; font-weight : 600 ; letter-spacing : .05 em ; text-transform : uppercase ; color : var ( -- muted - fg ); margin-bottom : 2 px ; }
. content > : first-child { margin-top : 0 ; } . content > : last-child { margin-bottom : 0 ; }
. content a { color : var ( -- accent ); }
. content h1 , . content h2 , . content h3 { line-height : 1.3 ; margin : 1.4 em 0 .5 em ; }
. content p , . content ul , . content ol { margin : .7 em 0 ; }
. content code { font-family : var ( -- mono ); font-size : 13.5 px ; font-variant-ligatures : none ; }
. content : not ( pre ) > code { background : var ( -- muted ); border : 1 px solid var ( -- border ); padding : 1 px 6 px ; border-radius : 6 px ; }
. content pre { position : relative ; margin : 1 em 0 ; padding : 16 px ; background : var ( -- muted ); border : 1 px solid var ( -- border ); border-radius : 12 px ; overflow-x : auto ; }
. content table { border-collapse : collapse ; display : block ; overflow-x : auto ; margin : 1 em 0 ; }
. content th , . content td { border : 1 px solid var ( -- border ); padding : 7 px 14 px ; text-align : left ; }
. content th { background : var ( -- muted ); font-weight : 600 ; }
. content blockquote { margin : 1 em 0 ; padding : 2 px 16 px ; border-left : 3 px solid var ( -- accent ); color : var ( -- muted - fg ); }
. content hr { border : 0 ; border-top : 1 px solid var ( -- border ); margin : 1.5 em 0 ; }
. mini { position : absolute ; top : 8 px ; right : 8 px ; height : 28 px ; padding : 0 10 px ; font-size : 12 px ; border-radius : 8 px ; border : 1 px solid var ( -- border ); background : var ( -- surface ); opacity : 0 ; transition : opacity .15 s ; }
pre : hover > . mini , . mini : focus-visible { opacity : 1 ; }
. actions { margin-top : 8 px ; opacity : 0 ; transition : opacity .15 s ; }
. msg : hover . actions , . actions : focus-within { opacity : 1 ; }
. actions . ghost { height : 30 px ; padding : 0 10 px ; font-size : 13 px ; color : var ( -- muted - fg ); border-color : transparent ; }
. actions . ghost svg { width : 15 px ; height : 15 px ; }
. err . content { color : var ( -- err ); border : 1 px solid var ( -- err ); border-radius : 12 px ; padding : 10 px 14 px ; white-space : pre-wrap ; background : color-mix ( in srgb , var ( -- err ) 8 % , var ( -- bg )); }
. wait { color : var ( -- muted - fg ); align-items : center ; }
. dots i { display : inline-block ; width : 7 px ; height : 7 px ; margin-right : 5 px ; border-radius : 50 % ; background : var ( -- accent ); animation : pulse 1.2 s infinite ease-in-out ; }
. dots i : nth-child ( 2 ) { animation-delay : .15 s ; } . dots i : nth-child ( 3 ) { animation-delay : .3 s ; }
@ keyframes pulse { 0 %, 80 %, 100 % { opacity : .25 } 40 % { opacity : 1 } }
/* schermata iniziale */
# empty { max-width : 640 px ; margin : 0 auto ; padding : 12 vh 20 px 0 ; text-align : center ; }
# empty . logo { width : 52 px ; height : 52 px ; border-radius : 16 px ; margin : 0 auto 18 px ; }
# empty . logo svg { width : 28 px ; height : 28 px ; }
# empty h1 { font-size : 30 px ; letter-spacing : -.02 em ; margin : 0 0 6 px ; }
# empty p { color : var ( -- muted - fg ); margin : 0 0 28 px ; }
. chips { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 10 px ; }
. chip { text-align : left ; padding : 12 px 16 px ; border-radius : 14 px ; border : 1 px solid var ( -- border ); background : var ( -- surface ); transition : border-color .15 s , transform .15 s , background .15 s ; }
. chip : hover { border-color : var ( -- accent ); background : var ( -- muted ); transform : translateY ( -1 px ); }
. chip small { display : block ; color : var ( -- muted - fg ); font-size : 12.5 px ; }
/* composer */
footer { flex : none ; padding : 8 px 20 px 12 px ; }
. composer { max-width : 780 px ; margin : 0 auto ; background : var ( -- surface ); border : 1 px solid var ( -- border ); border-radius : 20 px ; box-shadow : var ( -- shadow ); padding : 12 px 12 px 10 px 18 px ; transition : border-color .15 s ; }
. composer : focus-within { border-color : var ( -- accent ); }
textarea { display : block ; width : 100 % ; resize : none ; border : 0 ; outline : 0 ; background : transparent ; padding : 4 px 0 ; max-height : 200 px ; line-height : 1.55 ; }
textarea : focus-visible { outline : 0 ; } /* il focus è mostrato dal bordo del .composer */
textarea :: placeholder { color : var ( -- muted - fg ); }
. bar { display : flex ; align-items : center ; gap : 8 px ; margin-top : 6 px ; }
. field { display : inline-flex ; align-items : center ; gap : 6 px ; font-size : 13 px ; color : var ( -- muted - fg ); }
. field select , . field input { height : 32 px ; border-radius : 9 px ; border : 1 px solid var ( -- border ); background : var ( -- bg ); padding : 0 8 px ; font-size : 13 px ; color : var ( -- fg ); }
. field input { width : 190 px ; }
. spacer { flex : 1 ; }
# send { width : 40 px ; height : 40 px ; border-radius : 50 % ; border : 0 ; background : var ( -- accent ); color : var ( -- on - accent ); display : grid ; place-items : center ; transition : transform .15 s , opacity .15 s ; }
# send : hover : not ( : disabled ) { transform : scale ( 1.06 ); }
# send : disabled { opacity : .35 ; cursor : default ; }
# send svg { width : 20 px ; height : 20 px ; stroke-width : 2.4 ; }
. hint { max-width : 780 px ; margin : 8 px auto 0 ; text-align : center ; font-size : 12 px ; color : var ( -- muted - fg ); }
kbd { font : 11.5 px var ( -- mono ); padding : 1 px 5 px ; border : 1 px solid var ( -- border ); border-radius : 5 px ; background : var ( -- muted ); }
:: -webkit-scrollbar { width : 12 px ; height : 12 px ; } :: -webkit-scrollbar-thumb { background : var ( -- border ); border-radius : 8 px ; border : 3 px solid var ( -- bg ); }
@ media ( max-width : 720px ) { . chips { grid-template-columns : 1 fr ; } . field input { width : 120 px ; } }
@ media ( prefers-reduced-motion : reduce ) { * { animation : none !important ; transition : none !important ; scroll-behavior : auto !important ; } . dots i { opacity : .7 ; } }
</ style >
</ head >
< body >
2026-10-08 13:49:28 +02:00
< aside id = "side" aria-label = "Chat salvate" >
< div class = "side-top" >
< div class = "brand" >< span class = "logo" >< svg viewBox = "0 0 24 24" >< path d = "M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z" /></ svg ></ span > LlmRun</ div >
< button class = "ghost" id = "reset" aria-label = "Nuova chat" >< svg viewBox = "0 0 24 24" >< path d = "M12 5v14M5 12h14" /></ svg > Nuova chat</ button >
</ div >
< div class = "side-h" > Chat</ div >
< div id = "list" ></ div >
</ aside >
< main id = "main" >
< button class = "ghost" id = "toggle" aria-label = "Mostra o nascondi le chat" aria-expanded = "true" >< svg viewBox = "0 0 24 24" >< path d = "M4 6h16M4 12h16M4 18h16" /></ svg ></ button >
2026-10-08 13:29:53 +02:00
< div id = "scroll" >
< div id = "empty" >
< span class = "logo" >< svg viewBox = "0 0 24 24" >< path d = "M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z" /></ svg ></ span >
< h1 > Come posso aiutarti?</ h1 >
< p > Scrivi un messaggio oppure parti da uno di questi spunti.</ p >
< div class = "chips" >
< button class = "chip" data-p = "Spiega cosa fa questo progetto e come è organizzato." > Spiega il progetto< small > Panoramica e struttura</ small ></ button >
< button class = "chip" data-p = "Scrivi uno script PowerShell che " > Scrivi uno script< small > PowerShell, Python, C#…</ small ></ button >
< button class = "chip" data-p = "Rivedi questo codice e suggerisci miglioramenti: " > Rivedi del codice< small > Bug, leggibilità, prestazioni</ small ></ button >
< button class = "chip" data-p = "Riassumi in modo chiaro questo testo: " > Riassumi un testo< small > Punti chiave in breve</ small ></ button >
</ div >
</ div >
< div id = "chat" role = "log" aria-live = "polite" aria-label = "Conversazione" hidden ></ div >
</ div >
< footer >
< div class = "composer" >
< textarea id = "input" rows = "1" placeholder = "Scrivi un messaggio a Claude…" aria-label = "Messaggio" ></ textarea >
< div class = "bar" >
< label class = "field" > Modello
< select id = "model" aria-label = "Modello" > {{MODELS}}< option value = "__custom" > Altro ID…</ option ></ select >
< input id = "custom" placeholder = "es. claude-opus-5-5" aria-label = "ID modello personalizzato" hidden >
</ label >
< label class = "field" > Effort
< select id = "effort" aria-label = "Effort di ragionamento" >
< option value = "" > Predefinito</ option >< option > low</ option >< option > medium</ option >< option > high</ option >< option > xhigh</ option >< option > max</ option >
</ select >
</ label >
< span class = "spacer" ></ span >
< button id = "send" aria-label = "Invia messaggio" disabled >< svg viewBox = "0 0 24 24" >< path d = "M12 19V5M5 12l7-7 7 7" /></ svg ></ button >
</ div >
</ div >
< div class = "hint" >< kbd > Invio</ kbd > per inviare · < kbd > Maiusc</ kbd > +< kbd > Invio</ kbd > per andare a capo</ div >
</ footer >
2026-10-08 13:49:28 +02:00
</ main >
2026-10-08 13:29:53 +02:00
< script >
const $ = s => document . querySelector ( s );
const chat = $ ( '#chat' ), empty = $ ( '#empty' ), input = $ ( '#input' ), send = $ ( '#send' ), scroller = $ ( '#scroll' );
const host = window . chrome && window . chrome . webview ;
let busy = false ;
const ICON_COPY = '<svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>' ;
const STAR = '<svg viewBox="0 0 24 24"><path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z"/></svg>' ;
function post ( m ) { host && host . postMessage ( m ); }
function copy ( text ) {
const t = document . createElement ( 'textarea' ); t . value = text ; document . body . appendChild ( t ); t . select ();
document . execCommand ( 'copy' ); t . remove ();
}
function flash ( btn , label ) { const old = btn . innerHTML ; btn . textContent = label ; setTimeout (() => btn . innerHTML = old , 1400 ); }
function bottom () { scroller . scrollTop = scroller . scrollHeight ; }
function el ( html ) { const d = document . createElement ( 'div' ); d . innerHTML = html . trim (); return d . firstChild ; }
function esc ( s ) { return s . replace ( /[&<>"]/g , c => ({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' }[ c ])); }
function show ( node ) {
empty . hidden = true ; chat . hidden = false ;
chat . appendChild ( node ); bottom ();
}
function addUser ( text ) { show ( el ( `<div class="msg user"><div class="bubble"> ${ esc ( text ) } </div></div>` )); }
function addErr ( text ) { show ( el ( `<div class="msg err"><div class="body"><div class="content"> ${ esc ( text ) } </div></div></div>` )); }
function addAi ( html , md ) {
const m = el ( `<div class="msg ai"><span class="avatar"> ${ STAR } </span><div class="body"><div class="who">Claude</div><div class="content"> ${ html } </div>
<div class="actions"><button class="ghost" aria-label="Copia la risposta"> ${ ICON_COPY } Copia</button></div></div></div>` );
m . querySelectorAll ( 'pre' ). forEach ( p => {
const b = el ( '<button class="mini" aria-label="Copia il codice">Copia</button>' );
b . onclick = () => { copy ( p . querySelector ( 'code' ). innerText ); flash ( b , 'Copiato' ); };
p . appendChild ( b );
});
const a = m . querySelector ( '.actions button' ); a . onclick = () => { copy ( md ); flash ( a , 'Copiato' ); };
show ( m );
}
function setBusy ( v ) {
busy = v ; refresh ();
if ( v ) show ( el ( `<div class="msg wait" id="typing" role="status"><span class="avatar"> ${ STAR } </span><span class="dots"><i></i><i></i><i></i></span>Claude sta scrivendo…</div>` ));
else { const t = $ ( '#typing' ); t && t . remove (); input . focus (); }
}
function refresh () { send . disabled = busy || ! input . value . trim (); }
function grow () { input . style . height = 'auto' ; input . style . height = Math . min ( input . scrollHeight , 200 ) + 'px' ; refresh (); }
function submit () {
const text = input . value . trim ();
if ( ! text || busy ) return ;
const sel = $ ( '#model' ). value ;
const model = sel === '__custom' ? $ ( '#custom' ). value . trim () : sel ;
addUser ( text ); input . value = '' ; grow (); setBusy ( true );
post ({ type : 'send' , text , model , effort : $ ( '#effort' ). value });
}
input . addEventListener ( 'input' , grow );
input . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' && ! e . shiftKey && ! e . isComposing ) { e . preventDefault (); submit (); } });
send . onclick = submit ;
$ ( '#model' ). onchange = e => { const c = $ ( '#custom' ); c . hidden = e . target . value !== '__custom' ; if ( ! c . hidden ) c . focus (); };
2026-10-08 13:49:28 +02:00
function clearView () { chat . replaceChildren (); chat . hidden = true ; empty . hidden = false ; }
$ ( '#reset' ). onclick = () => { if ( busy ) return ; clearView (); refresh (); post ({ type : 'reset' }); input . focus (); };
$ ( '#toggle' ). onclick = () => { const c = $ ( '#side' ). classList . toggle ( 'closed' ); $ ( '#toggle' ). setAttribute ( 'aria-expanded' , String ( ! c )); };
const ICON_X = '<svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg>' ;
function renderList ( items , current ) {
const list = $ ( '#list' );
list . replaceChildren ();
if ( ! items . length ) { list . append ( el ( '<div class="none">Nessuna chat salvata</div>' )); return ; }
for ( const c of items ) {
const when = new Date ( c . updated ). toLocaleString ( 'it-IT' , { day : 'numeric' , month : 'short' , hour : '2-digit' , minute : '2-digit' });
const row = el ( `<div class="item ${ c . id === current ? ' active' : '' } "><button class="open" aria-label="Apri chat: ${ esc ( c . title ) } "></button><span class="t"> ${ esc ( c . title ) } </span><span class="d"> ${ esc ( when ) } </span><button class="x" aria-label="Elimina chat"> ${ ICON_X } </button></div>` );
row . querySelector ( '.open' ). onclick = () => { if ( ! busy && c . id !== current ) post ({ type : 'open' , id : c . id }); };
row . querySelector ( '.x' ). onclick = () => { if ( ! busy && confirm ( 'Eliminare questa chat?' )) post ({ type : 'delete' , id : c . id }); };
list . append ( row );
}
}
function load ( messages ) {
clearView ();
for ( const m of messages ) {
if ( m . role === 'user' ) addUser ( m . text );
else if ( m . role === 'ai' ) addAi ( m . html , m . text );
else addErr ( m . text );
}
bottom ();
}
2026-10-08 13:29:53 +02:00
document . querySelectorAll ( '.chip' ). forEach ( c => c . onclick = () => { input . value = c . dataset . p ; grow (); input . focus (); input . setSelectionRange ( input . value . length , input . value . length ); });
host && host . addEventListener ( 'message' , e => {
const m = e . data ;
if ( m . type === 'ai' ) addAi ( m . html , m . md );
else if ( m . type === 'err' ) addErr ( m . text );
else if ( m . type === 'done' ) setBusy ( false );
2026-10-08 13:49:28 +02:00
else if ( m . type === 'chats' ) renderList ( m . items , m . current );
else if ( m . type === 'load' ) load ( m . messages );
2026-10-08 13:29:53 +02:00
});
input . focus ();
2026-10-08 13:49:28 +02:00
post ({ type : 'list' });
2026-10-08 13:29:53 +02:00
</ script >
</ body >
</ html >