Skip to content

Franklin portrait branding, Poppins wordmark, web share dialog, Canvas link - #14

Open
KillerQueen-Z wants to merge 3 commits into
masterfrom
feat/franklin-portrait-share-canvas-link
Open

Franklin portrait branding, Poppins wordmark, web share dialog, Canvas link#14
KillerQueen-Z wants to merge 3 commits into
masterfrom
feat/franklin-portrait-share-canvas-link

Conversation

@KillerQueen-Z

Copy link
Copy Markdown
Collaborator

What

  • Branding: cute Franklin portrait → favicon / app icon / apple-touch + sidebar avatar (franklin-portrait.jpg). Dropped the old grayscale/sepia filter so it shows full color.
  • Wordmark + chat empty-title: now Poppins (geometric sans), via next/font.
  • Share dialog (new): the chat UI had no share button. Adds copy-link / copy-text / export-image (html-to-image), wired to the existing /api/share backend + /s/[id] page. Tool activity travels via the message activity.
  • Canvas link: in the sidebar More menu, shown only when NEXT_PUBLIC_CANVAS_URL is set (external, new tab). deploy.sh forwards that var as a build env.

Notes

KillerQueen-Z and others added 3 commits June 5, 2026 22:34
… Canvas link

- Branding: cute Franklin portrait for favicon / app icon / apple-touch +
  sidebar avatar (franklin-portrait.jpg), full color (drop grayscale/sepia)
- Wordmark + chat empty-state title now use Poppins (geometric sans, next/font)
- Share dialog: copy link / copy text / export image (html-to-image) — wires
  the chat UI to the existing /api/share backend (the web had no share UI)
- Canvas link in the More menu, gated by NEXT_PUBLIC_CANVAS_URL
- deploy.sh forwards NEXT_PUBLIC_CANVAS_URL as a build env
The share-link payload tagged messages with their original kind (user
uploads carry kind:"text"), but the /s page renders an image only when
kind==="image". Result: the inline data: image traveled in the payload
yet never appeared on the public page — while the in-dialog PNG export
*did* show it, so 'copy link' and 'export image' silently diverged.

Split each selected message into a kind:"image" bubble (when it has an
inline data: image) plus a text bubble, so a text+image message keeps
both and neither is dropped on the shared page.
#2 onExport swallowed every error in an empty catch — a failed PNG
render (cross-origin image taints the canvas, font never resolves, zero-
size node) just flipped the button back to idle with no preview and no
explanation. Now it shows a transient 'Render failed' state.

#3 A failed 'copy link' always showed the generic 'Share failed', even
when the route returned a specific 413 reason ('Too many messages' /
'Conversation too large to share'). Capture that reason and surface it
as the button's title so the user knows to deselect messages and retry.

@VickyXAI VickyXAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-landing review — 7 blockers, 15 informational

Cross-model review (Claude structured + adversarial/red-team, Codex adversarial + structured, plus testing/maintainability/performance/design specialists). Scope is clean — the PR delivers exactly what it describes — but it needs a rebase and six functional fixes before merge.

