import path from "node:path"; import fs from "fs-extra"; import type { CaptureRow, NormalizedConversation } from "./types.js"; import { artifactAssetsDir, artifactDir, artifactIndexPath } from "./artifacts.js"; import { config } from "./config.js"; const chatGptAvatar = ` `; function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function css(): string { return ` :root { color-scheme: light dark; --bg: #ffffff; --panel: #ffffff; --text: #1f2328; --muted: #6b7280; --border: #e5e7eb; --soft: #f7f7f8; --code: #0f172a; --code-bg: #f4f4f5; --accent: #10a37f; 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) { :root { --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; } } html[data-theme="light"] { --bg: #ffffff; --panel: #ffffff; --text: #1f2328; --muted: #6b7280; --border: #e5e7eb; --soft: #f7f7f8; --code: #0f172a; --code-bg: #f4f4f5; } html[data-theme="dark"] { --bg: #212121; --panel: #212121; --text: #ececec; --muted: #a8a8a8; --border: #3f3f46; --soft: #2f2f2f; --code: #e5e7eb; --code-bg: #171717; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.65; } a { color: inherit; } .page { min-height: 100vh; } .topbar { position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 92%, transparent); backdrop-filter: blur(12px); } .topbar-inner { max-width: 880px; margin: 0 auto; padding: 14px 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .snapshot-label { font-size: 13px; color: var(--muted); white-space: nowrap; } .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; } .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; } .role { color: var(--muted); font-size: 13px; margin-bottom: 4px; } .content { min-width: 0; overflow-wrap: anywhere; } .content p { margin: 0 0 1em; } .content p:last-child { margin-bottom: 0; } .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; } .content table { width: 100%; border-collapse: collapse; margin: 1em 0; display: block; overflow-x: auto; } .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; } .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; } .button.primary { background: var(--accent); border-color: var(--accent); color: #fff; } .source { max-width: 880px; margin: 0 auto; padding: 18px 20px 34px; color: var(--muted); font-size: 12px; } .source a { color: var(--muted); } .failure { max-width: 720px; margin: 0 auto; padding: 80px 20px; } .failure h1 { font-size: 32px; } @media (max-width: 640px) { body { font-size: 15px; } .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; } .downloads-inner { padding: 18px 16px; align-items: stretch; } .button, select { width: 100%; } }`; } function pageScript(slug: string): string { return ` (function () { var params = new URLSearchParams(location.search); var theme = params.get("theme"); if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme; var copy = document.querySelector("[data-copy]"); if (copy) copy.addEventListener("click", async function () { await navigator.clipboard.writeText(location.href.replace(/[?#].*$/, "")); copy.textContent = "Copied"; setTimeout(function () { copy.textContent = "Copy link"; }, 1400); }); var form = document.querySelector("[data-shot-form]"); if (form) form.addEventListener("submit", function (event) { event.preventDefault(); var device = form.querySelector("[name=device]").value; var selectedTheme = form.querySelector("[name=theme]").value; if (selectedTheme === "system") selectedTheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; location.href = "/aishare/${slug}/screenshots/" + device + "-" + selectedTheme + ".png"; }); })();`; } function messageAvatar(role: string): string { if (role === "assistant") return `
${chatGptAvatar}
`; if (role === "user") return `
You
`; return `
i
`; } export function renderConversationPage(slug: string, conversation: NormalizedConversation): string { const title = escapeHtml(conversation.title); const messages = conversation.messages .map((message) => `
${messageAvatar(message.role)}
${message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Message"}
${message.html}
`) .join("\n"); return ` ${title}
AI ShareChatGPT snapshot

${title}

${messages}
Download HTML ZIP Download HTML
`; } export function renderFailurePage(row: CaptureRow): string { const title = escapeHtml(row.title || "Capture failed"); const reason = escapeHtml(row.error_public || "This ChatGPT share could not be captured."); return ` ${title}

AI Share capture

${title}

${reason}

`; } export async function writeConversationArtifact(slug: string, conversation: NormalizedConversation): Promise { await fs.ensureDir(artifactAssetsDir(slug)); await fs.writeFile(artifactIndexPath(slug), renderConversationPage(slug, conversation), "utf8"); } export async function writeFailureArtifact(row: CaptureRow): Promise { if (!row.slug) return; await fs.ensureDir(artifactDir(row.slug)); await fs.writeFile(path.join(artifactDir(row.slug), "index.html"), renderFailurePage(row), "utf8"); }