mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(web): preserve live artifact preset metadata
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user