fix(web): preserve live artifact preset metadata

This commit is contained in:
qiongyu1999
2026-05-21 17:47:26 +08:00
parent bdc57655d6
commit 7fe4c3f581
2 changed files with 119 additions and 8 deletions
+30 -8
View File
@@ -224,6 +224,8 @@ export function HomeView({
const [pendingPluginUseHandoff, setPendingPluginUseHandoff] =
useState<PendingPluginUseHandoff | null>(null);
const [fallbackProjectKind, setFallbackProjectKind] = useState<ProjectKind | null>(null);
const [fallbackProjectMetadata, setFallbackProjectMetadata] =
useState<ProjectMetadata | null>(null);
const [active, setActive] = useState<ActivePlugin | null>(null);
const [activeSkill, setActiveSkill] = useState<SkillSummary | null>(null);
const [selectedPluginContexts, setSelectedPluginContexts] = useState<SelectedPluginContext[]>([]);
@@ -409,6 +411,7 @@ export function HomeView({
setSelectedMcpContexts([]);
setSelectedConnectorContexts([]);
setFallbackProjectKind('other');
setFallbackProjectMetadata(null);
if (promptHandoff.focus) {
pendingPromptFocusEndRef.current = true;
}
@@ -625,6 +628,7 @@ export function HomeView({
suppressPromptSync: suppressPromptUpdate,
});
setFallbackProjectKind(null);
setFallbackProjectMetadata(null);
setDetailsRecord(null);
if (!suppressPromptUpdate && optimisticPrompt !== null) {
setPrompt(optimisticPrompt);
@@ -860,10 +864,12 @@ export function HomeView({
function useExamplePlugin(record: InstalledPluginRecord, chipId: string, promptText: string) {
const projectKind = projectKindForExamplePlugin(record, chipId);
const projectMetadata = projectMetadataForExamplePlugin(chipId);
activePluginApplyRequestRef.current += 1;
setActive(null);
setActiveSkill(null);
setFallbackProjectKind(projectKind);
setFallbackProjectMetadata(projectMetadata);
setPendingApplyId(null);
setPendingChipId(null);
setError(null);
@@ -970,6 +976,7 @@ export function HomeView({
activePluginApplyRequestRef.current += 1;
setActive(null);
setFallbackProjectKind(null);
setFallbackProjectMetadata(null);
setPendingApplyId(null);
setPendingChipId(null);
setPrompt('');
@@ -980,6 +987,7 @@ export function HomeView({
activePluginApplyRequestRef.current += 1;
setActive(null);
setFallbackProjectKind(null);
setFallbackProjectMetadata(null);
setPendingApplyId(null);
setPendingChipId(null);
setError(null);
@@ -989,6 +997,8 @@ export function HomeView({
function useSkill(skill: SkillSummary, nextPrompt: string | null) {
setActiveSkill(skill);
setFallbackProjectKind(null);
setFallbackProjectMetadata(null);
setError(null);
const replacement = nextPrompt ?? skill.examplePrompt ?? '';
if (replacement.trim().length > 0) {
@@ -1028,6 +1038,7 @@ export function HomeView({
setActive(null);
setActiveSkill(null);
setFallbackProjectKind('other');
setFallbackProjectMetadata(null);
setError(null);
setPrompt(nextPrompt);
setPromptEditedByUser(false);
@@ -1237,7 +1248,7 @@ export function HomeView({
: homeCreateProjectMetadata(
submittedProjectKind,
submittedActive?.inputs ?? null,
submittedActive?.projectMetadata ?? null,
submittedActive?.projectMetadata ?? fallbackProjectMetadata ?? null,
);
onSubmit({
prompt: trimmed,
@@ -1459,13 +1470,6 @@ function projectKindForExamplePlugin(
record: InstalledPluginRecord,
chipId: string,
): ProjectKind {
const mode = homePluginManifestField(record, 'mode');
const surface = homePluginManifestField(record, 'surface');
if (mode === 'deck') return 'deck';
if (mode === 'prototype') return 'prototype';
if (mode === 'image' || surface === 'image') return 'image';
if (mode === 'video' || surface === 'video') return 'video';
if (mode === 'audio' || surface === 'audio') return 'audio';
const chip = findChip(chipId);
if (
chip?.action.kind === 'apply-scenario' ||
@@ -1473,9 +1477,27 @@ function projectKindForExamplePlugin(
) {
return chip.action.projectKind;
}
const mode = homePluginManifestField(record, 'mode');
const surface = homePluginManifestField(record, 'surface');
if (mode === 'deck') return 'deck';
if (mode === 'prototype') return 'prototype';
if (mode === 'image' || surface === 'image') return 'image';
if (mode === 'video' || surface === 'video') return 'video';
if (mode === 'audio' || surface === 'audio') return 'audio';
return 'other';
}
function projectMetadataForExamplePlugin(chipId: string): ProjectMetadata | null {
const chip = findChip(chipId);
if (
chip?.action.kind === 'apply-scenario' ||
chip?.action.kind === 'apply-figma-migration'
) {
return chip.action.projectMetadata ?? null;
}
return null;
}
function homePluginManifestField(
record: InstalledPluginRecord,
key: string,
@@ -170,6 +170,28 @@ const LIVE_ARTIFACT_PLUGIN = {
},
};
const LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN = {
...LIVE_ARTIFACT_PLUGIN,
id: 'image-template-notion-team-dashboard-live-artifact',
title: 'Notion live artifact',
source: '/tmp/notion-live-artifact',
fsPath: '/tmp/notion-live-artifact',
manifest: {
...LIVE_ARTIFACT_PLUGIN.manifest,
name: 'image-template-notion-team-dashboard-live-artifact',
title: 'Notion live artifact',
description: 'Create a live Notion dashboard artifact.',
od: {
...LIVE_ARTIFACT_PLUGIN.manifest.od,
mode: 'image',
surface: 'image',
useCase: {
query: 'Create a refreshable Notion dashboard live artifact.',
},
},
},
};
const AUTHORING_DEFAULT_SCENARIO_INPUTS = {
artifactKind: 'Open Design plugin',
audience: 'Open Design plugin authors',
@@ -630,6 +652,73 @@ describe('HomeView prompt handoff', () => {
expect(screen.queryByTestId('home-hero-prompt-slot-template')).toBeNull();
});
it('submits live-artifact example presets with chip metadata while keeping them plain-text only', async () => {
const fetchMock = vi.fn<typeof fetch>(async (url) => {
if (typeof url === 'string' && url === '/api/plugins') {
return new Response(JSON.stringify({
plugins: [LIVE_ARTIFACT_PLUGIN, LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
if (typeof url === 'string' && url.includes('/apply')) {
return new Response(JSON.stringify(LIVE_ARTIFACT_APPLY_RESULT), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`unexpected fetch ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
stubAnimationFrame();
const onSubmit = vi.fn();
render(
<HomeView
projects={[]}
onSubmit={onSubmit}
onOpenProject={() => undefined}
onViewAllProjects={() => undefined}
/>,
);
await clearActiveTypeChip();
fireEvent.click(await screen.findByTestId('home-hero-rail-live-artifact'));
await waitFor(() => {
expect(screen.getAllByTestId('home-hero-plugin-preset').length).toBeGreaterThan(0);
});
const liveArtifactTemplatePreset = screen.getAllByTestId('home-hero-plugin-preset')
.find((item) => item.getAttribute('data-plugin-id') === LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN.id);
if (!liveArtifactTemplatePreset) {
throw new Error('expected live artifact image template preset to render');
}
fireEvent.click(liveArtifactTemplatePreset);
const input = screen.getByTestId('home-hero-input') as HTMLTextAreaElement;
await waitFor(() => {
expect(input.value).toBe('Create a refreshable Notion dashboard live artifact.');
});
expect(fetchMock.mock.calls.some(([url]) => (
typeof url === 'string' && url.includes('/apply')
))).toBe(false);
expect(screen.queryByTestId('home-hero-active-type-chip')).toBeNull();
expect(screen.queryByTestId('plugin-inputs-form')).toBeNull();
fireEvent.click(screen.getByTestId('home-hero-submit'));
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
projectKind: 'prototype',
projectMetadata: expect.objectContaining({
kind: 'prototype',
intent: 'live-artifact',
fidelity: 'high-fidelity',
}),
prompt: 'Create a refreshable Notion dashboard live artifact.',
})));
});
it('binds the Home rail Live artifact chip with live-artifact metadata and applies it on submit', async () => {
const fetchMock = vi.fn<typeof fetch>(async (url) => {
if (typeof url === 'string' && url === '/api/plugins') {