Cronologia chat: salvataggio su disco, barra laterale e ripresa all'avvio
Ogni chat è salvata come JSON in %LOCALAPPDATA%\LlmRun\chats (nome file = id di sessione di claude). La barra laterale elenca le chat, permette di riaprirle con --resume e di eliminarle. Il messaggio dell'utente è salvato subito, prima della risposta, e all'avvio si riapre l'ultima conversazione. Documentazione e roadmap aggiornate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d1784c0f4
commit
c83d69f435
5 files changed
+166
-22
No files matched your search
@@ -21,19 +21,39 @@
|
||||
}
|
||||
* { 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; }
|
||||
body { display: flex; 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; }
|
||||
/* barra laterale */
|
||||
#side { width: 272px; flex: none; display: flex; flex-direction: column; background: var(--muted); border-right: 1px solid var(--border); transition: margin-left .2s; }
|
||||
#side.closed { margin-left: -272px; }
|
||||
.side-top { padding: 16px 14px 10px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: -.01em; padding: 0 4px; }
|
||||
.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); }
|
||||
.ghost:hover { background: var(--bg); }
|
||||
#reset { justify-content: center; background: var(--bg); }
|
||||
#reset:hover { border-color: var(--accent); }
|
||||
.side-h { padding: 8px 18px 4px; font-size: 12px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--muted-fg); }
|
||||
#list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
|
||||
.item { position: relative; border-radius: 10px; padding: 8px 38px 8px 12px; transition: background .15s; }
|
||||
.item:hover { background: var(--bg); }
|
||||
.item.active { background: var(--bg); box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.item .open { position: absolute; inset: 0; width: 100%; border: 0; background: transparent; border-radius: 10px; }
|
||||
.item .t, .item .d { position: relative; display: block; pointer-events: none; }
|
||||
.item .t { font-size: 14px; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.item .d { font-size: 12px; color: var(--muted-fg); }
|
||||
.item .x { position: absolute; top: 50%; right: 6px; transform: translateY(-50%); width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 7px; 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: 14px; height: 14px; }
|
||||
.none { padding: 10px 14px; font-size: 13px; color: var(--muted-fg); }
|
||||
#main { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; }
|
||||
#toggle { position: absolute; top: 12px; left: 12px; z-index: 2; width: 36px; height: 36px; padding: 0; justify-content: center; }
|
||||
|
||||
/* conversazione */
|
||||
#scroll { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
|
||||
@@ -106,10 +126,16 @@ kbd { font: 11.5px var(--mono); padding: 1px 5px; border: 1px solid var(--border
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<div id="scroll">
|
||||
<div id="empty">
|
||||
@@ -145,6 +171,7 @@ kbd { font: 11.5px var(--mono); padding: 1px 5px; border: 1px solid var(--border
|
||||
</div>
|
||||
<div class="hint"><kbd>Invio</kbd> per inviare · <kbd>Maiusc</kbd>+<kbd>Invio</kbd> per andare a capo</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const $ = s => document.querySelector(s);
|
||||
@@ -202,7 +229,32 @@ 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(); };
|
||||
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();
|
||||
}
|
||||
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 => {
|
||||
@@ -210,8 +262,11 @@ host && host.addEventListener('message', e => {
|
||||
if (m.type === 'ai') addAi(m.html, m.md);
|
||||
else if (m.type === 'err') addErr(m.text);
|
||||
else if (m.type === 'done') setBusy(false);
|
||||
else if (m.type === 'chats') renderList(m.items, m.current);
|
||||
else if (m.type === 'load') load(m.messages);
|
||||
});
|
||||
input.focus();
|
||||
post({ type: 'list' });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user