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 {