Streaming Markdown
Server-rendered HTML revealed at a steady pace as fuller renders of it arrive, so a reply reads as one smooth stream rather than the bursts it lands in. Parsing stays on the server; the element only reveals.
A reply arriving in bursts
Press Play: the renders arrive unevenly, the reveal doesn't. Lists never flicker as they grow. Under reduced motion each render paints at once.
Three candidates haven't
Three candidates haven't replied
Three candidates haven't replied since the invitations went out on
Three candidates haven't replied since the invitations went out on Monday.
- Ana
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Want me
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Want me to draft the nudge for Ana?
<div class="mx-auto max-w-2xl">
<button type="button" id="reveal_play" class="<%= button_classes %>">Play</button>
<%= streaming_markdown_tag id: "reveal_reply", class: "UnmagicProse mt-4 min-h-40" %>
<template id="reveal_flushes">
<% Unmagic::Components::Browser::Reply.snapshots.each do |html| %>
<template><%= turbo_stream.stream_markdown "reveal_reply", html %></template>
<% end %>
</template>
<script type="module">
document.getElementById("reveal_play").addEventListener("click", async () => {
const reply = document.getElementById("reveal_reply")
reply.replaceChildren()
for (const frame of document.getElementById("reveal_flushes").content.children) {
await new Promise((resolve) => setTimeout(resolve, 40 + Math.random() * 260))
document.body.append(frame.content.cloneNode(true))
}
})
</script>
</div>Stopped mid-reply
Stop swaps in a final element, and every flush still on its way is refused.
Response stopped.
Three candidates haven't
Three candidates haven't replied
Three candidates haven't replied since the invitations went out on
Three candidates haven't replied since the invitations went out on Monday.
- Ana
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Want me
Three candidates haven't replied since the invitations went out on Monday.
- Ana Silva opened hers twice but hasn't answered.
- Grace Okafor hasn't opened hers.
- Tom Reyes's bounced, so the address is probably wrong.
I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday.
invitations.unanswered.where(sent_at: ..3.days.ago)Want me to draft the nudge for Ana?
<div class="mx-auto max-w-2xl">
<div class="flex gap-2">
<button type="button" id="stopped_play" class="<%= button_classes %>">Play</button>
<button type="button" id="stopped_stop" class="<%= button_classes %>">Stop</button>
</div>
<div id="stopped_holder" class="mt-4 min-h-24">
<%= streaming_markdown_tag id: "stopped_reply", class: "UnmagicProse" %>
</div>
<template id="stopped_final">
<%= streaming_markdown_tag id: "stopped_reply", final: true, class: "UnmagicProse" do %>
<p class="text-neutral-500">Response stopped.</p>
<% end %>
</template>
<template id="stopped_fresh"><%= streaming_markdown_tag id: "stopped_reply", class: "UnmagicProse" %></template>
<template id="stopped_flushes">
<% Unmagic::Components::Browser::Reply.snapshots.each do |html| %>
<template><%= turbo_stream.stream_markdown "stopped_reply", html %></template>
<% end %>
</template>
<script type="module">
const swap = (id) => document.getElementById("stopped_reply").replaceWith(document.getElementById(id).content.cloneNode(true))
document.getElementById("stopped_stop").addEventListener("click", () => swap("stopped_final"))
document.getElementById("stopped_play").addEventListener("click", async () => {
swap("stopped_fresh")
for (const frame of document.getElementById("stopped_flushes").content.children) {
await new Promise((resolve) => setTimeout(resolve, 60 + Math.random() * 240))
document.body.append(frame.content.cloneNode(true))
}
})
</script>
</div>