feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row
This commit is contained in:
+52
-47
@@ -128,57 +128,62 @@
|
||||
</section>
|
||||
<p class="visually-hidden" id="steering-announcer" role="status" aria-live="polite" aria-atomic="true"></p>
|
||||
|
||||
<!-- Phase 14 (module-owned since phase 34 task 02): "New chat" resets
|
||||
the local (localStorage) conversation and clears the rendered list.
|
||||
The binding lives in header.js — dispatches "bor:new-chat" which
|
||||
app.js acts on (it owns the in-flight-turn guard + list reset). -->
|
||||
<button type="button" class="new-chat-btn" id="new-chat-btn" aria-label="New chat">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
<span class="new-chat-label">New chat</span>
|
||||
</button>
|
||||
<!-- Phase 55 (task 05, 2026-08-31, TODO.md L6, owner-locked A5):
|
||||
the chat-actions row — New chat + Share share ONE
|
||||
.chat-actions wrapper (a normal .chat-shell column child,
|
||||
replacing the two pills as its direct children): a
|
||||
HORIZONTAL row on desktop (side by side, left-aligned, each
|
||||
pill at its intrinsic width — the row's align-items: center
|
||||
beats the column's stretch) and a VERTICAL stack at the
|
||||
existing ≤640px breakpoint (New chat above Share,
|
||||
full-width). DOM order New chat → Share in both
|
||||
orientations; the 46rem column contract is untouched
|
||||
(PLAN §7). The kb-banner / stale-banner / steering /
|
||||
announcer structure around the row is unchanged.
|
||||
|
||||
<!-- Phase 50 (owner-locked 2026-08-29, `TODO.md` L5): "Save" stores
|
||||
the current conversation in Postgres (saved_chats) — admin-only.
|
||||
SHIPS HIDDEN: app.js reveals it only when whoami says admin
|
||||
(phase 16 absent-not-hidden — `hidden` is display:none, so
|
||||
anonymous visitors see no trace). Handler in app.js, upsert +
|
||||
?chat=<id> contract: an unlinked Save POSTs /api/chats (the
|
||||
server auto-titles from the first question, 120-char cap) and
|
||||
links the conversation to the created row's id; a re-Save PUTs
|
||||
the SAME row (the same conversation never spawns a second row);
|
||||
a 404 PUT unlinks and recreates; "New chat" unlinks. Booting at
|
||||
/?chat=<id> (valid uuid, admin) GETs the row and renders its
|
||||
messages through the SAME restore path as the phase-14 local
|
||||
session (pixel-identical), links the conversation, and mirrors
|
||||
it to localStorage — a later Save updates that row. A deleted /
|
||||
unknown id degrades to the normal local restore with a banner. -->
|
||||
<button type="button" class="save-chat-btn" id="save-chat-btn" aria-label="Save chat" hidden>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>
|
||||
<span class="save-chat-label">Save</span>
|
||||
</button>
|
||||
Phase 14 (module-owned since phase 34 task 02): "New chat"
|
||||
resets the local (localStorage) conversation and clears the
|
||||
rendered list. The binding lives in header.js — dispatches
|
||||
"bor:new-chat" which app.js acts on (it owns the
|
||||
in-flight-turn guard + list reset). -->
|
||||
<div class="chat-actions">
|
||||
<button type="button" class="new-chat-btn" id="new-chat-btn" aria-label="New chat">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
<span class="new-chat-label">New chat</span>
|
||||
</button>
|
||||
|
||||
<!-- Phase 51 (owner-locked 2026-08-29, `TODO.md` L6): "Share"
|
||||
<!-- Phase 55 (owner-locked A2, 2026-08-31): there is NO Save
|
||||
control — every conversation auto-saves itself (app.js
|
||||
persistConversation at the phase-14/20 save points). The
|
||||
History page is the visible proof; a failed auto-save leaves
|
||||
only a one-line status note (never a button, never a banner).
|
||||
|
||||
Phase 55 (owner-locked A1, 2026-08-31): the "Share" pill is
|
||||
STATIC, ALWAYS-VISIBLE markup — there is no reveal step
|
||||
(phase 51's admin-only ship-hidden gate is gone with it; task
|
||||
01 opened the save/share write surface to every visitor).
|
||||
|
||||
Phase 51 (owner-locked 2026-08-29, `TODO.md` L6): "Share"
|
||||
turns the current conversation into a PUBLIC read-only link —
|
||||
/shared/<token> (a 128-bit uuid4 on the saved_chats row,
|
||||
migration 0009; the anonymous page is task 03). The
|
||||
save-then-share contract: an UNSAVED (unlinked) conversation
|
||||
is saved AND shared in ONE action — app.js POSTs /api/chats
|
||||
with { messages, share: true } (the server sets the token in
|
||||
the same commit) and links the conversation to the created
|
||||
row; a saved (linked) one just POSTs /api/chats/<id>/share
|
||||
(idempotent — the existing token comes back unchanged). On
|
||||
success the ABSOLUTE link is copied to the clipboard; a
|
||||
non-secure (http) homelab origin that rejects the clipboard
|
||||
gets the inline link-field fallback instead (owner-locked —
|
||||
app.js renders .share-link-fallback near the status line).
|
||||
Admin-only — ships HIDDEN exactly like Save (absent-not-
|
||||
hidden, phase 16); app.js reveals it at boot (the same
|
||||
admin-reveal block) and binds the click to shareCurrentChat.
|
||||
Unsharing lives on the History page's Share column (task 04). -->
|
||||
<button type="button" class="share-chat-btn" id="share-chat-btn" aria-label="Share chat" hidden>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<span class="share-chat-label">Share</span>
|
||||
</button>
|
||||
migration 0009). The save-then-share contract: an UNSAVED
|
||||
(unlinked) conversation is saved AND shared in ONE action —
|
||||
app.js POSTs /api/chats with { messages, share: true } (the
|
||||
server sets the token in the same commit) and links the
|
||||
conversation to the created row; a saved (linked) one just
|
||||
POSTs /api/chats/<id>/share (idempotent — the existing token
|
||||
comes back unchanged). On success the ABSOLUTE link is
|
||||
copied to the clipboard; a non-secure (http) homelab origin
|
||||
that rejects the clipboard gets the inline link-field
|
||||
fallback instead (owner-locked — app.js renders
|
||||
.share-link-fallback near the status line). app.js binds the
|
||||
click to shareCurrentChat. Unsharing lives on the History
|
||||
page's Share column (admin-only). -->
|
||||
<button type="button" class="share-chat-btn" id="share-chat-btn" aria-label="Share chat">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<span class="share-chat-label">Share</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Phase 49 (2026-08-29, TODO.md L4): the meta row under a brain
|
||||
bubble can carry JS-injected actions (app.js) — Tune (admin
|
||||
|
||||
Reference in New Issue
Block a user