Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
4.6 KiB
4.6 KiB
Task 02 — Share button (chat) + the History share column
Phase: 51_share_chat · Source: TODO.md:6 — "Need a way to share a chat with a link so others can see it anonymously."
Story: n/a (TODO-derived)
Objective
Two share entry points for the admin: the Share button on the chat page (an unsaved conversation is saved + shared in one action) and a Share column on the History table (create link / copy / unshare) — both copy the link with a clipboard + inline-link fallback.
Work
frontend/index.html— beside#save-chat-btn,#share-chat-btn(the same ghost-pill family: an inline link/share SVG + the label "Share",aria-label="Share chat", shipshidden— revealed for admin exactly like Save); a comment documenting the save-then-share contract (2026-08-29,TODO.mdL6).app/schemas.py+app/api/chats.py(the small server side of this task):SavedChatCreategainsshare: bool = False; the create route, whenshareis true, setsshare_token = uuid.uuid4()in the same commit and the response carriesshare_url.SavedChatOutandSavedChatRoweach gainshare_url: str | None(None→ absent from the JSON) — the list endpoint populates it, so the History column renders fromGET /api/chatswithout a second fetch.frontend/assets/app.js:shareCurrentChat()— the#share-chat-btnhandler (the same empty-conversation no-op guard as Save): ifcurrentChatIdis set →POST /api/chats/<id>/share; else →POST /api/chatswith{ messages: conversation, share: true }→currentChatId = created.id(one action saves and shares — owner-locked). On success: copy the absolute URL ofshare_url—navigator.clipboard.writeText(...)in a try; on success the live region reads "Share link copied."; on failure (a non-secure http origin rejects the clipboard) render the inline fallback: a transient link field (an<a>styled as a select-on-focus field, carrying the full URL) near the status line + the live region "Share link ready — copy it from the field." (owner-locked fallback). On 403/5xx/network:showErrorBannerwith an actionable line.- Reveal on boot:
#share-chat-btnjoins the same admin-reveal block as#save-chat-btn. - Header comment: the share contract (2026-08-29,
TODO.mdL6).
frontend/history.html+frontend/assets/history.js— the Share column between Updated and the Actions/Delete cell (theth+ the per-row cell; the table stays full-width):- row already shared (
share_urlpresent): a Copy button (the same clipboard + fallback helper — the per-page duplication house style:history.jskeeps its own ~10-line copy of the helper rather than a new shared module) and an Unshare button (inline two-step, the phase-50 Delete-confirm pattern: "Unshare? [Yes] [No]" →POST /api/chats/<id>/unshare→ the cell re-renders to the unshared state + the live region). - row unshared: a Create link button →
POST /api/chats/<id>/share→ the cell re-renders to the shared state (Copy + Unshare) and the link is offered for copying (same fallback pattern).
- row already shared (
frontend/assets/styles.css—.share-chat-btn(the pill family), the.history-sharecell buttons (the Tune/Retry-family ghost buttons, ≥44px comfortable, focus-visible),.share-link-fallback(the inline link field — input-like look, select-on-focus), reusing the phase-50.history-confirmstyles for the unshare two-step.tests/integration/test_chats_api.py— extend: create-with-share (the response carriesshare_urlmatching the token shape and the row is immediately publicly readable), create-without-share (noshare_url), the list rows carryshare_urlwhen shared and absent otherwise,GET /{chat_id}carries it too.- Frontend source pins (house pattern): the
shareCurrentChatbranches (linked → POST share; unlinked → create-with-share +currentChatIdset); the clipboard try → fallback element on rejection; the History cell's three states (unshared / shared / confirming-unshare) + the two-step unshare;#share-chat-btnships hidden and reveals only for admin.
- ASSUMPTION (owner-locked 2026-08-29): the Share button saves + shares an unsaved conversation in one action; the clipboard copy has the inline-link fallback; unshare is inline two-step.
Testing & Quality
- Integration: as above; full suite green.
- Coverage: >90% on
app/(the create-with-share branch covered).
Completion Criteria
- Admin: the Share button works on a saved (linked) and an unsaved conversation; the History column creates / copies / unshares per row.
uv run pytestgreen;uv run ruff check . && uv run pyrightclean.