feat(chat): save and view chat history — admin-only saved_chats, History page, open-a-chat return

This commit is contained in:
2026-08-29 21:22:25 -04:00
parent 6832957ab0
commit ece93a7c8f
29 changed files with 3099 additions and 20 deletions
+173 -1
View File
@@ -308,6 +308,36 @@ html::after {
whole control below 640px. */
.new-chat-btn svg { width: 16px; height: 16px; display: none; }
/* Phase 50 (owner-locked 2026-08-29, TODO.md L5): the "Save" pill — the
EXACT visual family of .new-chat-btn (same declarations, so the two
chat-shell actions always read as a pair): solid brand pill, --bg text
on --brand (5.2:1, WCAG AA), borderless, ≥44px target, hover lightens
the brand fill, focus-visible via the global 3px rule. Shipped hidden
in index.html — app.js reveals it for admin only (phase 16
absent-not-hidden); ≤640px overrides below mirror the New chat ones
(label stays visible in .chat-shell, icon hidden). */
.save-chat-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 0;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
}
.save-chat-btn:hover { background: #f55a72; color: var(--bg); }
/* The save mark is hidden on desktop (the label carries the pill); it is
the whole control below 640px (mirrored in the ≤640 block below). */
.save-chat-btn svg { width: 16px; height: 16px; display: none; }
/* Phase 16: header auth controls (Sign in link / Sign out button) — the
same ghost pill as New chat, so the bar keeps one visual language.
ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1.
@@ -1746,6 +1776,136 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
box-shadow: var(--shadow);
}
/* ---------- History page (phase 50) ----------
/history.html: the admin-only saved-chats list (task 04). The
FULL-WIDTH table in the 72rem frame (AGENTS.md rule 5 — no skinny
single-column list), the same table language as the Sources page
(the sources-table family: --line hairlines, the brand-soft-tinted
thead, row hover, the scrollable .table-wrap). Every pair reuses
the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft
>=6.9:1, err 9.1:1. :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.history-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the sync-result shape —
ink-soft on surface (>=4.5:1), small mono; the min-height holds
the layout so a delete's line never reflows the table. */
.history-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
/* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the
standard .container; the .table-wrap card + its horizontal scroll
cover narrow widths (the phase-07 responsive contract). */
.history-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.history-table th, .history-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.history-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.history-table tbody tr:hover { background: var(--bg); }
.history-table tbody tr:last-child td { border-bottom: 0; }
/* Title cell: the Open link — the accent link (brand-ink on surface
6.9:1); the column ellipsizes, the full title sits in the title
attribute (on the cell AND the link). */
.history-title-cell {
max-width: 34rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-title-link {
color: var(--brand-ink);
font-weight: 600;
text-decoration: none;
}
.history-title-link:hover { text-decoration: underline; }
.history-title-link:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Messages: the mono numeric readout (ink-soft >=6.9:1). */
.history-count-cell { font-family: var(--mono); color: var(--ink-soft); }
/* Updated: locale date+time (ink-soft), the full ISO in the title
attribute (history.js). */
.history-updated-cell { color: var(--ink-soft); white-space: nowrap; }
/* Actions: the Delete ghost button (the tuning row-action language)
+ the inline two-step confirm pair (phase 50 task 04). */
.history-actions { display: inline-flex; align-items: center; gap: 0.4rem; }
.history-delete {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.history-delete:disabled { opacity: 0.5; cursor: wait; }
.history-confirm-text { color: var(--err-ink); font-size: 0.82rem; font-weight: 700; white-space: nowrap; }
/* Yes: the error-rose treatment (err-ink on err-bg 9.1:1, err-line
border); No: the ghost (transparent, --line border). */
.history-confirm-yes {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
background: var(--err-bg);
color: var(--err-ink);
font: inherit;
font-weight: 700;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-yes:hover:not(:disabled) { background: rgb(239 68 68 / 0.18); }
.history-confirm-yes:disabled { opacity: 0.6; cursor: wait; }
.history-confirm-no {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-no:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* Empty-state row: the muted centered message at full table width
(the .git-sources-empty language, inline in the table). */
.history-empty-row td {
padding: 2.25rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
}
/* ---------- Document viewer (phase 10; two-row header since phase 34) ---------- */
/* Phase 34 (owner confirmation 2026-08-26): the viewer header is TWO
rows in one sticky <header> — row 1 reuses the standard .app-header /
@@ -2177,7 +2337,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
designated clip target, pills squeeze next). */
.nav-link { padding: 0.4rem 0.5rem; font-size: 0.85rem; }
.app-nav { gap: 0.15rem; }
.new-chat-btn, .auth-link { padding: 0.45rem 0.5rem; }
.new-chat-btn, .save-chat-btn, .auth-link { padding: 0.45rem 0.5rem; }
}
/* ---------- Responsive (mobile-first adjustments) ---------- */
@@ -2280,10 +2440,17 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.new-chat-btn { padding: 0.4rem 0.3rem; }
.new-chat-label { display: none; }
.new-chat-btn svg { display: block; }
/* Phase 50: the Save pill squeezes with New chat (same family, same
rules — the chat-shell overrides below keep both labels visible). */
.save-chat-btn { padding: 0.4rem 0.3rem; }
.save-chat-label { display: none; }
.save-chat-btn svg { display: block; }
/* But on the chat page there is room — keep the label visible and
hide the icon (the button lives inside .chat-shell, not the navbar). */
.chat-shell .new-chat-label { display: inline; }
.chat-shell .new-chat-btn svg { display: none; }
.chat-shell .save-chat-label { display: inline; }
.chat-shell .save-chat-btn svg { display: none; }
/* Phase 16: the auth pill goes icon-only like New chat — brand text
ellipsizes as the designated squeeze target, no bar overflow. */
.auth-link { padding: 0.4rem 0.3rem; }
@@ -2392,6 +2559,11 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#archive-upload-file { min-width: 0; }
#git-source-add,
#archive-upload-btn { width: 100%; }
/* Phase 50: the History table keeps its full width (the .table-wrap
horizontal scroll already covers it); the actions cell wraps so
the two-step confirm pair fits the phone width. */
.history-actions-cell { white-space: normal; }
.history-actions { flex-wrap: wrap; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
main { padding-bottom: env(safe-area-inset-bottom, 0); }
/* Sync button goes icon-only on mobile; the label hides, aria-label