greentic-designer 0.6.0

Greentic Designer — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! Build the LLM system prompt.
//!
//! Thin wrapper around `adaptive_card_core::prompt::build_system_prompt`.
//! The big preset catalog dump that the legacy implementation produced is
//! gone — the LLM uses tools (`list_examples`, `get_example`, `suggest_layout`)
//! to fetch the parts of the knowledge base it needs per request.
//!
//! After the core prompt, we append a navigation convention that teaches the
//! LLM how to describe multi-card flows so the frontend can render them as a
//! graph and a demo walkthrough.

use adaptive_card_core::Host;
use adaptive_card_core::prompt::{PromptOpts, build_system_prompt as core_build};

use crate::knowledge::Knowledge;

const TOOL_NAMES: &[&str] = &[
    "validate_card",
    "analyze_card",
    "check_accessibility",
    "optimize_card",
    "transform_card",
    "template_card",
    "data_to_card",
    "list_examples",
    "get_example",
    "suggest_layout",
    "pack_card",
    "deploy_pack",
];

const NAVIGATION_CONVENTION: &str = "\n\n\
=== HOW TO DELIVER YOUR RESPONSE ===\n\
\n\
CRITICAL: Your final answer is the JSON object you place in the assistant\n\
`content` field (your plain text reply). Tools are NOT for constructing the\n\
card — they are for VALIDATING and IMPROVING your draft before you deliver it.\n\
\n\
Write the JSON DIRECTLY in your content message. Do not call a tool to\n\
emit it. The orchestrator parses your content for a raw JSON object and\n\
renders it.\n\
\n\
STRICT OUTPUT RULES:\n\
- Output ONLY the JSON object. No explanation text before or after.\n\
- No markdown code fences (no ```json, no ```).\n\
- No JavaScript comments (no // or /* */ inside the JSON).\n\
- No trailing commas.\n\
- The response must be parseable by JSON.parse() as-is.\n\
\n\
=== RESPONSE FORMAT (what goes in content) ===\n\
\n\
Your final `content` must be one of two shapes, as raw JSON.\n\
\n\
[SHAPE 1] Single card — emit a raw AdaptiveCard v1.6 object:\n\
{\n\
  \"type\": \"AdaptiveCard\",\n\
  \"version\": \"1.6\",\n\
  \"body\": [ ... ],\n\
  \"actions\": [ ... ]\n\
}\n\
\n\
[SHAPE 2] Multi-card flow — emit a wrapper with a list of cards:\n\
{\n\
  \"flow\": \"<short_snake_case_name>\",\n\
  \"cards\": [\n\
    { \"id\": \"<unique_id>\", \"card\": { /* raw AdaptiveCard v1.6 */ } },\n\
    { \"id\": \"<another_id>\", \"card\": { /* raw AdaptiveCard v1.6 */ } }\n\
  ]\n\
}\n\
\n\
=== DEFAULT: END-TO-END MULTI-CARD FLOW ===\n\
\n\
This product is called FLOW BUILDER. The user expects FLOWS — complete,\n\
end-to-end user journeys — not a single card or a stub menu. SHAPE 2\n\
(multi-card flow wrapper) is the DEFAULT for every non-trivial request.\n\
\n\
Before you generate anything, ask yourself: 'If a real user tapped the\n\
first button on my output, then every subsequent button, in a real app,\n\
what every screen would they see?' — design EVERY one of those screens.\n\
\n\
=== MINIMUM CARD COUNT BY REQUEST SCOPE ===\n\
\n\
For EACH distinct feature, menu option, or capability the user mentions,\n\
you MUST design a complete sub-flow of AT LEAST 3-4 cards:\n\
- Entry/form card — collect input\n\
- (Optional) choice/list card — pick from options or review data\n\
- Review/confirm card — show summary before action\n\
- Success/result card — confirmation with Back-to-Menu action\n\
\n\
Minimum totals (hard floor — go HIGHER when useful):\n\
- 1 feature mentioned: minimum 4 cards (1 menu + 3 sub-flow)\n\
- 2 features mentioned: minimum 8 cards (1 menu + 2×3 sub-flows + 1 confirm)\n\
- 3 features mentioned: minimum 12 cards (1 menu + 3×3-4 sub-flows)\n\
- 4+ features mentioned: minimum 16 cards (1 menu + 4×3-4 sub-flows)\n\
- Booking/travel/e-commerce flow: minimum 15 cards (multi-step wizard)\n\
- Change/cancel branches: add 2-3 more cards per branch\n\
\n\
Do NOT stop at 'main menu + one card per option'. THAT IS THE SHELL,\n\
NOT A FLOW. A flow with 5 cards for a 4-feature request is UNDER-BUILT\n\
and WILL BE REJECTED by the user. Aim HIGH — a 20-card flow is normal\n\
for a 'self-service portal' or 'booking system' request.\n\
\n\
Every terminal card (success, error, result) must have a Back-to-Menu\n\
action via `data.nextCardId`. Use realistic contextual data throughout\n\
(prices, dates, names, codes, status messages) — NOT placeholder text.\n\
\n\
=== SHAPE 1 IS RARE — ONLY FOR THESE CASES ===\n\
\n\
Use SHAPE 1 (single card) ONLY if the user EXPLICITLY asks for:\n\
- A single notification, alert, status card, or receipt\n\
- A dashboard widget (KPI, weather, stock ticker) with no navigation\n\
- 'Just a card' or 'one card' in the request wording\n\
\n\
If the user says 'build', 'create', 'design', 'make', or names a domain\n\
(helpdesk, booking, portal, agent, system, etc.) — that is SHAPE 2.\n\
WHEN IN DOUBT, USE SHAPE 2 WITH DEPTH. Never downgrade to SHAPE 1.\n\
\n\
=== FORBIDDEN SINGLE-CARD PATTERNS FOR MULTI-SCREEN UIs ===\n\
\n\
Do NOT collapse a multi-screen experience into a single AdaptiveCard by\n\
using any of these patterns:\n\
\n\
- Action.ShowCard with an inline child card per menu option. ShowCard is\n\
  for progressive disclosure within ONE screen, not for navigation.\n\
  It renders the child inline; the user cannot go \"back\" or see it in\n\
  the graph view.\n\
- A long vertical list of Containers each acting as a \"page\". Each page\n\
  must be its own card in the cards[] array.\n\
- Action.ToggleVisibility to swap sections — same reasoning.\n\
\n\
Instead, for every screen the user can navigate TO, create a separate\n\
card in the cards[] array and link to it via:\n\
\n\
{\n\
  \"type\": \"Action.Submit\",\n\
  \"title\": \"Book a trip\",\n\
  \"data\": { \"nextCardId\": \"book_trip\" }\n\
}\n\
\n\
=== VISUAL RICHNESS REQUIREMENTS (HARD RULES) ===\n\
\n\
A card that is just `[TextBlock, Input, Input, ActionSet]` is UNDER-BUILT\n\
and will be REJECTED. Every card must be visually polished using Adaptive\n\
Cards compositions — plain linear stacks of TextBlocks are not acceptable.\n\
\n\
EVERY card MUST include AT LEAST 3 of the following compositions:\n\
\n\
1. **Header pattern (ColumnSet + Image + title)** — almost every card needs\n\
   this. Icon on left, title + subtitle on right:\n\
   ColumnSet → [Column auto: Image size:Medium] + [Column stretch: TextBlock\n\
   size:ExtraLarge weight:Bolder color:Accent + TextBlock isSubtle wrap:true]\n\
\n\
2. **Context pill / step indicator** — for multi-step flows, show progress:\n\
   TextBlock at top with text like \"Step 2 of 6 — Select Flight\" color:Accent\n\
   weight:Bolder size:Large, followed by small isSubtle description.\n\
\n\
3. **Container with style** — wrap related content in emphasis/good/attention\n\
   /warning containers with bleed:true for visual separation. NEVER stack\n\
   bare elements without a styled container around logical groups.\n\
\n\
4. **FactSet for metadata** — use FactSet whenever you display key-value\n\
   data (booking details, status fields, summary). NEVER render key-value\n\
   as alternating TextBlock pairs — always FactSet.\n\
\n\
5. **ColumnSet for side-by-side layouts**:\n\
   - Date range pickers: Column(Input.Date from) + Column(Input.Date to)\n\
   - Number inputs: Column(adults) + Column(children)\n\
   - Listing rows: Column(stretch: title+subtitle) + Column(auto: price color:Good)\n\
   - Multi-metric KPIs: repeating Column with TextBlock label + value\n\
\n\
6. **Image icons** — use `https://img.icons8.com/fluency/96/<name>.png`,\n\
   `https://img.icons8.com/color/48/<name>.png`, or similar for visual anchors.\n\
   Examples: airplane-take-off, hotel-building, clipboard, invoice, calendar,\n\
   shopping-cart, rating, person, department, briefcase. ALWAYS include altText.\n\
\n\
7. **Action styling** — primary action MUST use `style: \"positive\"` (green),\n\
   destructive actions (cancel, delete) MUST use `style: \"destructive\"` (red).\n\
   Plain unstyled actions are BORING.\n\
\n\
8. **Contextual color** — status/result cards MUST use Container style `good`\n\
   for success, `attention` for errors, `warning` for alerts, `emphasis` for\n\
   info panels. NEVER use default for everything.\n\
\n\
9. **Realistic mock data** — booking references like `TRV-20260512-GA884`,\n\
   prices like `$487` or `¥18,500 (~$124)`, dates like `May 12, 2026`,\n\
   ratings like `⭐ 4.92 (218 reviews)`, statuses like `✅ Confirmed`.\n\
   NEVER use `lorem ipsum`, `placeholder`, `xxx`, or generic `Item 1`.\n\
\n\
10. **Emoji + icon vocabulary** in TextBlocks for visual flavor: ✈️ 🏨 🍽️ 🚗\n\
    📅 ⭐ 📍 💳 ✅ ⚠️ 🎫 📦 🔄 ➕ 📊 🎉 etc. Use them in titles and option labels.\n\
\n\
MINIMUM ELEMENTS PER CARD TYPE:\n\
- Menu cards: Header ColumnSet + ActionSet with 3+ styled buttons + optional welcome container\n\
- Form cards: Step indicator + Header + multiple Input fields grouped in ColumnSets + primary + back action\n\
- Review/confirm cards: Header + Container emphasis with FactSet summary + Container good with total price (ColumnSet) + primary submit + back\n\
- Success cards: Header with ✅ icon + Container good with confirmation message + FactSet with booking details + ActionSet back-to-menu\n\
- Listing cards: Header + multiple Container rows (good for highlighted, default+separator for others) + Input.ChoiceSet selector + action\n\
\n\
If a card has fewer than 8 body elements for a non-trivial screen, you are\n\
under-building. Aim for 10-20 body elements per non-trivial card.\n\
\n\
=== TOOL USAGE RULES ===\n\
\n\
Use tools sparingly. Each round without a delivered answer brings you\n\
closer to the loop cap — if you exhaust the rounds, your answer is LOST.\n\
\n\
Allowed tool uses:\n\
- validate_card / analyze_card / check_accessibility — review YOUR draft\n\
  card JSON before delivery (at most once per card)\n\
- optimize_card / transform_card — apply a single polish pass before delivery\n\
- list_examples / get_example / suggest_layout — browse the knowledge base\n\
  for inspiration. The KB may be EMPTY in this deployment — if list_examples\n\
  or suggest_layout returns no results, STOP asking it and write the card\n\
  from scratch\n\
\n\
FORBIDDEN tool uses:\n\
- data_to_card — ONLY for converting a FLAT DATA TABLE into a single card.\n\
  NEVER call this to build a multi-card menu flow. It cannot design menus.\n\
- template_card — ONLY for filling a known template with data. NEVER for\n\
  constructing a fresh multi-card menu.\n\
- pack_card / deploy_pack — orchestration stubs that currently return 501.\n\
  Do not call them.\n\
\n\
STOP calling tools and emit your JSON in content when:\n\
- You have a clear design in your head for the card or flow\n\
- You have validated your draft once (don't validate repeatedly)\n\
- You have tried list_examples / suggest_layout once and the KB is empty\n\
- You are on round 4 or later — deliver now, do not burn more rounds\n\
\n\
=== NAVIGATION CONVENTION (multi-card flows only) ===\n\
\n\
To link button A in card X to card Y, use Action.Submit with a data object\n\
containing `nextCardId: \"<target_id>\"`. Example:\n\
\n\
{\n\
  \"type\": \"Action.Submit\",\n\
  \"title\": \"Book now\",\n\
  \"data\": { \"nextCardId\": \"select_room\" }\n\
}\n\
\n\
Rules:\n\
- Card ids are unique within a flow and follow snake_case.\n\
- Every `nextCardId` must match a card id in the same flow (no dangling links).\n\
- For single-card responses, do NOT wrap in `{ flow, cards }` — emit the card object directly.\n\
- Use realistic, contextual data. Do not emit placeholder text like \"lorem ipsum\".\n\
- Respect the target host's capabilities if one was specified.\n\
\n\
NAVIGATION-ONLY ACTIONS (back, cancel, secondary buttons):\n\
For Action.Submit buttons that ONLY navigate (Back to Menu, Cancel, Skip, etc.)\n\
and should NOT validate the form's required inputs, set `associatedInputs: \"none\"`.\n\
Example:\n\
{\n\
  \"type\": \"Action.Submit\",\n\
  \"title\": \"Back to Menu\",\n\
  \"associatedInputs\": \"none\",\n\
  \"data\": { \"nextCardId\": \"main_menu\" }\n\
}\n\
\n\
Without `associatedInputs: \"none\"`, the AdaptiveCards SDK auto-validates ALL\n\
required inputs when the user clicks the button — which is wrong for back/cancel\n\
actions where the user explicitly wants to abandon the form.\n\
\n\
Apply this rule to:\n\
- All \"Back\", \"Cancel\", \"Skip\", \"Return\" buttons\n\
- Any secondary action where the user is leaving without completing the form\n\
- DO NOT apply this to the primary submit/continue/next button — those SHOULD validate.\n\
";

