- No HTML artifact source was provided. +
+ {resultText || "No HTML artifact source was provided."}
)}+)} @@ -179,8 +244,9 @@ export function ArtifactSurface({ return ({artifact.code}{ + if (event.target === event.currentTarget) onClose(); + }} > {content}diff --git a/studio/frontend/src/features/chat/artifacts/html-frame.tsx b/studio/frontend/src/features/chat/artifacts/html-frame.tsx index 26a9924467..f3814e7b4e 100644 --- a/studio/frontend/src/features/chat/artifacts/html-frame.tsx +++ b/studio/frontend/src/features/chat/artifacts/html-frame.tsx @@ -24,6 +24,9 @@ export function buildArtifactSrcDoc(code: string): string { return `${code}\n${resizeScript}`; } +// Preview iframes intentionally omit allow-downloads: generated artifacts can +// offer their own UI, but downloads must go through Studio's explicit +// copy/download controls outside the no-same-origin sandbox. export function ArtifactHtmlFrame({ code, title = "HTML artifact preview", diff --git a/studio/frontend/src/features/chat/artifacts/store.ts b/studio/frontend/src/features/chat/artifacts/store.ts index d75f79968b..764b0136bc 100644 --- a/studio/frontend/src/features/chat/artifacts/store.ts +++ b/studio/frontend/src/features/chat/artifacts/store.ts @@ -24,13 +24,13 @@ export const useChatArtifactsStore = create((set) => ({ openArtifact: (artifact, options) => set((state) => ({ artifactsById: { - ...state.artifactsById, [artifact.id]: artifact, }, selectedArtifactId: artifact.id, surface: options?.surface ?? state.surface, })), - closeArtifactSurface: () => set({ selectedArtifactId: null }), + closeArtifactSurface: () => + set({ artifactsById: {}, selectedArtifactId: null }), clearArtifactsForThread: (threadId) => set((state) => { if (!threadId) return state; diff --git a/studio/frontend/src/features/chat/artifacts/types.ts b/studio/frontend/src/features/chat/artifacts/types.ts index bfe75d83e4..4248becb40 100644 --- a/studio/frontend/src/features/chat/artifacts/types.ts +++ b/studio/frontend/src/features/chat/artifacts/types.ts @@ -43,12 +43,13 @@ export function createArtifactId(input: ChatArtifactInput): string { const threadSegment = input.threadId || "no-thread"; const sourceId = input.sourceToolCallId || input.sourceMessageId || "transient"; - return [ - input.source, - threadSegment, - sourceId, - hashArtifactCode(input.code), - ].join(":"); + const parts = [input.source, threadSegment, sourceId]; + + if (input.source !== "tool" || !input.sourceToolCallId) { + parts.push(hashArtifactCode(input.code)); + } + + return parts.join(":"); } export function createChatArtifact(input: ChatArtifactInput): ChatArtifact {