Match ChatGPT share message layout

This commit is contained in:
Codex
2026-07-08 05:28:36 +00:00
parent a86050f005
commit 743c1332fd
3 changed files with 31 additions and 35 deletions
+3 -7
View File
@@ -19,6 +19,8 @@ interface ExtractedConversation {
messages: NormalizedMessage[]; messages: NormalizedMessage[];
} }
const chatGptIconUrl = "https://chatgpt.com/cdn/assets/favicon-l4nq08hd.svg";
function publicErrorFor(error: unknown): string { function publicErrorFor(error: unknown): string {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
if (/captcha|verify|robot/i.test(message)) { if (/captcha|verify|robot/i.test(message)) {
@@ -108,7 +110,6 @@ function normalizeCopiedHtml(html: string): string {
return alt.length === 0; return alt.length === 0;
}); });
if (decorativeImages.length > 0) { if (decorativeImages.length > 0) {
decorativeImages.remove();
link.attr("data-citation", "true"); link.attr("data-citation", "true");
} }
}); });
@@ -125,12 +126,6 @@ function normalizeCopiedHtml(html: string): string {
); );
}); });
$("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(); $(":empty").not("br,img,hr").remove();
return $.html().trim(); return $.html().trim();
} }
@@ -392,6 +387,7 @@ export async function runCapture(row: CaptureRow): Promise<void> {
}; };
store.updateStatus(row.id, "rendering"); store.updateStatus(row.id, "rendering");
conversation.chatGptIconSrc = (await downloadAsset(chatGptIconUrl, slug).catch(() => null)) || chatGptIconUrl;
const assetResult = await localizeAssets(conversation, slug); const assetResult = await localizeAssets(conversation, slug);
await writeConversationArtifact(slug, conversation); await writeConversationArtifact(slug, conversation);
const screenshots = await generateScreenshots(slug); const screenshots = await generateScreenshots(slug);
+27 -28
View File
@@ -4,11 +4,7 @@ import type { CaptureRow, NormalizedConversation } from "./types.js";
import { artifactAssetsDir, artifactDir, artifactIndexPath } from "./artifacts.js"; import { artifactAssetsDir, artifactDir, artifactIndexPath } from "./artifacts.js";
import { config } from "./config.js"; import { config } from "./config.js";
const chatGptAvatar = ` const fallbackChatGptIconUrl = "https://chatgpt.com/cdn/assets/favicon-l4nq08hd.svg";
<svg viewBox="0 0 40 40" role="img" aria-label="ChatGPT" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="19" fill="var(--assistant-avatar-bg)" stroke="var(--assistant-avatar-border)"/>
<path fill="var(--assistant-avatar-fg)" d="M29.6 17.9a7 7 0 0 0-9.4-8.2 7 7 0 0 0-10.4 6.1 7 7 0 0 0 1.4 12.7 7 7 0 0 0 9.4 1.8 7 7 0 0 0 10.3-6.1 7 7 0 0 0-1.3-6.3Zm-9.4-6.2a5 5 0 0 1 6.9 5.4l-5.6-3.2a1 1 0 0 0-1 0l-7.6 4.4v-2.7l6.8-3.9h.5Zm-8.4 4.1a5 5 0 0 1 6.2-4.9l-5.5 3.2a1 1 0 0 0-.5.9v8.8l-2.4-1.4v-7.9c.6-.7 1.3-1.3 2.2-1.7Zm-1.2 10.5a5 5 0 0 1-2.8-8l.1 6.4c0 .4.2.7.5.9l7.6 4.4-2.4 1.4-6.8-3.9c-.2-.4-.2-.8-.2-1.2Zm9.2 3.9a5 5 0 0 1-6.9-5.4l5.6 3.2c.3.2.7.2 1 0l7.6-4.4v2.8l-6.8 3.9-.5-.1Zm8.4-4.1a5 5 0 0 1-6.2 4.9l5.5-3.2c.3-.2.5-.5.5-.9v-8.8l2.4 1.4v7.9c-.6.7-1.3 1.3-2.2 1.7Zm-8.2-.2-3-1.7v-3.5l3-1.7 3 1.7v3.5l-3 1.7Zm4-7-3.5-2a1 1 0 0 0-1 0l-3.5 2v-2.8l4-2.3 6.8 3.9c.2.4.2.8.2 1.2l-3-1.7Z"/>
</svg>`;
function escapeHtml(value: string): string { function escapeHtml(value: string): string {
return value return value
@@ -31,9 +27,7 @@ function css(): string {
--code: #0f172a; --code: #0f172a;
--code-bg: #f4f4f5; --code-bg: #f4f4f5;
--accent: #10a37f; --accent: #10a37f;
--assistant-avatar-bg: #111827; --user-bubble: #f4f4f4;
--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; 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) { @media (prefers-color-scheme: dark) {
@@ -46,16 +40,14 @@ function css(): string {
--soft: #2f2f2f; --soft: #2f2f2f;
--code: #e5e7eb; --code: #e5e7eb;
--code-bg: #171717; --code-bg: #171717;
--assistant-avatar-bg: #ececec; --user-bubble: #303030;
--assistant-avatar-fg: #111827;
--assistant-avatar-border: #ececec;
} }
} }
html[data-theme="light"] { html[data-theme="light"] {
--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; --bg: #ffffff; --panel: #ffffff; --text: #1f2328; --muted: #6b7280; --border: #e5e7eb; --soft: #f7f7f8; --code: #0f172a; --code-bg: #f4f4f5; --user-bubble: #f4f4f4;
} }
html[data-theme="dark"] { html[data-theme="dark"] {
--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; --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; --user-bubble: #303030;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.65; } body { margin: 0; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.65; }
@@ -66,12 +58,16 @@ a { color: inherit; }
.snapshot-label { font-size: 13px; color: var(--muted); white-space: nowrap; } .snapshot-label { font-size: 13px; color: var(--muted); white-space: nowrap; }
.title { max-width: 880px; margin: 0 auto; padding: 34px 20px 18px; } .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; } 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; } .conversation { max-width: 880px; margin: 0 auto; padding: 4px 20px 36px; }
.message { display: grid; grid-template-columns: 36px minmax(0, 1fr); gap: 16px; padding: 22px 0; border-top: 1px solid var(--border); } .message { padding: 18px 0; }
.avatar { width: 30px; height: 30px; display: grid; place-items: center; margin-top: 1px; } .message.assistant { display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: 14px; }
.avatar.user { border-radius: 50%; background: var(--soft); color: var(--text); font-weight: 650; font-size: 13px; } .message.user { display: flex; justify-content: flex-end; }
.avatar svg { width: 30px; height: 30px; display: block; flex: 0 0 auto; } .message.user .content-wrap { max-width: min(72%, 620px); }
.role { color: var(--muted); font-size: 13px; margin-bottom: 4px; } .message.user .content { background: var(--user-bubble); border-radius: 18px; padding: 10px 16px; line-height: 1.55; }
.message.user .content p { margin: 0; }
.avatar { width: 28px; height: 28px; display: grid; place-items: center; margin-top: 2px; }
.assistant-icon { width: 28px; height: 28px; display: block; border-radius: 50%; }
.role { display: none; }
.content { min-width: 0; overflow-wrap: anywhere; } .content { min-width: 0; overflow-wrap: anywhere; }
.content h2, .content h3, .content h4 { margin: 1.35em 0 0.55em; line-height: 1.35; font-weight: 700; } .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 h2 { font-size: 1.28em; }
@@ -92,7 +88,7 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa
.content blockquote { border-left: 3px solid var(--border); margin: 1em 0; padding-left: 14px; color: var(--muted); } .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 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] { 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; } .content a[data-citation] img { width: 14px; height: 14px; max-width: 14px; max-height: 14px; border-radius: 3px; object-fit: cover; vertical-align: -2px; margin: 0 3px 0 4px; }
.downloads { border-top: 1px solid var(--border); background: var(--soft); } .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; } .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; } .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; }
@@ -106,9 +102,11 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa
.topbar-inner { padding: 12px 16px; } .topbar-inner { padding: 12px 16px; }
.title { padding: 26px 16px 12px; } .title { padding: 26px 16px 12px; }
.conversation { padding: 0 16px 28px; } .conversation { padding: 0 16px 28px; }
.message { grid-template-columns: 30px minmax(0, 1fr); gap: 10px; padding: 18px 0; } .message { padding: 16px 0; }
.avatar, .avatar svg { width: 26px; height: 26px; } .message.assistant { grid-template-columns: 28px minmax(0, 1fr); gap: 8px; }
.avatar.user { font-size: 11px; } .message.user .content-wrap { max-width: 84%; }
.message.user .content { border-radius: 18px; padding: 9px 14px; }
.avatar, .assistant-icon { width: 26px; height: 26px; }
.content ul, .content ol { padding-left: 1.25em; } .content ul, .content ol { padding-left: 1.25em; }
.downloads-inner { padding: 18px 16px; align-items: stretch; } .downloads-inner { padding: 18px 16px; align-items: stretch; }
.button, select { width: 100%; } .button, select { width: 100%; }
@@ -138,10 +136,11 @@ function pageScript(slug: string): string {
})();`; })();`;
} }
function messageAvatar(role: string): string { function messageAvatar(role: string, chatGptIconSrc?: string): string {
if (role === "assistant") return `<div class="avatar">${chatGptAvatar}</div>`; if (role === "assistant") {
if (role === "user") return `<div class="avatar user">You</div>`; return `<div class="avatar"><img class="assistant-icon" src="${escapeHtml(chatGptIconSrc || fallbackChatGptIconUrl)}" alt="ChatGPT"></div>`;
return `<div class="avatar user">i</div>`; }
return "";
} }
export function renderConversationPage(slug: string, conversation: NormalizedConversation): string { export function renderConversationPage(slug: string, conversation: NormalizedConversation): string {
@@ -149,7 +148,7 @@ export function renderConversationPage(slug: string, conversation: NormalizedCon
const messages = conversation.messages const messages = conversation.messages
.map((message) => ` .map((message) => `
<article class="message ${message.role}"> <article class="message ${message.role}">
${messageAvatar(message.role)} ${messageAvatar(message.role, conversation.chatGptIconSrc)}
<div class="content-wrap"> <div class="content-wrap">
<div class="role">${message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Message"}</div> <div class="role">${message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Message"}</div>
<div class="content">${message.html}</div> <div class="content">${message.html}</div>
+1
View File
@@ -22,6 +22,7 @@ export interface NormalizedConversation {
title: string; title: string;
sourceUrl: string; sourceUrl: string;
capturedAt: string; capturedAt: string;
chatGptIconSrc?: string;
messages: NormalizedMessage[]; messages: NormalizedMessage[];
} }