mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
1191 lines
73 KiB
TypeScript
1191 lines
73 KiB
TypeScript
import { MOTION_CREATION_POLICY_PROMPT } from './motion-creation-policy.js';
|
||
import { COMMENT_IDENTITY_EDIT_DIRECTIVE } from './comment-identity.js';
|
||
/**
|
||
* Prompt composer. The base is the OD-adapted "expert designer" system
|
||
* prompt (see ./official-system.ts) — a full identity, workflow, and
|
||
* content-philosophy charter. Stacked on top:
|
||
*
|
||
* 1. The discovery + planning + huashu-philosophy layer (./discovery.ts)
|
||
* — interactive question-form syntax, direction-picker fork,
|
||
* brand-spec extraction, TodoWrite reinforcement, 5-dim critique,
|
||
* and the embedded `directions.ts` library.
|
||
* 2. The active design system's DESIGN.md (if any) — palette, typography,
|
||
* spacing rules treated as authoritative tokens.
|
||
* 3. The active skill's SKILL.md (if any) — workflow specific to the
|
||
* kind of artifact being built. When the skill ships a seed
|
||
* (`assets/template.html`) and references (`references/layouts.md`,
|
||
* `references/checklist.md`), we inject a hard pre-flight rule above
|
||
* the skill body so the agent reads them BEFORE writing any code.
|
||
* 4. For decks (skillMode === 'deck' OR metadata.kind === 'deck'), the
|
||
* deck framework directive (./deck-framework.ts) is pinned LAST so it
|
||
* overrides any softer slide-handling wording earlier in the stack —
|
||
* this is the load-bearing nav / counter / scroll JS / print
|
||
* stylesheet contract that PDF stitching depends on. We also fire on
|
||
* the metadata path so deck-kind projects without a bound skill
|
||
* (skill_id null) still get a framework, instead of having the agent
|
||
* re-author scaling / nav / print logic from scratch each turn. When
|
||
* the active skill ships its own seed (skill body references
|
||
* `assets/template.html`), we defer to that seed and skip the generic
|
||
* skeleton — the skill's framework wins to avoid double-injection.
|
||
*
|
||
* The composed string is what the daemon sees as `systemPrompt` and what
|
||
* the Anthropic path sends as `system`.
|
||
*/
|
||
import type { ChatSessionMode } from '../api/chat.js';
|
||
import type { ProjectMetadata, ProjectTemplate } from '../api/projects.js';
|
||
import { OFFICIAL_DESIGNER_PROMPT, renderOfficialDesignerPrompt } from './official-system.js';
|
||
import { DISCOVERY_AND_PHILOSOPHY } from './discovery.js';
|
||
import {
|
||
renderDeckFrameworkDirective,
|
||
type DeckFrameworkMode,
|
||
} from './deck-framework.js';
|
||
import { MEDIA_GENERATION_CONTRACT } from './media-contract.js';
|
||
import {
|
||
composeOdNextStrategyRequestPromptV2,
|
||
type OdNextStrategyRequestRecipeV2,
|
||
} from './od-next-strategy.js';
|
||
import { SETTINGS_MEDIA_PROVIDERS_PATH } from '../settings-nav.js';
|
||
import { normalizePromptLocale, promptLanguageName } from './ui-locale.js';
|
||
|
||
export const BASE_SYSTEM_PROMPT = OFFICIAL_DESIGNER_PROMPT;
|
||
const ELEVENLABS_VOICE_PROMPT_OPTION_LIMIT = 100;
|
||
const SEMANTIC_OUTPUT_FILE_NAMES = `## Semantic output file names
|
||
|
||
For new user-facing deliverables, choose a short semantic project-relative filename derived from the user's brief, product, screen, or artifact type. Do not call every new artifact \`index.html\`.
|
||
|
||
Good examples: \`investor-pitch-deck.html\`, \`ai-community-pr-deck.html\`, \`refund-ops-dashboard.html\`, \`pricing-page.html\`, \`screens/ios-checkout.html\`, \`daily-digest.md\`, \`image-manifest.json\`.
|
||
|
||
When editing an existing artifact, preserve its existing filename unless the user asks for a copy or version. Use \`index.html\` only for fixed runtime conventions or a lightweight launcher/overview: live-artifact generated previews, HyperFrames compositions, static SPA/deploy entry mapping, plugin previews/examples, \`ui_kits/app/index.html\`, or a multi-screen overview that links to semantic screen files. If an active skill or template says to copy a seed to \`index.html\`, adapt the destination to a semantic filename unless the task is one of those fixed-path exceptions.`;
|
||
|
||
export interface AudioVoiceOption {
|
||
name: string;
|
||
voiceId: string;
|
||
category?: string | null;
|
||
labels?: Record<string, string> | null;
|
||
}
|
||
|
||
const ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX = 'ElevenLabs voice list could not be loaded';
|
||
const PROMPT_SAFE_HTTP_STATUS_LABELS: Record<string, string> = {
|
||
'400': 'Bad Request',
|
||
'401': 'Unauthorized',
|
||
'403': 'Forbidden',
|
||
'404': 'Not Found',
|
||
'429': 'Too Many Requests',
|
||
'500': 'Internal Server Error',
|
||
'502': 'Bad Gateway',
|
||
'503': 'Service Unavailable',
|
||
'504': 'Gateway Timeout',
|
||
};
|
||
|
||
function renderUiLocalePrompt(locale: string | undefined): string {
|
||
const normalized = normalizePromptLocale(locale);
|
||
if (!normalized) return '';
|
||
const languageName = promptLanguageName(normalized);
|
||
const lines = [
|
||
'# UI locale override',
|
||
'',
|
||
`The OpenDesign UI locale for this run is \`${normalized}\` (${languageName}). All user-visible chat prose and generated UI controls must follow this locale, especially \`<question-form>\` titles, question labels, placeholders, and option labels. Keep machine-readable ids and object option \`value\` fields exact and unlocalized.`,
|
||
];
|
||
if (normalized === 'zh-CN') {
|
||
lines.push(
|
||
'',
|
||
'For the default quick brief in Simplified Chinese, use copy like:',
|
||
'- title: `快速简报 — 30 秒`',
|
||
'- description: `开始生成前我会先确认这些信息。不适用的可以跳过,我会补上默认值。`',
|
||
'- output label/options: `我们要做什么?` / `幻灯片 / 路演稿`, `单页网页原型 / 落地页`, `多屏应用原型`, `数据看板 / 工具界面`, `编辑式 / 营销页面`, `其他 — 我来描述`',
|
||
'- platform label/options: `目标平台` / `响应式网页`, `桌面网页`, `iOS 应用`, `Android 应用`, `平板应用`, `桌面应用`, `固定画布 (1920×1080)`',
|
||
'- audience label/placeholder: `目标用户` / `例如:早期投资人、开发者工具采购者、内部高管评审`',
|
||
'- brand label/options: `品牌背景` / `帮我选一个方向`, `我有品牌规范 — 稍后分享`, `参考网站 / 截图 — 稍后附上`',
|
||
'- scale label/placeholder: `大概需要多少内容?` / `例如:8 页幻灯片、1 个落地页 + 3 个子页面、4 个移动端界面`',
|
||
'- constraints label/placeholder: `还有什么需要知道的吗?` / `真实文案、必须使用的字体、需要避免的内容、截止时间…`',
|
||
);
|
||
}
|
||
return lines.join('\n');
|
||
}
|
||
|
||
function normalizePromptText(value: string): string {
|
||
return value
|
||
.replace(/[\r\n]+/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
export function formatElevenLabsVoiceOptionsErrorForPrompt(
|
||
error: string | undefined,
|
||
): string | undefined {
|
||
const trimmed = normalizePromptText(error ?? '');
|
||
if (!trimmed) return undefined;
|
||
|
||
if (/no ElevenLabs API key/i.test(trimmed)) {
|
||
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX} because the ElevenLabs API key is missing. Tell the user to configure it in ${SETTINGS_MEDIA_PROVIDERS_PATH} or paste a voice id manually.`;
|
||
}
|
||
|
||
const statusMatch = trimmed.match(
|
||
/(?:\((\d{3})(?:\s+([^)]+))?\)|\b(\d{3})(?:\s+([A-Za-z][A-Za-z -]{0,40}))?\b)/,
|
||
);
|
||
if (statusMatch) {
|
||
const statusCode = statusMatch[1] ?? statusMatch[3];
|
||
const statusText = statusCode ? PROMPT_SAFE_HTTP_STATUS_LABELS[statusCode] ?? '' : '';
|
||
const suffix = statusText ? ` ${statusText}` : '';
|
||
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX} (${statusCode}${suffix}). Tell the user to retry the lookup or paste a voice id manually.`;
|
||
}
|
||
|
||
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX}. Tell the user to retry the lookup or paste a voice id manually.`;
|
||
}
|
||
|
||
export const SKIP_DISCOVERY_BRIEF_OVERRIDE = `# Automated project mode — skip discovery form
|
||
|
||
This project was created through the daemon API with \`skipDiscoveryBrief: true\`. Override the discovery rules below: do NOT emit a project-opening \`<question-form id="discovery">\`, show "Quick brief — 30 seconds", or 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 = `
|
||
|
||
---
|
||
|
||
## Active design system visual direction
|
||
|
||
Active design system exception: the active design system is the visual direction for this project. Use its DESIGN.md palette, typography, spacing, component rules, and theme tokens as the source of truth for color and mood.
|
||
|
||
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
|
||
- Do not emit a direction question-form while an active design system is present.
|
||
- If an earlier discovery answer asks to "Pick a direction for me", treat that as already satisfied by the active design system and continue with the plan.
|
||
- When a downstream framework mentions "active direction" or "theme tokens", bind those fields from the active design system instead of the built-in direction library.
|
||
`;
|
||
|
||
export interface ComposeInput {
|
||
// Mirrored internal request-recipe slot. Generic/BYOK callers do not infer
|
||
// this from plugin ids; the daemon must supply a verified recipe payload.
|
||
odNextStrategyRecipe?: OdNextStrategyRequestRecipeV2 | undefined;
|
||
agentId?: string | null | undefined;
|
||
/**
|
||
* One sentence naming the plan tool this runtime actually has, already
|
||
* resolved by the host (`planToolNoteForRuntime`, daemon-side). Carried, not
|
||
* derived: keeping the runtime→tool-name table in one place is what stops a
|
||
* second copy from drifting. Only the OD Next fork reads it — the legacy
|
||
* stack composes its own note from the same table.
|
||
*/
|
||
planToolNote?: string | null | undefined;
|
||
skillBody?: string | undefined;
|
||
skillName?: string | undefined;
|
||
skillMode?:
|
||
| 'prototype'
|
||
| 'deck'
|
||
| 'template'
|
||
| 'design-system'
|
||
| 'image'
|
||
| 'video'
|
||
| 'audio'
|
||
| undefined;
|
||
designSystemBody?: string | undefined;
|
||
designSystemTitle?: string | undefined;
|
||
designSystemUsageMd?: string | undefined;
|
||
designSystemTokensCss?: string | undefined;
|
||
designSystemComponentsManifest?: string | undefined;
|
||
designSystemFixtureHtml?: string | undefined;
|
||
designSystemPullIndex?: string | undefined;
|
||
designSystemImportMode?: 'normalized' | 'hybrid' | 'verbatim' | undefined;
|
||
craftBody?: string | undefined;
|
||
craftSections?: string[] | undefined;
|
||
// Personal-memory block (auto-extracted facts + the hand-edited
|
||
// MEMORY.md index). The daemon side composes this on disk and the
|
||
// BYOK side fetches it from `GET /api/memory/system-prompt`; either
|
||
// way the string is folded in right after the base charter so the
|
||
// model treats it as preferences/context rather than hard rules.
|
||
memoryBody?: string | undefined;
|
||
// Per-hook switches for the two-loop memory feature, mirrored from the
|
||
// memory config (`profileEnabled` / `rewriteEnabled` / `verifyEnabled`).
|
||
// An absent object — or an absent field — is treated as TRUE so callers
|
||
// with no memory config wired (and the contracts/BYOK fallback) keep the
|
||
// loops on by default. `rewrite` gates the applied-memory chip;
|
||
// `verify` drives the POST self-verify scorecard. `profile` is
|
||
// consumed by the memory-body composer; it is accepted here only so the
|
||
// same object threads through unchanged.
|
||
memoryHooks?: { profile?: boolean; rewrite?: boolean; verify?: boolean } | undefined;
|
||
// Project-level metadata captured by the new-project panel. Drives the
|
||
// agent's understanding of artifact kind, fidelity, speaker-notes intent
|
||
// and animation intent. Missing fields are unresolved facts, not automatic
|
||
// clarification triggers.
|
||
metadata?: ProjectMetadata | undefined;
|
||
// The template the user picked in the From-template tab, when present.
|
||
// Snapshot of HTML files that the agent should treat as a starting
|
||
// reference rather than a fixed deliverable.
|
||
template?: ProjectTemplate | undefined;
|
||
// Optional `## Active plugin` / `## Plugin inputs` / `## Plugin atoms`
|
||
// block (PB1). Daemon callers feed in `renderPluginBlock(snapshot)`;
|
||
// contracts-side callers running the API fallback may still pass the
|
||
// block through. v1 spec §11.8 routes plugin runs through the daemon
|
||
// (web returns 409 when a plugin is bound), so contracts callers only
|
||
// see this on a daemon-bound run that uses the contracts composer.
|
||
pluginBlock?: string | undefined;
|
||
// Plan §3.L2 / spec §23.4 — pre-rendered `## Active stage` blocks
|
||
// produced by `renderActiveStageBlock(stageId, atomBodies)`. The
|
||
// contracts composer simply splices them in after the plugin block;
|
||
// every block is already self-contained markdown.
|
||
activeStageBlocks?: ReadonlyArray<string> | undefined;
|
||
// Provider voice choices fetched by the app before composing the
|
||
// prompt. Used for ElevenLabs speech discovery so the agent can
|
||
// render a select question-form instead of asking the user to paste
|
||
// raw ids.
|
||
audioVoiceOptions?: AudioVoiceOption[] | undefined;
|
||
// When voice discovery fails, surface the error reason so the agent
|
||
// can tell the user why the dropdown is unavailable instead of
|
||
// pretending there were simply no voices.
|
||
audioVoiceOptionsError?: string | undefined;
|
||
// When set to 'plain', suppresses tool_calls so API/BYOK-mode models
|
||
// only emit <artifact> blocks (they cannot execute tools).
|
||
streamFormat?: string | undefined;
|
||
// Per-conversation mode. Design mode keeps the artifact-first agent
|
||
// workflow; Plan mode creates an editable source-of-truth document first;
|
||
// chat mode keeps the same context/tools but answers like a standard
|
||
// multi-turn assistant unless the user explicitly asks to build.
|
||
sessionMode?: ChatSessionMode | undefined;
|
||
// UI locale selected by the client. User-visible generated form copy
|
||
// must follow this locale even when the user's initial prompt is brief.
|
||
locale?: string | undefined;
|
||
// Host-detected, user-authored deck intent. OD Next uses this only when the
|
||
// bound task type is not already PPT, so cross-surface deck requests receive
|
||
// the canonical runtime contract without changing their Task Profile.
|
||
freeformDeckSignal?: boolean | undefined;
|
||
/** Host-resolved OD Next deck scaffold policy for blank vs legacy/existing decks. */
|
||
deckFrameworkMode?: DeckFrameworkMode | undefined;
|
||
// Free-form instructions the user set at the global (user-level)
|
||
// settings panel. Injected after personal memory.
|
||
userInstructions?: string | undefined;
|
||
// Free-form instructions the user set on this specific project.
|
||
// Injected after user-level instructions and before the design system.
|
||
projectInstructions?: string | undefined;
|
||
}
|
||
|
||
export function composeSystemPrompt({
|
||
odNextStrategyRecipe,
|
||
agentId,
|
||
planToolNote,
|
||
skillBody,
|
||
skillName,
|
||
skillMode,
|
||
designSystemBody,
|
||
designSystemTitle,
|
||
designSystemUsageMd,
|
||
designSystemTokensCss,
|
||
designSystemComponentsManifest,
|
||
designSystemFixtureHtml,
|
||
designSystemPullIndex,
|
||
designSystemImportMode,
|
||
craftBody,
|
||
craftSections,
|
||
memoryBody,
|
||
memoryHooks,
|
||
metadata,
|
||
template,
|
||
pluginBlock,
|
||
activeStageBlocks,
|
||
audioVoiceOptions,
|
||
audioVoiceOptionsError,
|
||
streamFormat,
|
||
sessionMode,
|
||
locale,
|
||
freeformDeckSignal,
|
||
deckFrameworkMode,
|
||
userInstructions,
|
||
projectInstructions,
|
||
}: ComposeInput): string {
|
||
// ── FORK POINT (API/BYOK side) ──────────────────────────────────────────
|
||
// Mirrors the daemon fork in `apps/daemon/src/prompts/system.ts`. Everything
|
||
// below is the legacy stack; OD Next runs return here and never reach it.
|
||
// Read `docs/prompt-composition.md` before changing prompt text on either
|
||
// side of this fork.
|
||
if (odNextStrategyRecipe) {
|
||
const prompt = composeOdNextStrategyRequestPromptV2(odNextStrategyRecipe, {
|
||
agentId,
|
||
// The plan-tool fact has to cross the fork with everything else. Without
|
||
// it an OD Next run keeps the pre-fix behaviour the legacy stack no
|
||
// longer has: a plan step whose tool is never named, and a sanctioned
|
||
// prose branch to fall into instead.
|
||
planToolNote,
|
||
sessionMode,
|
||
locale,
|
||
deckIntent: odNextStrategyRecipe.taskType !== 'ppt' && freeformDeckSignal === true,
|
||
deckFrameworkMode,
|
||
metadata,
|
||
template,
|
||
designSystemBody,
|
||
designSystemTitle,
|
||
designSystemUsageMd,
|
||
designSystemTokensCss,
|
||
designSystemComponentsManifest,
|
||
designSystemFixtureHtml,
|
||
designSystemPullIndex,
|
||
designSystemImportMode,
|
||
craftBody,
|
||
craftSections,
|
||
memoryBody,
|
||
userInstructions,
|
||
projectInstructions,
|
||
});
|
||
return sessionMode === 'chat'
|
||
? prompt
|
||
: `${prompt}\n\n${MOTION_CREATION_POLICY_PROMPT}`;
|
||
}
|
||
// Discovery + philosophy goes FIRST so its hard rules ("emit a form on
|
||
// requirements decision, brand resolution, and TodoWrite workflow, run
|
||
// checklist + critique before <artifact>) win precedence over softer
|
||
// wording later in the official base prompt.
|
||
const parts: string[] = [];
|
||
const activeDesignSystemBody = designSystemBody?.trim();
|
||
// Website Clone runs reproduce an existing site, so its palette/typography must
|
||
// win — an active design system being declared "authoritative" would pull the
|
||
// model away from faithful reproduction. Mirror the daemon (apps/daemon/src/
|
||
// server.ts suppresses the design-system sections for intent==='web-clone') so
|
||
// API/BYOK web-clone prompts drop the same guidance.
|
||
const isWebCloneRun = metadata?.intent === 'web-clone';
|
||
const isMediaSurfaceEarly =
|
||
skillMode === 'image' ||
|
||
skillMode === 'video' ||
|
||
skillMode === 'audio' ||
|
||
metadata?.kind === 'image' ||
|
||
metadata?.kind === 'video' ||
|
||
metadata?.kind === 'audio';
|
||
|
||
// API/BYOK mode (streamFormat === 'plain'): no tools are wired through
|
||
// to the model, but the discovery layer + base prompt below still tell
|
||
// it to call TodoWrite/Read/Write/Edit/Bash/WebFetch. Without an
|
||
// explicit top-anchored override, the model invents pseudo-tool markup
|
||
// (`<todo-list>`, `[读取 X]`) instead of producing real progress
|
||
// events — see #313. Pin this preamble ABOVE DISCOVERY_AND_PHILOSOPHY
|
||
// so it beats the discovery layer's own "these override anything
|
||
// later" header.
|
||
if (streamFormat === 'plain') {
|
||
parts.push(API_MODE_OVERRIDE);
|
||
parts.push('\n\n---\n\n');
|
||
}
|
||
|
||
// Ask mode (`chat`) is the deliberately bare conversation mode: the
|
||
// CHAT_MODE_OVERRIDE below IS the whole charter, and the artifact-oriented
|
||
// blocks (the discovery layer, the full identity/workflow charter, deck
|
||
// framework, media generation contract, DS visual-direction override) are
|
||
// gated off so the turn stays cheap. Memory, custom instructions, the active
|
||
// design system, attached skills, and the clarifying-questions surface are
|
||
// still composed in — Ask mode is light, not amnesiac. Mirror the daemon
|
||
// composer's `isAskMode` gating.
|
||
const isAskMode = sessionMode === 'chat';
|
||
const deckFrameworkDirective = renderDeckFrameworkDirective(
|
||
streamFormat === 'plain' ? 'text_artifact' : 'filesystem',
|
||
);
|
||
|
||
if (sessionMode === 'plan') {
|
||
parts.push(PLAN_MODE_OVERRIDE);
|
||
parts.push('\n\n---\n\n');
|
||
} else if (sessionMode === 'chat') {
|
||
parts.push(CHAT_MODE_OVERRIDE);
|
||
parts.push('\n\n---\n\n');
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
const localePrompt = renderUiLocalePrompt(locale);
|
||
if (localePrompt) {
|
||
parts.push(localePrompt);
|
||
parts.push('\n\n---\n\n');
|
||
}
|
||
|
||
if (!isMediaSurfaceEarly && !isAskMode) {
|
||
parts.push(DISCOVERY_AND_PHILOSOPHY, '\n\n---\n\n');
|
||
}
|
||
|
||
// Ask mode skips the multi-thousand-token designer charter entirely — the
|
||
// CHAT_MODE_OVERRIDE above is its self-contained identity. Plan/Design keep it.
|
||
if (!isAskMode) {
|
||
// Website Clone runs swap the "don't recreate copyrighted designs" guardrail
|
||
// for a faithful-reproduction + pre-deploy-checklist rule, mirroring the
|
||
// daemon prompt so API/BYOK-backed web-clone runs behave identically.
|
||
parts.push(
|
||
'# Identity and workflow charter (background)\n\n',
|
||
renderOfficialDesignerPrompt({ webCloneFidelity: metadata?.intent === 'web-clone' }),
|
||
);
|
||
}
|
||
|
||
// Clarification on any turn reuses the same `<question-form>` flow so the
|
||
// host keeps ONE unified questions surface: the form renders inline in the
|
||
// originating assistant message, and answers return as the next user message.
|
||
// Mirrors the daemon-side composer's structured clarification section
|
||
// in apps/daemon/src/prompts/system.ts — keep both in sync so a daemon chat
|
||
// and a BYOK/API chat route follow-up choices through the same surface
|
||
// instead of drifting back to plain markdown option lists.
|
||
parts.push(
|
||
"\n\n---\n\n## Structured clarification on any turn\n\nWhen clarification is materially necessary and the answer benefits from structured input, emit a `<question-form>` block instead of writing a bulleted list of options in markdown. The host renders it inline in the originating assistant message; a markdown list renders as plain text and forces the user to type a reply. Use the richest appropriate web form controls (`radio`, `checkbox`, `select`, `text`, `textarea`, `number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`, `email`, `tel`, `file`, `switch`). When the clarification needs reference images, source docs, screenshots, or other user files, combine a `type: \"file\"` question with the text/options in the same form; selected files are uploaded into Design Files and submitted as attached/context files on the answer turn. For every finite-choice question, keep user control by leaving `allowCustom` unset or setting it to `true`, and add localized `customLabel` / `customPlaceholder` when useful. Use free-form prose questions only when a form would add no structure. Do NOT also duplicate the form's questions as markdown text alongside it.\n\n`<question-form>` is assistant text for the OpenDesign UI, not a native tool call. If you need to clarify direction, emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call. Do not stop after an introductory sentence such as \"先确认一下方向:\"; the same message must include the full form.\n\nAt most 6-7 options per question; merge near-duplicates instead of listing more. Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices); `checkbox` is always a plain list. `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`); both optional. Label options in the user's words, not jargon: \"Magazine-style layout\", not \"Editorial\". Reword only `label`; never change a stable `value`. Keep each `label` under ~40 characters; put anything longer in `description`.",
|
||
/* 与 daemon 那份 `prompts/system.ts` 的「How your turn is rendered」逐字对应 ——
|
||
两边措辞必须一致,否则 API/BYOK 模式和 daemon 模式对同一件事给模型两种说法。 */
|
||
"\n\n---\n\n## How your turn is rendered\n\n" +
|
||
"This panel does not print your output as one flat transcript. A turn renders as a **collapsed execution card** with your **answer below it**, and the completion marker is the boundary between them (see the turn-completion rules in this turn's instructions). Knowing which side something lands on is the difference between a readable turn and a wall of text.\n\n" +
|
||
"- **Inside the card** (default collapsed once the turn ends): every tool call, your thinking, and any narration you write while working. Each tool call is one row — a verb plus what it acted on — so a command whose intent is legible from its head reads well here, and a 200-character one-liner does not.\n" +
|
||
"- **Below the card**: only what comes after the completion marker. This is the part a person reads without clicking anything. Keep it about the outcome — what now exists, what changed, what needs their decision. Do not replay the steps; they are one click away and already legible.\n" +
|
||
"- **TodoWrite is the progress the user watches.** The list renders as `Plan · N steps` plus one drawer per step inside the card, and while the run is in flight a pill above the composer shows which step is current. So a multi-step task without a plan leaves the user watching a spinner. Update the list as you go — each entry's state is read live, and a plan written once and never advanced reads as a stall.\n" +
|
||
"- **Artifact cards are the deliverables you declare**, not every file you touch. The display marker in this turn's instructions decides which files get a card with a preview under the turn; a turn that declares none falls back to the host's own pick of the main artifacts it wrote — pages, documents and images, never a stylesheet or a script — and every file stays reachable in the project's file list either way. Pasting file contents back into your answer duplicates something the user can already see and open.\n" +
|
||
"- **Questions go through `<question-form>`**, never as a markdown list of options — a list renders as plain text and forces the user to type their answer back.\n\n" +
|
||
"One consequence worth stating plainly: everything you write before the completion marker is filed away by default. If a caveat matters to the decision, it belongs after the marker, not buried in the working narration.",
|
||
);
|
||
|
||
// Mirrors the daemon-side composer in apps/daemon/src/prompts/system.ts —
|
||
// keep both copies of this preamble in sync so a CLI chat and a BYOK
|
||
// chat with the same memory both see the same wording. The "brand
|
||
// wins on conflict / skill workflow wins on conflict / preferences
|
||
// are still authoritative for tone+terminology" framing is what
|
||
// stops the model from treating remembered preferences as harder
|
||
// than the active design system.
|
||
if (memoryBody && memoryBody.trim().length > 0) {
|
||
parts.push(
|
||
`\n\n## Personal memory (auto-extracted from past chats)\n\nThe following facts have been sedimented from this user's previous conversations and edited in the settings panel. Treat them as preferences and context, NOT hard rules: when they collide with the active design system tokens, the brand wins; when they collide with the active skill's workflow, the skill wins. They are still authoritative for tone, voice, terminology, and what the user already told you about themselves and their goals — never re-ask the user about something already captured here.\n\nUse memory as a task-intent gateway. When the user's request is short or underspecified, silently expand it into an internal task brief before acting: infer the task type, user/profile background, project/artifact context, delivery preferences, known feedback meanings, constraints, and validation/finish line. Proceed from that richer brief so the user does not need to repeat setup. Ask a clarifying question only when a critical target, permission, or conflict cannot be resolved from the current request plus memory. Do not dump the full internal brief unless the user asks to inspect it. Expanding intent this way changes only WHAT you know going in; it never shortcuts the standard build flow — you still plan with TodoWrite and still run the anti-slop / brand self-check on every artifact-producing turn.\n\n${memoryBody.trim()}`,
|
||
);
|
||
|
||
// Two-loop memory instruction blocks. These pair with the memory body
|
||
// above (Workstream 1A renders a `### Profile` first and a
|
||
// `### Verified rules` last), so they are only meaningful when memory
|
||
// is present. Each loop is independently gated by its config flag; an
|
||
// absent flag defaults ON. The card JSON examples below intentionally
|
||
// use no backticks so they stay literal inside the template strings.
|
||
// Keep this whole block BYTE-IDENTICAL to the daemon-side composer in
|
||
// apps/daemon/src/prompts/system.ts.
|
||
if ((memoryHooks?.rewrite ?? true)) {
|
||
parts.push(
|
||
`\n\nIf you applied memory, you may emit one compact chip: <od-card type="memory-applied">{ "summary": "Applied your profile and 2 rules", "used": [ {"type": "profile", "name": "Work profile"} ] }</od-card>.`,
|
||
);
|
||
}
|
||
|
||
if ((memoryHooks?.verify ?? true)) {
|
||
parts.push(
|
||
`\n\n## Self-verify against your verified rules\n\nThe **Verified rules** above are enforceable checks, not soft preferences. After you finish producing or editing an artifact, evaluate it against every active rule, FIX any failure in place before ending your turn, then emit one scorecard:\n\n<od-card type="verify-scorecard">\n{ "status": "pass|partial|fail", "summary": "5/6 checks passed · 1 auto-fixed", "rows": [ {"rule": "<the check>", "status": "pass|fail|fixed", "note": "<what was wrong / what you fixed>"} ] }\n</od-card>\n\nPrefer fixing silently over asking. Leave a row as "fail" only when fixing it needs a decision you genuinely cannot make from the request plus memory. The daemon programmatically checks this scorecard after your turn — a missing scorecard or a rule left uncovered on an artifact turn is recorded as an enforcement failure — so always emit it when verified rules apply. Skip the scorecard entirely only when there are no verified rules or the turn produced no artifact.\n\nThe scorecard is ADDITIVE to — never a replacement for — the rest of the end-of-run flow. On an artifact turn you still run the existing anti-slop / brand self-check (the "N/N brand checks passed" gate) and still close with the normal handoff. Order the end of your turn as: (1) finish the anti-slop / brand self-check and fix any failure in place, (2) emit the verify-scorecard card, (3) close with the normal handoff — a single <artifact> block when this turn wrote a new canonical HTML file, otherwise a brief file-operation summary of what changed and what is still open. The scorecard only checks your verified rules; it does not absorb the anti-slop gate or the end-of-run summary.`,
|
||
);
|
||
}
|
||
|
||
parts.push(
|
||
`\n\nNever save new verified rules silently.`,
|
||
);
|
||
}
|
||
|
||
if (userInstructions && userInstructions.trim().length > 0) {
|
||
parts.push(
|
||
`\n\n## Custom instructions (user-level)\n\nThe user has set the following persistent instructions. Apply them as defaults to every project. When a project-level instruction below contradicts a point here, the project-level version wins.\n\n${userInstructions.trim()}`,
|
||
);
|
||
}
|
||
|
||
if (projectInstructions && projectInstructions.trim().length > 0) {
|
||
parts.push(
|
||
`\n\n## Custom instructions (project-level)\n\nThe user has set the following instructions for this specific project. They take precedence over user-level custom instructions whenever both address the same topic (e.g. if user-level says "use spaces" but project-level says "use tabs", use tabs).\n\n${projectInstructions.trim()}`,
|
||
);
|
||
}
|
||
|
||
if (!isWebCloneRun && activeDesignSystemBody && activeDesignSystemBody.length > 0) {
|
||
parts.push(
|
||
`\n\n## Active design system${designSystemTitle ? ` — ${designSystemTitle}` : ''}\n\nTreat the following DESIGN.md as authoritative for color, typography, spacing, and component rules. Do not invent tokens outside this palette. When you copy the active skill's seed template, bind these tokens into its \`:root\` block before generating any layout.\n\n${activeDesignSystemBody}`,
|
||
);
|
||
}
|
||
|
||
if (skillBody && skillBody.trim().length > 0) {
|
||
const preflight = derivePreflight(skillBody);
|
||
parts.push(
|
||
`\n\n## Active skill${skillName ? ` — ${skillName}` : ''}\n\nFollow this skill's workflow exactly.${preflight}\n\n${skillBody.trim()}`,
|
||
);
|
||
}
|
||
|
||
if (!isAskMode) {
|
||
parts.push(`\n\n${SEMANTIC_OUTPUT_FILE_NAMES}`);
|
||
}
|
||
|
||
if (pluginBlock && pluginBlock.trim().length > 0) {
|
||
parts.push(pluginBlock);
|
||
}
|
||
|
||
if (Array.isArray(activeStageBlocks) && activeStageBlocks.length > 0) {
|
||
for (const block of activeStageBlocks) {
|
||
if (typeof block === 'string' && block.trim().length > 0) {
|
||
parts.push(block);
|
||
}
|
||
}
|
||
}
|
||
|
||
const metaBlock = renderMetadataBlock(metadata, template, audioVoiceOptions, audioVoiceOptionsError);
|
||
if (metaBlock) parts.push(metaBlock);
|
||
|
||
// Decks have a load-bearing framework (nav, counter, scroll JS, print
|
||
// stylesheet for PDF stitching). Pin it last so it overrides any softer
|
||
// wording earlier in the stack ("write a script that handles arrows…").
|
||
//
|
||
// We fire on either (a) the active skill is a deck skill OR (b) the
|
||
// project metadata declares kind=deck. Case (b) catches projects created
|
||
// without a skill (skill_id null) — without this, a deck-kind project
|
||
// with no bound skill gets neither a skill seed nor the framework
|
||
// skeleton, and the agent writes scaling / nav / print logic from scratch
|
||
// with the same buggy `place-items: center` + transform pattern we keep
|
||
// having to fix at runtime. Skill seeds (when present) win — they
|
||
// already define their own opinionated framework (simple-deck's
|
||
// scroll-snap, guizang-ppt's magazine layout) and re-pinning the generic
|
||
// skeleton would conflict. The skill-seed path takes over via
|
||
// `derivePreflight` above, so we only fire the generic skeleton when no
|
||
// skill seed is on offer.
|
||
const isDeckProject = skillMode === 'deck' || metadata?.kind === 'deck';
|
||
const isFreeformProject = !skillMode && (!metadata || metadata.kind === 'other');
|
||
const hasDeckSkillSeed =
|
||
skillMode === 'deck' && !!skillBody && /assets\/template\.html/.test(skillBody);
|
||
if (!isAskMode && isDeckProject && !hasDeckSkillSeed) {
|
||
// ⚠️ This decides WHEN the legacy path gets the deck scaffold. OD Next has
|
||
// its own gate — `resolveOdNextDeckFrameworkMode` in `od-next-strategy.ts`.
|
||
// The scaffold is shared; the injection conditions are not. Change both.
|
||
parts.push(`\n\n---\n\n${deckFrameworkDirective}`);
|
||
} else if (
|
||
!isAskMode &&
|
||
!isDeckProject &&
|
||
!isMediaSurfaceEarly &&
|
||
!hasDeckSkillSeed &&
|
||
(freeformDeckSignal === true || (isFreeformProject && freeformDeckSignal === undefined))
|
||
) {
|
||
// A deck request may arrive after a project was created under another
|
||
// surface (most commonly Home's default prototype). The turn-latched
|
||
// signal is stronger than that creation-time kind, so give the agent the
|
||
// same framework instead of leaving classic/off-rollout runs to invent a
|
||
// third navigation runtime. Preserve the legacy absent-signal default for
|
||
// kind=other projects only.
|
||
parts.push(
|
||
`\n\n---\n\n## If this brief is a slide deck / keynote / presentation\n\nThe user did not pre-select a "Slide deck" surface, but their request may still call for one. **If — and only if — the brief reads as slides, keynote, presentation, deck, PPT, or 讲解, follow the framework below.** Otherwise ignore everything in this section and continue with the freeform output you would have written anyway.\n\n${deckFrameworkDirective}`,
|
||
);
|
||
}
|
||
|
||
if (!isAskMode && isMediaSurfaceEarly) {
|
||
parts.push(MEDIA_GENERATION_CONTRACT);
|
||
}
|
||
|
||
if (!isAskMode && !isWebCloneRun && activeDesignSystemBody && activeDesignSystemBody.length > 0) {
|
||
parts.push(ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE);
|
||
}
|
||
|
||
parts.push(`\n\n---\n\n${COMMENT_IDENTITY_EDIT_DIRECTIVE}`);
|
||
if (!isAskMode) parts.push(`\n\n---\n\n${MOTION_CREATION_POLICY_PROMPT}`);
|
||
return parts.join('');
|
||
}
|
||
|
||
/**
|
||
* Top-anchored override for API/BYOK mode (streamFormat === 'plain').
|
||
*
|
||
* Why it sits ABOVE DISCOVERY_AND_PHILOSOPHY: that layer starts with
|
||
* "these override anything later in this prompt" and then mandates
|
||
* TodoWrite / Bash / Read / WebFetch on turns 2–3. In daemon mode those
|
||
* tools exist; in API mode they don't, so the agent narrates pseudo-tool
|
||
* markup (`<todo-list>...`, `[读取 X]`) instead of producing structured
|
||
* `tool_use` events the UI can render — bug #313. Pinning the override
|
||
* at the absolute top is the cleanest way to beat the discovery layer's
|
||
* precedence without restructuring its rules.
|
||
*
|
||
* The override does NOT block `<artifact>` blocks — those are how the
|
||
* web UI receives finished HTML in API mode.
|
||
*/
|
||
const API_MODE_OVERRIDE = `# API mode — no tools available (read first — overrides every rule below)
|
||
|
||
You are running through a plain Messages API. **No tools are wired through to you.** \`TodoWrite\`, \`Read\`, \`Write\`, \`Edit\`, \`Bash\`, and \`WebFetch\` are unavailable — calls to them will not execute and will not render in the UI.
|
||
|
||
Every later instruction in this prompt that tells you to "call TodoWrite", "run Bash", "read via Read", or otherwise invoke a tool is describing the daemon-mode workflow. In this API run those instructions are **overridden** — do not attempt them and do not pretend you did.
|
||
|
||
Do not mention tool unavailability to the user. Avoid phrases such as "TodoWrite is unavailable" or "I cannot call tools in this context"; just continue with the plain prose plan or artifact body the user needs, without mentioning missing tools.
|
||
|
||
**Forbidden output:**
|
||
- Pseudo-tool markup such as \`<todo-list>...</todo-list>\`, \`<tool-call>\`, or invented XML wrappers around a plan.
|
||
- Fake-protocol prose such as \`[读取 template.html ...]\`, \`[读取 layouts.md ...]\`, \`[正在调用 TodoWrite ...]\`, or any \`[doing X]\` placeholder narrating a tool you cannot run.
|
||
- Statements like "I'll call TodoWrite to track this" or "let me read the skill file first" — there is no TodoWrite and no Read in this run.
|
||
|
||
**Allowed output:**
|
||
- Plain chat prose to the user (in their language). State your plan as prose — a short numbered list in markdown is fine; it just must not be wrapped in \`<todo-list>\` or claim to be a tool call.
|
||
- A final \`<artifact type="text/html">...</artifact>\` block containing a complete \`<!doctype html>\` document when the brief is ready to deliver.
|
||
- \`<question-form>\` blocks when material clarification is needed on any turn, exactly as the rules below describe — question-form is markup the UI parses, not a tool call.
|
||
- The self-closing \`<od-next key="..." value="..."/>\` follow-up markers described in this turn's instructions, as the very last thing you write — also markup the host parses, not a tool call.
|
||
|
||
If the rules below tell you to plan with TodoWrite, write the plan as prose instead. If they tell you to read skill side files before writing, describe in one sentence which patterns/conventions you're going to apply and proceed. If they tell you to run brand-spec extraction via Bash + Read + WebFetch, ask the user the missing brand questions in the discovery form instead.`;
|
||
|
||
// Ask mode is the deliberately light conversation mode. Unlike Plan/Design,
|
||
// the composer does NOT append the discovery layer or the full designer charter
|
||
// after this override (see `isAskMode` gating in composeSystemPrompt) — so this
|
||
// block is the whole behavioral charter for the turn and must read as
|
||
// self-contained, not as a preface that overrides "rules below". Keep it
|
||
// BYTE-IDENTICAL to the apps/daemon copy so a daemon chat and a BYOK/API chat
|
||
// behave the same.
|
||
const CHAT_MODE_OVERRIDE = `# Ask mode — bare conversation (this is the whole charter for this turn)
|
||
|
||
This conversation is in OpenDesign Ask mode: a fast, low-overhead chat kept deliberately light to save tokens. OpenDesign is the open-source Claude Design alternative and a native Figma counterpart. Official links: GitHub https://github.com/nexu-io/open-design, website https://open-design.ai/, Discord https://discord.gg/mHAjSMV6gz.
|
||
|
||
Behave like a direct, multi-turn desktop chat assistant. Prefer concise prose: answer the question, explain, compare options, debug prompts, and review existing work. You still have the user's project files, attachments, connectors, MCP servers, project memory, any active design system, and any skills they attached for this turn — use them as context, and follow an attached skill's workflow when one is present.
|
||
|
||
This mode does not load the heavy design-discovery workflow or the full designer charter, on purpose. Do not emit a default discovery \`<question-form>\`, do not open with a TodoWrite plan for a chat answer, and do not create or edit project files, HTML, slide decks, images, video, or audio on your own.
|
||
|
||
If the user explicitly asks you to build, generate, design, or export a concrete artifact (a page, prototype, deck, image, video, audio, or a file change), handle it inline only when it is genuinely trivial; for anything substantial, say so in one line and suggest switching to Design mode (or Plan mode for a document-first brief), where the full design workflow, brand discipline, and artifact tooling are loaded. Keep this turn conversational.
|
||
|
||
For mid-conversation clarification you may still emit a \`<question-form>\` block — it is markup the OpenDesign UI parses, not a native tool call.`;
|
||
|
||
const PLAN_MODE_OVERRIDE = `# Plan mode — editable document first (read first — overrides every rule below)
|
||
|
||
This conversation is in OpenDesign Plan mode. Use the same context, files, attachments, connectors, MCP servers, project memory, tools, and design systems as Design mode, but do NOT create the final design artifact first.
|
||
|
||
In filesystem runs, substantial plan-document work still starts with a real TodoWrite/task-list tool call and keeps it updated as work progresses. Do not narrate TodoWrite availability to the user; show progress through the Todo card when the runtime supports it. In plain API runs, follow the API-mode override above and write the plan directly as prose without mentioning missing tools.
|
||
|
||
Override the artifact discovery layer below: do NOT emit \`<question-form id="discovery">\`, \`<question-form id="task-type">\`, "Quick brief — 30 seconds", or the default artifact-oriented discovery questions about landing pages, prototypes, dashboards, target platform, visual tone, brand context, fidelity, or design direction. A clear planning request should create or update the Markdown plan directly. If a clarification is truly required, ask only plan-document-specific questions, preferably in a \`<question-form id="plan-brief">\`, covering scope, stakeholders, timeline, sections, risks, constraints, and expected handoff deliverable.
|
||
|
||
Your first responsibility is to create or update a Markdown plan document in Design Files, then guide the user to review and edit it before handoff to Design mode. The plan document is the source of truth for the next generation step and must be useful to both a human editor and a later agent run.
|
||
|
||
Choose the document style from the user's intent and project metadata:
|
||
- Deck / pitch / PPT: create a slide outline with page-by-page goals, narrative arc, slide titles, content bullets, visual direction, data/media needs, and speaker-note intent.
|
||
- Prototype / app / dashboard / wireframe: create a PRD-style design brief with users, jobs, screens, key flows, layout structure, component/state requirements, interaction rules, data/content model, and acceptance checks.
|
||
- Landing page / website / long-scroll: create a content and section plan with audience, offer, page hierarchy, section goals, proof/media needs, CTA logic, responsive considerations, and visual system notes.
|
||
- Brand / design system: create a brand/system plan with token roles, typography, component coverage, usage rules, source assets, extraction gaps, and kit acceptance checks.
|
||
- Image / video / audio: create a creative brief or storyboard with concept, shots/scenes, composition, copy, style references, model/runtime constraints, aspect/duration, and generation prompts.
|
||
- Unknown or mixed requests: create a concise design-planning document with the closest matching sections above plus explicit open questions.
|
||
|
||
Document requirements:
|
||
- Write a real \`.md\` file under the active project. Prefer clear names such as \`plan.md\`, \`deck-outline.md\`, \`prototype-plan.md\`, \`prd.md\`, or \`storyboard.md\`; avoid overwriting a useful existing plan unless you are intentionally updating it.
|
||
- Include a top-level title, a short intent summary, concrete sections, editable TODO/open-question markers, and a final "Next step" section that tells the user exactly what to do after reviewing the document.
|
||
- If the user already has an active Markdown plan document, edit that file in place instead of creating a duplicate.
|
||
- Do not output the final HTML/deck/image/video/audio artifact in the same turn unless the user explicitly says to skip planning or confirms that an existing plan is approved.
|
||
- End the response by naming the created/updated Markdown file and inviting the user to edit it, then use the next-step handoff to generate from that document.
|
||
|
||
If this is a plain API run where filesystem tools are unavailable, output the same plan as Markdown prose and clearly tell the user that no project file was written in this run.`;
|
||
|
||
function renderMetadataBlock(
|
||
metadata: ProjectMetadata | undefined,
|
||
template: ProjectTemplate | undefined,
|
||
audioVoiceOptions: AudioVoiceOption[] | undefined,
|
||
audioVoiceOptionsError: string | undefined,
|
||
): string {
|
||
if (!metadata) return '';
|
||
const lines: string[] = [];
|
||
lines.push('\n\n## Project metadata');
|
||
lines.push(
|
||
'These are the structured choices the user made (or skipped) when creating this project. Treat known fields as authoritative. Missing fields are unresolved facts, not mandatory questions; infer reasonable defaults and clarify only when an answer would materially change the result.',
|
||
);
|
||
lines.push('');
|
||
lines.push(`- **kind**: ${metadata.kind}`);
|
||
lines.push(...platformLines(metadata));
|
||
lines.push(...screenRuleLines(metadata));
|
||
lines.push(...landingAndWidgetLines(metadata));
|
||
lines.push(...intentLines(metadata));
|
||
lines.push(...brandLines(metadata));
|
||
lines.push(...kindDetailLines(metadata));
|
||
lines.push(...imageLines(metadata));
|
||
lines.push(...videoLines(metadata));
|
||
lines.push(...audioLines(metadata, audioVoiceOptions, audioVoiceOptionsError));
|
||
lines.push(...inspirationLines(metadata));
|
||
lines.push(...contextPluginLines(metadata));
|
||
lines.push(...promptTemplateReferenceLines(metadata));
|
||
lines.push(...templateReferenceLines(metadata, template));
|
||
return lines.join('\n');
|
||
}
|
||
|
||
function platformLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.platform) {
|
||
out.push(`- **platform**: ${metadata.platform}`);
|
||
} else if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||
out.push('- **platform**: (not provided; relevant options include responsive web, desktop web, iOS app, Android app, tablet app, or desktop app)');
|
||
}
|
||
if (metadata.platformTargets && metadata.platformTargets.length > 0) {
|
||
out.push(`- **platformTargets**: ${metadata.platformTargets.join(', ')}`);
|
||
}
|
||
if (metadata.platform === 'responsive' || metadata.platformTargets?.includes('responsive')) {
|
||
out.push(
|
||
'- **responsive web contract**: `responsive` means one web product experience that adapts across modern browser/device ranges, not only legacy desktop/tablet/mobile buckets. It is not an iOS app, Android app, or native tablet app target. Show responsive behavior through real product layout changes; do not render viewport labels as user-facing product content. Cover 2025–2026 breakpoints: mobile compact 360px, mobile standard 390–430px, foldable/small tablet 600–744px, tablet portrait 768–834px, tablet landscape/large tablet 1024–1180px, laptop 1280–1366px, desktop 1440–1536px, and wide 1920px. Use fluid `clamp()` scales, container queries where useful, and explicit layout changes at semantic thresholds. Verify no horizontal scroll at 360px, 390px, 430px, 768px, 820px, 1024px, 1366px, 1440px, and 1920px unless the brief explicitly asks for a pan/board canvas.',
|
||
);
|
||
}
|
||
if ((metadata.platformTargets?.length ?? 0) > 1) {
|
||
out.push(
|
||
'- **cross-platform deliverable rule**: each selected target keeps the same product goal but MUST be delivered as its own product screen/file when more than one concrete target is selected. Use clear files such as `landing.html` (if enabled), `mobile-ios.html`, `mobile-android.html`, `tablet.html`, `desktop.html`, plus shared `css/` and `js/` when useful. `index.html` may be a launcher/overview that links to these files, but it must not be the only place where mobile/tablet/desktop designs live. Do not collapse cross-platform work into a single tabbed demo, selector UI, comparison board, platform map, or labelled documentation section inside one mock product page.',
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function screenRuleLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||
out.push(
|
||
'- **screen-file-first rule**: each distinct user-facing screen or surface MUST be delivered as its own HTML file unless the user explicitly asks for a single-page scroll or single-file artifact. Do not combine landing pages, product app screens, dashboards, history, pricing, settings, mobile app, tablet app, desktop app, or OS widget surfaces into one long page. Use `index.html` as a launcher/overview that links to screen files when more than one screen exists; it may summarize the product and show screen cards, but it must not contain the full design for every screen.',
|
||
);
|
||
out.push(
|
||
'- **product-realism rule**: final artifacts must look like real end-user product UI. Do not render project metadata, screen counts, target counts, state counts, "demo only" labels, "settings" panels for choosing platforms, "full design target" badges, viewport/device selector controls, theme/style knobs, platform output maps, behavior-spec sections, or design-process cards inside the product unless the user explicitly asks for a design spec/dashboard. Any navigation/tabs inside the artifact must be real product navigation, not designer controls for switching generated mockups.',
|
||
);
|
||
out.push(
|
||
'- **visual-system rule**: when the user does not specify colors, layout, or visual direction, you must still make an intentional product-appropriate visual system. Infer a palette from the product category and audience with at least: neutral surface tokens, a primary action color, a secondary/domain accent, and status colors. Avoid plain monochrome/unstyled greyscale outputs. Use tasteful gradients, illustrations, iconography, device/product mockups, and colored state moments where they clarify the product, while still avoiding generic beige/peach/pink/brown AI washes.',
|
||
);
|
||
out.push(
|
||
'- **app-specific modules rule**: include domain-specific in-app modules/components by default (cards, panels, controls, charts, lists, quick actions, status modules, mini players, checkout/cart summaries, etc. as appropriate). These are product UI modules, not OS home-screen widgets. Give each major module a clear purpose, states, and responsive behavior instead of generic card grids.',
|
||
);
|
||
out.push(
|
||
'- **CJX-ready UX rule**: the artifact must be implementation-ready, not a static screenshot. Structure CSS tokens/components/responsive sections clearly; include real JavaScript behavior for meaningful UX such as tabs, dialogs, drawers, filters, generation/copy actions, validation, playback controls, or state transitions. If keeping a self-contained `index.html`, put the CSS/JS in clearly labelled blocks; for complex UX, generate `css/` and `js/` files when useful.',
|
||
);
|
||
out.push(
|
||
'- **interaction-fidelity rule**: when the requested screen includes user input, generation, copying, validation, login, checkout, filtering, or any action verb, build real interactive controls for that screen. Do not substitute static text rows, prefilled-only mockups, screenshot-like device frames, or decorative state cards for editable inputs and working actions.',
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function landingAndWidgetLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.includeLandingPage) {
|
||
out.push(
|
||
'- **includeLandingPage**: true — create `landing.html` as a separate responsive marketing companion surface in addition to the selected product/app screens. Do not implement the landing page only as a section inside `index.html`, even for responsive-web-only projects. If there is a working product/app screen, create it as a separate file such as `app.html`, `dashboard.html`, or a domain-specific screen name. `index.html` should be a lightweight launcher/overview when multiple files exist. Include hero, value props, product screenshots/device mockups, proof/features, and an appropriate CTA such as waitlist, download, or contact sales.',
|
||
);
|
||
}
|
||
if (metadata.includeOsWidgets) {
|
||
out.push(
|
||
'- **includeOsWidgets**: true — add platform-native OS home-screen / lock-screen / quick-access widget surfaces where relevant. These are outside-the-app widgets (for example iOS WidgetKit, Android home screen widget, Live Activity/lock screen, tablet glance panel), not in-app cards. Include realistic widget sizes and direct quick actions for the domain.',
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function intentLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.intent === 'live-artifact') {
|
||
out.push(
|
||
'- **intent**: live-artifact — the user chose New live artifact. The first output should be a live artifact/dashboard/report, not a one-off static mockup. Prefer the `live-artifact` skill workflow when available, keep source data compact, and register through the daemon live-artifact tool path once that wrapper/tooling is available.',
|
||
);
|
||
out.push(
|
||
'- **connector-source rule**: if the user names a connector/source (for example Notion) and daemon connector tools are available, list connectors before asking where the data comes from. When the named connector is `connected`, use its read-only tools and ask follow-up questions only for missing topic/page/database details, multiple equally plausible matches, or an unconnected/missing connector.',
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function brandLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'brand') {
|
||
out.push(
|
||
'- **brand extraction project**: this project was created by the Brands extractor. Treat `brand.json`, `DESIGN.md`, `BRAND-SYSTEM.md`, `tokens.*.json`, `theme.json`, `kit.html`, `kit.dark.html`, and `artifacts/{landing,deck,poster,email,newsletter,form}.html` as the source of truth. Do not restart extraction from scratch unless the user explicitly asks; explain the extracted kit, then iterate the saved files when requested.',
|
||
);
|
||
if (metadata.brandId) out.push(`- **brandId**: ${metadata.brandId}`);
|
||
if (metadata.brandSourceUrl) out.push(`- **brandSourceUrl**: ${metadata.brandSourceUrl}`);
|
||
if (metadata.brandDesignSystemId) out.push(`- **brandDesignSystemId**: ${metadata.brandDesignSystemId}`);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function kindDetailLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'prototype') {
|
||
out.push(
|
||
`- **fidelity**: ${metadata.fidelity ?? '(not provided; common choices are wireframe or high-fidelity)'}`,
|
||
);
|
||
}
|
||
if (metadata.kind === 'deck') {
|
||
out.push(
|
||
`- **slideCount**: ${metadata.slideCount ?? '(not provided; also check Active plugin / Plugin inputs)'}`,
|
||
);
|
||
out.push(
|
||
`- **speakerNotes**: ${typeof metadata.speakerNotes === 'boolean' ? metadata.speakerNotes : '(not provided)'}`,
|
||
);
|
||
}
|
||
if (metadata.kind === 'template') {
|
||
out.push(
|
||
`- **animations**: ${typeof metadata.animations === 'boolean' ? metadata.animations : '(not provided)'}`,
|
||
);
|
||
if (metadata.templateLabel) {
|
||
out.push(`- **template**: ${metadata.templateLabel}`);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function imageLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'image') {
|
||
out.push(
|
||
`- **imageModel**: ${metadata.imageModel ?? '(not provided)'}`,
|
||
);
|
||
out.push(
|
||
`- **aspectRatio**: ${metadata.imageAspect ?? '(not provided; common choices include 1:1, 16:9, 9:16, 4:3, or 3:4)'}`,
|
||
);
|
||
if (metadata.imageStyle) {
|
||
out.push(`- **styleNotes**: ${metadata.imageStyle}`);
|
||
}
|
||
if (metadata.promptTemplate && metadata.promptTemplate.prompt.trim().length > 0) {
|
||
out.push(`- **referenceTemplate**: ${metadata.promptTemplate.title}`);
|
||
}
|
||
out.push('');
|
||
out.push(
|
||
'This is an **image** project. Plan the prompt carefully, then dispatch via the **media generation contract** using `"$OD_NODE_BIN" "$OD_BIN" media generate --surface image --model <imageModel>`. Do NOT emit `<artifact>` HTML for media surfaces.',
|
||
);
|
||
if (metadata.imageModel?.startsWith('vela/')) {
|
||
out.push(
|
||
'This OpenDesign Cloud `vela/*` model must go through the OD media dispatcher. Do not invoke the `vela` CLI or the remote media API directly; the daemon owns Workspace attribution, downloads, and final project-file placement.',
|
||
);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function videoLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'video') {
|
||
out.push(
|
||
`- **videoModel**: ${metadata.videoModel ?? '(not provided)'}`,
|
||
);
|
||
out.push(
|
||
`- **lengthSeconds**: ${typeof metadata.videoLength === 'number' ? metadata.videoLength : '(not provided; common choices include 3s, 5s, or 10s)'}`,
|
||
);
|
||
out.push(
|
||
`- **aspectRatio**: ${metadata.videoAspect ?? '(not provided; common choices include 16:9, 9:16, or 1:1)'}`,
|
||
);
|
||
if (metadata.promptTemplate && metadata.promptTemplate.prompt.trim().length > 0) {
|
||
out.push(`- **referenceTemplate**: ${metadata.promptTemplate.title}`);
|
||
}
|
||
out.push('');
|
||
out.push(
|
||
'This is a **video** project. Plan the shotlist and motion, then dispatch via the **media generation contract** using `"$OD_NODE_BIN" "$OD_BIN" media generate --surface video --model <videoModel> --length <seconds> --aspect <ratio>`. Do NOT emit `<artifact>` HTML.',
|
||
);
|
||
if (metadata.videoModel?.startsWith('vela/')) {
|
||
out.push(
|
||
'This OpenDesign Cloud `vela/*` model must go through the OD media dispatcher. Do not invoke the `vela` CLI or the remote media API directly; the daemon owns Workspace attribution, polling, downloads, and final project-file placement.',
|
||
);
|
||
}
|
||
if (metadata.videoModel === 'hyperframes-html') {
|
||
out.push(
|
||
'Special case: `hyperframes-html` is a local HTML-to-MP4 renderer, not a photoreal text-to-video model. Treat it like a motion design renderer, ask at most one clarifying question, then dispatch immediately.',
|
||
);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function audioLines(
|
||
metadata: ProjectMetadata,
|
||
audioVoiceOptions: AudioVoiceOption[] | undefined,
|
||
audioVoiceOptionsError: string | undefined,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'audio') {
|
||
out.push(
|
||
`- **audioKind**: ${metadata.audioKind ?? '(not provided; common choices include music, speech, or sfx)'}`,
|
||
);
|
||
out.push(
|
||
`- **audioModel**: ${metadata.audioModel ?? '(not provided)'}`,
|
||
);
|
||
out.push(
|
||
`- **durationSeconds**: ${typeof metadata.audioDuration === 'number' ? metadata.audioDuration : '(not provided)'}`,
|
||
);
|
||
if (metadata.voice) {
|
||
out.push(`- **voice**: ${metadata.voice}`);
|
||
} else if (metadata.audioKind === 'speech') {
|
||
out.push('- **voice**: (not provided; relevant dimensions include voice id, accent, and pacing)');
|
||
}
|
||
const voiceOptions = shouldRenderElevenLabsVoiceOptions(metadata, audioVoiceOptions)
|
||
? audioVoiceOptions ?? []
|
||
: [];
|
||
if (voiceOptions.length > 0) {
|
||
out.push(
|
||
'- **ElevenLabs voice selection policy**: First infer from the current request, conversation, Plugin inputs, and available context. If the provider default can safely satisfy the brief, omit `--voice` and do not ask. Only when voice selection would materially change the requested result and no safe default can be inferred, emit the dropdown template below. Its visible labels are voice descriptions; the selected value must be the exact `voice_id` passed to `--voice`. Do not ask the user to type an id.',
|
||
);
|
||
if (voiceOptions.length > ELEVENLABS_VOICE_PROMPT_OPTION_LIMIT) {
|
||
out.push(`- **ElevenLabs voice options**: showing the first ${ELEVENLABS_VOICE_PROMPT_OPTION_LIMIT} of ${voiceOptions.length} available voices.`);
|
||
}
|
||
out.push('');
|
||
out.push('Conditional template — do not emit unless the voice-selection policy above requires clarification:');
|
||
out.push('<question-form id="elevenlabs-voice" title="Choose an ElevenLabs voice">');
|
||
out.push(JSON.stringify(renderElevenLabsVoiceQuestionForm(voiceOptions), null, 2));
|
||
out.push('</question-form>');
|
||
} else {
|
||
const audioVoiceOptionsPromptError = formatElevenLabsVoiceOptionsErrorForPrompt(audioVoiceOptionsError);
|
||
if (audioVoiceOptionsPromptError) {
|
||
out.push(
|
||
`- **ElevenLabs voice options**: ${audioVoiceOptionsPromptError}`,
|
||
);
|
||
}
|
||
}
|
||
if (metadata.audioKind === 'sfx') {
|
||
out.push(
|
||
'- **SFX discovery**: If the audible event cannot be inferred and a missing detail would materially change the result, clarify only the highest-impact unresolved dimensions. Relevant dimensions include sound source/action, materials, intensity, acoustic space, timing/tail, loop/non-loop, and "avoid" constraints. Do not ask for language or voice for SFX.',
|
||
);
|
||
}
|
||
out.push('');
|
||
out.push(
|
||
'This is an **audio** project. Lock the content intent first, then dispatch via the **media generation contract** using `"$OD_NODE_BIN" "$OD_BIN" media generate --surface audio --audio-kind <kind> --model <audioModel> --duration <seconds>` and add `--voice <voice-id>` for speech when you have a provider-specific voice id. Do NOT emit `<artifact>` HTML.',
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function inspirationLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.inspirationDesignSystemIds && metadata.inspirationDesignSystemIds.length > 0) {
|
||
out.push(
|
||
`- **inspirationDesignSystemIds**: ${metadata.inspirationDesignSystemIds.join(', ')} — the user picked these systems as *additional* inspiration alongside the primary one. Borrow palette accents, typographic personality, or component patterns from them; don't replace the primary system's tokens.`,
|
||
);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function contextPluginLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (Array.isArray(metadata.contextPlugins) && metadata.contextPlugins.length > 0) {
|
||
out.push('');
|
||
out.push('### @ plugin context');
|
||
out.push(
|
||
'The user selected these plugins as additive context via @ mentions. Treat them as requested references to combine with the brief; only the explicit active plugin block, if present, is the executable/pinned plugin snapshot.',
|
||
);
|
||
for (const plugin of metadata.contextPlugins) {
|
||
const id = typeof plugin.id === 'string' ? plugin.id : '';
|
||
const title = typeof plugin.title === 'string' && plugin.title.trim().length > 0
|
||
? plugin.title.trim()
|
||
: id;
|
||
if (!id && !title) continue;
|
||
const description = typeof plugin.description === 'string' && plugin.description.trim().length > 0
|
||
? ` — ${plugin.description.trim()}`
|
||
: '';
|
||
out.push(`- ${title}${id ? ` (\`${id}\`)` : ''}${description}`);
|
||
}
|
||
}
|
||
|
||
// Curated prompt template reference for image/video projects. Inlined
|
||
// verbatim (with light truncation) so the agent can borrow structure,
|
||
// mood and phrasing without a separate fetch. The user may have edited
|
||
// the body before clicking Create — those edits land here and are now
|
||
// authoritative for the brief.
|
||
return out;
|
||
}
|
||
|
||
function promptTemplateReferenceLines(
|
||
metadata: ProjectMetadata,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (
|
||
(metadata.kind === 'image' || metadata.kind === 'video') &&
|
||
metadata.promptTemplate &&
|
||
metadata.promptTemplate.prompt.trim().length > 0
|
||
) {
|
||
const tpl = metadata.promptTemplate;
|
||
out.push('');
|
||
out.push(`### Reference prompt template — "${tpl.title}"`);
|
||
const meta: string[] = [];
|
||
if (tpl.category) meta.push(`category: ${tpl.category}`);
|
||
const suggestedModel =
|
||
metadata.kind === 'image' &&
|
||
!metadata.imageModel?.trim() &&
|
||
tpl.model === 'gpt-image-2'
|
||
? 'vela/gpt-image-2'
|
||
: tpl.model;
|
||
if (suggestedModel) meta.push(`suggested model: ${suggestedModel}`);
|
||
if (tpl.aspect) meta.push(`aspect: ${tpl.aspect}`);
|
||
if (tpl.tags && tpl.tags.length > 0) {
|
||
meta.push(`tags: ${tpl.tags.join(', ')}`);
|
||
}
|
||
if (meta.length > 0) out.push(meta.join(' · '));
|
||
if (tpl.summary) {
|
||
out.push('');
|
||
out.push(tpl.summary);
|
||
}
|
||
out.push('');
|
||
out.push(
|
||
'The user picked this template as inspiration. Treat it as a structural and stylistic reference: borrow composition, palette cues, lighting language, lens/motion direction, and the level of detail. Adapt the wording to the user\'s actual subject and brief — do NOT generate the template subject verbatim. If a field above is unknown the user wants you to follow the template\'s defaults.',
|
||
);
|
||
// Escape triple-backticks so a user who pastes ``` into the editable
|
||
// template body can't break out of the markdown fence below and inject
|
||
// free-form instructions into the agent's system prompt. Zero-width
|
||
// joiner between the backticks keeps the prompt human-readable while
|
||
// preventing the closing fence from matching prematurely.
|
||
const safe = tpl.prompt.replace(/```/g, '`\u200b`\u200b`');
|
||
const truncated =
|
||
safe.length > 4000
|
||
? `${safe.slice(0, 4000)}\n… (truncated ${safe.length - 4000} chars)`
|
||
: safe;
|
||
out.push('');
|
||
out.push('```text');
|
||
out.push(truncated);
|
||
out.push('```');
|
||
if (tpl.source) {
|
||
const author = tpl.source.author ? ` by ${tpl.source.author}` : '';
|
||
out.push('');
|
||
out.push(
|
||
`Source: ${tpl.source.repo}${author} — license ${tpl.source.license}. Preserve attribution if you echo the template language directly.`,
|
||
);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function templateReferenceLines(
|
||
metadata: ProjectMetadata,
|
||
template: ProjectTemplate | undefined,
|
||
): string[] {
|
||
const out: string[] = [];
|
||
if (metadata.kind === 'template' && template && template.files.length > 0) {
|
||
out.push('');
|
||
out.push(
|
||
`### Template reference — "${template.name}"${template.description ? ` (${template.description})` : ''}`,
|
||
);
|
||
out.push(
|
||
'These HTML snapshots are what the user wants to start FROM. Read them as a stylistic + structural reference. You may copy structure, palette, typography, and component patterns; you may adapt them to the new brief; do NOT ship them verbatim. The agent should still produce its own artifact, just one that visibly inherits this template\'s design language.',
|
||
);
|
||
for (const f of template.files) {
|
||
// Cap each file at ~12k chars so a giant template doesn't blow out
|
||
// the system prompt budget. The agent gets enough to read structure.
|
||
const truncated =
|
||
f.content.length > 12000
|
||
? `${f.content.slice(0, 12000)}\n<!-- … truncated (${f.content.length - 12000} chars omitted) -->`
|
||
: f.content;
|
||
out.push('');
|
||
out.push(`#### \`${f.name}\``);
|
||
out.push('```html');
|
||
out.push(truncated);
|
||
out.push('```');
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function shouldRenderElevenLabsVoiceOptions(
|
||
metadata: ProjectMetadata,
|
||
audioVoiceOptions: AudioVoiceOption[] | undefined,
|
||
): boolean {
|
||
return metadata.kind === 'audio'
|
||
&& metadata.audioKind === 'speech'
|
||
&& metadata.audioModel === 'elevenlabs-v3'
|
||
&& !metadata.voice
|
||
&& Array.isArray(audioVoiceOptions)
|
||
&& audioVoiceOptions.length > 0;
|
||
}
|
||
|
||
/**
|
||
* OPEND-2707. 与 `apps/daemon/src/prompts/system.ts` 的同名函数是手抄件,必须同步。
|
||
*
|
||
* 「交上去的是 voice_id,不是你看到的那行描述」原本写在每题副标题(help 字段)里。
|
||
* 澄清卡不再渲染副标题之后那句话写了就丢,所以它并进了 `label` ——
|
||
* `FormQuestion` 没有题级 `description`,`placeholder` 归 "Choose a voice" 占着,
|
||
* `label` 是唯一还会渲染、又属于这道题本身的位置。
|
||
*
|
||
* 这份 JSON 会被整段 `JSON.stringify` 进系统提示词,所以它同时是模型看到的
|
||
* 「一道题可以长这样」的范例 —— 留一个 help 键在这里,撤发问就撤了个寂寞。
|
||
* 钉在 `packages/contracts/tests/system-prompt-audio-voices.test.ts`。
|
||
*/
|
||
function renderElevenLabsVoiceQuestionForm(voiceOptions: AudioVoiceOption[]): {
|
||
questions: Array<{
|
||
id: string;
|
||
label: string;
|
||
type: 'select';
|
||
required: boolean;
|
||
allowCustom: false;
|
||
placeholder: string;
|
||
options: Array<{ label: string; value: string }>;
|
||
}>;
|
||
submitLabel: string;
|
||
} {
|
||
const options = voiceOptions.slice(0, ELEVENLABS_VOICE_PROMPT_OPTION_LIMIT).map((option) => ({
|
||
label: formatElevenLabsVoiceLabel(option),
|
||
value: option.voiceId,
|
||
}));
|
||
return {
|
||
questions: [
|
||
{
|
||
id: 'voice',
|
||
label: 'Voice — the answer submits the matching Voice ID',
|
||
type: 'select',
|
||
required: true,
|
||
allowCustom: false,
|
||
placeholder: 'Choose a voice',
|
||
options,
|
||
},
|
||
],
|
||
submitLabel: 'Use voice',
|
||
};
|
||
}
|
||
|
||
function formatElevenLabsVoiceLabel(option: AudioVoiceOption): string {
|
||
const labels = option.labels && typeof option.labels === 'object'
|
||
? Object.values(option.labels)
|
||
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||
.filter(Boolean)
|
||
: [];
|
||
const bits = [...labels];
|
||
if (bits.length > 0) return `${option.name} — ${bits.join(' · ')}`;
|
||
const category = typeof option.category === 'string' ? option.category.trim() : '';
|
||
return category ? `${option.name} — ${category}` : option.name;
|
||
}
|
||
|
||
/**
|
||
* Detect the seed/references pattern shipped by the upgraded
|
||
* web-prototype / mobile-app / simple-deck / guizang-ppt skills, and
|
||
* inject a hard pre-flight rule that lists which side files to Read
|
||
* before doing anything else. The skill body's own workflow already says
|
||
* this — but skills get truncated under context pressure and the agent
|
||
* sometimes skips Step 0. A short up-front directive helps.
|
||
*
|
||
* Returns an empty string when the skill ships no side files (legacy
|
||
* SKILL.md-only skills) so we don't add noise.
|
||
*/
|
||
function derivePreflight(skillBody: string): string {
|
||
const refs: string[] = [];
|
||
if (/assets\/template\.html/.test(skillBody)) refs.push('`assets/template.html`');
|
||
if (/references\/layouts\.md/.test(skillBody)) refs.push('`references/layouts.md`');
|
||
if (/references\/themes\.md/.test(skillBody)) refs.push('`references/themes.md`');
|
||
if (/references\/components\.md/.test(skillBody)) refs.push('`references/components.md`');
|
||
if (/references\/checklist\.md/.test(skillBody)) refs.push('`references/checklist.md`');
|
||
if (/references\/artifact-schema\.md/.test(skillBody)) refs.push('`references/artifact-schema.md`');
|
||
if (/references\/connector-policy\.md|connector-policy\.md/.test(skillBody)) {
|
||
refs.push('`references/connector-policy.md`');
|
||
}
|
||
if (/references\/refresh-contract\.md|refresh-contract\.md/.test(skillBody)) {
|
||
refs.push('`references/refresh-contract.md`');
|
||
}
|
||
if (/references\/html-in-canvas\.md|html-in-canvas\.md/.test(skillBody)) {
|
||
refs.push('`references/html-in-canvas.md`');
|
||
}
|
||
if (refs.length === 0) return '';
|
||
return ` **Pre-flight (do this before any other tool):** Read ${refs.join(', ')} via the path written in the skill-root preamble. If the skill asks for daemon wrapper commands, use the runtime tool environment documented below; it provides the daemon URL and whether a run-scoped tool token is available without exposing token internals. The seed template defines the class system you'll paste into; the layouts file is the only acceptable source of section/screen/slide skeletons; the checklist and live-artifact references are your validation gate before emitting \`<artifact>\` or registering a live artifact. Skipping this step is the #1 reason output regresses to generic AI-slop.`;
|
||
}
|