mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
feat: skip discovery questions for unmodified example prompts (#3257)
* feat: skip discovery questions for unmodified example prompts When a user selects a curated example prompt from the gallery and sends it without modification, the agent now skips all discovery/clarification questions and directly generates the artifact at full quality. Adds an `examplePrompt` metadata flag that injects a dedicated system prompt override instructing the agent to treat the prompt as a complete creative brief requiring showcase-quality generation. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com> * feat: enrich example prompt override with title and pre-filled brief - Pass example prompt title and artifact-type-specific discovery brief through metadata so the agent receives full context without asking - Only skip discovery on the user's first example prompt usage (localStorage gated), subsequent uses follow normal discovery flow - Convert EXAMPLE_PROMPT_OVERRIDE from static string to dynamic builder that interpolates title and brief key-value pairs * fix: prevent handlePromptChange from clearing examplePromptInfoRef The ref is now only cleared by onExamplePromptStatusChange(null) which HomeHero triggers when the user actually edits the text or clicks clear. This prevents the race where usePromptExample sets the ref and then onPromptChange immediately clears it in the same tick. * fix: honor example-prompt discovery skip in API/BYOK mode API/BYOK runs compose their system prompt through @open-design/contracts, whose ProjectMetadata and composeSystemPrompt only knew about skipDiscoveryBrief. Unmodified gallery example prompts therefore skipped discovery in daemon-backed runs but still asked discovery questions in API mode, making the feature mode-dependent. Mirror the daemon's examplePrompt/examplePromptTitle/examplePromptBrief metadata and buildExamplePromptOverride into contracts so both composers behave identically. Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code) --------- Co-authored-by: qiongyu1999 <2694684348@qq.com> Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
co-authored by
qiongyu1999
Claude Opus 4
parent
8f9ec58c26
commit
4aea5fdb01
@@ -129,6 +129,9 @@ type ProjectMetadata = {
|
||||
platformTargets?: string[] | null;
|
||||
inspirationDesignSystemIds?: string[];
|
||||
skipDiscoveryBrief?: boolean | null;
|
||||
examplePrompt?: boolean | null;
|
||||
examplePromptTitle?: string | null;
|
||||
examplePromptBrief?: Record<string, string> | null;
|
||||
imageModel?: string | null;
|
||||
imageAspect?: string | null;
|
||||
imageStyle?: string | null;
|
||||
@@ -278,6 +281,35 @@ printf '%s\\n' "\$last"
|
||||
|
||||
For the best fal image model use \`--model flux-pro-ultra\`. For video use \`--model veo-3-fal\` or \`--model wan-2.1-t2v\`. Always pass \`--surface\` explicitly (\`image\`, \`video\`, or \`audio\`). Any \`fal-ai/*\` path (e.g. \`fal-ai/flux/schnell\`, \`fal-ai/wan-i2v\`) is also a valid \`--model\` value for image/video — pass it through as-is without substitution.`;
|
||||
|
||||
export function buildExamplePromptOverride(
|
||||
title?: string | null,
|
||||
brief?: Record<string, string> | null,
|
||||
): string {
|
||||
let text = `# Example prompt mode — full-quality direct generation
|
||||
|
||||
The user selected a curated example prompt from the gallery and sent it without modification. This prompt is a complete, self-contained creative brief that has been carefully designed to produce a showcase-quality artifact.`;
|
||||
|
||||
if (title) {
|
||||
text += `\n\nSelected example: "${title}"`;
|
||||
}
|
||||
|
||||
if (brief && Object.keys(brief).length > 0) {
|
||||
text += `\n\nPre-filled creative brief (treat as if the user already answered all discovery questions):`;
|
||||
for (const [key, value] of Object.entries(brief)) {
|
||||
text += `\n- ${key.replace(/_/g, ' ')}: ${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
text += `\n\nRules:
|
||||
1. Do NOT emit \`<question-form id="discovery">\`, do NOT show "Quick brief — 30 seconds", and do NOT ask any clarifying questions.
|
||||
2. Treat the user's message as the FULL specification — it contains all visual direction, content themes, and structural intent needed.
|
||||
3. Generate the artifact at your absolute highest quality. This is a showcase piece — match or exceed the standard of a hand-crafted design.
|
||||
4. Infer any unspecified details (copy, layout choices, imagery descriptions) in a way that is maximally coherent with the stated creative direction.
|
||||
5. Proceed directly to planning and building. Output your TodoWrite plan and then the artifact immediately.`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = `
|
||||
|
||||
---
|
||||
@@ -510,7 +542,10 @@ export function composeSystemPrompt({
|
||||
metadata?.kind === 'video' ||
|
||||
metadata?.kind === 'audio';
|
||||
|
||||
if (metadata?.skipDiscoveryBrief === true) {
|
||||
if (metadata?.examplePrompt === true) {
|
||||
parts.push(buildExamplePromptOverride(metadata.examplePromptTitle, metadata.examplePromptBrief));
|
||||
parts.push('\n\n---\n\n');
|
||||
} else if (metadata?.skipDiscoveryBrief === true) {
|
||||
parts.push(SKIP_DISCOVERY_BRIEF_OVERRIDE);
|
||||
parts.push('\n\n---\n\n');
|
||||
}
|
||||
|
||||
@@ -526,6 +526,11 @@ export function EntryShell({
|
||||
? { contextConnectors: payload.contextConnectors }
|
||||
: {}),
|
||||
...(payload.workingDir ? { userWorkingDir: payload.workingDir } : {}),
|
||||
...(payload.examplePromptContext ? {
|
||||
examplePrompt: true,
|
||||
examplePromptTitle: payload.examplePromptContext.title,
|
||||
examplePromptBrief: payload.examplePromptContext.brief,
|
||||
} : {}),
|
||||
};
|
||||
onCreateProject({
|
||||
name,
|
||||
|
||||
@@ -61,6 +61,12 @@ export interface HomeHeroSubmitHandler {
|
||||
(): void;
|
||||
}
|
||||
|
||||
export interface ExamplePromptInfo {
|
||||
title: string;
|
||||
artifactType: string;
|
||||
brief: Record<string, string>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
prompt: string;
|
||||
onPromptChange: (value: string) => void;
|
||||
@@ -107,6 +113,7 @@ interface Props {
|
||||
contextItemCount: number;
|
||||
error: string | null;
|
||||
showActivePluginChip?: boolean;
|
||||
onExamplePromptStatusChange?: (info: ExamplePromptInfo | null) => void;
|
||||
}
|
||||
|
||||
interface HomeHeroDesignSystemOption {
|
||||
@@ -192,6 +199,7 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
|
||||
contextItemCount,
|
||||
error,
|
||||
showActivePluginChip = true,
|
||||
onExamplePromptStatusChange,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -541,6 +549,7 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
|
||||
function clearSelectedPromptExample() {
|
||||
if (selectedPromptExample) {
|
||||
onPromptChange('');
|
||||
onExamplePromptStatusChange?.(null);
|
||||
}
|
||||
setSelectedPromptExample(null);
|
||||
}
|
||||
@@ -550,6 +559,11 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
|
||||
label: promptExampleChipLabel(example),
|
||||
promptText: example,
|
||||
});
|
||||
onExamplePromptStatusChange?.({
|
||||
title: promptExampleChipLabel(example),
|
||||
artifactType: activeChipId ?? 'prototype',
|
||||
brief: briefForChipId(activeChipId ?? 'prototype'),
|
||||
});
|
||||
onPromptChange(example);
|
||||
setSelectedIndex(0);
|
||||
requestAnimationFrame(() => {
|
||||
@@ -567,6 +581,11 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
|
||||
label: record.title,
|
||||
promptText,
|
||||
});
|
||||
onExamplePromptStatusChange?.({
|
||||
title: record.title,
|
||||
artifactType: chipId,
|
||||
brief: briefForPluginPreset(record, chipId),
|
||||
});
|
||||
onPickExamplePlugin(record, chipId, promptText);
|
||||
}
|
||||
|
||||
@@ -826,6 +845,7 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
|
||||
onPromptChange(e.target.value);
|
||||
if (selectedPromptExample && e.target.value !== selectedPromptExample.promptText) {
|
||||
setSelectedPromptExample(null);
|
||||
onExamplePromptStatusChange?.(null);
|
||||
}
|
||||
setSelectedIndex(0);
|
||||
}}
|
||||
@@ -3070,3 +3090,34 @@ function homeHeroChipPromptExamples(chipId: string, locale: Locale): string[] {
|
||||
function isChineseLocale(locale: Locale): boolean {
|
||||
return locale === 'zh-CN' || locale === 'zh-TW';
|
||||
}
|
||||
|
||||
function briefForChipId(chipId: string): Record<string, string> {
|
||||
switch (chipId) {
|
||||
case 'prototype':
|
||||
return { artifact_type: 'web prototype', audience: 'product evaluators', fidelity: 'high-fidelity' };
|
||||
case 'deck':
|
||||
return { artifact_type: 'pitch deck / presentation', audience: 'decision makers', slide_count: '10-15 pages' };
|
||||
case 'image':
|
||||
return { artifact_type: 'image', style: 'cinematic, high-quality, on-brand' };
|
||||
case 'video':
|
||||
return { artifact_type: 'video', style: 'cinematic, high-quality, on-brand' };
|
||||
case 'hyperframes':
|
||||
return { artifact_type: 'motion graphic / animated sequence', style: 'cinematic, polished transitions' };
|
||||
case 'audio':
|
||||
return { artifact_type: 'audio', style: 'professional, polished, brand-appropriate' };
|
||||
default:
|
||||
return { artifact_type: chipId };
|
||||
}
|
||||
}
|
||||
|
||||
function briefForPluginPreset(record: InstalledPluginRecord, chipId: string): Record<string, string> {
|
||||
const brief: Record<string, string> = { ...briefForChipId(chipId) };
|
||||
const fields = record.manifest?.od?.inputs ?? [];
|
||||
for (const field of fields) {
|
||||
const value = field.default ?? field.placeholder;
|
||||
if (value != null && typeof value === 'string' && value.trim()) {
|
||||
brief[field.name] = value;
|
||||
}
|
||||
}
|
||||
return brief;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// surface by lifting its plugin orchestration up here so the prompt
|
||||
// textarea can live centered in the hero.
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type {
|
||||
ApplyResult,
|
||||
ConnectorDetail,
|
||||
@@ -51,7 +51,7 @@ import type {
|
||||
SkillSummary,
|
||||
} from '../types';
|
||||
import { inlineMentionToken } from '../utils/inlineMentions';
|
||||
import { HomeHero } from './HomeHero';
|
||||
import { HomeHero, type ExamplePromptInfo } from './HomeHero';
|
||||
import { findChip, HOME_HERO_CHIPS, type HomeHeroChip } from './home-hero/chips';
|
||||
import {
|
||||
buildHomeMediaComposer,
|
||||
@@ -240,6 +240,10 @@ export function HomeView({
|
||||
const [mcpLoading, setMcpLoading] = useState(true);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [promptEditedByUser, setPromptEditedByUser] = useState(false);
|
||||
const examplePromptInfoRef = useRef<ExamplePromptInfo | null>(null);
|
||||
const handleExamplePromptStatusChange = useCallback((info: ExamplePromptInfo | null) => {
|
||||
examplePromptInfoRef.current = info;
|
||||
}, []);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [designSystemLogoById, setDesignSystemLogoById] = useState<Record<string, string>>({});
|
||||
const [elevenLabsVoices, setElevenLabsVoices] = useState<AudioVoiceOption[]>([]);
|
||||
@@ -1271,6 +1275,13 @@ export function HomeView({
|
||||
contextMcpServers,
|
||||
contextConnectors,
|
||||
attachments: stagedFiles,
|
||||
...(() => {
|
||||
if (!examplePromptInfoRef.current) return {};
|
||||
const key = 'od:example-prompt-used';
|
||||
if (localStorage.getItem(key)) return {};
|
||||
localStorage.setItem(key, '1');
|
||||
return { examplePromptContext: examplePromptInfoRef.current };
|
||||
})(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1330,6 +1341,7 @@ export function HomeView({
|
||||
onPickChip={pickChip}
|
||||
contextItemCount={contextItemCount}
|
||||
error={error}
|
||||
onExamplePromptStatusChange={handleExamplePromptStatusChange}
|
||||
/>
|
||||
|
||||
<RecentProjectsStrip
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface PluginLoopSubmit {
|
||||
// Files staged on Home before the project exists. App uploads them
|
||||
// into the created project's Design Files before the first auto-send.
|
||||
attachments?: File[];
|
||||
examplePromptContext?: { title: string; artifactType: string; brief: Record<string, string> };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -153,6 +153,14 @@ export interface ProjectMetadata {
|
||||
// Batch/API-created projects can opt out of the initial discovery form so
|
||||
// the first agent turn builds immediately from the submitted brief.
|
||||
skipDiscoveryBrief?: boolean;
|
||||
// Set when the user submits an unmodified curated example prompt from the
|
||||
// gallery. Skips discovery AND requests full-quality direct generation,
|
||||
// treating the curated title/brief as the complete creative brief. Honored
|
||||
// by both the daemon and contracts (API/BYOK) system-prompt composers so the
|
||||
// feature is not mode-dependent.
|
||||
examplePrompt?: boolean;
|
||||
examplePromptTitle?: string;
|
||||
examplePromptBrief?: Record<string, string>;
|
||||
// Plugins selected through @ mentions on Home. These are additive
|
||||
// context references; the explicit "Use plugin" snapshot, when present,
|
||||
// remains the primary executable plugin for the run.
|
||||
|
||||
@@ -124,6 +124,35 @@ export const SKIP_DISCOVERY_BRIEF_OVERRIDE = `# Automated project mode — skip
|
||||
|
||||
This project was created through the daemon API with \`skipDiscoveryBrief: true\`. Override the discovery rules below: do NOT emit \`<question-form id="discovery">\`, do NOT show "Quick brief — 30 seconds", and do NOT ask a first-turn clarification form. Do not call AskUserQuestion, do not emit any question form or choice card, and do not wait for user input. Treat the user's first message and project metadata as the brief, choose reasonable defaults for any missing details, then proceed directly to planning/building under the normal artifact workflow.`;
|
||||
|
||||
export function buildExamplePromptOverride(
|
||||
title?: string | null,
|
||||
brief?: Record<string, string> | null,
|
||||
): string {
|
||||
let text = `# Example prompt mode — full-quality direct generation
|
||||
|
||||
The user selected a curated example prompt from the gallery and sent it without modification. This prompt is a complete, self-contained creative brief that has been carefully designed to produce a showcase-quality artifact.`;
|
||||
|
||||
if (title) {
|
||||
text += `\n\nSelected example: "${title}"`;
|
||||
}
|
||||
|
||||
if (brief && Object.keys(brief).length > 0) {
|
||||
text += `\n\nPre-filled creative brief (treat as if the user already answered all discovery questions):`;
|
||||
for (const [key, value] of Object.entries(brief)) {
|
||||
text += `\n- ${key.replace(/_/g, ' ')}: ${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
text += `\n\nRules:
|
||||
1. Do NOT emit \`<question-form id="discovery">\`, do NOT show "Quick brief — 30 seconds", and do NOT ask any clarifying questions.
|
||||
2. Treat the user's message as the FULL specification — it contains all visual direction, content themes, and structural intent needed.
|
||||
3. Generate the artifact at your absolute highest quality. This is a showcase piece — match or exceed the standard of a hand-crafted design.
|
||||
4. Infer any unspecified details (copy, layout choices, imagery descriptions) in a way that is maximally coherent with the stated creative direction.
|
||||
5. Proceed directly to planning and building. Output your TodoWrite plan and then the artifact immediately.`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = `
|
||||
|
||||
---
|
||||
@@ -247,7 +276,10 @@ export function composeSystemPrompt({
|
||||
parts.push('\n\n---\n\n');
|
||||
}
|
||||
|
||||
if (metadata?.skipDiscoveryBrief === true) {
|
||||
if (metadata?.examplePrompt === true) {
|
||||
parts.push(buildExamplePromptOverride(metadata.examplePromptTitle, metadata.examplePromptBrief));
|
||||
parts.push('\n\n---\n\n');
|
||||
} else if (metadata?.skipDiscoveryBrief === true) {
|
||||
parts.push(SKIP_DISCOVERY_BRIEF_OVERRIDE);
|
||||
parts.push('\n\n---\n\n');
|
||||
}
|
||||
|
||||
@@ -134,4 +134,52 @@ describe('composeSystemPrompt — API mode (#313)', () => {
|
||||
expect(prompt).toContain('choose reasonable defaults for any missing details');
|
||||
});
|
||||
});
|
||||
|
||||
// Regression coverage for #3257 — example-prompt discovery skip must be
|
||||
// honored in API/BYOK mode (which composes prompts through this contracts
|
||||
// composer), not only in daemon-backed runs. Without the examplePrompt
|
||||
// handling here, the same unmodified gallery prompt skipped discovery in
|
||||
// daemon mode but still asked discovery questions in API mode.
|
||||
describe('example prompt mode (#3257)', () => {
|
||||
it('injects the example-prompt override and skips discovery when metadata.examplePrompt is true', () => {
|
||||
const prompt = composeSystemPrompt({
|
||||
metadata: { kind: 'prototype', examplePrompt: true },
|
||||
});
|
||||
expect(prompt).toContain('Example prompt mode — full-quality direct generation');
|
||||
expect(prompt).toMatch(/do NOT emit `?<question-form id="discovery">`?/i);
|
||||
});
|
||||
|
||||
it('interpolates the curated title and pre-filled brief', () => {
|
||||
const prompt = composeSystemPrompt({
|
||||
metadata: {
|
||||
kind: 'prototype',
|
||||
examplePrompt: true,
|
||||
examplePromptTitle: 'Neon dashboard',
|
||||
examplePromptBrief: { target_audience: 'developers', fidelity: 'high' },
|
||||
},
|
||||
});
|
||||
expect(prompt).toContain('Selected example: "Neon dashboard"');
|
||||
expect(prompt).toContain('target audience: developers');
|
||||
expect(prompt).toContain('fidelity: high');
|
||||
});
|
||||
|
||||
it('pins the example-prompt override above the discovery layer in API mode', () => {
|
||||
const prompt = composeSystemPrompt({
|
||||
streamFormat: 'plain',
|
||||
metadata: { kind: 'prototype', examplePrompt: true },
|
||||
});
|
||||
const overrideIdx = prompt.indexOf('Example prompt mode — full-quality direct generation');
|
||||
const discoveryIdx = prompt.indexOf('# OD core directives');
|
||||
expect(overrideIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(overrideIdx).toBeLessThan(discoveryIdx);
|
||||
});
|
||||
|
||||
it('prefers the example-prompt override over the plain skip-discovery override', () => {
|
||||
const prompt = composeSystemPrompt({
|
||||
metadata: { kind: 'prototype', examplePrompt: true, skipDiscoveryBrief: true },
|
||||
});
|
||||
expect(prompt).toContain('Example prompt mode — full-quality direct generation');
|
||||
expect(prompt).not.toContain(SKIP_DISCOVERY_BRIEF_OVERRIDE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user