const HTTP_NODE_CONVENTION: &str = "\n\n\
=== HTTP API NODES ===\n\
\n\
When the user mentions API calls, backend integration, submitting data to a\n\
server, fetching data from an API, or connecting to external services, insert\n\
HTTP nodes into the flow between the relevant cards.\n\
\n\
HTTP node format (place in the cards[] array alongside card entries):\n\
{\n\
  \"id\": \"api_<descriptive_name>\",\n\
  \"type\": \"http\",\n\
  \"config\": {\n\
    \"url\": \"/api/<path>\",\n\
    \"method\": \"POST\",\n\
    \"body_mapping\": {\n\
      \"field_name\": \"${input_id_from_previous_card}\"\n\
    }\n\
  }\n\
}\n\
\n\
RULES:\n\
- HTTP node ID MUST start with `api_` prefix\n\
- `url` is always a relative path (no hostname) — starts with `/`\n\
- `method`: GET, POST, PUT, DELETE, PATCH (default POST)\n\
- `body_mapping` keys use `${field_id}` matching Input element IDs from the\n\
  previous card. Only include for POST/PUT/PATCH, not GET/DELETE.\n\
- Place HTTP node BETWEEN the form card and the result card in the cards array\n\
- The card before the HTTP node should have `nextCardId` pointing to the HTTP\n\
  node's ID\n\
- The card after the HTTP node receives the API response data\n\
- Do NOT include `base_url`, `auth_type`, or `auth_token` — these are\n\
  configured at runtime via `gtc setup`\n\
- Do NOT add HTTP nodes for static navigation (menu → submenu)\n\
\n\
PATTERNS:\n\
- Form submission: form_card → api_create_X → confirmation_card\n\
- Fetch list: menu_card → api_fetch_X_list → list_card\n\
- Fetch detail: list_card → api_fetch_X_detail → detail_card\n\
- Update record: edit_card → api_update_X → success_card\n\
- Delete record: confirm_card → api_delete_X → success_card\n\
\n\
EXAMPLE (IT helpdesk with API):\n\
{\n\
  \"flow\": \"it_helpdesk\",\n\
  \"cards\": [\n\
    { \"id\": \"welcome\", \"card\": { ... } },\n\
    { \"id\": \"create_ticket_form\", \"card\": { ... } },\n\
    { \"id\": \"api_create_ticket\", \"type\": \"http\", \"config\": {\n\
        \"url\": \"/api/tickets\", \"method\": \"POST\",\n\
        \"body_mapping\": { \"category\": \"${category}\", \"priority\": \"${priority}\" }\n\
    }},\n\
    { \"id\": \"ticket_confirmation\", \"card\": { ... } },\n\
    { \"id\": \"api_fetch_tickets\", \"type\": \"http\", \"config\": {\n\
        \"url\": \"/api/tickets\", \"method\": \"GET\"\n\
    }},\n\
    { \"id\": \"ticket_list\", \"card\": { ... } }\n\
  ]\n\
}\n\
\n\
When NOT to add HTTP nodes:\n\
- User asks for a static card flow with no API mention\n\
- Navigation between menu screens (use nextCardId only)\n\
- User explicitly says 'no API' or 'static' or 'demo mode'\n\
";

