diff --git a/src/render.ts b/src/render.ts index 183d081..caee4e0 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,5 +1,6 @@ import path from "node:path"; import fs from "fs-extra"; +import * as cheerio from "cheerio"; import type { CaptureRow, NormalizedConversation } from "./types.js"; import { artifactAssetsDir, artifactDir, artifactIndexPath } from "./artifacts.js"; import { config } from "./config.js"; @@ -14,6 +15,21 @@ function escapeHtml(value: string): string { .replace(/"/g, """); } +function prepareMessageHtml(html: string): string { + const $ = cheerio.load(html, {}, false); + $("a").each((_, anchor) => { + const link = $(anchor); + if (link.is("[data-citation]")) return; + + link.find("svg").remove(); + const href = (link.attr("href") || "").trim(); + if (/^https?:\/\//i.test(href) && link.find(".external-link-icon").length === 0) { + link.append(''); + } + }); + return $.html(); +} + const icons = { download: ``, image: ``, @@ -61,7 +77,7 @@ html[data-theme="dark"] { --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; --user-bubble: #303030; } * { box-sizing: border-box; } -body, .topbar, .message.user .content, .content pre, .content :not(pre) > code, .code-block, .code-block .code-header, .content th, .content td, .content blockquote, .content a[data-citation], .downloads, .icon-button, .shot-menu, .shot-menu button, .toast { +body, .topbar, .message.user .content, .content pre, .content :not(pre) > code, .code-block, .code-block .code-header, .content th, .content td, .content blockquote, .content a, .downloads, .icon-button, .shot-menu, .shot-menu button, .toast { transition: background-color .22s ease, border-color .22s ease, color .22s ease, box-shadow .22s ease; } body { margin: 0; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.65; } @@ -110,6 +126,9 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa .content th, .content td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; } .content blockquote { border-left: 3px solid var(--border); margin: 1em 0; padding-left: 14px; color: var(--muted); } .content img { max-width: 100%; height: auto; border-radius: 8px; } +.content a:not([data-citation]) { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; } +.content a:not([data-citation]):hover { color: color-mix(in srgb, var(--text) 78%, var(--accent) 22%); } +.external-link-icon { display: inline-block; width: 0.82em; height: 0.82em; margin-left: 0.18em; vertical-align: -0.05em; background: currentColor; opacity: 0.72; -webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%2024%2024%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20fill%3D%27black%27%20d%3D%27M14%203h7v7h-2V6.41l-9.29%209.3-1.42-1.42%209.3-9.29H14V3ZM5%205h6v2H7v10h10v-4h2v6H5V5Z%27/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%2024%2024%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20fill%3D%27black%27%20d%3D%27M14%203h7v7h-2V6.41l-9.29%209.3-1.42-1.42%209.3-9.29H14V3ZM5%205h6v2H7v10h10v-4h2v6H5V5Z%27/%3E%3C/svg%3E") center / contain no-repeat; } .content math { color: inherit; font-family: math, "STIX Two Math", "Cambria Math", "Noto Sans Math", serif; font-size: 1.02em; } .content > math { display: block; max-width: 100%; margin: 0.95em 0; overflow-x: auto; overflow-y: hidden; } .content p math, .content li math { vertical-align: -0.08em; } @@ -271,7 +290,7 @@ export function renderConversationPage(slug: string, conversation: NormalizedCon ${messageAvatar(message.role, conversation.chatGptIconSrc)}
${message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Message"}
-
${message.html}
+
${prepareMessageHtml(message.html)}
`) .join("\n");