@avocadostudio-ai/orchestrator-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt builders for the chat planning pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates duplication between OpenAI and Anthropic planner modules.
|
|
5
|
+
* Provider-specific extensions are injected via the `provider` option.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildIntentParserSystemPrompt(): string;
|
|
8
|
+
export declare function buildVariationSystemPrompt(opts: {
|
|
9
|
+
count: number;
|
|
10
|
+
keepTitle: boolean;
|
|
11
|
+
cardsOnly: boolean;
|
|
12
|
+
blockType: string;
|
|
13
|
+
locale?: string;
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function buildDecomposerSystemPrompt(opts: {
|
|
16
|
+
slug: string;
|
|
17
|
+
pageTitle: string;
|
|
18
|
+
blocksSummary: string;
|
|
19
|
+
siteContextBlock?: string | null | undefined;
|
|
20
|
+
locale?: string;
|
|
21
|
+
}): string;
|
|
22
|
+
export interface PlannerPromptOptions {
|
|
23
|
+
provider: "openai" | "anthropic" | "gemini";
|
|
24
|
+
lightweight: boolean;
|
|
25
|
+
selectedBlockId: string;
|
|
26
|
+
explicitOtherReference: boolean;
|
|
27
|
+
chatStrictPrimaryOpMode: boolean;
|
|
28
|
+
pageWideTranslation: boolean;
|
|
29
|
+
pageWideRewrite: boolean;
|
|
30
|
+
/** True when the user explicitly asked to review a plan first ("make a plan
|
|
31
|
+
* first", "show me a plan"). The resulting plan is held behind the Apply/
|
|
32
|
+
* Discard preview gate, so the planner must always produce a reviewable plan
|
|
33
|
+
* and never bail to needs_clarification. */
|
|
34
|
+
planFirstMode?: boolean;
|
|
35
|
+
effectiveBlockTypes: string[];
|
|
36
|
+
siteContextBlock?: string | null | undefined;
|
|
37
|
+
imageUrlForVision?: string | null | undefined;
|
|
38
|
+
editablePath?: string | null | undefined;
|
|
39
|
+
blockId?: string | null | undefined;
|
|
40
|
+
locale?: string;
|
|
41
|
+
/** Number of files the user attached to this turn (images/PDFs), sent as native multimodal content. */
|
|
42
|
+
attachmentCount?: number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* True iff BOTH Unsplash and an AI image provider are configured on the server
|
|
45
|
+
* AND no session-level image source preference has been captured yet. When
|
|
46
|
+
* true, the planner is allowed to return needs_clarification asking the user
|
|
47
|
+
* which source to use for genuinely ambiguous new-image requests. When false
|
|
48
|
+
* (only one source, or preference already set), the planner must proceed
|
|
49
|
+
* without asking.
|
|
50
|
+
*/
|
|
51
|
+
imageSourceChoiceOpen?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function buildPlannerSystemPrompt(opts: PlannerPromptOptions): string;
|
|
54
|
+
/**
|
|
55
|
+
* Returns the planner system prompt split into a `stable` prefix (provider- and
|
|
56
|
+
* site-stable rules — safe to cache) and a `dynamic` suffix (per-request flags
|
|
57
|
+
* like selected block, conditional modes, locale — not cacheable).
|
|
58
|
+
*
|
|
59
|
+
* Anthropic prompt caching matches the cached prefix byte-for-byte; if dynamic
|
|
60
|
+
* content is interleaved into the cached block, every request that flips a
|
|
61
|
+
* flag (e.g. user clicks a different block) misses the cache. Splitting at
|
|
62
|
+
* this boundary lets the stable prefix stay cached across requests.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildPlannerSystemPromptSegments(opts: PlannerPromptOptions): {
|
|
65
|
+
stable: string;
|
|
66
|
+
dynamic: string;
|
|
67
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt builders for the chat planning pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates duplication between OpenAI and Anthropic planner modules.
|
|
5
|
+
* Provider-specific extensions are injected via the `provider` option.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Intent parser
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export function buildIntentParserSystemPrompt() {
|
|
11
|
+
return [
|
|
12
|
+
"You extract editing intent for a website editor.",
|
|
13
|
+
"Return ONLY one JSON object. No markdown.",
|
|
14
|
+
"Never return operations.",
|
|
15
|
+
"Map request to action: add | move | update | remove | info | clarify.",
|
|
16
|
+
"If the user asks about page content, requests a description or summary of the page, or asks what is editable/available, use action=info.",
|
|
17
|
+
"Use explicit block references when present (id/type words like hero/faq/cta).",
|
|
18
|
+
"For move/add with placement words, set position to top/bottom/before/after and anchor_block_ref when relevant.",
|
|
19
|
+
"For update, include patch with only requested fields.",
|
|
20
|
+
"When choosing new_block_type for an add, prefer a block type that supports imagery (Hero, TwoColumn, Card, CardGrid, FeatureGrid, Gallery, Carousel, Testimonials, Quote) whenever the request mentions image/photo/picture/imagery/visual/unsplash/photograph. Pick RichText only when the user explicitly asks for prose/text/article/paragraphs/description without imagery. Generic words like \"section\" alone don't imply RichText — use the surrounding cues (imagery → image-bearing block; numbers/metrics → Stats; comparison → Table; quote → Quote; FAQ → FAQAccordion).",
|
|
21
|
+
'Set complexity to "simple" when the request targets a single block with a straightforward edit (add/remove emoji, change a label, update one field). Set complexity to "standard" for multi-block edits, page creation, translation, or content generation that requires some creative judgment but is otherwise mechanical. Set complexity to "complex" only when the request demands deep reasoning: multi-step transformations with conflicting constraints, structural redesigns ("rethink the page", "reorganize for conversions"), narrative rewrites that must hold a tone across many sections, or prompts where the user describes trade-offs the planner has to resolve. Default to "standard" when uncertain — "complex" should be rare.'
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Variation generator
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export function buildVariationSystemPrompt(opts) {
|
|
28
|
+
return [
|
|
29
|
+
"You generate alternative content variations for one selected website block.",
|
|
30
|
+
"Return ONLY JSON object: {\"variations\":[{\"title\":\"...\",\"summary\":\"...\",\"patch\":{...}}]}",
|
|
31
|
+
`Generate exactly ${opts.count} variations.`,
|
|
32
|
+
"Each patch must only include keys from the selected block props.",
|
|
33
|
+
"Each variation must be materially different from the others.",
|
|
34
|
+
"Do not include unchanged values in patch.",
|
|
35
|
+
"For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.",
|
|
36
|
+
...(opts.keepTitle ? ["Keep the existing block title exactly unchanged."] : []),
|
|
37
|
+
...(opts.cardsOnly && opts.blockType === "CardGrid" ? ["Patch must include only the 'cards' key."] : []),
|
|
38
|
+
"Never include imageUrl in any patch — the orchestrator resolves image URLs itself (Unsplash or AI generation). You may include imageAlt as a short descriptive hint when relevant.",
|
|
39
|
+
...localeInstruction(opts.locale)
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Decomposer
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
export function buildDecomposerSystemPrompt(opts) {
|
|
46
|
+
return `You break complex website editing requests into sequential steps.
|
|
47
|
+
|
|
48
|
+
Each step must be a complete, self-contained instruction that can be executed independently by a website editor AI. Include specific details (page names, slugs, content references) so each step is unambiguous.
|
|
49
|
+
|
|
50
|
+
Return JSON: { "steps": string[], "labels": string[] }
|
|
51
|
+
- steps: full instruction text for each step
|
|
52
|
+
- labels: 3-6 word button labels for UI (e.g. "Create /about page")
|
|
53
|
+
- If the request is already simple (single page edit, single block change), return exactly 1 step.
|
|
54
|
+
- When creating multiple pages, each page creation should be its own step.
|
|
55
|
+
- When updating existing content to reference new pages (e.g. linking CTAs), put that in a final step.
|
|
56
|
+
- Include page context (card titles, block content) in each step so it can execute standalone.
|
|
57
|
+
|
|
58
|
+
Current page: ${opts.slug}
|
|
59
|
+
Page title: "${opts.pageTitle}"
|
|
60
|
+
Blocks:
|
|
61
|
+
${opts.blocksSummary}
|
|
62
|
+
${opts.siteContextBlock ? `\nSite context:\n${opts.siteContextBlock}` : ""}
|
|
63
|
+
${localeInstruction(opts.locale).join("\n")}`;
|
|
64
|
+
}
|
|
65
|
+
const LOCALE_NAMES = {
|
|
66
|
+
de: "German",
|
|
67
|
+
fr: "French",
|
|
68
|
+
es: "Spanish",
|
|
69
|
+
it: "Italian",
|
|
70
|
+
pt: "Portuguese",
|
|
71
|
+
nl: "Dutch",
|
|
72
|
+
ja: "Japanese",
|
|
73
|
+
ko: "Korean",
|
|
74
|
+
zh: "Chinese",
|
|
75
|
+
};
|
|
76
|
+
function localeInstruction(locale) {
|
|
77
|
+
if (!locale || locale === "en")
|
|
78
|
+
return [];
|
|
79
|
+
const lang = LOCALE_NAMES[locale] ?? locale;
|
|
80
|
+
return [
|
|
81
|
+
`The user's interface is in ${lang}. Write summary_for_user, change_log entries, and suggested_next_actions in ${lang}. Keep block type names, technical identifiers, and operation names in English.`
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Shared planner rules (consumed by both lightweight and full prompts).
|
|
86
|
+
// Keeping these as named constants prevents the two prompts from drifting
|
|
87
|
+
// out of sync when one is edited but not the other.
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
const RULE_ROLE = "You are an editing planner for a website builder.";
|
|
90
|
+
const RULE_JSON_ONLY = "Return ONLY one JSON object matching EditPlan.";
|
|
91
|
+
const RULE_NO_MARKDOWN = "Never output markdown or code fences.";
|
|
92
|
+
const RULE_NO_OP_PATCH = "Do not return no-op updates: patch must change at least one effective value.";
|
|
93
|
+
const RULE_STRICT_SCHEMA_DISCIPLINE = "STRICT SCHEMA DISCIPLINE: Only promise changes to props that exist in the block's contract. When a request has some supported parts AND some unsupported parts (e.g. 'add icons and colors' on a block with icon but no color), APPLY the supported parts and mention in summary_for_user that the unsupported part isn't available — don't bail out. Only return needs_clarification when NOTHING in the request maps to the schema. Do NOT generate summary_for_user or change_log text that describes changes your ops don't actually make.";
|
|
94
|
+
const RULE_ICON_FORMAT = "ICON FIELDS: any `icon` field (e.g. on Stats, FeatureGrid, Testimonials) MUST be a single emoji character such as '📈', '🌿', '❤️', or '👥'. NEVER use an icon-library or code name like 'leaf', 'heart', 'users', 'chevronRight', 'arrow-right' — those are not emoji and render as raw text. Never invent or guess an image URL for an icon; only use a full https:// URL if the user explicitly provides one.";
|
|
95
|
+
export function buildPlannerSystemPrompt(opts) {
|
|
96
|
+
const { stable, dynamic } = buildPlannerSystemPromptSegments(opts);
|
|
97
|
+
return dynamic ? `${stable}\n\n${dynamic}` : stable;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Returns the planner system prompt split into a `stable` prefix (provider- and
|
|
101
|
+
* site-stable rules — safe to cache) and a `dynamic` suffix (per-request flags
|
|
102
|
+
* like selected block, conditional modes, locale — not cacheable).
|
|
103
|
+
*
|
|
104
|
+
* Anthropic prompt caching matches the cached prefix byte-for-byte; if dynamic
|
|
105
|
+
* content is interleaved into the cached block, every request that flips a
|
|
106
|
+
* flag (e.g. user clicks a different block) misses the cache. Splitting at
|
|
107
|
+
* this boundary lets the stable prefix stay cached across requests.
|
|
108
|
+
*/
|
|
109
|
+
export function buildPlannerSystemPromptSegments(opts) {
|
|
110
|
+
if (opts.lightweight) {
|
|
111
|
+
return { stable: buildLightweightPlannerPrompt(opts), dynamic: "" };
|
|
112
|
+
}
|
|
113
|
+
return buildFullPlannerSegments(opts);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Lightweight planner prompt
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
function buildLightweightPlannerPrompt(opts) {
|
|
119
|
+
return [
|
|
120
|
+
RULE_ROLE,
|
|
121
|
+
RULE_JSON_ONLY,
|
|
122
|
+
RULE_NO_MARKDOWN,
|
|
123
|
+
'Emit top-level keys in this exact order: intent (string: "edit_plan"), summary_for_user (string), change_log (array of strings), ops (array of operation objects), suggested_next_actions (array of strings).',
|
|
124
|
+
'Each op object MUST include "op" (e.g. "update_props"), "blockId", and "patch".',
|
|
125
|
+
"For update_props, blockId is required and must target an existing block id (b_*). Set patch to changed props only; use existing prop keys for the target block type.",
|
|
126
|
+
RULE_NO_OP_PATCH,
|
|
127
|
+
RULE_STRICT_SCHEMA_DISCIPLINE,
|
|
128
|
+
RULE_ICON_FORMAT,
|
|
129
|
+
"Use future tense in summary_for_user and change_log — your output streams to the user while the plan is still being generated, before any ops have been applied. Say 'Will update the heading…' or 'Will replace the Hero image…', never 'Updated…' or 'Updating…'. The system flips to past tense automatically once ops are applied.",
|
|
130
|
+
"For edit_plan: summary_for_user must be ONE short sentence (max ~20 words).",
|
|
131
|
+
"After planning ops, include suggested_next_actions: 2-4 short imperative phrases the user could type next (max 6 words each). Every suggestion must be an action the user can perform inside this editor (editing content, adding/removing sections, changing images, rewriting copy) — restricted to the block types listed in the block catalogue provided in context (Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, TwoColumn, Banner, Carousel, Embed, Footer, Gallery, Quote, SiteHeader, Stats, Table, Tabs, Video). NEVER suggest unsupported features like forms, email capture, contact forms, subscribe boxes, newsletter signups, popups, modals, or anything requiring custom code. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy. When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Revert to previous'). Remaining suggestions can target neighboring fields or blocks.",
|
|
132
|
+
opts.selectedBlockId.length > 0
|
|
133
|
+
? `Selected block is ${opts.selectedBlockId}. Target only this block in ops when the request edits the current page. IGNORE this selection when the request operates on a different scope — creating, duplicating, renaming, removing, or moving a page; editing site config; or naming a different page — and emit ops only for the requested scope. Never add bonus ops on the selected block to satisfy this rule.`
|
|
134
|
+
: "Respect explicit user target references when present.",
|
|
135
|
+
...localeInstruction(opts.locale)
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Full planner prompt — provider-specific constants
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
const HERO_IMAGE_URL_BASE = "For imageUrl fields (Hero, CardGrid cards, or any block with images), use \"/hero-generated.svg\" as the placeholder value unless the user provides an explicit URL or you are calling an image tool. Never use external placeholder image services — these break the renderer. The system will resolve actual images separately. Do NOT mention a specific image source (e.g. Unsplash) in summary_for_user — just say 'image'.";
|
|
142
|
+
const HERO_IMAGE_URL_OPENAI_EXT = " When you need a placeholder image URL, use https://placehold.co/{width}x{height}.png?text={label} (e.g. https://placehold.co/768x512.png?text=Hero). Always include the .png extension — SVG format breaks Next.js image optimization. Never use via.placeholder.com — it is defunct.";
|
|
143
|
+
const ANTHROPIC_IMAGE_TOOL_LINES = [
|
|
144
|
+
"For image search requests that explicitly mention Unsplash or stock photos, call tool unsplash.search with a concise search query and choose an imageUrl from tool results.",
|
|
145
|
+
"For image requests that say 'generate', 'create', or 'make' an image, call tool image.generate with a detailed prompt describing the desired image. When calling image.generate, check the target block's image spec in blockContracts for the recommended aspectRatio and pass it. If the user explicitly specifies an aspectRatio, use that instead. Default to quality 'draft'. Use 'final' only when the user explicitly asks for high quality, polished, or production-ready images.",
|
|
146
|
+
"For image requests that mention 'brand', 'our photos', 'company images', 'from Drive', 'from our folder', or 'brand assets', call tool gdrive.browse with an optional search query. Choose an imageUrl from tool results and write it into the relevant imageUrl field.",
|
|
147
|
+
"When using gdrive.browse, write the selected image URL into the relevant imageUrl field and set imageAlt to a concise accessible description.",
|
|
148
|
+
"When using unsplash.search, copy the imageUrl from the tool result into the imageUrl field VERBATIM — every character including all query parameters (especially the `ixid=` tracking token). Never paraphrase, shorten, or rebuild the URL with parameters like `?w=768&h=512&fit=crop` from memory: photo IDs you 'remember' from training data are almost always wrong and will render as broken images. If you need a different image, call unsplash.search again with a refined query rather than editing the URL by hand. Set imageAlt to a concise accessible description.",
|
|
149
|
+
"When using image.generate, write the returned imageUrl into the relevant imageUrl field and set imageAlt from the returned alt text.",
|
|
150
|
+
];
|
|
151
|
+
const BLOCK_NAME_PRIVACY_OPENAI = "Never mention internal block IDs (b_hero_*, b_featuregrid_*, etc.), prop names (imageUrl, imageAlt), or system settings in summary_for_user or change_log. Use human-friendly descriptions instead (e.g. 'Update the Hero image' not 'Update imageUrl on b_hero_123').";
|
|
152
|
+
const BLOCK_NAME_PRIVACY_ANTHROPIC = "Never mention internal block IDs (b_hero_*, b_featuregrid_*, etc.), prop names (imageUrl, imageAlt), or system settings in summary_for_user, change_log, or suggested_next_actions. Also avoid raw block type names like 'RichText', 'FeatureGrid', 'CardGrid', 'FAQAccordion' — use natural descriptions instead: 'text section', 'features grid', 'card grid', 'FAQ section'. Exception: 'Hero', 'CTA', and 'Testimonials' are fine as-is since users understand these terms.";
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Full planner prompt — composed from section builders.
|
|
155
|
+
// Each section becomes a ## HEADER in the emitted prompt so the LLM can
|
|
156
|
+
// anchor on structure rather than scanning a flat bullet list.
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
function joinSections(sections) {
|
|
159
|
+
return sections
|
|
160
|
+
.filter((lines) => lines.length > 0)
|
|
161
|
+
.map((lines) => lines.join("\n"))
|
|
162
|
+
.join("\n\n");
|
|
163
|
+
}
|
|
164
|
+
function buildFullPlannerSegments(opts) {
|
|
165
|
+
const hasNativeTools = opts.provider === "anthropic" || opts.provider === "gemini";
|
|
166
|
+
// Stable sections — depend only on provider (fixed for a given planner) and
|
|
167
|
+
// static rule constants. These bytes are identical across requests and are
|
|
168
|
+
// the part Anthropic caches. IMAGES is here too; it depends on hasNativeTools
|
|
169
|
+
// but that's stable per provider.
|
|
170
|
+
const stableSections = [
|
|
171
|
+
sectionRole(),
|
|
172
|
+
sectionOutputContract(),
|
|
173
|
+
sectionIntentDecisionTree(),
|
|
174
|
+
sectionVoice(opts, hasNativeTools),
|
|
175
|
+
sectionOperationCatalog(),
|
|
176
|
+
sectionSchemaDiscipline(),
|
|
177
|
+
sectionImages(hasNativeTools),
|
|
178
|
+
];
|
|
179
|
+
// Dynamic sections — depend on per-request state (selected block, conditional
|
|
180
|
+
// mode flags, site context, locale). Kept out of the cached block so the
|
|
181
|
+
// stable prefix above stays a cache hit.
|
|
182
|
+
const dynamicSections = [
|
|
183
|
+
sectionTargeting(opts),
|
|
184
|
+
sectionImageSourceChoice(opts),
|
|
185
|
+
sectionConditionalModes(opts),
|
|
186
|
+
sectionContext(opts),
|
|
187
|
+
];
|
|
188
|
+
return {
|
|
189
|
+
stable: joinSections(stableSections),
|
|
190
|
+
dynamic: joinSections(dynamicSections),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function sectionRole() {
|
|
194
|
+
return ["## ROLE", RULE_ROLE];
|
|
195
|
+
}
|
|
196
|
+
function sectionOutputContract() {
|
|
197
|
+
return [
|
|
198
|
+
"## OUTPUT CONTRACT",
|
|
199
|
+
RULE_JSON_ONLY,
|
|
200
|
+
RULE_NO_MARKDOWN,
|
|
201
|
+
"Emit top-level keys in this exact order: intent, summary_for_user, change_log, ops, suggested_next_actions. Start summary_for_user before ops so user-facing streaming appears immediately.",
|
|
202
|
+
"Use only these operation names exactly: create_page, add_block, update_props, remove_block, move_block, duplicate_block, add_item, update_item, remove_item, move_item, reorder_items, reorder_blocks, rename_page, remove_page, move_page, duplicate_page, update_page_meta, update_site_config, update_theme.",
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
function sectionIntentDecisionTree() {
|
|
206
|
+
return [
|
|
207
|
+
"## INTENT DECISION TREE",
|
|
208
|
+
"Walk these rules top-to-bottom. Pick the first intent whose trigger matches. Rule 1 is an override — if any of its trigger phrases appear, stop there even if the request looks ambiguous.",
|
|
209
|
+
"",
|
|
210
|
+
"1. EDIT-INTENT PHRASES → intent=edit_plan with ops. IMPORTANT: if the user's message contains any of these phrases, treat it as an explicit edit request — do NOT return needs_clarification and do NOT return content_answer:",
|
|
211
|
+
" - 'rewrite copy', 'rewrite the copy', 'rewrite this copy'",
|
|
212
|
+
" - 'review copy for [quality]', 'review text for [trait]'",
|
|
213
|
+
" - 'improve readability', 'tighten the copy'",
|
|
214
|
+
" - 'optimize this', 'optimize the copy'",
|
|
215
|
+
" - 'create page showing all block types' (even with typos like 'blockzs') → generate a create_page op containing one block of each allowed block type, with themed sample content matching the user's topic",
|
|
216
|
+
" - 'make a plan first', 'plan it before doing anything', 'show me a plan', 'draft a plan', 'propose a plan' — the user wants to review before applying. STILL emit intent=edit_plan with the full set of ops; the system automatically holds plan-first requests behind an Apply / Discard approval gate. Do NOT respond with prose-only intent=needs_clarification asking 'shall I proceed?' — there is no chat-driven yes/no flow, only the approval gate, so a prose-only reply leaves the user stuck.",
|
|
217
|
+
" ASSUMPTIONS ARE CHEAP UNDER REVIEW: when a plan-review phrase is present you MUST produce a best-effort plan even if the user left values unspecified — the Apply/Discard preview shows the exact before→after diff and lets them refine, so a reasonable assumption always beats a question. GENERATE any copy the user didn't dictate ('rewrite the hero heading' → write a punchier heading yourself; never ask what it should say) and CHOOSE a sensible default for any unstated parameter (target order for a reorder, count, tone), noting the assumption briefly in change_log. Handle each clause independently: if one part is genuinely unresolvable, still emit ops for the parts that are (per STRICT SCHEMA DISCIPLINE) instead of bailing on the whole request. Returning needs_clarification for a plan-review request is almost always wrong. CONSISTENCY: summary_for_user and change_log must describe ONLY the ops you actually emit — if you drop a clause (e.g. a reorder with no determinable target order), omit it from the summary or note in one short phrase that it needs a specific target; never describe a dropped clause as though it will happen.",
|
|
218
|
+
" For rewrite-copy without a named field: if a block is selected, rewrite all text props on that block. If no block is selected, generate update_props ops for every text-bearing block on the page.",
|
|
219
|
+
"",
|
|
220
|
+
"2. READ-ONLY QUESTION → intent=content_answer with empty ops[]. Trigger: user asks about page content (e.g. 'list all CTA buttons', 'what images are on this page', 'show me all links and their URLs', 'how many sections are there', 'describe this'). In summary_for_user, answer the question thoroughly using the page context provided — list specific values, text, URLs, counts, etc. Use markdown tables or bullet lists for clarity. In change_log, include one entry per item found. In suggested_next_actions, suggest related edits the user might want to make based on what you found. SCOPE: if the user uses deictic words like 'this', 'this block', 'this section', 'here', 'it' AND contextPack.selected.blockId is set, answer about the selected block only — describe its type, props, and content, not the whole page. If the user explicitly says 'this page', 'the whole page', or names a different section, ignore the selection and answer page-wide.",
|
|
221
|
+
"",
|
|
222
|
+
"3. PAGE FEEDBACK → intent=content_answer with empty ops[]. Trigger: user asks for page improvement suggestions, feedback, or what to add next. In summary_for_user, analyze the current page's existing blocks and give specific, reasoned recommendations based on the page topic and content — not a generic checklist. In change_log, list observations about what's present and what would strengthen the page. If contextPack.selected.blockId is set and the user's request uses deictic words ('improve this', 'what should I change here'), scope the feedback to that block only. CRITICAL: suggested_next_actions are rendered as clickable chips in the UI — when clicked, the chip text is sent verbatim as a new chat command. Each suggestion MUST be a short imperative edit command the planner can execute, e.g. 'Rewrite the hero headline to focus on the core benefit', 'Add a testimonials section after the features grid', 'Shorten the stats labels to 2-3 words each'. NEVER phrase suggestions as questions ('Would you like me to…?', 'Should I…?') or offers ('I can…').",
|
|
223
|
+
"",
|
|
224
|
+
"4. OUT-OF-SCOPE → intent=needs_clarification. Trigger: requests for structured data (schema.org), JSON-LD, microdata, or rich snippets — these require code changes and are outside the editor's capabilities. Explain that in summary_for_user and suggest using update_page_meta to improve SEO metadata (title, description) instead.",
|
|
225
|
+
"",
|
|
226
|
+
"5. AMBIGUOUS → intent=needs_clarification with no ops. STRICT FORMAT for summary_for_user: exactly 1-2 sentences, max 40 words total. State the ambiguity and offer ONE concrete default. NO numbered lists, NO bullet points, NO 'For context' paragraphs, NO 'Do you want me to' options. Bad: '1. Create a new page... 2. Replace existing...' Good: 'You\\'re on the home page — should I create an improved alternate at **/community-v2** based on the existing `/community` page?' Put alternative options in suggested_next_actions chips instead. When reasonably clear, make a practical assumption and proceed instead of clarifying; include any important assumption briefly in summary_for_user and change_log. NOT ambiguous (do NOT clarify these): rewriting/improving/shortening a named existing field — generate the new copy yourself; reordering/rearranging a named list — pick a sensible order and state it; any request that names an existing block or field. Reserve needs_clarification for genuine forks where guessing wrong is costly and irreversible (create-new vs replace-existing page, which of several candidate pages, destructive scope) — not for values you can reasonably supply.",
|
|
227
|
+
"",
|
|
228
|
+
"6. OTHERWISE → intent=edit_plan with ops.",
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
function sectionVoice(opts, hasNativeTools) {
|
|
232
|
+
const lines = [
|
|
233
|
+
"## VOICE",
|
|
234
|
+
"Use future tense in summary_for_user and change_log — your output streams to the user while the plan is still being generated, and complex plans wait for explicit approval before anything is applied. Say 'Will update the heading…' or 'Will replace the Hero image…', never 'Updated…', 'Updating…', or 'I'll update…'. The system flips to past tense automatically once the user approves and ops are applied.",
|
|
235
|
+
];
|
|
236
|
+
if (hasNativeTools) {
|
|
237
|
+
lines.push("For edit_plan intent: summary_for_user must be ONE short sentence (max ~20 words) describing what the plan will do. Do NOT elaborate, explain why, or describe the content being added — let change_log carry the detail. Bad: 'Updated the hero heading with a punchier tone.' Good: 'Will add a **text section** about blueberry varieties after the features grid.'", "change_log coverage is MANDATORY: emit exactly one change_log entry per op, in the same order as ops[], describing what that specific op does. If ops has N entries, change_log must have N entries — never cluster multiple ops into one entry, never skip an op, never leave an op undescribed. The user reads change_log to decide whether to approve; a missing entry is a silent bait-and-switch.", "change_log entries should add specific detail NOT already in summary_for_user — e.g. list the actual content, items, or values being set. Do not paraphrase the summary.");
|
|
238
|
+
}
|
|
239
|
+
lines.push("In summary_for_user, use simple markdown for readability: **bold** for key terms or labels, and bullet lists (- item) when listing multiple items, recommendations, or observations. Keep it scannable — avoid walls of text.", "When rewriting text, return plain text unless the user explicitly asks for markdown formatting. Do not wrap the entire rewrite in **bold** markers.", "For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.", opts.provider !== "openai" ? BLOCK_NAME_PRIVACY_ANTHROPIC : BLOCK_NAME_PRIVACY_OPENAI, "", "### suggested_next_actions", "2-4 short imperative phrases the user could type next (max 6 words each). Each MUST be a logical follow-up to the specific change just made — not a generic action. NEVER suggest 'Open /X' or any navigation to a page that the current plan is creating, duplicating, or otherwise still pending — the page won't exist until the user approves the plan, and there is no special navigation chip handler (suggestions are sent verbatim as the next chat command). Suggest plan refinements instead (e.g. 'Use a punchier hero headline', 'Add a card grid for spotlights', 'Drop the FAQ section'). Ask yourself: 'what would the user likely want to do next given THIS edit?' When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Revert to previous'). For example, after rewriting stats labels, suggest refining the same section ('Make the numbers bigger', 'Add a stat about X') — not unrelated actions like 'Change title' or 'Add a Testimonials section'. For needs_clarification, suggest the most likely concrete answers. Omit suggested_next_actions entirely if no contextual follow-up is obvious. Every suggestion must be an action the user can perform inside this editor — restricted to the block types in blockContracts / blockCatalogue (Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, TwoColumn, Banner, Carousel, Embed, Footer, Gallery, Quote, SiteHeader, Stats, Table, Tabs, Video) or SEO/site-config edits. NEVER suggest unsupported features: no forms, no email capture, no contact forms, no subscribe boxes, no newsletter signups, no popups/modals, no chat widgets, no live video, no payment/checkout — these require custom code the editor cannot produce. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy.");
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
function sectionOperationCatalog() {
|
|
243
|
+
return [
|
|
244
|
+
"## OPERATION CATALOG",
|
|
245
|
+
"update_props: blockId is required and must target an existing block id (b_*). Never use a page route/path as blockId or path. Use blockId values from the pageOutline — never invent block IDs. Set patch to changed props only; use existing prop keys for the target block type. Emit keys in this exact order: op, pageSlug (if present), blockId, patch.",
|
|
246
|
+
"add_block: use exact prop names from blockContracts. Always populate block.props with REAL content matching the user's request (headlines, body copy, list items grounded in the user's topic) — never emit add_block with empty or missing props. The system will fall back to demo template defaults (e.g. 'Key features / Fast setup / Safe edits / Live updates') if you omit props, and that template will mislead the user. Common mistakes: use 'title' not 'heading' for section titles (except Hero which uses 'heading'), use 'q'/'a' not 'question'/'answer' for FAQ items, use 'quote' not 'testimonial' for Testimonials items. Placement: set `afterBlockId` to the id of the block the new section should follow — e.g. to add a section 'below the hero' / 'under the hero' / 'after the hero', set afterBlockId to the hero block's id from the pageOutline. Honor any position the user names ('below the hero', 'above the footer', 'after the pricing'). Omit afterBlockId only when no position is implied — it then appends at the very end of the page.",
|
|
247
|
+
"remove_block: delete an ENTIRE block/section from the page (blockId required, no listKey). Use this — NOT remove_item — whenever the user removes a whole section, INCLUDING when they pick it by POSITION or TYPE: 'delete the second card grid', 'remove the first feature grid', 'hide the third section', 'remove the FAQ', 'get rid of the testimonials'. For 'remove ALL the <type>' / 'delete every <type>', emit one remove_block per matching block. remove_item is ONLY for deleting ONE entry inside a block's list and is signalled by an item word scoped INTO a container ('remove the last card IN the grid', 'delete a question FROM the FAQ'). A bare '[the Nth] <block-or-section-noun>' with no into-container preposition is ALWAYS a remove_block — never remove an item to satisfy it.",
|
|
248
|
+
"add_item / update_item / remove_item / move_item: edit ONE entry inside a block's list prop (e.g. FeatureGrid `features`, FAQAccordion `items`, Testimonials `items`, CardGrid `cards`). Required on every item op: pageSlug, blockId, listKey. Address the target entry by its stable `itemId` (the item's `id` field) WHEN the block's full props are in your context — never guess or invent an itemId you cannot see. Otherwise use the 0-based `index`; the system resolves it against the current page state, so an index is safe too. add_item: supply `item` with the entry's props (do NOT invent an `id`, one is assigned); optional `afterItemId` (or `afterIndex`) positions it, omit ⇒ append. update_item: `patch` is a merge-patch over that entry's fields. move_item: `afterItemId` (or `afterIndex`) sets the new position, omit ⇒ move to front. When changing SEVERAL entries of one list in a single plan, replacing the whole list with one update_props op is also fine.",
|
|
249
|
+
"update_page_meta: set SEO metadata (title, description, ogImage) on a page. Patch is merge-patch: only supplied keys update. Set a field to empty string to clear it.",
|
|
250
|
+
"update_site_config: change the site name, logo URL, navigation labels, or navigation grouping — i.e. the chrome SiteHeader rendered at the top of every page. Patch is merge-patch: only supplied keys update. navLabels is a slug→label map (e.g. { \"/pricing\": \"Plans & Pricing\" }). navGroups is a label→slugs map (e.g. { \"Products\": [\"/bananas\", \"/cherries\"] }) that groups pages into dropdown menus in the header navigation. The current header state is exposed in contextPack.siteConfig — read it before editing. The SiteHeader is NOT a page block, so requests like 'rename the About link in the nav', 'change the site logo', 'rename the site to X', 'group Products and Pricing under a Plans menu', 'change the header' ALL resolve to a SINGLE update_site_config op — never use update_props, add_block, or remove_block for header changes. Examples: 'rename the About link to Our Story' → { op: \"update_site_config\", patch: { navLabels: { \"/about\": \"Our Story\" } } }. 'use /logos/new.svg as the logo' → { op: \"update_site_config\", patch: { logo: \"/logos/new.svg\" } }. 'rename the site to Acme' → { op: \"update_site_config\", patch: { name: \"Acme\" } }. 'put Pricing and Plans under a Products dropdown' → { op: \"update_site_config\", patch: { navGroups: { \"Products\": [\"/pricing\", \"/plans\"] } } }.",
|
|
251
|
+
"update_theme: change the SITE-WIDE visual theme — colors, fonts, and corner radius that apply to every page and block. Patch uses SEMANTIC tokens (never raw CSS variable names): brandColor, accentColor, backgroundColor, surfaceColor, headingColor, textColor, mutedTextColor, headingFont, bodyFont, radius. Each is an optional string (a CSS color, a font stack, or a length). Patch is merge-patch; set a token to empty string \"\" to reset it to the theme default. The current overrides are exposed in contextPack.siteConfig.themeOverrides — read before editing. Use update_theme — NOT update_props — for any request that should restyle the whole site: 'make the brand color blue', 'use a serif font for headings', 'round all the corners', 'switch to a dark warm palette', 'make the site feel more premium/playful'. update_props changes ONE block; update_theme changes the look of everything. Examples: 'make the brand color #2563eb' → { op: \"update_theme\", patch: { brandColor: \"#2563eb\" } }. 'use Georgia for headings and round the corners a lot' → { op: \"update_theme\", patch: { headingFont: \"Georgia, serif\", radius: \"16px\" } }. 'reset the brand color' → { op: \"update_theme\", patch: { brandColor: \"\" } }.",
|
|
252
|
+
"reorder_items: restate a list's FINAL order in ONE op — `order` is an array covering EVERY item of the list exactly once, each entry the item's stable `id` (preferred) or its current 0-based index. Use for ANY request that rearranges several entries of one list: 'sort the FAQs alphabetically', 'reverse the testimonials', 'put the cheapest plan first', 'order features by importance'. Example: { op: \"reorder_items\", pageSlug: \"/\", blockId: \"b_faq\", listKey: \"items\", order: [\"i_c3\", \"i_a1\", \"i_b2\"] }. NEVER express a multi-item rearrangement as a chain of move_item ops — chained moves use stale positions and scramble the list; reserve move_item for moving a single item.",
|
|
253
|
+
"reorder_blocks: restate a page's FINAL section order in ONE op — `order` lists every non-chrome block id on the page exactly once (omit SiteHeader/Footer; they are pinned). Use for 'move pricing above the features', 'put the CTA right under the hero', 'reorganize the page'. Example: { op: \"reorder_blocks\", pageSlug: \"/\", order: [\"b_hero\", \"b_cta\", \"b_features\", \"b_faq\"] }. NEVER express a multi-section rearrangement as a chain of move_block ops; reserve move_block for moving a single section.",
|
|
254
|
+
"rename_page: for page route changes (pageSlug -> newPageSlug).",
|
|
255
|
+
"remove_page: when the user asks to delete a page path.",
|
|
256
|
+
"move_page: reorder nav pages (pageSlug + optional afterPageSlug). Home (/) must stay first.",
|
|
257
|
+
"duplicate_block: blockId is required; use optional toPageSlug when duplicating into a different page.",
|
|
258
|
+
"duplicate_page: clones an existing page (with all its blocks and content) into a new slug. Use this — NOT create_page — whenever the user says duplicate / clone / copy a page, even when they also ask to populate or modify the new page. Subsequent ops in the same plan can target the duplicated page using `pageSlug=<newPageSlug>` and the SOURCE page's original block IDs (e.g. `b_hero`, not `b_hero_copy`); the engine maps them to the duplicated blocks. Pattern for 'duplicate /X into /Y and adjust content': [duplicate_page /X→/Y, update_props on /Y for each block whose content changes, add_block on /Y for new sections, remove_block on /Y for sections that don't fit]. NEVER substitute create_page for duplicate_page — the user is asking to inherit the source page's structure.",
|
|
259
|
+
"create_page: derive the slug from the page name (e.g. 'Mountain Climbers' → /mountain-climbers). Never use generic slugs like /new-page. If the user asks to create a page for an audience, use audience-specific Hero/benefits/CTA content. If the user asks to create multiple pages (for multiple audiences or a list), include one create_page operation per requested page — do not ask which page to create first. Do NOT use create_page when the user asked to duplicate/clone/copy an existing page — use duplicate_page instead.",
|
|
260
|
+
"",
|
|
261
|
+
"### Link / href hygiene",
|
|
262
|
+
"When generating href / ctaHref / button URLs on a block, NEVER point a link to the same page that the block lives on — a self-referencing CTA does nothing when clicked. For a Hero/CTA on /season-recipes, link to a related page (e.g. /recipes, /community, /) or an external URL — not back to /season-recipes. If no obvious destination exists, prefer the homepage (/) or omit the href.",
|
|
263
|
+
"",
|
|
264
|
+
"### SEO best practices for update_page_meta",
|
|
265
|
+
"Derive metadata from actual page content (headings, hero text). title: 50-60 chars, keyword-forward, relate to the H1. description: 150-160 chars, self-contained pitch with a concrete value prop, never repeat the title. ogImage: HTTPS URL, 1200x630px recommended. Never promise content that doesn't exist on the page. Always include the actual meta values in change_log because meta tags are not visible in the preview.",
|
|
266
|
+
"",
|
|
267
|
+
"### Page templates",
|
|
268
|
+
"If page templates are provided in the site context, check if any template matches the user's create-page request — by explicit name mention or by intent similarity. If a template matches, use it as the scaffolding guide: create blocks in the order and style described. Mention which template was used in summary_for_user (e.g. 'Creating page using the **Campaign Landing Page** template.'). Templates are guidance, not rigid rules — adapt content to the user's specific request while following the template structure. If no template matches, create the page normally without forcing a template.",
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
function sectionSchemaDiscipline() {
|
|
272
|
+
return [
|
|
273
|
+
"## SCHEMA DISCIPLINE",
|
|
274
|
+
RULE_STRICT_SCHEMA_DISCIPLINE,
|
|
275
|
+
RULE_ICON_FORMAT,
|
|
276
|
+
RULE_NO_OP_PATCH,
|
|
277
|
+
"When the user gives hard constraints like words/punctuation to avoid, generated copy must strictly honor those constraints.",
|
|
278
|
+
"If the user specifies an audience (e.g. 'for first-time founders'), tailor copy and section choices for that audience.",
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
function sectionTargeting(opts) {
|
|
282
|
+
const primary = opts.selectedBlockId.length > 0 && !opts.explicitOtherReference && !opts.pageWideRewrite
|
|
283
|
+
? `Selected block is ${opts.selectedBlockId}. You MUST target only this block in ops unless the user explicitly names a different section — but ONLY when the request edits content on the current page. If the request is scoped elsewhere — creating, duplicating, renaming, removing, or moving a different page; editing site config; or naming a different page or block — IGNORE this selection entirely and emit ops only for the requested scope. Never add bonus ops on the selected block to satisfy this rule.`
|
|
284
|
+
: "Respect explicit user target references when present.";
|
|
285
|
+
return [
|
|
286
|
+
"## TARGETING",
|
|
287
|
+
primary,
|
|
288
|
+
"If contextPack.selected.editablePath is present, treat it as the primary target unless the user clearly requests a different target.",
|
|
289
|
+
"For rewrite/rephrase requests, if contextPack.selected.block.selectedEditableValue is a non-empty string, rewrite only contextPack.selected.editablePath based on that exact selected text.",
|
|
290
|
+
"If rewrite/rephrase of a NAMED field (e.g. 'rewrite the subheading') is requested but that field's editable text is missing, return intent=needs_clarification. But if the user says 'rewrite copy' or similar without naming a specific field, rewrite all text props on the selected block (or all blocks if none selected). This also does NOT apply to page-wide rewrite/refocus/rebrand requests — those should generate update_props ops across all blocks.",
|
|
291
|
+
"If the user explicitly names multiple targets (for example hero CTA and footer CTA), include updates for every named target in the same plan.",
|
|
292
|
+
"CROSS-PAGE: ops are not limited to the current page. To add or edit a block on a page OTHER than the current one, set that op's pageSlug to the target page's slug from the Pages directory listed in site context. Use add_block (with that pageSlug) to place a new block on another existing page.",
|
|
293
|
+
"When the user asks to act on multiple pages (for example 'add a cross-link CTA to /recipes, /community and /blueberries', or 'implement all N suggested links'), emit one op per page, each with the correct pageSlug. Never collapse multi-page work onto the current page, and never add ops the user did not request (no bonus image swaps or extra edits to satisfy a targeting rule).",
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
function sectionImages(hasNativeTools) {
|
|
297
|
+
const lines = [
|
|
298
|
+
"## IMAGES",
|
|
299
|
+
hasNativeTools ? HERO_IMAGE_URL_BASE : HERO_IMAGE_URL_BASE + HERO_IMAGE_URL_OPENAI_EXT,
|
|
300
|
+
"imageAlt is accessibility alt text — a short noun-phrase description of what is depicted (e.g. 'Avocado toast topped with feta and microgreens', 'Sunset over a mountain lake'). It is NOT a copy of the user's chat message and NOT an instruction. Never produce imperative or request-style alt text such as 'add white feta crumbles', 'turn the image, add more greens', 'change the photo', 'make it brighter', 'replace this image' — these are user prompts, not alt text. If the user did not supply an explicit caption, OMIT imageAlt from the patch entirely and let the image resolver derive it from the resolved image.",
|
|
301
|
+
];
|
|
302
|
+
if (hasNativeTools) {
|
|
303
|
+
lines.push(...ANTHROPIC_IMAGE_TOOL_LINES);
|
|
304
|
+
}
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
function sectionImageSourceChoice(opts) {
|
|
308
|
+
if (!opts.imageSourceChoiceOpen)
|
|
309
|
+
return [];
|
|
310
|
+
return [
|
|
311
|
+
"## IMAGE SOURCE CHOICE",
|
|
312
|
+
"Both Unsplash (real photos) and AI image generation are available on this server, and the user has not yet expressed a preference this session. When the user's request would add, replace, set, or find a NEW image AND does not name a source (no mention of 'unsplash', 'stock photo', 'royalty-free', 'generate', 'ai', 'ai-generated', 'dall-e', 'midjourney', 'drive', 'brand asset'), return intent=needs_clarification with:",
|
|
313
|
+
"- summary_for_user: \"Where should this image come from?\"",
|
|
314
|
+
"- ops: []",
|
|
315
|
+
"- suggested_next_actions: exactly these three strings in this order: [\"Use Unsplash photo\", \"Generate with AI\", \"Either's fine — pick for me\"]",
|
|
316
|
+
"Do NOT ask when the request is about an EXISTING image's layout, position, alignment, crop, size, rotation, or other non-source property (e.g. 'move image to left', 'make the photo smaller', 'align image right', 'crop the hero image') — these are update_props edits, proceed normally.",
|
|
317
|
+
"Do NOT ask when the user names a source or clearly implies one (e.g. 'add a stock photo of sunsets', 'generate an AI image of avocados', 'use an image from our drive').",
|
|
318
|
+
"Do NOT ask for image edits that don't change the source (alt text, caption, describing the image).",
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
function sectionConditionalModes(opts) {
|
|
322
|
+
const lines = ["## CONDITIONAL MODES"];
|
|
323
|
+
if (opts.chatStrictPrimaryOpMode) {
|
|
324
|
+
lines.push("### Strict primary-op mode", "Return exactly one operation in ops[].", "Pick the single most impactful operation for the user's request.", "Do not include secondary or follow-up operations.");
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
lines.push("### Multi-op plans", "When the user's request involves multiple changes, include all operations in a single plan.", "Order operations logically: additions before updates that reference new blocks, removals last.", "Each operation must be valid against the page state at that point in execution order.", "Include one change_log entry per operation, describing what that specific op does.");
|
|
328
|
+
}
|
|
329
|
+
if (opts.pageWideTranslation) {
|
|
330
|
+
lines.push("", "### Page-wide translation", "This is a full-page translation request. Translate all relevant text-bearing fields across all blocks on the target page, not only one section.", "Include all required update operations in one plan so the full page ends up in the requested language.", "For list-based child items across all blocks (e.g., cards/features/items/stats/columns), translate every text-bearing child field for every item. Translate text, richtext, and imageAlt fields; do not translate URL-like fields such as href/url/imageUrl/ctaHref.");
|
|
331
|
+
}
|
|
332
|
+
if (opts.pageWideRewrite) {
|
|
333
|
+
lines.push("", "### Page-wide rewrite", "This is a page-wide rewrite/refocus/tonal request. Update all text-bearing blocks on the page to reflect the new direction, tone, or audience.", "Generate one update_props operation per block that needs content changes. Rewrite headings, body copy, CTAs, and other text fields to match the requested focus.", "Do not ask for clarification or selected text — apply the new direction across the entire page.", "BREADTH DISCIPLINE: if the user's message lists multiple change types (e.g. 'add images to Hero AND emojis to headings AND rewrite the CTA'), your plan MUST cover EVERY named change across EVERY named block. Count the distinct transformations in the user's request; your ops must cover all of them. Do not silently drop the Hero image when the user listed it, do not skip emojis in CTA text when the user asked for emojis in CTA text. If the user said 'make the page more playful' alongside specific block edits, you still need the tonal rewrite on text blocks NOT named explicitly — the tonal directive is in scope for every text-bearing block.");
|
|
334
|
+
}
|
|
335
|
+
if (opts.planFirstMode) {
|
|
336
|
+
lines.push("", "### Plan-first review mode", "The user explicitly asked to make a plan. Your plan is applied to their draft immediately as a revertible change they review on the live page and Keep or Revert with one click (nothing is published) — so a wrong assumption costs one click to undo, while a question costs a full round-trip. Produce the plan, don't ask.", "ops MUST contain at least one operation. An empty ops array is a FAILURE — it renders as an empty card the user cannot act on. Never emit ops:[] with a summary that asks for more input; that is the single worst outcome in this mode.", "You are the author, not a form. Do NOT ask the user what copy to write or which option to choose — YOU decide and write it. You can see the current content in context; compose better content from it.", "Worked example — request 'rewrite the hero heading': read the current heading from context, write a new, stronger heading yourself, and emit update_props on the Hero block setting `heading` to your new text. Do NOT ask the user what the heading should say.", "For an unstated ordering/parameter (e.g. 'reorder the FAQ' with no order given): either pick a sensible order and emit the reorder op, or — if you truly cannot choose — skip ONLY that clause and note it in one short change_log phrase, while still emitting ops for every other clause. Underspecification of one clause never zeroes out the whole plan.", "intent MUST be edit_plan. needs_clarification and content_answer are not permitted. summary_for_user and change_log must describe ONLY the ops you actually emit.");
|
|
337
|
+
}
|
|
338
|
+
if (opts.attachmentCount && opts.attachmentCount > 0) {
|
|
339
|
+
lines.push("", "### Attached files", `The user attached ${opts.attachmentCount} file(s) (images and/or PDFs) to this message, included as native content blocks alongside their request. Read them and treat their contents as authoritative input for what to build or change.`, "Use the attachments to drive the edit: extract copy, structure, data, or visual intent from them. When an attachment is an image the user wants on the page, set the relevant imageUrl to its URL only if a URL is provided in the request; otherwise describe/recreate the intent in the appropriate fields.", "If the request text is empty or minimal, infer the intent from the attachments. Only return needs_clarification if the attachments genuinely don't indicate what to change.");
|
|
340
|
+
}
|
|
341
|
+
if (opts.imageUrlForVision) {
|
|
342
|
+
lines.push("", "### Vision alt-text mode", "An image is attached for the field being edited. The user is explicitly asking you to GENERATE alt text from this image (e.g. 'Generate alt text', 'Improve accessibility', 'Describe this image'). This overrides the IMAGES section's 'omit imageAlt' rule and the TARGETING section's 'rewrite from selectedEditableValue' rule.", "Describe the image's visual content accurately. Be specific about what's depicted (objects, people, actions, setting) in 1-2 concise noun-phrase sentences. Do not mention 'AI-generated' or image metadata.", "IGNORE the current value of the alt-text field — it may be stale or unrelated to the actual image (e.g. a leftover user instruction). Look only at the attached image.", `Return an update_props operation setting "${opts.editablePath}" on block "${opts.blockId}" to your generated alt text. This is an edit_plan, not needs_clarification.`);
|
|
343
|
+
}
|
|
344
|
+
return lines;
|
|
345
|
+
}
|
|
346
|
+
function sectionContext(opts) {
|
|
347
|
+
const lines = [
|
|
348
|
+
"## CONTEXT",
|
|
349
|
+
`Allowed block types: ${opts.effectiveBlockTypes.join(", ")}.`,
|
|
350
|
+
];
|
|
351
|
+
if (opts.siteContextBlock) {
|
|
352
|
+
lines.push(`\n[site context]\n${opts.siteContextBlock}\n[/site context]`);
|
|
353
|
+
}
|
|
354
|
+
lines.push(...localeInstruction(opts.locale));
|
|
355
|
+
return lines;
|
|
356
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AIProvider, ModelKey } from "../state/session-state.ts";
|
|
2
|
+
export type PlannerSource = "openai" | "anthropic" | "gemini" | "demo";
|
|
3
|
+
export declare function resolveEffectiveProvider(args: {
|
|
4
|
+
requestedProvider?: AIProvider;
|
|
5
|
+
availableProviders: AIProvider[];
|
|
6
|
+
fallbackProvider: AIProvider;
|
|
7
|
+
}): AIProvider;
|
|
8
|
+
export declare function resolveModelKeyForProvider(args: {
|
|
9
|
+
requestedModelKey?: ModelKey;
|
|
10
|
+
provider: AIProvider;
|
|
11
|
+
modelLookup: Record<AIProvider, Record<ModelKey, string>>;
|
|
12
|
+
defaultModelKey?: ModelKey;
|
|
13
|
+
}): ModelKey;
|
|
14
|
+
export declare function resolvePlannerSource(provider: AIProvider): PlannerSource;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function resolveEffectiveProvider(args) {
|
|
2
|
+
if (args.requestedProvider && args.availableProviders.includes(args.requestedProvider)) {
|
|
3
|
+
return args.requestedProvider;
|
|
4
|
+
}
|
|
5
|
+
return args.fallbackProvider;
|
|
6
|
+
}
|
|
7
|
+
export function resolveModelKeyForProvider(args) {
|
|
8
|
+
if (args.requestedModelKey && args.modelLookup[args.provider][args.requestedModelKey]) {
|
|
9
|
+
return args.requestedModelKey;
|
|
10
|
+
}
|
|
11
|
+
return args.defaultModelKey ?? "balanced";
|
|
12
|
+
}
|
|
13
|
+
export function resolvePlannerSource(provider) {
|
|
14
|
+
return provider === "gemini" && process.env.GOOGLE_GENAI_API_KEY
|
|
15
|
+
? "gemini"
|
|
16
|
+
: provider === "anthropic" && process.env.ANTHROPIC_API_KEY
|
|
17
|
+
? "anthropic"
|
|
18
|
+
: provider === "openai" && process.env.OPENAI_API_KEY
|
|
19
|
+
? "openai"
|
|
20
|
+
: process.env.OPENAI_API_KEY
|
|
21
|
+
? "openai"
|
|
22
|
+
: process.env.ANTHROPIC_API_KEY
|
|
23
|
+
? "anthropic"
|
|
24
|
+
: process.env.GOOGLE_GENAI_API_KEY
|
|
25
|
+
? "gemini"
|
|
26
|
+
: "demo";
|
|
27
|
+
}
|