🔴 Blockers

  1. Merge conflict — rebase required. The branch is based on pre-redesign master (merge base fd73648, before the homepage redesign in 290bd86); GitHub reports CONFLICTING. Good news: after rebasing, the biggest design worry (the marketing homepage piping the new full-color cartoon portrait through its old grayscale/sepia hero and header filters) evaporates — current master's homepage no longer consumes franklin-portrait.jpg; only the /chat sidebar avatar does.

  2. ShareDialog.tsx onShareLink — the primary action fails on Safari/iOS and orphans a snapshot per retry. await fetch(SHARE_API) runs before copyText(url); Safari's transient user activation expires across the network await, so the clipboard write rejects, copyText returns false, and the code throws — after the snapshot was already persisted to GCS. The user sees "Share failed", the URL is never shown anywhere, and every retry writes another immutable orphan object. Fix: show the URL in the dialog on success (make copy optional) rather than gating success on the clipboard. Same gesture bug in onCopyImg (the await fetch(previewUrl) before clipboard.write) — pass the blob promise into ClipboardItem synchronously instead.

  3. Shares of generated (remote-URL) media render as empty bubbles on /s. Generated images come back as remote URLs, not data:, so dataImg is undefined and the payload sends {role:"assistant", content:"", kind:"image"} with no image — the public page shows a "Franklin" label with nothing under it. That's the flagship use case for this feature. Send a placeholder ([image] / the prose) or render an "image omitted" state on /s. Video/music with prose also lose the prose.

  4. ShareDialog.tsx:203 — the canvas-size clamp never engages. Math.max(1, Math.min(2, MAX / height, MAX / width)): when height exceeds 16000px, MAX/height < 1 gets clamped back up to 1, so the oversized render happens anyway (crop/failure), exactly what the comment says it prevents. Drop the outer Math.max(1, …) so the ratio can go below 1. (Found independently by both Codex passes.)

  5. Multi-image messages silently drop images 2..n. The link payload takes only the first data: image (.find(...)) and renderMsg renders only imgs[0], while the main chat renders all of images[]. A 3-attachment message shares as a 1-attachment message with no warning.

  6. Hidden tool activity is published without being shown in the picker. The selection list renders only the message bubbles, but entries injects each assistant message's search queries and tool calls into the link payload and exported image. Users can publish raw search queries they never saw in the selector. Show the tool blocks in the picker (or make their inclusion an explicit toggle).

  7. javascript: URL injection on public franklin.run/s/* pages. MessageContent.renderInline sets href from markdown with no scheme allowlist, and /api/share is unauthenticated with Access-Control-Allow-Origin: * — anyone can mint a share page whose links run script on click, hosted on the trusted domain. The files are pre-existing, but this PR ships the first web UI feeding that pipeline. One-line fix: allow only http(s): hrefs in MessageContent.

Informational

  • Perf: the off-screen capture DOM is always mounted while the dialog is open — every selected message renders twice (full markdown parse + duplicate base64 image decode + double <video>/<audio> mounting, which html-to-image can't rasterize anyway); mount it only during export and use preload="none" / placeholders. Lazy-load html-to-image (await import in onExport) and ShareDialog (next/dynamic) so they leave the /chat initial bundle.
  • Fonts: Poppins loads 4 variants globally but only weight-600-normal has any CSS consumer — trim the config, and consider scoping it to /chat. Also .try-empty-title uses var(--font-wordmark, var(--font-serif)) — that fallback only applies when the var is undefined (it never is), so the zh title's CJK glyphs render in synthetic-bold size-adjusted Arial instead of Noto Serif SC. Use a comma stack: var(--font-wordmark), var(--font-noto-sc), sans-serif.
  • A11y: both modals declare role="dialog" aria-modal but have no Escape handling, focus trap, initial focus, or accessible name; selection rows are <button>s wrapping MessageContent's links/buttons and <video controls> (invalid nesting, breaks keyboard/AT); checked state is visual-only (no aria-checked); white-on-gold primary buttons are ~2.4:1 contrast; the over-limit reason lives in a hover-only title on a disabled button.
  • i18n: LABELS is a parallel dictionary bypassing try-i18n (the repo convention), and "You", "Search", and the sidebar "Canvas" label are hardcoded English — zh/es users get mixed-language share images and copied text.
  • Types: SharePayloadMsg/ToolStep hand-mirror SharedMessage/SharedToolStep from share-store.ts — import the types (type-only) so the wire contract can't drift.
  • Assets/CDN: franklin-icon.png is 115KB and referenced 3× in root metadata; apple-touch-icon is 60KB (both should be <25KB after pngquant/oxipng). The icons/portrait changed bytes at unchanged paths while upload-assets.sh stamps max-age=31536000, immutable — with CDN enabled the old assets serve stale up to a year (and the un-filtered sidebar will show the OLD banknote portrait), and franklin-icon.png isn't in ROOT_FILES at all. Version the filenames or add cache-bust; add franklin-icon to the upload list.
  • deploy.sh: comma-joined --set-build-env-vars corrupts values containing commas (legal in URLs) — use gcloud's ^@^ alternate-delimiter syntax or reject commas. Also NEXT_PUBLIC_SITE_URL is never set, so a staging service would mint https://franklin.run/s/… links.
  • Share limits: one b64_json provider image can exceed the 2MB payload cap → a single-image share 413s with only a 2.5s transient error; pre-measure the payload client-side. The 30-message image cap doesn't bound the link path, which fans each message into up to 3 payload messages (can hit the server's 200-message cap).
  • /s page: getShare runs twice per view (metadata + page) under force-dynamic — wrap in React.cache(); and the page-level openGraph replaces the root one wholesale, so share unfurls have no og:image (the one place unfurls matter). Sanitize the title for control/bidi chars before it hits <title>/OG.
  • Dialog lifecycle: backdrop click closes mid-share with no abort (orphan snapshot + post-unmount clipboard write); the Share button is live mid-stream, so captures can snapshot half an answer; previewUrl holds a potentially tens-of-MB data: URL in state (use toBlob + object URL with revocation); cacheBust: true appends a query param to remote media on every export — if those are signed URLs it 403s into a generic "Render failed" (verify).
  • Pre-existing but now user-facing: /api/share is an unauthenticated durable-write endpoint with CORS *, no rate limit or TTL (the route's own comment says to add rate-limiting before public launch — this PR is that launch); the storage error e.message is returned verbatim to callers.

Verified along the way: /api/share handles kind:"tools" and strips non-data: images; /s renders tools blocks; activityUsed exists in all three dicts; double-click double-POST is effectively guarded; sel index coupling survives streaming appends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants