From 6cf20be6226ce7e0d8d7526d4274f4ede0c2677f Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:49:47 +0800 Subject: [PATCH] feat(a2ui-playground): persist and reuse the conversation share link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sharing a conversation uploads the serialized document and builds an import link. Because each upload uses a fresh random storage path, every Share click — and every share after a page reload — minted a different link for the same, unchanged conversation. Persist the published document URL on the conversation's IndexedDB snapshot, paired with the meta.updatedAt it was generated for, and reuse it while the conversation is unchanged. Repeated shares (within a session and across reloads) now copy the same link; a new turn or rename bumps updatedAt (and a turn also rewrites the snapshot), so the link refreshes exactly when the shared content changes. - types.ts: DataModelSnapshot.sharePayload { url, updatedAt } - conversationRepo.ts: saveConversationSharePayload() - AIChatPage.tsx: shareConversation reuses the persisted URL, or uploads and persists a fresh one --- .../a2ui-playground/src/pages/AIChatPage.tsx | 26 ++++++++++++++++--- .../src/storage/conversationRepo.ts | 21 +++++++++++++++ .../a2ui-playground/src/storage/types.ts | 7 +++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx index 0a05de9945..86cf173260 100644 --- a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx @@ -25,7 +25,10 @@ import type { StaticDemo } from '../demos.js'; import { useConversation } from '../hooks/useConversation.js'; import type { ModelChatMessage } from '../hooks/useConversation.js'; import { useResizablePanels } from '../hooks/useResizablePanels.js'; -import { loadConversation } from '../storage/conversationRepo.js'; +import { + loadConversation, + saveConversationSharePayload, +} from '../storage/conversationRepo.js'; import { isSharedConversationDoc, serializeConversation, @@ -2154,8 +2157,25 @@ export function AIChatPage( showCopyToast(false); return; } - const doc = serializeConversation(record, protocol.name); - const conversationUrl = await publishConversation(doc); + // Reuse the link already published for this conversation while it is + // unchanged, so repeated shares — including after a page reload — copy + // the same link instead of uploading a fresh copy (and minting a new + // URL) each time. `meta.updatedAt` bumps on every turn or rename, and a + // new turn rewrites the snapshot and drops the cached payload. + const cached = record.snapshot?.sharePayload; + let conversationUrl: string | undefined; + if (cached && cached.updatedAt === record.meta.updatedAt) { + conversationUrl = cached.url; + } + if (!conversationUrl) { + const doc = serializeConversation(record, protocol.name); + conversationUrl = await publishConversation(doc); + await saveConversationSharePayload( + id, + conversationUrl, + record.meta.updatedAt, + ); + } const link = buildConversationShareUrl( conversationUrl, baseUrl, diff --git a/packages/genui/a2ui-playground/src/storage/conversationRepo.ts b/packages/genui/a2ui-playground/src/storage/conversationRepo.ts index 8c7d1117b7..ece8cf8d85 100644 --- a/packages/genui/a2ui-playground/src/storage/conversationRepo.ts +++ b/packages/genui/a2ui-playground/src/storage/conversationRepo.ts @@ -132,6 +132,27 @@ export async function saveConversationMessages( await tx.done; } +/** + * Persist the durable URL of the published share document on the conversation + * snapshot, paired with the `meta.updatedAt` it was generated for. Touches only + * the snapshot's share field (not `meta.updatedAt`), so it does not invalidate + * itself; a later turn rewrites the snapshot and drops it. + */ +export async function saveConversationSharePayload( + conversationId: string, + url: string, + updatedAt: number, +): Promise { + const db = await getDB(); + const tx = db.transaction('snapshots', 'readwrite'); + const store = tx.objectStore('snapshots'); + const snapshot = await store.get(conversationId); + if (snapshot) { + await store.put({ ...snapshot, sharePayload: { url, updatedAt } }); + } + await tx.done; +} + export function previewTextFromSharedMessages( messages: SharedConversationDoc['messages'], ): string { diff --git a/packages/genui/a2ui-playground/src/storage/types.ts b/packages/genui/a2ui-playground/src/storage/types.ts index 6ef6ba0c61..d70561d7e6 100644 --- a/packages/genui/a2ui-playground/src/storage/types.ts +++ b/packages/genui/a2ui-playground/src/storage/types.ts @@ -41,6 +41,13 @@ export interface DataModelSnapshot { previewMessages?: unknown[]; previewPayloadUrls?: PreviewPayloadUrls; updatedAt: number; + /** + * Durable URL of the most recently published "share whole conversation" + * document, paired with the `meta.updatedAt` it was generated for. Lets + * repeated shares (including after a page reload) reuse the same link until + * the conversation changes. + */ + sharePayload?: { url: string; updatedAt: number }; } export interface MetaRecord {