<main class="thread">
<p class="msg-user">How do I format a relative time like "3 hours ago" in JavaScript without a library?</p>
<article class="msg-ai" aria-label="Assistant reply">
<header class="msg-head">
<span class="msg-badge">
<span class="msg-ico" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M10 4Q10 12 18 12Q10 12 10 20Q10 12 2 12Q10 12 10 4Z"></path>
<path d="M19 2Q19 5.5 22.5 5.5Q19 5.5 19 9Q19 5.5 15.5 5.5Q19 5.5 19 2Z"></path>
</svg>
</span>
Nova 4
</span>
<span class="msg-meta">JavaScript · 0.6s</span>
</header>
<div class="reply-body" id="reply-body" aria-busy="false"></div>
<div class="action-bar" role="toolbar" aria-label="Reply actions">
<div class="branch-nav" id="branch-nav" role="group" aria-label="Reply versions">
<button type="button" class="icon-btn" id="prev-branch" aria-label="Previous version">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg>
</button>
<span class="branch-count" id="branch-count">2 / 2</span>
<button type="button" class="icon-btn" id="next-branch" aria-label="Next version">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18l6-6-6-6"></path></svg>
</button>
</div>
<button type="button" class="icon-btn" id="copy-btn" aria-label="Copy reply">
<svg class="icon-copy" viewBox="0 0 24 24" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="icon-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"></path></svg>
</button>
<button type="button" class="icon-btn" id="up-btn" aria-label="Good response" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 10v12M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path></svg>
</button>
<button type="button" class="icon-btn" id="down-btn" aria-label="Bad response" aria-pressed="false" aria-expanded="false" aria-controls="feedback-drawer">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 14V2M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"></path></svg>
</button>
<button type="button" class="regen-btn" id="regen-btn">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16M16 16h5v5"></path></svg>
<span id="regen-label">Regenerate</span>
</button>
</div>
<div class="drawer" id="feedback-drawer">
<div class="drawer-inner">
<form class="feedback-form" id="feedback-form">
<p class="feedback-title">What went wrong with this reply?</p>
<div class="reasons" id="reasons" role="group" aria-label="Feedback reasons"></div>
<label class="sr-only" for="feedback-note">Additional details</label>
<input type="text" id="feedback-note" placeholder="Optional note">
<div class="feedback-actions">
<button type="button" class="btn-ghost" id="feedback-cancel">Cancel</button>
<button type="submit" class="btn-solid">Send feedback</button>
</div>
</form>
</div>
</div>
<p class="thanks" id="thanks" hidden></p>
<p class="sr-only" id="status" role="status"></p>
</article>
</main>*, *::before, *::after {
box-sizing: border-box;
}
:root {
--page: #f1f5f9;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--soft: #f1f5f9;
--hover: #e2e8f0;
--bubble: #e2e8f0;
--accent: #2563eb;
--accent-soft: #eff6ff;
--accent-text: #1d4ed8;
--down-soft: #fef2f2;
--down-text: #dc2626;
--ok: #15803d;
--ok-soft: #dcfce7;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background: var(--page);
color: var(--text);
}
.thread {
display: grid;
grid-template-columns: minmax(0, 620px);
justify-content: center;
align-content: start;
gap: 14px;
min-height: 100vh;
padding: max(20px, calc(50vh - 191px)) 16px 20px;
}
.msg-user {
justify-self: end;
max-width: 82%;
margin: 0;
padding: 11px 16px;
border-radius: 20px 20px 6px 20px;
background: var(--bubble);
color: var(--text);
font-size: 0.9375rem;
line-height: 1.5;
}
.msg-ai {
padding: 22px 24px 16px;
border-radius: 22px;
background: var(--card);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 18px 40px -14px rgba(15, 23, 42, 0.14);
}
.msg-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
}
.msg-badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
}
.msg-ico {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 8px;
background: var(--accent-soft);
color: var(--accent);
}
.msg-ico svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.msg-meta {
font-family: ui-monospace, Consolas, monospace;
font-size: 0.75rem;
color: var(--muted);
}
.reply-body p {
margin: 0 0 12px;
line-height: 1.65;
}
.reply-body pre {
margin: 0;
padding: 14px 16px;
overflow-x: auto;
border-radius: 14px;
background: #0f172a;
color: #e2e8f0;
font-family: ui-monospace, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.6;
}
.action-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
margin-top: 12px;
}
.branch-nav {
display: inline-flex;
align-items: center;
gap: 2px;
margin-right: 4px;
padding: 2px;
border-radius: 999px;
background: var(--soft);
}
.branch-count {
min-width: 38px;
font-family: ui-monospace, Consolas, monospace;
font-size: 0.75rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text);
text-align: center;
}
.icon-btn {
display: grid;
place-items: center;
width: 34px;
height: 34px;
padding: 0;
border: 0;
border-radius: 9px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.branch-nav .icon-btn {
width: 28px;
height: 28px;
border-radius: 50%;
}
.icon-btn:hover:not(:disabled) {
background: var(--soft);
color: var(--text);
}
.branch-nav .icon-btn:hover:not(:disabled) {
background: var(--card);
}
.icon-btn:disabled {
opacity: 0.35;
cursor: default;
}
.icon-btn svg,
.regen-btn svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-width: 1.9;
stroke-linecap: round;
stroke-linejoin: round;
}
.icon-check {
display: none;
color: var(--ok);
}
.icon-btn.is-copied .icon-copy {
display: none;
}
.icon-btn.is-copied .icon-check {
display: block;
}
#up-btn[aria-pressed="true"] {
background: var(--accent-soft);
color: var(--accent-text);
}
#down-btn[aria-pressed="true"] {
background: var(--down-soft);
color: var(--down-text);
}
.regen-btn {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 34px;
margin-left: auto;
padding: 5px 13px;
border: 0;
border-radius: 999px;
background: var(--soft);
color: var(--text);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.regen-btn:hover:not(:disabled) {
background: var(--hover);
}
.regen-btn:disabled {
opacity: 0.55;
cursor: default;
}
.regen-btn.is-busy svg {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.drawer {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.22s ease;
}
.drawer.is-open {
grid-template-rows: 1fr;
}
.drawer-inner {
overflow: hidden;
}
.feedback-form {
display: grid;
gap: 10px;
margin-top: 12px;
padding: 14px;
border-radius: 16px;
background: var(--soft);
}
.feedback-title {
margin: 0;
font-size: 0.8125rem;
font-weight: 600;
}
.reasons {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.reason-chip {
min-height: 30px;
padding: 4px 11px;
border: 0;
border-radius: 999px;
background: var(--card);
color: var(--text);
font: inherit;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
}
.reason-chip[aria-pressed="true"] {
background: var(--accent);
color: #ffffff;
}
.feedback-form input {
width: 100%;
padding: 8px 11px;
border: 0;
border-radius: 10px;
background: var(--card);
color: var(--text);
font: inherit;
font-size: 0.8125rem;
}
.feedback-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-ghost,
.btn-solid {
min-height: 32px;
padding: 5px 13px;
border: 0;
border-radius: 9px;
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}
.btn-ghost {
background: var(--card);
color: var(--text);
}
.btn-solid {
background: var(--text);
color: var(--card);
}
.thanks {
margin: 10px 0 0;
padding: 8px 12px;
border-radius: 10px;
background: var(--ok-soft);
color: var(--ok);
font-size: 0.8125rem;
font-weight: 600;
}
.icon-btn:focus-visible,
.regen-btn:focus-visible,
.reason-chip:focus-visible,
.btn-ghost:focus-visible,
.btn-solid:focus-visible,
.feedback-form input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (max-width: 520px) {
.thread {
padding-top: 20px;
}
}
@media (pointer: coarse) {
.icon-btn,
.branch-nav .icon-btn {
width: 44px;
height: 44px;
}
.regen-btn,
.reason-chip,
.btn-ghost,
.btn-solid,
.feedback-form input {
min-height: 44px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--page: #0b1120;
--card: #151e30;
--text: #f1f5f9;
--muted: #94a3b8;
--soft: #1e293b;
--hover: #293548;
--bubble: #1e293b;
--accent: #3b82f6;
--accent-soft: #172554;
--accent-text: #93c5fd;
--down-soft: #3b1822;
--down-text: #fca5a5;
--ok: #86efac;
--ok-soft: #14352a;
}
.reply-body pre {
background: #090d16;
}
}
@media (forced-colors: active) {
.msg-ai {
border: 1px solid CanvasText;
}
.icon-btn[aria-pressed="true"],
.reason-chip[aria-pressed="true"] {
outline: 2px solid Highlight;
}
}
@media (prefers-reduced-motion: reduce) {
.drawer {
transition: none;
}
.regen-btn.is-busy svg {
animation: none;
}
}const REASONS = ["Inaccurate", "Outdated API", "Code has a bug", "Too wordy"];
const branches = [
{
lead: "Subtract the timestamp from Date.now(), divide by 3,600,000 to get hours, and return a short string:",
code: "function hoursAgo(date) {\n const hours = Math.round((Date.now() - date) / 3600000);\n return hours + \" hours ago\";\n}",
rating: null,
reason: ""
},
{
lead: "Intl.RelativeTimeFormat formats relative timestamps in the user's locale without loading a date library. Setting numeric to \"auto\" also gives phrases like \"yesterday\":",
code: "const rtf = new Intl.RelativeTimeFormat(\"en\", { numeric: \"auto\" });\nconst hours = rtf.format(-3, \"hour\"); // \"3 hours ago\"\nconst day = rtf.format(-1, \"day\"); // \"yesterday\"",
rating: null,
reason: ""
}
];
const EXTRA_BRANCH = {
lead: "If you need automatic unit selection from seconds up to days, divide through a table of cutoffs before calling Intl.RelativeTimeFormat:",
code: "const rtf = new Intl.RelativeTimeFormat(\"en\", { numeric: \"auto\" });\nconst UNITS = [[\"day\", 86400], [\"hour\", 3600], [\"minute\", 60], [\"second\", 1]];\n\nfunction timeAgo(date) {\n const diff = (date - Date.now()) / 1000;\n for (const [unit, s] of UNITS) {\n if (Math.abs(diff) >= s || unit === \"second\") {\n return rtf.format(Math.round(diff / s), unit);\n }\n }\n}"
};
const replyBody = document.getElementById("reply-body");
const prevBtn = document.getElementById("prev-branch");
const nextBtn = document.getElementById("next-branch");
const countEl = document.getElementById("branch-count");
const copyBtn = document.getElementById("copy-btn");
const upBtn = document.getElementById("up-btn");
const downBtn = document.getElementById("down-btn");
const regenBtn = document.getElementById("regen-btn");
const regenLabel = document.getElementById("regen-label");
const drawer = document.getElementById("feedback-drawer");
const form = document.getElementById("feedback-form");
const reasonsEl = document.getElementById("reasons");
const noteInput = document.getElementById("feedback-note");
const cancelBtn = document.getElementById("feedback-cancel");
const thanksEl = document.getElementById("thanks");
const status = document.getElementById("status");
let current = 1;
let selectedReason = "";
let copyTimer = 0;
let streamTimer = 0;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
}
REASONS.forEach(function(label) {
const btn = el("button", "reason-chip", label);
btn.type = "button";
btn.setAttribute("aria-pressed", "false");
btn.addEventListener("click", function() {
selectedReason = selectedReason === label ? "" : label;
syncReasons();
});
reasonsEl.append(btn);
});
function syncReasons() {
Array.from(reasonsEl.children).forEach(function(btn) {
btn.setAttribute("aria-pressed", String(btn.textContent === selectedReason));
});
}
function setDrawer(open) {
drawer.classList.toggle("is-open", open);
downBtn.setAttribute("aria-expanded", String(open));
}
function renderBranch() {
const item = branches[current];
replyBody.textContent = "";
const p = el("p", "", item.lead);
const pre = el("pre", "");
const code = el("code", "", item.code);
pre.append(code);
replyBody.append(p, pre);
countEl.textContent = (current + 1) + " / " + branches.length;
const atStart = current === 0;
const atEnd = current === branches.length - 1;
prevBtn.disabled = false;
nextBtn.disabled = false;
if (atStart && document.activeElement === prevBtn) nextBtn.focus();
if (atEnd && document.activeElement === nextBtn) prevBtn.focus();
prevBtn.disabled = atStart;
nextBtn.disabled = atEnd;
upBtn.setAttribute("aria-pressed", String(item.rating === "up"));
downBtn.setAttribute("aria-pressed", String(item.rating === "down"));
selectedReason = item.reason || "";
syncReasons();
setDrawer(false);
if (item.reason) {
thanksEl.hidden = false;
thanksEl.textContent = "Feedback saved for this version (" + item.reason + ").";
} else {
thanksEl.hidden = true;
}
}
prevBtn.addEventListener("click", function() {
if (current > 0) {
current -= 1;
renderBranch();
status.textContent = "Showing version " + (current + 1) + " of " + branches.length;
}
});
nextBtn.addEventListener("click", function() {
if (current < branches.length - 1) {
current += 1;
renderBranch();
status.textContent = "Showing version " + (current + 1) + " of " + branches.length;
}
});
// Copy the active branch text and code to the clipboard
copyBtn.addEventListener("click", function() {
const item = branches[current];
const fullText = item.lead + "\n\n" + item.code;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(fullText).catch(function() {});
}
copyBtn.classList.add("is-copied");
status.textContent = "Copied reply to clipboard";
clearTimeout(copyTimer);
copyTimer = setTimeout(function() {
copyBtn.classList.remove("is-copied");
}, 1600);
});
upBtn.addEventListener("click", function() {
const item = branches[current];
item.rating = item.rating === "up" ? null : "up";
item.reason = "";
upBtn.setAttribute("aria-pressed", String(item.rating === "up"));
downBtn.setAttribute("aria-pressed", "false");
setDrawer(false);
thanksEl.hidden = true;
status.textContent = item.rating === "up" ? "Marked response as helpful" : "Cleared rating";
});
downBtn.addEventListener("click", function() {
const item = branches[current];
const active = item.rating === "down";
if (active && drawer.classList.contains("is-open")) {
item.rating = null;
downBtn.setAttribute("aria-pressed", "false");
setDrawer(false);
status.textContent = "Cleared rating";
return;
}
item.rating = "down";
upBtn.setAttribute("aria-pressed", "false");
downBtn.setAttribute("aria-pressed", "true");
thanksEl.hidden = true;
setDrawer(true);
const firstChip = reasonsEl.querySelector("button");
if (firstChip) firstChip.focus();
});
cancelBtn.addEventListener("click", function() {
setDrawer(false);
downBtn.focus();
});
form.addEventListener("submit", function(e) {
e.preventDefault();
const item = branches[current];
const note = noteInput.value.trim();
item.reason = selectedReason || note || "General feedback";
noteInput.value = "";
setDrawer(false);
thanksEl.hidden = false;
thanksEl.textContent = "Thanks for the feedback (" + item.reason + ").";
status.textContent = thanksEl.textContent;
});
// Stream a new reply version word by word
regenBtn.addEventListener("click", function() {
if (streamTimer) return;
const fresh = {
lead: EXTRA_BRANCH.lead,
code: EXTRA_BRANCH.code,
rating: null,
reason: ""
};
branches.push(fresh);
current = branches.length - 1;
setDrawer(false);
thanksEl.hidden = true;
upBtn.setAttribute("aria-pressed", "false");
downBtn.setAttribute("aria-pressed", "false");
countEl.textContent = (current + 1) + " / " + branches.length;
prevBtn.disabled = true;
nextBtn.disabled = true;
regenBtn.disabled = true;
regenBtn.classList.add("is-busy");
regenLabel.textContent = "Regenerating";
replyBody.setAttribute("aria-busy", "true");
status.textContent = "Generating version " + branches.length;
replyBody.textContent = "";
const p = el("p", "");
replyBody.append(p);
const words = fresh.lead.split(" ");
let i = 0;
streamTimer = setInterval(function() {
p.textContent += (i ? " " : "") + words[i];
i += 1;
if (i >= words.length) {
clearInterval(streamTimer);
streamTimer = 0;
const pre = el("pre", "");
pre.append(el("code", "", fresh.code));
replyBody.append(pre);
replyBody.setAttribute("aria-busy", "false");
regenBtn.disabled = false;
regenBtn.classList.remove("is-busy");
regenLabel.textContent = "Regenerate";
prevBtn.disabled = false;
status.textContent = "Version " + branches.length + " ready";
}
}, 45);
});
renderBranch();Click any element to find its code Esc
Check this code before you run it
It came from a shared link. It runs with fewer permissions, so it cannot open pop-ups, submit forms or start downloads.
element.animate() call shows up here as it plays.