From 746fe2a65dd47f03918bcd6baf7891ef15f5ab87 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 06:10:27 +0000 Subject: [PATCH] Compact export toolbar and inline HTML assets --- src/artifacts.ts | 29 +++++++++++++++++++ src/render.ts | 72 ++++++++++++++++++++++++++++++++++-------------- src/server.ts | 23 ++++++++++++++++ 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/artifacts.ts b/src/artifacts.ts index 380f5eb..2dd9f2b 100644 --- a/src/artifacts.ts +++ b/src/artifacts.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import path from "node:path"; import fs from "fs-extra"; import archiver from "archiver"; +import * as cheerio from "cheerio"; import { config } from "./config.js"; import type { CaptureRow } from "./types.js"; @@ -61,3 +62,31 @@ export async function zipArtifact(slug: string, destination: string): Promise { + const html = await fs.readFile(artifactIndexPath(slug), "utf8"); + const $ = cheerio.load(html); + + for (const image of $("img").toArray()) { + const src = $(image).attr("src"); + if (!src || src.startsWith("data:") || /^https?:\/\//i.test(src)) continue; + const normalized = path.normalize(src).replace(/^(\.\.(\/|\\|$))+/, ""); + if (!normalized.startsWith("assets/")) continue; + const assetPath = path.join(artifactDir(slug), normalized); + const bytes = await fs.readFile(assetPath); + const data = `data:${contentTypeForAsset(assetPath)};base64,${bytes.toString("base64")}`; + $(image).attr("src", data); + } + + return $.html(); +} diff --git a/src/render.ts b/src/render.ts index 8de00c1..d0ddbb4 100644 --- a/src/render.ts +++ b/src/render.ts @@ -14,6 +14,13 @@ function escapeHtml(value: string): string { .replace(/"/g, """); } +const icons = { + download: ``, + image: ``, + share: ``, + chevron: `` +}; + function css(): string { return ` :root { @@ -95,9 +102,19 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa .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 { 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-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; } +.downloads-inner { max-width: 880px; margin: 0 auto; padding: 14px 20px; display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; overflow-x: auto; } +.icon-button { width: 36px; height: 36px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--text); padding: 0; display: inline-grid; place-items: center; flex: 0 0 auto; cursor: pointer; text-decoration: none; } +.icon-button svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } +.icon-button:hover { background: color-mix(in srgb, var(--panel) 88%, var(--text) 12%); } +.shot-control { display: inline-flex; align-items: stretch; flex: 0 0 auto; position: relative; } +.shot-control .shot-main { border-radius: 8px 0 0 8px; border-right: 0; } +.shot-control details { position: relative; } +.shot-control summary { list-style: none; border-radius: 0 8px 8px 0; width: 28px; } +.shot-control summary::-webkit-details-marker { display: none; } +.shot-control summary svg { width: 15px; height: 15px; } +.shot-menu { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 4; min-width: 150px; padding: 6px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.12); } +.shot-menu button { width: 100%; border: 0; border-radius: 6px; background: transparent; color: var(--text); padding: 8px 10px; font: inherit; font-size: 13px; text-align: left; cursor: pointer; } +.shot-menu button:hover, .shot-menu button[data-current="true"] { background: var(--soft); } .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; } @@ -113,14 +130,21 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa .message.user .content { border-radius: 18px; padding: 9px 14px; } .avatar, .assistant-icon { width: 26px; height: 26px; } .content ul, .content ol { padding-left: 1.25em; } - .downloads-inner { padding: 18px 16px; align-items: stretch; } - .button, select { width: 100%; } + .downloads-inner { padding: 12px 16px; } }`; } function pageScript(slug: string): string { return ` (function () { + function currentVariant() { + var device = matchMedia("(max-width: 640px)").matches ? "mobile" : "desktop"; + var theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + return device + "-" + theme; + } + function downloadShot(variant) { + location.href = "/aishare/${slug}/screenshots/" + variant + ".png"; + } var params = new URLSearchParams(location.search); var theme = params.get("theme"); if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme; @@ -140,13 +164,15 @@ function pageScript(slug: string): string { setTimeout(function () { button.textContent = "Copy"; }, 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"; + var defaultShot = document.querySelector("[data-shot-current]"); + if (defaultShot) defaultShot.addEventListener("click", function () { + downloadShot(currentVariant()); + }); + document.querySelectorAll("[data-shot-variant]").forEach(function (button) { + if (button.dataset.shotVariant === currentVariant()) button.dataset.current = "true"; + button.addEventListener("click", function () { + downloadShot(button.dataset.shotVariant); + }); }); })();`; } @@ -189,14 +215,20 @@ export function renderConversationPage(slug: string, conversation: NormalizedCon
- Download HTML ZIP - Download HTML -
- - - -
- + ${icons.download} +
+ +
+ ${icons.chevron} +
+ + + + +
+
+
+