Files
agent-wiki-ui/ui.html
T
davideandClaude Sonnet 5.5 0d1784c0f4 Interfaccia riscritta in HTML/CSS e finestra 16:10
Tutta la UI passa in ui.html (WebView2): header, schermata iniziale con
suggerimenti, composer a scheda con selettori modello/effort, messaggi
aggiunti al DOM senza ricaricare la pagina, pulsanti Copia, attesa
animata, tema chiaro/scuro di Windows. Gui.cs diventa un host sottile
che dialoga con la pagina via WebMessage. La finestra ha proporzioni
16:10 calcolate sull'area utile dello schermo. Documentazione aggiornata.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 13:29:53 +02:00

218 lines
14 KiB
HTML

<!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 1px 2px rgba(15,23,42,.06), 0 8px 24px 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 1px 2px rgba(0,0,0,.3), 0 8px 24px rgba(0,0,0,.35); }
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { display: flex; flex-direction: column; font: 15.5px/1.65 var(--font); background: var(--bg); color: var(--fg); overflow: hidden; }
button, select, textarea, input { font: inherit; color: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
/* header */
header { display: flex; align-items: center; justify-content: space-between; height: 54px; padding: 0 20px; border-bottom: 1px solid var(--border); flex: none; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: -.01em; }
.logo { width: 28px; height: 28px; border-radius: 8px; background: var(--accent); color: var(--on-accent); display: grid; place-items: center; }
.logo svg { width: 16px; height: 16px; fill: currentColor; stroke: none; }
.ghost { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px; border-radius: 10px; border: 1px solid var(--border); background: transparent; transition: background .15s, border-color .15s; }
.ghost:hover { background: var(--muted); }
/* conversazione */
#scroll { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
#chat { max-width: 780px; margin: 0 auto; padding: 28px 20px 12px; }
.msg { display: flex; gap: 14px; margin-bottom: 26px; animation: in .18s ease-out; }
@keyframes in { from { opacity: 0; transform: translateY(6px); } }
.msg.user { justify-content: flex-end; }
.bubble { max-width: 85%; padding: 10px 16px; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--muted); border: 1px solid var(--border); border-radius: 18px 18px 4px 18px; }
.avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: var(--on-accent); display: grid; place-items: center; flex: none; margin-top: 2px; }
.avatar svg { width: 16px; height: 16px; fill: currentColor; stroke: none; }
.body { min-width: 0; flex: 1; position: relative; }
.who { font-size: 12px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--muted-fg); margin-bottom: 2px; }
.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.4em 0 .5em; }
.content p, .content ul, .content ol { margin: .7em 0; }
.content code { font-family: var(--mono); font-size: 13.5px; font-variant-ligatures: none; }
.content :not(pre) > code { background: var(--muted); border: 1px solid var(--border); padding: 1px 6px; border-radius: 6px; }
.content pre { position: relative; margin: 1em 0; padding: 16px; background: var(--muted); border: 1px solid var(--border); border-radius: 12px; overflow-x: auto; }
.content table { border-collapse: collapse; display: block; overflow-x: auto; margin: 1em 0; }
.content th, .content td { border: 1px solid var(--border); padding: 7px 14px; text-align: left; }
.content th { background: var(--muted); font-weight: 600; }
.content blockquote { margin: 1em 0; padding: 2px 16px; border-left: 3px solid var(--accent); color: var(--muted-fg); }
.content hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }
.mini { position: absolute; top: 8px; right: 8px; height: 28px; padding: 0 10px; font-size: 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); opacity: 0; transition: opacity .15s; }
pre:hover > .mini, .mini:focus-visible { opacity: 1; }
.actions { margin-top: 8px; opacity: 0; transition: opacity .15s; }
.msg:hover .actions, .actions:focus-within { opacity: 1; }
.actions .ghost { height: 30px; padding: 0 10px; font-size: 13px; color: var(--muted-fg); border-color: transparent; }
.actions .ghost svg { width: 15px; height: 15px; }
.err .content { color: var(--err); border: 1px solid var(--err); border-radius: 12px; padding: 10px 14px; 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: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s infinite ease-in-out; }
.dots i:nth-child(2) { animation-delay: .15s; } .dots i:nth-child(3) { animation-delay: .3s; }
@keyframes pulse { 0%, 80%, 100% { opacity: .25 } 40% { opacity: 1 } }
/* schermata iniziale */
#empty { max-width: 640px; margin: 0 auto; padding: 12vh 20px 0; text-align: center; }
#empty .logo { width: 52px; height: 52px; border-radius: 16px; margin: 0 auto 18px; }
#empty .logo svg { width: 28px; height: 28px; }
#empty h1 { font-size: 30px; letter-spacing: -.02em; margin: 0 0 6px; }
#empty p { color: var(--muted-fg); margin: 0 0 28px; }
.chips { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.chip { text-align: left; padding: 12px 16px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); transition: border-color .15s, transform .15s, background .15s; }
.chip:hover { border-color: var(--accent); background: var(--muted); transform: translateY(-1px); }
.chip small { display: block; color: var(--muted-fg); font-size: 12.5px; }
/* composer */
footer { flex: none; padding: 8px 20px 12px; }
.composer { max-width: 780px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border); border-radius: 20px; box-shadow: var(--shadow); padding: 12px 12px 10px 18px; transition: border-color .15s; }
.composer:focus-within { border-color: var(--accent); }
textarea { display: block; width: 100%; resize: none; border: 0; outline: 0; background: transparent; padding: 4px 0; max-height: 200px; 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: 8px; margin-top: 6px; }
.field { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted-fg); }
.field select, .field input { height: 32px; border-radius: 9px; border: 1px solid var(--border); background: var(--bg); padding: 0 8px; font-size: 13px; color: var(--fg); }
.field input { width: 190px; }
.spacer { flex: 1; }
#send { width: 40px; height: 40px; border-radius: 50%; border: 0; background: var(--accent); color: var(--on-accent); display: grid; place-items: center; transition: transform .15s, opacity .15s; }
#send:hover:not(:disabled) { transform: scale(1.06); }
#send:disabled { opacity: .35; cursor: default; }
#send svg { width: 20px; height: 20px; stroke-width: 2.4; }
.hint { max-width: 780px; margin: 8px auto 0; text-align: center; font-size: 12px; color: var(--muted-fg); }
kbd { font: 11.5px var(--mono); padding: 1px 5px; border: 1px solid var(--border); border-radius: 5px; background: var(--muted); }
::-webkit-scrollbar { width: 12px; height: 12px; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; border: 3px solid var(--bg); }
@media (max-width: 720px) { .chips { grid-template-columns: 1fr; } .field input { width: 120px; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; scroll-behavior: auto !important; } .dots i { opacity: .7; } }
</style>
</head>
<body>
<header>
<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>
</header>
<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:&#10;&#10;">Rivedi del codice<small>Bug, leggibilità, prestazioni</small></button>
<button class="chip" data-p="Riassumi in modo chiaro questo testo:&#10;&#10;">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>
<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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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(); };
$('#reset').onclick = () => { chat.replaceChildren(); chat.hidden = true; empty.hidden = false; busy = false; refresh(); post({ type: 'reset' }); input.focus(); };
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);
});
input.focus();
</script>
</body>
</html>