Skip to content

Commit e98e75f

Browse files
elihahah666qiongyu1999
andauthored
fix(web): seed example/replicate composer prompts with the curated description (#4103)
The example-prompt cards and the detail modal's 'Replicate this content' seeded the composer with the en query's leading paragraph. For 19 of the _official/examples plugins that paragraph is a generator-facing build spec (stack/file-layout instructions, raw HTML for aerocore, or a dangling 'as described below' whose referent was truncated away) — noise in the textarea instead of the curated natural-language description. examplePresetSeedPrompt now prefers the curated description for every locale (zh already did), keeping the rendered query head only for input-templated queries (raw {{...}} placeholders in the leading paragraph) so placeholder write-back into plugin inputs survives, and as the fallback when no description exists. Co-authored-by: qiongyu1999 <2694684348@qq.com>
1 parent 88d1eab commit e98e75f

3 files changed

Lines changed: 168 additions & 9 deletions

File tree

apps/web/src/components/plugins-home/presetSeedPrompt.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ export function isMetaInstructionSeed(value: string): boolean {
9999
return /|\s*en\s*|verbatim|example\.html/iu.test(value);
100100
}
101101

102+
// Non-global twin of INPUT_PLACEHOLDER_PATTERN — `.test()` on a /g/ regex is
103+
// stateful (lastIndex), so probing uses this one.
104+
const HAS_INPUT_PLACEHOLDER_PATTERN = /\{\{\s*[a-zA-Z_][\w-]*\s*\}\}/;
105+
106+
function usableQueryHead(record: InstalledPluginRecord, query: string): string | null {
107+
const head = firstPromptParagraph(renderPluginPresetQuery(record, query));
108+
// Skip meta-instructions that reference fields/assets the model can't see
109+
// from the textarea.
110+
if (head && !isMetaInstructionSeed(head)) return head;
111+
return null;
112+
}
113+
102114
export interface PresetSeed {
103115
text: string;
104116
// True when `text` is the rendered plugin query itself (a human-friendly,
@@ -126,14 +138,24 @@ export function examplePresetSeedPrompt(
126138
return { text: description, fromRenderedQuery: false };
127139
}
128140
const query = pluginPresetQuery(record, locale);
129-
if (query) {
130-
const head = firstPromptParagraph(renderPluginPresetQuery(record, query));
131-
// Skip meta-instructions that reference fields/assets the model can't see
132-
// from the textarea; fall back to the description.
133-
if (head && !isMetaInstructionSeed(head)) {
134-
return { text: head, fromRenderedQuery: true };
135-
}
141+
// Input-templated queries (raw `{{...}}` placeholders in the leading
142+
// paragraph) are authored as editable human seeds; keep the rendered head so
143+
// editing a hydrated value in the composer still writes back into the
144+
// plugin inputs.
145+
if (query && HAS_INPUT_PLACEHOLDER_PATTERN.test(firstPromptParagraph(query))) {
146+
const head = usableQueryHead(record, query);
147+
if (head) return { text: head, fromRenderedQuery: true };
136148
}
149+
// Otherwise prefer the curated natural-language description: for many
150+
// example plugins the en query opens with a generator-facing build spec
151+
// (stack/file-layout instructions, raw HTML, or a paragraph dangling "as
152+
// described below" whose referent was truncated away) that reads as noise
153+
// in the composer. The full spec still reaches the agent as plugin context
154+
// (SKILL.md + example.html) once the plugin is applied.
137155
if (description) return { text: description, fromRenderedQuery: false };
156+
if (query) {
157+
const head = usableQueryHead(record, query);
158+
if (head) return { text: head, fromRenderedQuery: true };
159+
}
138160
return { text: fallback(), fromRenderedQuery: false };
139161
}

apps/web/tests/components/HomeView.prefill.test.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,8 +1053,11 @@ describe('HomeView prompt handoff', () => {
10531053
fireEvent.click(liveArtifactTemplatePreset);
10541054

10551055
screen.getByTestId('home-hero-input');
1056+
// The composer seed prefers the curated description over the query head
1057+
// (the query is generator-facing; it still reaches the agent as plugin
1058+
// context on apply).
10561059
await waitFor(() => {
1057-
expect(homeHeroPromptText()).toBe('Create a refreshable Notion dashboard live artifact.');
1060+
expect(homeHeroPromptText()).toBe('Create a live Notion dashboard artifact.');
10581061
});
10591062
expect(fetchMock.mock.calls.some(([url]) => (
10601063
typeof url === 'string' && url.includes('/apply')
@@ -1081,7 +1084,7 @@ describe('HomeView prompt handoff', () => {
10811084
intent: 'live-artifact',
10821085
fidelity: 'high-fidelity',
10831086
}),
1084-
prompt: 'Create a refreshable Notion dashboard live artifact.',
1087+
prompt: 'Create a live Notion dashboard artifact.',
10851088
})));
10861089
});
10871090

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Seed contract for the Home example-prompt cards and the plugin detail
2+
// modal's "Replicate this content" action (issue: the composer was seeded with
3+
// the en query's leading paragraph, which for many example plugins is a
4+
// generator-facing build spec — stack/file-layout instructions, raw HTML, or a
5+
// dangling "as described below" — instead of the curated natural-language
6+
// description).
7+
8+
import { describe, expect, it } from 'vitest';
9+
import type { InstalledPluginRecord } from '@open-design/contracts';
10+
import { examplePresetSeedPrompt } from '../../src/components/plugins-home/presetSeedPrompt';
11+
12+
function fixture(overrides: {
13+
id: string;
14+
description?: string;
15+
query?: string | Record<string, string>;
16+
inputs?: Array<Record<string, unknown>>;
17+
}): InstalledPluginRecord {
18+
return {
19+
id: overrides.id,
20+
title: overrides.id,
21+
version: '0.1.0',
22+
sourceKind: 'bundled',
23+
source: '/tmp',
24+
trust: 'bundled',
25+
capabilitiesGranted: ['prompt:inject'],
26+
manifest: {
27+
name: overrides.id,
28+
version: '0.1.0',
29+
...(overrides.description ? { description: overrides.description } : {}),
30+
od: {
31+
...(overrides.query ? { useCase: { query: overrides.query } } : {}),
32+
...(overrides.inputs ? { inputs: overrides.inputs } : {}),
33+
},
34+
},
35+
fsPath: '/tmp',
36+
installedAt: 0,
37+
updatedAt: 0,
38+
} as InstalledPluginRecord;
39+
}
40+
41+
const fallback = () => 'fallback seed';
42+
43+
describe('examplePresetSeedPrompt', () => {
44+
it('prefers the curated description over a generator-facing build-spec query head (en)', () => {
45+
const record = fixture({
46+
id: 'dreamcore-landing',
47+
description:
48+
'Immersive single-page parallax landing: a sticky viewport zooms a portal image toward you on scroll.',
49+
query: {
50+
en: 'Build a single-page immersive parallax landing page in React + TypeScript + Tailwind CSS using Vite. Everything lives in a single `src/App.tsx` file.\n\nScene one: ...',
51+
},
52+
});
53+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
54+
expect(seed.text).toBe(record.manifest.description);
55+
expect(seed.fromRenderedQuery).toBe(false);
56+
});
57+
58+
it('prefers the curated description over a raw-HTML query head (en)', () => {
59+
const record = fixture({
60+
id: 'aerocore',
61+
description: 'Premium scroll-cinematic aerospace propulsion marketing site.',
62+
query: {
63+
en: '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="UTF-8" />\n</head>\n\n<body>...</body>',
64+
},
65+
});
66+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
67+
expect(seed.text).toBe(record.manifest.description);
68+
expect(seed.fromRenderedQuery).toBe(false);
69+
});
70+
71+
it('prefers the curated description over a query head that dangles "as described below" (en)', () => {
72+
const record = fixture({
73+
id: 'orbis-nft',
74+
description: 'Dark space-themed NFT collection landing page with a liquid-glass UI.',
75+
query: {
76+
en: 'Create an NFT landing page called "Orbis.Nft" with 4 sections. Recreate it exactly as described below.\n\n## Sections\n...',
77+
},
78+
});
79+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
80+
expect(seed.text).toBe(record.manifest.description);
81+
expect(seed.fromRenderedQuery).toBe(false);
82+
});
83+
84+
it('keeps the rendered query head for input-templated queries so placeholder write-back survives', () => {
85+
const record = fixture({
86+
id: 'web-prototype',
87+
description: 'General-purpose desktop web prototype.',
88+
query: {
89+
en: 'Create a premium product-studio {{fidelity}} {{artifactKind}} for {{audience}}: sharp information architecture.\n\nDetails below.',
90+
},
91+
inputs: [
92+
{ name: 'fidelity', default: 'high-fidelity' },
93+
{ name: 'artifactKind', default: 'prototype' },
94+
{ name: 'audience', default: 'designers' },
95+
],
96+
});
97+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
98+
expect(seed.text).toBe(
99+
'Create a premium product-studio high-fidelity prototype for designers: sharp information architecture.',
100+
);
101+
expect(seed.fromRenderedQuery).toBe(true);
102+
});
103+
104+
it('keeps the zh description-first behavior', () => {
105+
const record = fixture({
106+
id: 'dreamcore-landing',
107+
description: '沉浸式视差落地页。',
108+
query: { 'zh-CN': '构建梦核风格的沉浸式视差落地页(详见 en 字段的完整规格说明,以 en 为准)。' },
109+
});
110+
const seed = examplePresetSeedPrompt(record, 'zh-CN', fallback);
111+
expect(seed.text).toBe('沉浸式视差落地页。');
112+
expect(seed.fromRenderedQuery).toBe(false);
113+
});
114+
115+
it('falls back to the query head when there is no description', () => {
116+
const record = fixture({
117+
id: 'no-description',
118+
query: { en: 'Create a moody portfolio landing page.\n\nDetails.' },
119+
});
120+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
121+
expect(seed.text).toBe('Create a moody portfolio landing page.');
122+
expect(seed.fromRenderedQuery).toBe(true);
123+
});
124+
125+
it('falls back to the caller fallback when the query is a meta-instruction and there is no description', () => {
126+
const record = fixture({
127+
id: 'meta-only',
128+
query: { en: 'Follow the en field verbatim; start from example.html.' },
129+
});
130+
const seed = examplePresetSeedPrompt(record, 'en', fallback);
131+
expect(seed.text).toBe('fallback seed');
132+
expect(seed.fromRenderedQuery).toBe(false);
133+
});
134+
});

0 commit comments

Comments
 (0)