diff --git a/src/capture.ts b/src/capture.ts index 3f5d6c2..03833ec 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -96,6 +96,45 @@ async function localizeAssets(conversation: NormalizedConversation, slug: string return { failures }; } +function normalizeCopiedHtml(html: string): string { + const $ = cheerio.load(html, {}, false); + + $("script, style, iframe, object, embed, form, button, textarea, input, nav").remove(); + + $("a").each((_, anchor) => { + const link = $(anchor); + const decorativeImages = link.find("img").filter((_, image) => { + const alt = ($(image).attr("alt") || "").trim(); + return alt.length === 0; + }); + if (decorativeImages.length > 0) { + decorativeImages.remove(); + link.attr("data-citation", "true"); + } + }); + + $("span").each((_, span) => { + const element = $(span); + if (Object.keys(span.attribs || {}).length === 0) element.replaceWith(element.contents()); + }); + + $("p, li, blockquote").each((_, node) => { + const element = $(node); + element.html( + (element.html() || "").replace(/\*\*([^*<>\n][^*<>]*?)\*\*/g, (_match, text: string) => `${text}`) + ); + }); + + $("a").each((_, anchor) => { + const link = $(anchor); + const text = link.text().replace(/\s+/g, " ").trim(); + if (link.attr("data-citation") && text) link.text(text); + }); + + $(":empty").not("br,img,hr").remove(); + return $.html().trim(); +} + function findChromiumExecutable(): string { const preferred = chromium.executablePath(); if (nodeFs.existsSync(preferred)) return preferred; @@ -256,12 +295,10 @@ async function extractConversation(sourceUrl: string): Promise message.text.length > 0); @@ -348,7 +385,10 @@ export async function runCapture(row: CaptureRow): Promise { title: extracted.title || "ChatGPT Share", sourceUrl: row.source_url, capturedAt: new Date().toISOString(), - messages: extracted.messages + messages: extracted.messages.map((message) => ({ + ...message, + html: normalizeCopiedHtml(message.html) + })) }; store.updateStatus(row.id, "rendering"); diff --git a/src/render.ts b/src/render.ts index 73e685b..2599c47 100644 --- a/src/render.ts +++ b/src/render.ts @@ -6,8 +6,8 @@ import { config } from "./config.js"; const chatGptAvatar = ` - - + + `; function escapeHtml(value: string): string { @@ -31,6 +31,9 @@ function css(): string { --code: #0f172a; --code-bg: #f4f4f5; --accent: #10a37f; + --assistant-avatar-bg: #111827; + --assistant-avatar-fg: #ffffff; + --assistant-avatar-border: #111827; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", "Noto Sans CJK SC", "Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans Thai", "Helvetica Neue", Arial, sans-serif; } @media (prefers-color-scheme: dark) { @@ -43,13 +46,16 @@ function css(): string { --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; + --assistant-avatar-bg: #ececec; + --assistant-avatar-fg: #111827; + --assistant-avatar-border: #ececec; } } html[data-theme="light"] { - --bg: #ffffff; --panel: #ffffff; --text: #1f2328; --muted: #6b7280; --border: #e5e7eb; --soft: #f7f7f8; --code: #0f172a; --code-bg: #f4f4f5; + --bg: #ffffff; --panel: #ffffff; --text: #1f2328; --muted: #6b7280; --border: #e5e7eb; --soft: #f7f7f8; --code: #0f172a; --code-bg: #f4f4f5; --assistant-avatar-bg: #111827; --assistant-avatar-fg: #ffffff; --assistant-avatar-border: #111827; } html[data-theme="dark"] { - --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; + --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; --assistant-avatar-bg: #ececec; --assistant-avatar-fg: #111827; --assistant-avatar-border: #ececec; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.65; } @@ -61,14 +67,23 @@ a { color: inherit; } .title { max-width: 880px; margin: 0 auto; padding: 34px 20px 18px; } h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spacing: 0; } .conversation { max-width: 880px; margin: 0 auto; padding: 0 20px 36px; } -.message { display: grid; grid-template-columns: 38px minmax(0, 1fr); gap: 16px; padding: 22px 0; border-top: 1px solid var(--border); } -.avatar { width: 32px; height: 32px; display: grid; place-items: center; margin-top: 2px; } +.message { display: grid; grid-template-columns: 36px minmax(0, 1fr); gap: 16px; padding: 22px 0; border-top: 1px solid var(--border); } +.avatar { width: 30px; height: 30px; display: grid; place-items: center; margin-top: 1px; } .avatar.user { border-radius: 50%; background: var(--soft); color: var(--text); font-weight: 650; font-size: 13px; } -.avatar svg { width: 32px; height: 32px; display: block; } +.avatar svg { width: 30px; height: 30px; display: block; flex: 0 0 auto; } .role { color: var(--muted); font-size: 13px; margin-bottom: 4px; } .content { min-width: 0; overflow-wrap: anywhere; } -.content p { margin: 0 0 1em; } +.content h2, .content h3, .content h4 { margin: 1.35em 0 0.55em; line-height: 1.35; font-weight: 700; } +.content h2 { font-size: 1.28em; } +.content h3 { font-size: 1.12em; } +.content h4 { font-size: 1em; } +.content p { margin: 0 0 0.9em; } .content p:last-child { margin-bottom: 0; } +.content ul, .content ol { margin: 0.6em 0 1em; padding-left: 1.45em; } +.content li { margin: 0.3em 0; padding-left: 0.1em; } +.content li > p { margin: 0.2em 0; } +.content li > ul, .content li > ol { margin: 0.35em 0 0.45em; padding-left: 1.25em; } +.content strong { font-weight: 700; } .content pre { margin: 1em 0; overflow: auto; padding: 14px 16px; border-radius: 8px; background: var(--code-bg); color: var(--code); font-size: 14px; line-height: 1.55; } .content code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Noto Sans Mono", monospace; } .content :not(pre) > code { background: var(--code-bg); border-radius: 5px; padding: 0.1em 0.32em; } @@ -76,6 +91,8 @@ 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[data-citation] { color: var(--muted); font-size: 0.92em; white-space: nowrap; text-decoration-thickness: 1px; text-underline-offset: 2px; } +.content a[data-citation] img { display: none; } .downloads { border-top: 1px solid var(--border); background: var(--soft); } .downloads-inner { max-width: 880px; margin: 0 auto; padding: 22px 20px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .button, select { min-height: 38px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--text); padding: 8px 12px; font: inherit; font-size: 14px; text-decoration: none; } @@ -89,8 +106,10 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa .topbar-inner { padding: 12px 16px; } .title { padding: 26px 16px 12px; } .conversation { padding: 0 16px 28px; } - .message { grid-template-columns: 32px minmax(0, 1fr); gap: 12px; padding: 18px 0; } - .avatar, .avatar svg { width: 28px; height: 28px; } + .message { grid-template-columns: 30px minmax(0, 1fr); gap: 10px; padding: 18px 0; } + .avatar, .avatar svg { width: 26px; height: 26px; } + .avatar.user { font-size: 11px; } + .content ul, .content ol { padding-left: 1.25em; } .downloads-inner { padding: 18px 16px; align-items: stretch; } .button, select { width: 100%; } }`;