Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b12f53a
Draft shinychat release blog post
gadenbuie Sep 9, 2026
36e3096
docs(shinychat): polish release post draft
gadenbuie Sep 9, 2026
8f5ca27
docs(shinychat): highlight Python 0.7.1 in release post
gadenbuie Sep 9, 2026
9fa5b18
docs(shinychat): add screenshot harness and demo apps
gadenbuie Sep 10, 2026
e095cc1
docs(shinychat): rework release post screenshots and videos
gadenbuie Sep 10, 2026
874220d
docs(blog): use toolbar_input_button in shinychat toolbar examples
gadenbuie Sep 10, 2026
9359af9
docs(shinychat): crop header border out of message screenshots
gadenbuie Sep 10, 2026
0794dd4
Restructure around-the-chat section into feature subsections
gadenbuie Sep 10, 2026
15610c5
docs(shinychat): document demo app screenshot/video harness
gadenbuie Sep 10, 2026
bb08cbf
feat(shinychat): animate a mouse cursor in the edit-branches video
gadenbuie Sep 10, 2026
5a99c14
feat(blog): add og-header social preview card for shinychat post
gadenbuie Sep 10, 2026
2119e4e
feat(shinychat): animate a mouse cursor in the complete-app video
gadenbuie Sep 10, 2026
ff000d0
docs(blog): polish shinychat release post prose and structure
gadenbuie Sep 10, 2026
290a7e6
docs(shinychat): wait out composer fade before greeting screenshot
gadenbuie Sep 10, 2026
0f462ae
docs(blog): link tool UI docs from tool grouping section
gadenbuie Sep 10, 2026
2006510
docs(blog): add related post and doc links to shinychat post
gadenbuie Sep 10, 2026
ee38ac1
feat(blog): add streaming greeting video to shinychat 0.5.0 post
gadenbuie Sep 10, 2026
d241a41
feat(blog): re-record complete-app demo with slower pacing
gadenbuie Sep 10, 2026
c152183
chore(blog): pre-date shinychat 0.5.0 post for Sept 15
gadenbuie Sep 10, 2026
3eefa31
feat(blog): embed greeting stream video in shinychat 0.5.0 post
gadenbuie Sep 10, 2026
1e6bd47
chore(blog): losslessly optimize shinychat post screenshots
gadenbuie Sep 10, 2026
ee8fe1c
Revise shinychat release post
gadenbuie Sep 11, 2026
ff1da6c
Refine shinychat release post
gadenbuie Sep 11, 2026
61f4e57
Polish shinychat release post
gadenbuie Sep 11, 2026
b45b026
closer to final edits
gadenbuie Sep 11, 2026
a10cd77
Address shinychat release post review comments
gadenbuie Sep 11, 2026
c9fe1d5
Mention querychat and btw in shinychat post
gadenbuie Sep 11, 2026
890ba4e
Address shinychat conversation history review
gadenbuie Sep 11, 2026
61e509b
Rename shinychat release post folder
gadenbuie Sep 11, 2026
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
82 changes: 82 additions & 0 deletions content/blog/shinychat-r-0.5.0-python-0.7.1/apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Screenshot and video generation for the shinychat post

Every image and video in `../images/` is generated programmatically — no manual
screen recording. Each subdirectory here is one demo:

- `app.R` — a Shiny app using `bslib::page_chat()` + shinychat, backed by a
mocked ellmer client (`_mock-client.R`: keyword-matched canned replies,
streamed via a `coro::async_generator`; `mock_stream_reply()` streams tool
requests in as running activity rows before their results land and text
word by word). No network or LLM calls.
- `screenshot.R` — a chromote (Chrome DevTools Protocol) automation script that
drives the app in headless Chrome and writes PNGs/MP4s into `../images/`.

Run one demo or everything:

```bash
Rscript apps/edit-branches/screenshot.R
Rscript apps/run-all.R
```

Requires R packages `chromote`, `callr`, `jsonlite`, `httpuv`, `shiny`,
`bslib`, `shinychat`, `ellmer`, `coro`, plus `ffmpeg` on PATH and a
launchable headless Chrome for chromote.

## `_common.R` harness

Shared helpers sourced by every `screenshot.R`:

- App lifecycle: `start_app()` (runs `shiny::runApp()` in a `callr` background
process on a random free port, waits for readiness), `stop_app()`.
- Browser session: `connect()` (opens the app with a viewport and forces
`prefers-color-scheme: light`), `set_viewport()`, `set_zoom()`.
`set_zoom()` uses CSS zoom so the desktop layout survives at smaller
viewport widths; because vh units scale with zoom, it also shrinks the
`shiny-chat-page` root to keep the app exactly filling the window.
- Waiting: `wait_for()` / `wait_for_text()` / `wait_for_gone()` /
`wait_for_app_ready()`.
- Interaction: `chat_type()` (CDP `Input$insertText` + optional Enter key),
`press_enter()` (Enter key alone, e.g. after `type_natural()`),
`click_selector()`, plus `js()` for arbitrary expressions.
- Stills: `viewport_png()`, `full_page_png()`, `element_png()`,
`union_png()` (screenshot of the bounding box of several selectors, with
padding — used for the tight cropped figures in the post).
- Movies: `movie_start(b, fps)` returns a recorder; `rec$snap()` captures a
JPEG frame with a timestamp, `rec$loop(seconds)` snapshots at the target
fps for that long, and `rec$loop_until(expr)` snapshots while polling for a
JS condition (so streaming responses stay animated). Actions performed
between `loop()` calls are captured as single frames, so anything not
wrapped in `loop()` appears instant.
`movie_save(rec, path, fps = 15, width = NULL)` writes frames to a temp
dir, builds an ffmpeg concat list with real inter-frame durations, and runs
roughly `ffmpeg -f concat -i list.txt -vf "fps=15,scale=2400:-2,..."`.
Set `rec$clip <- movie_clip_fit(b, selectors)` to record only part of the
app (e.g. just the conversation, no header or input); the clip is fit to
exactly 4:3 so it fills the `aspect-ratio="4x3"` embed, and `movie_save()`
should then be called with `width = rec$clip$width * 2` to keep the output
at native retina resolution.
- Fake cursor: CDP clicks are invisible in captured frames, so
`cursor_start(b)` injects a pointer element that `cursor_glideto()` /
`cursor_glideto_el()` animate in step with the movie frames, dispatching
real CDP mouse events along the way (these also trigger `:hover`, which is
what reveals the message edit buttons). `rect_of_text()` finds the bounding
box of the first element matching a selector *and* containing text, for
gliding to a specific history entry. `cursor_click_here()` clicks with a
brief press animation, `cursor_leave()` retreats off the right edge.
Coordinates are client space; because root CSS zoom scales the rendered
position of fixed elements, `cursor_place()` divides by the zoom factor.
- Natural input: `type_natural()` types one character at a time with
human-ish delays, `select_edit_text()` selects an exact substring inside a
contenteditable editor, and `press_backspace()` deletes the selection.

## Notes for editing recordings

- Videos are embedded with `aspect-ratio="4x3"`, so the recorded viewport must
be exactly 4:3 (e.g. 1200x900); see the comment in
`edit-branches/screenshot.R`.
- `movie_save(width = 2400)` scales output to 2400px wide for retina
sharpness; `-movflags +faststart` for web embedding.
- Timing tweaks (lead-in, how long a streaming response plays) are just
`rec$loop()` durations around the actions.
- Frame captures run at ~12 fps while actions happen, so fast UI transitions
may only get one frame; wrap anything that must look animated in `loop()`.
Loading
Loading