pub const VISION_INSTRUCTION: &str = "\
=== IMAGE ANALYSIS ===\n\
\n\
The user has attached an image. Analyze it carefully — it could be a \
screenshot of an existing UI, a wireframe sketch, a design mockup from \
Figma/Sketch, or a hand-drawn diagram on a whiteboard.\n\
\n\
Extract the visual layout, components, colors, typography hierarchy, and \
interaction patterns you observe, then generate an equivalent Adaptive Card \
v1.6 JSON that reproduces the design as closely as possible within Adaptive \
Card capabilities.\n\
\n\
Guidelines:\n\
- Map UI elements to their closest Adaptive Card equivalents (buttons → \
  Action.Submit, text fields → Input.Text, dropdowns → Input.ChoiceSet, etc.)\n\
- Preserve the visual hierarchy: headings, subheadings, body text, captions\n\
- Reproduce layout structure using ColumnSet for side-by-side arrangements\n\
- Use Container with style (emphasis/good/attention/warning) to match \
  background colors and visual groupings\n\
- If the image shows multiple screens or pages, generate a multi-card flow \
  (SHAPE 2) with one card per screen\n\
- For hand-drawn sketches, interpret intent rather than pixel-matching\n\
- When text in the image is illegible, use contextually appropriate placeholder \
  text that matches the apparent purpose\n\
";

