refactor(web): dock chat inline below the summary, integrated view (ADR-027)
The chat was a separate page — opening it left the summary behind, the very
context you're asking about. Now the chat docks open IN PLACE below the summary:
"Dig deeper" reveals the chat section (HTMX, outerHTML over the closed dock) so
the summary stays on screen above it; no navigation. The no-JS fallback renders
the full summary AND the open chat on one page (the same integrated view), so
progressive enhancement holds.
Extracts a shared summaryBody templ so the detail page and the chat page render
one identical summary, not two divergent ones. GET /v/{id}/chat returns just the
open chat section as a fragment for the inline reveal, or the full summary+chat
page for a no-JS navigation; POST swaps the panel inline or re-renders the whole
page. Tests assert summary+chat coexist on the page and the reveal is a fragment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,8 @@ func TestChatEntryAffordanceOnSummaryView(t *testing.T) {
|
||||
html := body(t, do(t, withChat, httptest.NewRequest(http.MethodGet, "/v/"+videoX, nil)))
|
||||
require.Contains(t, html, "Dig deeper", "the deeper-dive affordance is shown when chat is enabled")
|
||||
require.Contains(t, html, "/v/"+videoX+"/chat", "it links to this video's chat")
|
||||
require.Contains(t, html, `id="chat-section"`, "the dock lives on the detail page")
|
||||
require.Contains(t, html, `hx-get="/v/`+videoX+`/chat"`, "it opens the chat in place (HTMX), not a navigation")
|
||||
|
||||
// With no chat backend wired the affordance is absent (routes unmounted).
|
||||
noChat := newApp(t)
|
||||
@@ -145,6 +147,39 @@ func TestChatEntryAffordanceOnSummaryView(t *testing.T) {
|
||||
require.NotContains(t, html, "Dig deeper", "no affordance when chat is disabled")
|
||||
}
|
||||
|
||||
// The summary and the chat live together (the integrated UX): the no-JS chat page
|
||||
// renders the full summary alongside the chat, and the HTMX reveal returns just
|
||||
// the open chat section as a fragment so it docks in below the summary already on
|
||||
// screen — the summary is never navigated away from.
|
||||
func TestChatIntegratedWithSummaryOnSamePage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
chatter := &fakeChatter{models: []string{"phi4-mini", "gemma4-26b"}}
|
||||
app := newChatApp(t, chatter, nil)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
require.NoError(t, deliver(ctx, app, videoX, "SUMMARY-BODY-MARKER"))
|
||||
seedVideo(t, p, videoX, "X Title", "https://x", time.Time{})
|
||||
seedTranscript(t, app.Store.(*store.Store), videoX, "the transcript")
|
||||
|
||||
// No-JS full page: the summary payload and the chat are on one page.
|
||||
html := body(t, getChat(t, app, videoX))
|
||||
require.Contains(t, html, "SUMMARY-BODY-MARKER", "the summary text is shown on the chat page")
|
||||
require.Contains(t, html, "takeaway one", "takeaways shown alongside the chat")
|
||||
require.Contains(t, html, "highlight one", "highlights shown alongside the chat")
|
||||
require.Contains(t, html, "Ask about this video", "the chat sits on the same page as the summary")
|
||||
require.Contains(t, html, `name="question"`, "the ask form is present")
|
||||
|
||||
// HTMX reveal: the open chat section ONLY (a fragment) — no full-page chrome and
|
||||
// no duplicated summary, so it swaps in below the summary already rendered.
|
||||
req := httptest.NewRequest(http.MethodGet, "/v/"+videoX+"/chat", nil)
|
||||
req.Header.Set("HX-Request", "true")
|
||||
frag := body(t, do(t, app, req))
|
||||
require.NotContains(t, frag, "<html", "the reveal is a fragment, not a full page")
|
||||
require.NotContains(t, frag, "SUMMARY-BODY-MARKER", "the reveal does not re-send the summary (it's already on screen)")
|
||||
require.Contains(t, frag, `id="chat-section"`, "the fragment replaces the dock in place")
|
||||
require.Contains(t, frag, `name="question"`, "the ask form is in the revealed section")
|
||||
}
|
||||
|
||||
// THE KEY SAFETY ASSERTION (ADR-027): a chat answer is produced entirely from the
|
||||
// stored transcript — the model receives the stored text, and neither the
|
||||
// summarize→fetch path nor the YouTube fetch path is ever touched. The tripwire
|
||||
|
||||
Reference in New Issue
Block a user