Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pkg-r/R/chat.R
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ commons_chat_dependency <- function() {
version = paste0("0.0.0.9000.", as.integer(stamp)),
src = c(file = src),
stylesheet = "commons-chat.css",
script = "commons-chat.js",
all_files = TRUE
)
}
7 changes: 5 additions & 2 deletions pkg-r/R/provenance.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ derive_provenance_tag <- function(tags, verified) {
}
}

provenance_aside <- function(tag) {
# Live answers omit the "Cited" marker: their verified citation asides
# already say as much. Review contexts set include_cited = TRUE so every
# classified answer carries its outcome.
provenance_aside <- function(tag, include_cited = FALSE) {
entry <- provenance_display[[tag]]
if (is.null(entry) || identical(tag, "B")) {
if (is.null(entry) || (identical(tag, "B") && !include_cited)) {
return("")
}
icon <- commons_icon_url(entry$icon)
Expand Down
88 changes: 64 additions & 24 deletions pkg-r/R/trajectory-review.R
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,35 @@ trajectory_exchange_messages <- function(turns, exchange) {
messages
}

# Replay the transcript the way shinychat itself restores a bookmarked chat
# (client_set_ui()): yield each message's contents from a generator through
# chat_append(), so every item is classified on its own (markdown vs HTML)
# exactly as in the live stream. Static chat_ui(messages = ...) would fold
# mixed content (tool cards plus markdown text) into a single raw-HTML
# island, escaping the text and hiding it from aside grouping.
replay_transcript <- function(id, messages, session) {
for (message in messages) {
replay_transcript_message(id, message, session)
}
invisible(NULL)
}

# One function call per message so the generator closes over this message's
# content; coro generators evaluate their body lazily.
replay_transcript_message <- function(id, message, session) {
content <- message$content
if (is.list(content) && !is.object(content)) {
stream <- coro::generator(function() {
for (x in content) {
coro::yield(x)
}
})
shinychat::chat_append(id, stream(), role = message$role, session = session)
} else {
shinychat::chat_append(id, content, role = message$role, session = session)
}
}

sanitize_trajectory_turns <- function(turns) {
sanitized <- lapply(turns, function(turn) {
if (turn@role %in% c("assistant", "user")) {
Expand Down Expand Up @@ -315,8 +344,11 @@ add_message_provenance <- function(messages, provenance) {
} else {
record$provenance_tag %||% NA_character_
}
pill <- commons_answer_pill(tag)
if (is.null(pill)) {
# The transcript replays what the live chat streamed: the same
# <shiny-aside> marker, grouped and positioned by shinychat itself.
# Reviewers also get a "Cited" marker the live chat omits.
aside <- provenance_aside(tag, include_cited = TRUE)
if (!nzchar(aside)) {
next
}
candidates <- which(vapply(
Expand All @@ -331,42 +363,49 @@ add_message_provenance <- function(messages, provenance) {
next
}
index <- candidates[[length(candidates)]]
messages[[index]]$content <- append_provenance_pill(
messages[[index]]$content <- append_provenance_aside(
messages[[index]]$content,
pill
aside
)
}
messages
}

append_provenance_pill <- function(content, pill) {
# The aside must stay markdown, not HTML: htmltools::HTML() would mark the
# chunk as raw HTML, which shinychat's aside grouping does not reach into.
# A trailing markdown chunk is also exactly how a live-streamed aside lands:
# replay_transcript() yields each content item on its own, and the client
# folds consecutive markdown chunks into one block.
append_provenance_aside <- function(content, aside) {
if (is.character(content)) {
return(list(content, pill))
return(paste(paste(content, collapse = "\n"), aside, sep = "\n\n"))
}
c(content, list(pill))
if (is.list(content) && !is.object(content)) {
return(c(content, list(aside)))
}
list(content, aside)
}

# The question list is ordinary server-rendered HTML outside shinychat's
# React tree, so a bslib tooltip (Popper, container = body) owns positioning.
commons_answer_pill <- function(tag) {
entry <- provenance_display[[tag]]
if (is.null(entry)) {
return(NULL)
}
htmltools::tags$span(
pill <- htmltools::tags$span(
class = paste0(
"commons-answer-pill commons-answer-pill-",
entry$pill_class
),
title = entry$body,
`aria-label` = paste0(entry$label, ". ", entry$body),
# No aria-label: the visible text already announces the label, and
# Bootstrap sets aria-describedby when the tooltip shows the body.
tabindex = "0",
commons_pill_icon(entry$icon, entry$label),
htmltools::tags$span(entry$label),
commons_pill_tooltip(entry$body)
# Decorative: the adjacent span already names the label
commons_pill_icon(entry$icon, ""),
htmltools::tags$span(entry$label)
)
}

commons_pill_tooltip <- function(text) {
htmltools::tags$span(class = "commons-tooltip", role = "tooltip", text)
bslib::tooltip(pill, entry$body)
}

commons_pill_icon <- function(file, alt) {
Expand Down Expand Up @@ -763,23 +802,24 @@ viewer_server <- function(
"Select a conversation to view its transcript."
))
}
shinychat::chat_ui(
transcript_id(conversation),
messages = selected_messages(),
height = "100%"
)
# Messages are replayed from the server once the element is bound;
# see replay_transcript() for why they don't go through chat_ui().
shinychat::chat_ui(transcript_id(conversation), height = "100%")
})

# Seed decorations only after the new chat element is bound in the browser.
# Replay and seed decorations only after the new chat element is bound in
# the browser. Replay first so the seed's message indices line up.
shiny::observeEvent(selected_conversation(), {
conversation <- selected_conversation()
exchange <- selected_exchange()
messages <- selected_messages()
session$onFlushed(
function() {
id <- transcript_id(conversation)
replay_transcript(id, messages, session)
seed_transcript_decorations(
session,
transcript_id(conversation),
id,
messages,
selected_exchange = exchange
)
Expand Down
23 changes: 19 additions & 4 deletions pkg-r/inst/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2533,13 +2533,28 @@
},
"files": {
"app.R": {
"checksum": "62078c5a9372fa4b817dfffd20433d9e"
"checksum": "b86881fd81fa7e0826066b09f7f22818"
},
"www/commons-chat/commons-chat.css": {
"checksum": "a860c7bf65906fae00195204b2964a44"
"checksum": "09486e2b6df927713f09ef448c7b6e66"
},
"www/commons-chat/commons-chat.js": {
"checksum": "09f5a710fec03d91711010e2557e1115"
"www/commons-chat/figs/citation-definition.svg": {
"checksum": "eeec1c079ecac49ca50a1b89092d2b98"
},
"www/commons-chat/figs/citation-mark.svg": {
"checksum": "828219c3a6d441b3aa4f03073df4949b"
},
"www/commons-chat/figs/citation-prose.svg": {
"checksum": "ee624ea71c7f023becbadd75c934ed26"
},
"www/commons-chat/figs/citation-schema.svg": {
"checksum": "9ca6eb80eea77978c03cf0df422298ac"
},
"www/commons-chat/figs/trusted-icon.svg": {
"checksum": "fc93941fc788e8930bea0c177161c6a6"
},
"www/commons-chat/figs/warning-icon.svg": {
"checksum": "aeca364302762c1097ed473a32338df6"
}
},
"users": null
Expand Down
62 changes: 0 additions & 62 deletions pkg-r/inst/www/commons-chat/commons-chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ shiny-chat-container .shiny-chat-input .tiptap {
line-height: 1.2;
max-width: 100%;
padding: 0.18rem 0.45rem;
position: relative;
vertical-align: text-bottom;
}

Expand Down Expand Up @@ -69,67 +68,6 @@ shiny-chat-container .shiny-chat-input .tiptap {
color: #6b4b1b;
}

/* ---- Provenance tooltips ---------------------------------------------- */

/* Use a real element so JavaScript can keep tooltips inside the pane. */
.commons-tooltip {
background: var(--bs-body-bg, #fff);
border: 1px solid var(--bs-border-color, #dee2e6);
border-radius: 6px;
bottom: calc(100% + 0.45rem);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
color: var(--bs-body-color, #212529);
display: none;
font-size: 0.74rem;
font-weight: 400;
left: 50%;
line-height: 1.25;
max-width: min(18rem, 70vw);
padding: 0.42rem 0.55rem;
pointer-events: none;
position: absolute;
text-align: left;
transform: translateX(calc(-50% + var(--commons-tooltip-shift, 0px)));
white-space: normal;
width: max-content;
z-index: 1000;
}

.commons-answer-pill:hover > .commons-tooltip,
.commons-answer-pill:focus > .commons-tooltip,
.commons-answer-pill:focus-within > .commons-tooltip {
display: block;
}

.commons-answer-pill:hover::before,
.commons-answer-pill:focus::before,
.commons-answer-pill:focus-within::before {
background: var(--bs-body-bg, #fff);
border-bottom: 1px solid var(--bs-border-color, #dee2e6);
border-right: 1px solid var(--bs-border-color, #dee2e6);
bottom: calc(100% + 0.28rem);
content: "";
height: 0.55rem;
left: 50%;
pointer-events: none;
position: absolute;
transform: translateX(-50%) rotate(45deg);
width: 0.55rem;
z-index: 1000;
}

/* Markers with too little room above open downward instead */
.commons-tooltip-below > .commons-tooltip {
bottom: auto;
top: calc(100% + 0.45rem);
}

.commons-answer-pill.commons-tooltip-below::before {
bottom: auto;
top: calc(100% + 0.28rem);
transform: translateX(-50%) rotate(225deg);
}

/* Citations are labeled identity asides: shinychat accumulates those
* sharing a paragraph into one pill with a "+N" overflow and a carousel
* popover. They render icon-only like the provenance markers, in a quieter
Expand Down
78 changes: 0 additions & 78 deletions pkg-r/inst/www/commons-chat/commons-chat.js

This file was deleted.

3 changes: 2 additions & 1 deletion pkg-r/tests/testthat/test-chat.R
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ test_that("commons_theme() bundles the commons chat assets", {

commons_dep <- deps[[which(names == "commons-chat")]]
expect_identical(commons_dep$stylesheet, "commons-chat.css")
expect_identical(commons_dep$script, "commons-chat.js")
# Tooltip positioning is owned by shinychat (asides) and bslib (pills).
expect_null(commons_dep$script)
})

test_that("icon URLs resolve inside the commons-chat dependency", {
Expand Down
4 changes: 4 additions & 0 deletions pkg-r/tests/testthat/test-provenance.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ test_that("provenance_aside renders A and C, nothing for B/NA", {

expect_identical(provenance_aside("B"), "")
expect_identical(provenance_aside(NA_character_), "")

cited <- provenance_aside("B", include_cited = TRUE)
expect_match(cited, '^<shiny-aside label="Cited"')
expect_match(cited, "verified against a trusted source", fixed = TRUE)
})
Loading
Loading