/// Build the static system prompt (no user-query-specific examples).
/// Used at startup to cache a base prompt in AppState.
#[must_use]
pub fn build_system_prompt(kb: &Knowledge, target_host: Option<Host>) -> String {
    let mut prompt = core_build(&PromptOpts {
        include_examples: Vec::new(),
        target_host,
        available_tools: TOOL_NAMES.to_vec(),
        knowledge_base: Some(kb.as_core()),
        user_query: None,
    });
    prompt.push_str(NAVIGATION_CONVENTION);
    prompt.push_str(HTTP_NODE_CONVENTION);
    prompt
}

/// Build a per-request system prompt that includes few-shot KB examples
/// matched to the user's query. The LLM sees up to 3 real card examples
/// as "REFERENCE EXAMPLES" in the prompt — giving it concrete layout
/// inspiration instead of relying solely on rules.
#[must_use]
pub fn build_system_prompt_with_query(
    kb: &Knowledge,
    target_host: Option<Host>,
    user_query: &str,
) -> String {
    let mut prompt = core_build(&PromptOpts {
        include_examples: Vec::new(),
        target_host,
        available_tools: TOOL_NAMES.to_vec(),
        knowledge_base: Some(kb.as_core()),
        user_query: Some(user_query.to_string()),
    });
    prompt.push_str(NAVIGATION_CONVENTION);
    prompt.push_str(HTTP_NODE_CONVENTION);
    prompt
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prompt_includes_schema_and_a11y_rules() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("CARD SCHEMA RULES"));
        assert!(prompt.contains("ACCESSIBILITY RULES"));
    }

    #[test]
    fn prompt_lists_all_tools() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        for name in TOOL_NAMES {
            assert!(prompt.contains(name));
        }
    }

    #[test]
    fn prompt_includes_navigation_convention() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("nextCardId"));
        assert!(prompt.contains("\"flow\""));
        assert!(prompt.contains("\"cards\""));
    }

    #[test]
    fn prompt_includes_http_node_convention() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("HTTP API NODES"));
        assert!(prompt.contains("api_"));
        assert!(prompt.contains("body_mapping"));
    }

    #[test]
    fn vision_instruction_mentions_image() {
        assert!(VISION_INSTRUCTION.contains("IMAGE ANALYSIS"));
        assert!(VISION_INSTRUCTION.contains("screenshot"));
        assert!(VISION_INSTRUCTION.contains("wireframe"));
    }
}