@webmcp-auto-ui/agent 2.5.32 → 2.5.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/agent",
3
- "version": "2.5.32",
3
+ "version": "2.5.34",
4
4
  "description": "LLM agent loop + remote/WASM/local providers + MCP wrapper",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -12,7 +12,6 @@ import notebookRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/notebook
12
12
  // Notebook widget renderer (vanilla JS) — import via subpath to avoid pulling
13
13
  // the .svelte exports of the ui package root through tsc.
14
14
  import { render as renderNotebook } from '@webmcp-auto-ui/ui/widgets/notebook/notebook.js';
15
- import { render as renderRecipeBrowser } from '@webmcp-auto-ui/ui/widgets/notebook/recipe-browser.js';
16
15
 
17
16
  // Inline recipe for recipe-browser (real vanilla widget)
18
17
  const recipeBrowserRecipe = `---
@@ -143,7 +142,7 @@ Call widget_display({name: "list", params: {items: ["A", "B", "C"]}}).
143
142
  // ── chart ────────────────────────────────────────────────────────────────
144
143
  `---
145
144
  widget: chart
146
- description: Simple bar chart. Labels + numeric values.
145
+ description: Simple bar chart. bars is an array of tuples [label: string, value: number]. Each entry MUST be an array of length exactly 2, NOT an object.
147
146
  schema:
148
147
  type: object
149
148
  required:
@@ -153,8 +152,11 @@ schema:
153
152
  type: string
154
153
  bars:
155
154
  type: array
155
+ description: Array of tuples [label, value]. Each item is an array [string, number] of length exactly 2. Do NOT use objects like {label, value}.
156
156
  items:
157
157
  type: array
158
+ minItems: 2
159
+ maxItems: 2
158
160
  ---
159
161
 
160
162
  ## When to use
@@ -162,6 +164,7 @@ Pour un graphique a barres simple avec des labels et valeurs numeriques.
162
164
 
163
165
  ## How to use
164
166
  Call widget_display({name: "chart", params: {bars: [["Jan", 10], ["Fev", 20]]}}).
167
+ Each bar is a tuple array of exactly 2 elements: [label: string, value: number]. NEVER use objects.
165
168
  `,
166
169
 
167
170
  // ── alert ────────────────────────────────────────────────────────────────
@@ -765,57 +768,6 @@ Call widget_display({name: "carousel", params: {slides: [{src: "https://...", ti
765
768
 
766
769
  ## Common mistakes
767
770
  - NEVER fabricate image URLs for src — only use those returned by MCP tools
768
- `,
769
-
770
- // ── map ──────────────────────────────────────────────────────────────────
771
- `---
772
- widget: map
773
- description: Interactive Leaflet map with markers. Dark CARTO basemap.
774
- schema:
775
- type: object
776
- properties:
777
- title:
778
- type: string
779
- center:
780
- type: object
781
- description: Centre de la carte
782
- required:
783
- - lat
784
- - lng
785
- properties:
786
- lat:
787
- type: number
788
- lng:
789
- type: number
790
- zoom:
791
- type: number
792
- description: Niveau de zoom (1-18)
793
- height:
794
- type: string
795
- description: Hauteur CSS de la carte (ex "400px")
796
- markers:
797
- type: array
798
- items:
799
- type: object
800
- required:
801
- - lat
802
- - lng
803
- properties:
804
- lat:
805
- type: number
806
- lng:
807
- type: number
808
- label:
809
- type: string
810
- color:
811
- type: string
812
- ---
813
-
814
- ## When to use
815
- Display a geographic map with markers.
816
-
817
- ## How to use
818
- Call widget_display({name: "map", params: {center: {lat: 48.8, lng: 2.3}, zoom: 12, markers: [{lat: 48.8, lng: 2.3, label: "Paris"}]}}).
819
771
  `,
820
772
 
821
773
  // ── stat-card ────────────────────────────────────────────────────────────
@@ -882,9 +834,10 @@ schema:
882
834
  type: string
883
835
  rows:
884
836
  type: array
885
- description: Tableau de tableaux de valeurs (row-major)
837
+ description: Array of rows, row-major. Each row is an array of primitive values (string/number/boolean) with length equal to the number of columns. Do NOT use objects as rows.
886
838
  items:
887
839
  type: array
840
+ minItems: 1
888
841
  highlights:
889
842
  type: array
890
843
  description: Cellules a coloriser
@@ -907,34 +860,6 @@ Pour des grilles de donnees avec mise en valeur de cellules (heatmap, comparaiso
907
860
 
908
861
  ## How to use
909
862
  Call widget_display({name: "grid-data", params: {columns: [{key:"a",label:"A"}], rows: [[1,2],[3,4]], highlights: [{row:0,col:1,color:"#ff0"}]}}).
910
- `,
911
-
912
- // ── d3 ───────────────────────────────────────────────────────────────────
913
- `---
914
- widget: d3
915
- description: D3.js visualization (hex-heatmap, radial, treemap, force graph).
916
- schema:
917
- type: object
918
- required:
919
- - preset
920
- - data
921
- properties:
922
- title:
923
- type: string
924
- preset:
925
- type: string
926
- enum: [hex-heatmap, radial, treemap, force]
927
- data:
928
- type: object
929
- config:
930
- type: object
931
- ---
932
-
933
- ## When to use
934
- Pour des visualisations avancees D3.js (heatmap hexagonale, radial, treemap, graphe de force).
935
-
936
- ## How to use
937
- Call widget_display({name: "d3", params: {preset: "treemap", data: {name: "root", children: [...]}}}).
938
863
  `,
939
864
 
940
865
  // ── js-sandbox ───────────────────────────────────────────────────────────
@@ -970,6 +895,31 @@ Pour des visualisations custom, animations, ou prototypes interactifs en JS pur.
970
895
  Call widget_display({name: "js-sandbox", params: {code: "document.getElementById('root').innerHTML = '<h1>Hello</h1>'"}}).
971
896
  `,
972
897
 
898
+ `---
899
+ widget: chat-input
900
+ description: Minimal inline chat bar with a text input and stop button. Use when the agent needs to solicit a short free-form user reply inside the current canvas (e.g. "explain this result").
901
+ group: rich
902
+ schema:
903
+ type: object
904
+ properties:
905
+ placeholder:
906
+ type: string
907
+ description: Placeholder text shown in the input (default "Your reply...").
908
+ value:
909
+ type: string
910
+ description: Optional initial value.
911
+ disabled:
912
+ type: boolean
913
+ description: Disable the input (while the agent is processing).
914
+ ---
915
+
916
+ ## When to use
917
+ When you need the user to reply with a short text — clarifying questions, follow-ups, free-form input mid-canvas. Prefer this over a modal for lightweight conversation inside a widget layout.
918
+
919
+ ## How to use
920
+ Call widget_display({name: "chat-input", params: {placeholder: "Your reply..."}}). The widget emits a bubbling 'widget:interact' CustomEvent with detail={action: "submit", payload: {text}} when the user submits, and detail={action: "stop"} when the stop button is pressed.
921
+ `,
922
+
973
923
  ];
974
924
 
975
925
  // ---------------------------------------------------------------------------
@@ -999,14 +949,11 @@ for (const recipe of RECIPES) {
999
949
  autoui.registerWidget(recipe, undefined);
1000
950
  }
1001
951
 
1002
- // Notebook widget — vanilla renderer (resolved via WidgetRenderer vanilla path)
1003
- const NOTEBOOK_WIDGETS: Array<[string, (container: HTMLElement, data: any) => any]> = [
1004
- [notebookRecipe as string, renderNotebook],
1005
- [recipeBrowserRecipe, renderRecipeBrowser],
1006
- ];
1007
- for (const [recipe, renderer] of NOTEBOOK_WIDGETS) {
1008
- autoui.registerWidget(recipe, renderer as any);
1009
- }
952
+ // Notebook widget — vanilla renderer (wrapped by <auto-notebook> custom element)
953
+ autoui.registerWidget(notebookRecipe as string, renderNotebook as any);
954
+
955
+ // Recipe browser — resolved by WidgetRenderer as <auto-recipe-browser> custom element
956
+ autoui.registerWidget(recipeBrowserRecipe, undefined);
1010
957
 
1011
958
  // Register flow recipes (multi-step procedures) from the global recipe registry
1012
959
  // that declare this server (autoui) in their frontmatter.
package/src/index.ts CHANGED
@@ -79,6 +79,14 @@ export type { RepairResult } from './auto-repair.js';
79
79
  // Pipeline trace
80
80
  export { PipelineTrace, type TraceEntry } from './pipeline-trace.js';
81
81
 
82
+ // onnxruntime-web version pins (centralised — see ort-version.ts)
83
+ export {
84
+ ORT_VERSION,
85
+ ORT_CDN_BASE,
86
+ ORT_TRANSFORMERS_VERSION,
87
+ ORT_TRANSFORMERS_CDN_BASE,
88
+ } from './ort-version.js';
89
+
82
90
  // Trace observer — live visual trace for runAgentLoop
83
91
  export { createTraceObserver, type TraceObserver, type TraceObserverContext, type RoundTripDetail } from './trace-observer.js';
84
92
 
package/src/loop.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  } from './types.js';
11
11
  import type { ToolLayer, SchemaTransformOptions } from './tool-layers.js';
12
12
  import { buildToolsFromLayers, buildDiscoveryToolsWithAliases, activateServerTools, toProviderTools, sanitizeServerName } from './tool-layers.js';
13
- import { buildSystemPromptWithAliases, buildSystemPrompt } from './prompts/index.js';
13
+ import { buildSystemPromptWithAliases } from './prompts/index.js';
14
14
  import type { DiscoveryCache } from './discovery-cache.js';
15
15
  import { unflattenParams, validateJsonSchema } from '@webmcp-auto-ui/core';
16
16
  import type { JsonSchema } from '@webmcp-auto-ui/core';
@@ -395,13 +395,19 @@ export async function runAgentLoop(
395
395
  const protocol = tokenToProtocol(token);
396
396
  const serverKey = `${serverName}_${token}`;
397
397
  if (!activatedServers.has(serverKey)) {
398
- activatedServers.add(serverKey);
399
398
  const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
400
399
  if (layer) {
401
- const act = activateServerTools(activeTools, layer, schemaOptions, trace);
402
- activeTools = act.tools;
403
- // Merge new pathMaps so unflattenParams works for lazily-activated tools.
404
- for (const [k, v] of act.pathMaps) localPathMaps.set(k, v);
400
+ try {
401
+ const act = activateServerTools(activeTools, layer, schemaOptions, trace);
402
+ activeTools = act.tools;
403
+ // Merge new pathMaps so unflattenParams works for lazily-activated tools.
404
+ for (const [k, v] of act.pathMaps) localPathMaps.set(k, v);
405
+ activatedServers.add(serverKey);
406
+ } catch (e) {
407
+ trace.push('activate', name, `activation failed for ${serverKey}: ${e instanceof Error ? e.message : String(e)}`, 'error');
408
+ }
409
+ } else {
410
+ trace.push('activate', name, `layer not found for server="${serverName}" protocol="${protocol}"`, 'warn');
405
411
  }
406
412
  }
407
413
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type * as OrtTypes from 'onnxruntime-web';
12
+ import { ORT_CDN_BASE } from '../ort-version.js';
12
13
 
13
14
  export const EMBEDDING_DIMS = 384;
14
15
 
@@ -47,7 +48,7 @@ export class Embedder {
47
48
 
48
49
  // Use WASM backend, load WASM binaries from CDN (avoids bundling 70MB in builds)
49
50
  ort.env.wasm.numThreads = 1;
50
- ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.21.0/dist/';
51
+ ort.env.wasm.wasmPaths = `${ORT_CDN_BASE}/dist/`;
51
52
 
52
53
  const modelUrl =
53
54
  'https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/onnx/model_quantized.onnx';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Centralised onnxruntime-web version pins.
3
+ *
4
+ * Two distinct usages exist in the codebase, each requiring a different ORT
5
+ * build, so we expose two constants rather than one:
6
+ *
7
+ * 1. STANDALONE (`ORT_VERSION` / `ORT_CDN_BASE`) — used by the nano-RAG
8
+ * embedder (packages/agent/src/nano-rag/embedder.ts). The embedder does
9
+ * `await import('onnxruntime-web')`, which resolves through the host app's
10
+ * importmap (apps/flex, apps/notebook-viewer). The matching .wasm binaries
11
+ * must be served from a CDN that mirrors the *exact* same release. We pin
12
+ * 1.21.0 because it is a stable release present on jsdelivr/esm.sh and
13
+ * matches the embedder's tested chain.
14
+ *
15
+ * 2. TRANSFORMERS-PINNED (`ORT_TRANSFORMERS_VERSION` /
16
+ * `ORT_TRANSFORMERS_CDN_BASE`) — used by transformers.worker.ts when it
17
+ * loads transformers.js 4.1.0 from esm.sh. transformers.js 4.1.0 ships
18
+ * with ORT 1.26.0-dev.20260410-5e55544225 internally; we override the
19
+ * wasm paths to fetch the matching native binaries from jsdelivr (esm.sh
20
+ * serves the JS, jsdelivr serves the .wasm). Bumping this requires
21
+ * bumping the transformers.js version in lock-step.
22
+ *
23
+ * The two pins are intentionally NOT the same: mixing 1.21 wasm with the
24
+ * 1.26-dev JS bundle (or vice versa) crashes at runtime with cryptic
25
+ * "wasm backend not initialised" / signature mismatch errors.
26
+ */
27
+
28
+ // Standalone embedder usage (nano-RAG). Stable release.
29
+ export const ORT_VERSION = '1.21.0';
30
+ export const ORT_CDN_BASE = `https://cdn.jsdelivr.net/npm/onnxruntime-web@${ORT_VERSION}`;
31
+
32
+ // transformers.js 4.1.0 internal pin. Must match the version baked into
33
+ // https://esm.sh/@huggingface/transformers@4.1.0.
34
+ export const ORT_TRANSFORMERS_VERSION = '1.26.0-dev.20260410-5e55544225';
35
+ export const ORT_TRANSFORMERS_CDN_BASE = `https://cdn.jsdelivr.net/npm/onnxruntime-web@${ORT_TRANSFORMERS_VERSION}`;
@@ -43,7 +43,12 @@ export function buildSystemPromptWithAliases(
43
43
  return { prompt, aliasMap: refs.aliasMap };
44
44
  }
45
45
 
46
- /** Backward-compat wrapper — also populates the deprecated global toolAliasMap. */
46
+ /**
47
+ * @deprecated Use `buildSystemPromptWithAliases()` instead and pass the returned
48
+ * `aliasMap` to the agent loop explicitly. This wrapper still populates the
49
+ * deprecated global `toolAliasMap` singleton as a side-effect, which is not
50
+ * parallel-safe across concurrent agent loops.
51
+ */
47
52
  export function buildSystemPrompt(
48
53
  layers: ToolLayer[],
49
54
  options?: { providerKind?: ProviderKind },
@@ -28,6 +28,7 @@
28
28
 
29
29
  import type { ContentBlock } from '../types.js';
30
30
  import type { TransformersModelEntry } from './transformers-models.js';
31
+ import { ORT_TRANSFORMERS_CDN_BASE } from '../ort-version.js';
31
32
 
32
33
  // --------------------------------------------------------------------------
33
34
  // Gemma 4 chat_template override.
@@ -204,7 +205,7 @@ async function loadModel(modelEntry: TransformersModelEntry): Promise<void> {
204
205
  // on jsdelivr). For the 3.8.1 path, ORT 1.22.0-dev is a transformers.js-
205
206
  // internal build not mirrored on jsdelivr — let transformers.js use its
206
207
  // default wasmPaths (which resolve against esm.sh, matching the JS bundle).
207
- env.backends.onnx.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260410-5e55544225/dist/';
208
+ env.backends.onnx.wasm.wasmPaths = `${ORT_TRANSFORMERS_CDN_BASE}/dist/`;
208
209
  }
209
210
  if (env) {
210
211
  env.allowLocalModels = false;
@@ -1500,6 +1500,354 @@ component("table", {columns: ["Title", "Date", "NOR"], rows: results})
1500
1500
  - **Overly broad queries**: always use LIMIT and precise WHERE filters
1501
1501
  - **Displaying raw full text**: use the \`text\` component with Markdown formatting rather than dumping the JSON
1502
1502
  - **Forgetting to specify the text status**: an article can be repealed, amended or in force — always indicate it
1503
+ `,
1504
+ 'showcase-carto': `---
1505
+ id: showcase-cartography
1506
+ name: Showcase cartographic widgets — points, polygons, aggregations, tiles, 3D
1507
+ when: the user asks specifically for a map / cartography / geo demo, e.g. "showcase carto", "demo carto", "show me geo widgets", "what kinds of maps can you render?"
1508
+ servers: [deckgl, h3, s2, turf, maplibre]
1509
+ components_used: [deckgl-scatterplot, deckgl-arc, deckgl-polygon, deckgl-h3-hexagon, deckgl-heatmap, deckgl-tile, deckgl-trips]
1510
+ layout:
1511
+ type: grid
1512
+ columns: 2
1513
+ arrangement: 7 deck.gl maps in a 2-column grid
1514
+ ---
1515
+
1516
+ ## When to use
1517
+
1518
+ The user wants to see the **cartographic capabilities** of the system. Typical phrases:
1519
+ - "Montre-moi un showcase carto"
1520
+ - "Demo cartographie"
1521
+ - "What kinds of maps / geo widgets can you render?"
1522
+ - "Showcase deckgl"
1523
+
1524
+ This recipe covers the four major deck.gl layer families: **points/lines**, **polygons**, **aggregation** (hexagon/H3/heatmap), **tiles & animation**.
1525
+
1526
+ ## How to use
1527
+
1528
+ Mount **7 deck.gl widgets**, each with realistic Paris-region demo data. **Do NOT** stuff all the widget names into a single \`deckgl-text\` widget — that is not a showcase, that is a label list. Each widget must render its own layer family with real geometric data.
1529
+
1530
+ Use exact widget names and exact parameter names below. Schemas come from each widget's recipe.
1531
+
1532
+ 1. **Scatterplot** — points around Paris (\`deckgl-scatterplot\`, key: \`points\`):
1533
+ \`\`\`
1534
+ widget_display({name: "deckgl-scatterplot", params: {
1535
+ points: [
1536
+ {lng: 2.3522, lat: 48.8566, radius: 200, color: [255, 100, 100]},
1537
+ {lng: 2.2945, lat: 48.8584, radius: 180, color: [100, 200, 255]},
1538
+ {lng: 2.3499, lat: 48.8530, radius: 150, color: [120, 255, 120]},
1539
+ {lng: 2.3376, lat: 48.8606, radius: 220, color: [255, 200, 80]},
1540
+ {lng: 2.3326, lat: 48.8867, radius: 160, color: [200, 100, 255]}
1541
+ ],
1542
+ center: [2.3522, 48.8566], zoom: 12
1543
+ }})
1544
+ \`\`\`
1545
+
1546
+ 2. **Arc** — flows from Paris to other French cities (\`deckgl-arc\`, key: \`arcs\`):
1547
+ \`\`\`
1548
+ widget_display({name: "deckgl-arc", params: {
1549
+ arcs: [
1550
+ {from: [2.3522, 48.8566], to: [4.8357, 45.7640], width: 3},
1551
+ {from: [2.3522, 48.8566], to: [5.3698, 43.2965], width: 4},
1552
+ {from: [2.3522, 48.8566], to: [-1.5536, 47.2184], width: 2},
1553
+ {from: [2.3522, 48.8566], to: [7.7521, 48.5734], width: 3}
1554
+ ],
1555
+ center: [3.5, 46.5], zoom: 5
1556
+ }})
1557
+ \`\`\`
1558
+
1559
+ 3. **Polygon** — three quadrilaterals around central Paris (\`deckgl-polygon\`, key: \`polygons\`, color key: \`fillColor\`):
1560
+ \`\`\`
1561
+ widget_display({name: "deckgl-polygon", params: {
1562
+ polygons: [
1563
+ {polygon: [[2.32, 48.86], [2.36, 48.86], [2.36, 48.88], [2.32, 48.88], [2.32, 48.86]], fillColor: [255, 100, 100, 120]},
1564
+ {polygon: [[2.36, 48.86], [2.40, 48.86], [2.40, 48.88], [2.36, 48.88], [2.36, 48.86]], fillColor: [100, 200, 255, 120]},
1565
+ {polygon: [[2.32, 48.84], [2.36, 48.84], [2.36, 48.86], [2.32, 48.86], [2.32, 48.84]], fillColor: [100, 255, 120, 120]}
1566
+ ],
1567
+ center: [2.35, 48.86], zoom: 12
1568
+ }})
1569
+ \`\`\`
1570
+
1571
+ 4. **H3 hexagon** — Uber H3 cells with values (\`deckgl-h3-hexagon\`, key: \`cells\`). If the \`h3\` server is activated, prefer to call \`latLngToCell({lat, lng, res: 8})\` first to obtain valid indices for Paris. Otherwise use known-valid Paris-area indices below:
1572
+ \`\`\`
1573
+ widget_display({name: "deckgl-h3-hexagon", params: {
1574
+ cells: [
1575
+ {hex: "881fb46625fffff", value: 12},
1576
+ {hex: "881fb46627fffff", value: 8},
1577
+ {hex: "881fb4662dfffff", value: 22},
1578
+ {hex: "881fb46663fffff", value: 6},
1579
+ {hex: "881fb46669fffff", value: 15}
1580
+ ],
1581
+ extruded: true, elevationScale: 30,
1582
+ center: [2.35, 48.86], zoom: 11
1583
+ }})
1584
+ \`\`\`
1585
+
1586
+ 5. **Heatmap** — weighted point density (\`deckgl-heatmap\`, key: \`points\`):
1587
+ \`\`\`
1588
+ widget_display({name: "deckgl-heatmap", params: {
1589
+ points: [
1590
+ {lng: 2.35, lat: 48.86, weight: 5}, {lng: 2.36, lat: 48.86, weight: 8},
1591
+ {lng: 2.34, lat: 48.87, weight: 3}, {lng: 2.33, lat: 48.85, weight: 12},
1592
+ {lng: 2.37, lat: 48.88, weight: 6}, {lng: 2.32, lat: 48.84, weight: 9}
1593
+ ],
1594
+ center: [2.35, 48.86], zoom: 12
1595
+ }})
1596
+ \`\`\`
1597
+
1598
+ 6. **Tile** — OSM raster tiles (\`deckgl-tile\`, key: \`tileUrl\`):
1599
+ \`\`\`
1600
+ widget_display({name: "deckgl-tile", params: {
1601
+ tileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
1602
+ center: [2.35, 48.86], zoom: 10
1603
+ }})
1604
+ \`\`\`
1605
+
1606
+ 7. **Trips** — animated trajectory (\`deckgl-trips\`, key: \`trips\`):
1607
+ \`\`\`
1608
+ widget_display({name: "deckgl-trips", params: {
1609
+ trips: [
1610
+ {path: [[2.30, 48.85], [2.33, 48.86], [2.36, 48.87], [2.40, 48.88]], timestamps: [0, 200, 400, 600], color: [253, 128, 93]}
1611
+ ],
1612
+ trailLength: 200, animationSpeed: 1,
1613
+ center: [2.35, 48.86], zoom: 12
1614
+ }})
1615
+ \`\`\`
1616
+
1617
+ ## Important
1618
+
1619
+ - **Never** call \`deckgl-text\` with a list of widget *names* as labels. That is not a showcase.
1620
+ - Each widget must contain real geometric data (positions, polygons, H3 cells…), not labels.
1621
+ - Use exactly the parameter keys above (\`points\` not \`data\`, \`arcs\` not \`data\`, \`polygons\` not \`data\`, \`cells\` not \`data\`, \`tileUrl\` not \`url\`, \`trips\` not \`data\`).
1622
+ - If the user asks for a specific layer family, drill down into the per-widget recipe (\`scatterplot\`, \`arc\`, \`h3-hexagon\`, etc.).
1623
+ - Keep the canvas under 8 maps to stay within WebGL context limits.
1624
+
1625
+ ## Output text
1626
+
1627
+ Return a single sentence such as: "Tour cartographique : 7 widgets deck.gl — points, arcs, polygones, H3, heatmap, tiles OSM et trajets animés."
1628
+ `,
1629
+ 'showcase-dashboard': `---
1630
+ id: showcase-dashboard
1631
+ name: Showcase analytics dashboard widgets — Tremor, ECharts, Observable Plot, Recharts, Nivo, Perspective
1632
+ when: the user asks for a dashboard / analytics / charts demo, e.g. "showcase dashboard", "demo dashboard", "show me charts", "analytics demo"
1633
+ servers: [echarts, observable-plot, recharts, nivo, perspective, tremor, chartjs, d3]
1634
+ components_used: [tremor-kpi-card, echarts-line, observable-plot-dot, recharts-bar, nivo-pie, perspective-table]
1635
+ layout:
1636
+ type: grid
1637
+ columns: 3
1638
+ arrangement: KPI strip on top, then charts in a grid, table at the bottom
1639
+ ---
1640
+
1641
+ ## When to use
1642
+
1643
+ The user wants to see the **analytics / dashboard** capabilities of the system — non-cartographic visualizations powered by JS chart libraries. Typical phrases:
1644
+ - "Montre-moi un showcase dashboard"
1645
+ - "Demo analytics / charts"
1646
+ - "Show me what charts you can do"
1647
+ - "Showcase ECharts / Observable / Recharts"
1648
+
1649
+ This recipe covers the four major dashboard widget families: **KPIs**, **time series & comparisons**, **distributions & breakdowns**, **interactive tables**.
1650
+
1651
+ ## How to use
1652
+
1653
+ Mount **6-7 widgets** drawn from different chart libraries to demonstrate variety. Pick one widget per server when possible. **Do not collapse everything into a single chart** — the showcase value is the variety.
1654
+
1655
+ Use exact widget names and exact parameter keys below. Schemas come from each widget's recipe.
1656
+
1657
+ 1. **3 KPI cards** in a row (\`tremor-kpi-card\`, keys: \`title\`, \`metric\`, \`delta\`, \`deltaType\`):
1658
+ \`\`\`
1659
+ widget_display({name: "tremor-kpi-card", params: {title: "MRR", metric: "€ 184 200", delta: "+12.4%", deltaType: "increase"}})
1660
+ widget_display({name: "tremor-kpi-card", params: {title: "Active users", metric: "12 480", delta: "+8.2%", deltaType: "increase"}})
1661
+ widget_display({name: "tremor-kpi-card", params: {title: "Churn", metric: "3.1 %", delta: "-0.4 pts", deltaType: "decrease"}})
1662
+ \`\`\`
1663
+
1664
+ 2. **Time-series line chart** (\`echarts-line\`, keys: \`categories\`, \`series\`):
1665
+ \`\`\`
1666
+ widget_display({name: "echarts-line", params: {
1667
+ title: "Signups vs activations",
1668
+ categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"],
1669
+ series: [
1670
+ {name: "Signups", data: [120, 145, 162, 198, 240, 285, 312, 358]},
1671
+ {name: "Activations", data: [80, 102, 121, 148, 188, 225, 252, 290]}
1672
+ ],
1673
+ smooth: true
1674
+ }})
1675
+ \`\`\`
1676
+
1677
+ 3. **Scatter / dot plot** (\`observable-plot-dot\`, keys: \`data\`, \`xKey\`, \`yKey\`, \`fill\`):
1678
+ \`\`\`
1679
+ widget_display({name: "observable-plot-dot", params: {
1680
+ title: "Cohort distribution",
1681
+ data: [
1682
+ {x: 1.2, y: 3.4, group: "A"}, {x: 2.5, y: 5.1, group: "A"}, {x: 3.8, y: 4.2, group: "B"},
1683
+ {x: 4.1, y: 6.8, group: "B"}, {x: 5.4, y: 5.9, group: "C"}, {x: 6.2, y: 7.3, group: "C"},
1684
+ {x: 7.0, y: 6.1, group: "A"}, {x: 8.3, y: 8.4, group: "B"}
1685
+ ],
1686
+ xKey: "x", yKey: "y", fill: "group", tip: true
1687
+ }})
1688
+ \`\`\`
1689
+
1690
+ 4. **Bar chart** (\`recharts-bar\`, keys: \`rows\`, \`bars\`, \`xKey\`):
1691
+ \`\`\`
1692
+ widget_display({name: "recharts-bar", params: {
1693
+ title: "Users by plan",
1694
+ rows: [
1695
+ {plan: "Free", users: 8420},
1696
+ {plan: "Pro", users: 3120},
1697
+ {plan: "Team", users: 740},
1698
+ {plan: "Enterprise", users: 200}
1699
+ ],
1700
+ xKey: "plan",
1701
+ bars: [{dataKey: "users", color: "#4f8cff"}]
1702
+ }})
1703
+ \`\`\`
1704
+
1705
+ 5. **Pie / donut** (\`nivo-pie\`, key: \`data\` with \`{id, label, value}\`):
1706
+ \`\`\`
1707
+ widget_display({name: "nivo-pie", params: {
1708
+ data: [
1709
+ {id: "direct", label: "Direct", value: 38},
1710
+ {id: "search", label: "Search", value: 27},
1711
+ {id: "ref", label: "Referral", value: 18},
1712
+ {id: "social", label: "Social", value: 12},
1713
+ {id: "email", label: "Email", value: 5}
1714
+ ],
1715
+ innerRadius: 0.5
1716
+ }})
1717
+ \`\`\`
1718
+
1719
+ 6. **Interactive datagrid** (\`perspective-table\`, key: \`rows\`):
1720
+ \`\`\`
1721
+ widget_display({name: "perspective-table", params: {
1722
+ title: "Revenue by region & plan",
1723
+ rows: [
1724
+ {date: "2026-01", region: "EU", plan: "Pro", revenue: 18400},
1725
+ {date: "2026-01", region: "US", plan: "Pro", revenue: 22100},
1726
+ {date: "2026-02", region: "EU", plan: "Pro", revenue: 19800},
1727
+ {date: "2026-02", region: "US", plan: "Pro", revenue: 24200},
1728
+ {date: "2026-03", region: "EU", plan: "Team", revenue: 31200},
1729
+ {date: "2026-03", region: "US", plan: "Team", revenue: 38400}
1730
+ ]
1731
+ }})
1732
+ \`\`\`
1733
+
1734
+ 7. **Optional 7th widget** for extra variety, if the corresponding server is activated:
1735
+ - \`chartjs-radar\` (multi-axis comparison),
1736
+ - \`nivo-radar\` (alternative radar),
1737
+ - \`d3-force-graph\` (mini network),
1738
+ - \`recharts-area\` (stacked area).
1739
+
1740
+ ## Important
1741
+
1742
+ - **Variety wins**: pick widgets from *different* libraries. Don't stack 5 ECharts widgets — the goal is to demonstrate the breadth of the dashboard ecosystem.
1743
+ - Use exactly the parameter keys above (\`metric\` not \`value\`, \`delta\` not \`trend\`, \`deltaType\` not \`trendDir\`, \`categories\` not \`xAxis\`, \`rows\` not \`data\` for recharts-bar / perspective-table, \`fill\` not \`color\` for observable-plot-dot).
1744
+ - Use realistic dashboard data (revenue, users, traffic sources, plans, regions). Avoid abstract \`[1, 2, 3]\` series.
1745
+ - For a **cartography**-focused showcase, use \`showcase-carto\` instead.
1746
+ - For a **mixed** showcase (carto + dashboard + others), use \`showcase\`.
1747
+
1748
+ ## Output text
1749
+
1750
+ Return a single sentence such as: "Showcase dashboard : 6 widgets — KPIs Tremor, série ECharts, scatter Observable Plot, bar Recharts, pie Nivo, table Perspective."
1751
+ `,
1752
+ 'showcase': `---
1753
+ id: showcase-mixed-widgets
1754
+ name: Showcase a mix of widgets across categories
1755
+ components_used: [stat-card, chart-rich, data-table, deckgl-scatterplot, kv]
1756
+ when: the user asks for a generic widget showcase, demo, or sampler — phrases like "show me widgets", "demo", "showcase", "what can you display?"
1757
+ servers: [autoui, deckgl]
1758
+ layout:
1759
+ type: grid
1760
+ columns: 3
1761
+ arrangement: KPIs row, then a chart and a table side-by-side, then a map and a kv
1762
+ ---
1763
+
1764
+ ## When to use
1765
+
1766
+ The user wants to see what kinds of widgets the system can render, without specifying a topic. Typical phrases:
1767
+ - "Montre-moi des widgets pour tester"
1768
+ - "Show me a demo / a showcase"
1769
+ - "What can you render?"
1770
+ - "I just want to see widgets"
1771
+
1772
+ This recipe deliberately covers **multiple widget families** (KPI, chart, table, map, key/value) so the user gets a representative sampler in a single canvas.
1773
+
1774
+ ## How to use
1775
+
1776
+ Mount **6 widgets** in this order, each with realistic demo data. **Do NOT list widget names as text labels on a map** — that defeats the purpose. Each widget must render real data of its own type.
1777
+
1778
+ Use exact widget names and exact parameter keys below.
1779
+
1780
+ 1. **3 stat-cards** in a row (\`stat-card\`, keys: \`label\`, \`value\`, \`delta?\`, \`unit?\`):
1781
+ \`\`\`
1782
+ widget_display({name: "stat-card", params: {label: "Active users", value: 12480, unit: "users", delta: 8.2, variant: "success"}})
1783
+ widget_display({name: "stat-card", params: {label: "Revenue (Q1)", value: 184200, unit: "€", delta: 12.4, variant: "success"}})
1784
+ widget_display({name: "stat-card", params: {label: "Churn", value: 3.1, unit: "%", delta: -0.4, variant: "warning"}})
1785
+ \`\`\`
1786
+
1787
+ 2. **A chart** (\`chart-rich\`, keys: \`type\`, \`labels\`, \`data\`):
1788
+ \`\`\`
1789
+ widget_display({name: "chart-rich", params: {
1790
+ title: "Signups (last 6 months)",
1791
+ type: "line",
1792
+ labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
1793
+ data: [{label: "Signups", values: [120, 145, 162, 198, 240, 285]}]
1794
+ }})
1795
+ \`\`\`
1796
+
1797
+ 3. **A table** (\`data-table\`, keys: \`rows\`, \`columns\`):
1798
+ \`\`\`
1799
+ widget_display({name: "data-table", params: {
1800
+ title: "Plans",
1801
+ columns: [
1802
+ {key: "plan", label: "Plan"},
1803
+ {key: "users", label: "Users"},
1804
+ {key: "mrr", label: "MRR"},
1805
+ {key: "delta", label: "Δ 30d"}
1806
+ ],
1807
+ rows: [
1808
+ {plan: "Free", users: 8420, mrr: "€0", delta: "+5%"},
1809
+ {plan: "Pro", users: 3120, mrr: "€62 400", delta: "+11%"},
1810
+ {plan: "Team", users: 740, mrr: "€88 800", delta: "+18%"},
1811
+ {plan: "Enterprise", users: 200, mrr: "€33 000", delta: "+4%"}
1812
+ ]
1813
+ }})
1814
+ \`\`\`
1815
+
1816
+ 4. **A map** with a few markers (\`deckgl-scatterplot\`, key: \`points\`):
1817
+ \`\`\`
1818
+ widget_display({name: "deckgl-scatterplot", params: {
1819
+ points: [
1820
+ {lng: 2.3522, lat: 48.8566, radius: 600, color: [255, 100, 100]},
1821
+ {lng: 4.8357, lat: 45.7640, radius: 500, color: [100, 200, 255]},
1822
+ {lng: 5.3698, lat: 43.2965, radius: 500, color: [120, 255, 120]}
1823
+ ],
1824
+ center: [3.5, 46.5], zoom: 5
1825
+ }})
1826
+ \`\`\`
1827
+
1828
+ 5. **A kv** for metadata / sources (\`kv\`, key: \`rows\` with \`[[k, v], ...]\`):
1829
+ \`\`\`
1830
+ widget_display({name: "kv", params: {
1831
+ title: "About this showcase",
1832
+ rows: [
1833
+ ["Source", "Demo dataset"],
1834
+ ["Generated", "live"],
1835
+ ["Refreshed", "just now"]
1836
+ ]
1837
+ }})
1838
+ \`\`\`
1839
+
1840
+ ## Important
1841
+
1842
+ - **Do NOT** call a single text/label widget that only contains widget *names*. Each widget shown must be a different visual type with its own real data.
1843
+ - For \`kv\`, the property is \`rows\` (not \`pairs\`), and each item is a \`[key, value]\` array of strings.
1844
+ - For \`chart\`, values are \`[label, value]\` tuples; for \`chart-rich\`, values are objects \`{label, values}\` with parallel arrays — pick the matching shape.
1845
+ - For a **cartography**-only showcase use \`showcase-carto\`; for a **dashboard / charts** showcase use \`showcase-dashboard\`.
1846
+ - Keep total widgets between 5 and 8 — beyond that the canvas becomes unreadable.
1847
+
1848
+ ## Output text
1849
+
1850
+ After the tool calls, return a single sentence such as: "Voici un échantillon de 5 widgets — KPIs, courbe, table, carte et métadonnées."
1503
1851
  `,
1504
1852
  'weather-viz': `---
1505
1853
  id: visualize-weather-forecasts-with-charts-and-kpis
@@ -1,12 +1,84 @@
1
1
  // Recipe loader — imports auto-generated .md strings, parses them, exports ready-to-use recipes
2
2
 
3
- export type { Recipe, McpRecipe } from './types.js';
4
- export { parseRecipe, parseRecipes } from './parser.js';
5
-
3
+ import { parseFrontmatter } from '@webmcp-auto-ui/core';
4
+ import type { Recipe } from './types.js';
6
5
  import { RAW_RECIPES } from './_generated.js';
7
- import { parseRecipes } from './parser.js';
8
6
  import { registerRecipes } from '../recipe-registry.js';
9
7
 
8
+ export type { Recipe, McpRecipe } from './types.js';
9
+
10
+ /**
11
+ * Parse a single recipe from its raw markdown string.
12
+ *
13
+ * Supports two formats:
14
+ * - **Structured**: YAML frontmatter between `---` delimiters + markdown body
15
+ * - **Freeform**: plain markdown without frontmatter (id derived from fileKey)
16
+ *
17
+ * @param raw - The raw markdown string
18
+ * @param fileKey - Optional file key (e.g. "gallery-images") used as fallback id for freeform recipes
19
+ */
20
+ export function parseRecipe(raw: string, fileKey?: string): Recipe {
21
+ const { frontmatter, body } = parseFrontmatter(raw);
22
+
23
+ // No frontmatter found → freeform recipe
24
+ if (Object.keys(frontmatter).length === 0) {
25
+ return parseRecipeFreeform(raw, fileKey);
26
+ }
27
+
28
+ return {
29
+ id: (frontmatter.id as string) ?? fileKey ?? '',
30
+ name: (frontmatter.name as string) ?? (frontmatter.id as string) ?? fileKey ?? '',
31
+ description: frontmatter.description as string | undefined,
32
+ components_used: parseStringArray(frontmatter.components_used),
33
+ layout: frontmatter.layout as Recipe['layout'] | undefined,
34
+ interactions: parseInteractions(frontmatter.interactions),
35
+ when: (frontmatter.when as string) ?? '',
36
+ servers: parseStringArray(frontmatter.servers),
37
+ body: body.trim(),
38
+ };
39
+ }
40
+
41
+ /** Parse a freeform .md recipe (no frontmatter). Extracts id from fileKey, name from first heading. */
42
+ function parseRecipeFreeform(raw: string, fileKey?: string): Recipe {
43
+ const body = raw.trim();
44
+ const headingMatch = body.match(/^#+ +(.+)$/m);
45
+ const name = headingMatch?.[1] ?? fileKey ?? 'untitled';
46
+ const id = fileKey ?? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
47
+
48
+ const whenMatch = body.match(/##\s*Quand[^\n]*\n([\s\S]*?)(?=\n##|\n$|$)/i);
49
+ const when = whenMatch?.[1]?.trim().split('\n')[0] ?? '';
50
+
51
+ return { id, name, when, body };
52
+ }
53
+
54
+ /** Parse all raw recipe strings into Recipe objects. Skips invalid ones with a warning. */
55
+ export function parseRecipes(raws: Record<string, string>): Recipe[] {
56
+ const recipes: Recipe[] = [];
57
+ for (const [key, raw] of Object.entries(raws)) {
58
+ try {
59
+ recipes.push(parseRecipe(raw, key));
60
+ } catch (e) {
61
+ console.warn(`[recipes] Failed to parse "${key}":`, e);
62
+ }
63
+ }
64
+ return recipes;
65
+ }
66
+
67
+ function parseStringArray(val: unknown): string[] | undefined {
68
+ if (!val) return undefined;
69
+ if (Array.isArray(val)) return val.map(String);
70
+ if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean);
71
+ return undefined;
72
+ }
73
+
74
+ function parseInteractions(val: unknown): Recipe['interactions'] | undefined {
75
+ if (!Array.isArray(val)) return undefined;
76
+ return val.filter(
77
+ (v): v is { source: string; target: string; event: string; action: string } =>
78
+ typeof v === 'object' && v !== null && 'source' in v && 'target' in v,
79
+ );
80
+ }
81
+
10
82
  /** All built-in WebMCP UI recipes, parsed and ready to use */
11
83
  export const WEBMCP_RECIPES = parseRecipes(RAW_RECIPES);
12
84
 
@@ -0,0 +1,124 @@
1
+ ---
2
+ id: showcase-cartography
3
+ name: Showcase cartographic widgets — points, polygons, aggregations, tiles, 3D
4
+ when: the user asks specifically for a map / cartography / geo demo, e.g. "showcase carto", "demo carto", "show me geo widgets", "what kinds of maps can you render?"
5
+ servers: [deckgl, h3, s2, turf, maplibre]
6
+ components_used: [deckgl-scatterplot, deckgl-arc, deckgl-polygon, deckgl-h3-hexagon, deckgl-heatmap, deckgl-tile, deckgl-trips]
7
+ layout:
8
+ type: grid
9
+ columns: 2
10
+ arrangement: 7 deck.gl maps in a 2-column grid
11
+ ---
12
+
13
+ ## When to use
14
+
15
+ The user wants to see the **cartographic capabilities** of the system. Typical phrases:
16
+ - "Montre-moi un showcase carto"
17
+ - "Demo cartographie"
18
+ - "What kinds of maps / geo widgets can you render?"
19
+ - "Showcase deckgl"
20
+
21
+ This recipe covers the four major deck.gl layer families: **points/lines**, **polygons**, **aggregation** (hexagon/H3/heatmap), **tiles & animation**.
22
+
23
+ ## How to use
24
+
25
+ Mount **7 deck.gl widgets**, each with realistic Paris-region demo data. **Do NOT** stuff all the widget names into a single `deckgl-text` widget — that is not a showcase, that is a label list. Each widget must render its own layer family with real geometric data.
26
+
27
+ Use exact widget names and exact parameter names below. Schemas come from each widget's recipe.
28
+
29
+ 1. **Scatterplot** — points around Paris (`deckgl-scatterplot`, key: `points`):
30
+ ```
31
+ widget_display({name: "deckgl-scatterplot", params: {
32
+ points: [
33
+ {lng: 2.3522, lat: 48.8566, radius: 200, color: [255, 100, 100]},
34
+ {lng: 2.2945, lat: 48.8584, radius: 180, color: [100, 200, 255]},
35
+ {lng: 2.3499, lat: 48.8530, radius: 150, color: [120, 255, 120]},
36
+ {lng: 2.3376, lat: 48.8606, radius: 220, color: [255, 200, 80]},
37
+ {lng: 2.3326, lat: 48.8867, radius: 160, color: [200, 100, 255]}
38
+ ],
39
+ center: [2.3522, 48.8566], zoom: 12
40
+ }})
41
+ ```
42
+
43
+ 2. **Arc** — flows from Paris to other French cities (`deckgl-arc`, key: `arcs`):
44
+ ```
45
+ widget_display({name: "deckgl-arc", params: {
46
+ arcs: [
47
+ {from: [2.3522, 48.8566], to: [4.8357, 45.7640], width: 3},
48
+ {from: [2.3522, 48.8566], to: [5.3698, 43.2965], width: 4},
49
+ {from: [2.3522, 48.8566], to: [-1.5536, 47.2184], width: 2},
50
+ {from: [2.3522, 48.8566], to: [7.7521, 48.5734], width: 3}
51
+ ],
52
+ center: [3.5, 46.5], zoom: 5
53
+ }})
54
+ ```
55
+
56
+ 3. **Polygon** — three quadrilaterals around central Paris (`deckgl-polygon`, key: `polygons`, color key: `fillColor`):
57
+ ```
58
+ widget_display({name: "deckgl-polygon", params: {
59
+ polygons: [
60
+ {polygon: [[2.32, 48.86], [2.36, 48.86], [2.36, 48.88], [2.32, 48.88], [2.32, 48.86]], fillColor: [255, 100, 100, 120]},
61
+ {polygon: [[2.36, 48.86], [2.40, 48.86], [2.40, 48.88], [2.36, 48.88], [2.36, 48.86]], fillColor: [100, 200, 255, 120]},
62
+ {polygon: [[2.32, 48.84], [2.36, 48.84], [2.36, 48.86], [2.32, 48.86], [2.32, 48.84]], fillColor: [100, 255, 120, 120]}
63
+ ],
64
+ center: [2.35, 48.86], zoom: 12
65
+ }})
66
+ ```
67
+
68
+ 4. **H3 hexagon** — Uber H3 cells with values (`deckgl-h3-hexagon`, key: `cells`). If the `h3` server is activated, prefer to call `latLngToCell({lat, lng, res: 8})` first to obtain valid indices for Paris. Otherwise use known-valid Paris-area indices below:
69
+ ```
70
+ widget_display({name: "deckgl-h3-hexagon", params: {
71
+ cells: [
72
+ {hex: "881fb46625fffff", value: 12},
73
+ {hex: "881fb46627fffff", value: 8},
74
+ {hex: "881fb4662dfffff", value: 22},
75
+ {hex: "881fb46663fffff", value: 6},
76
+ {hex: "881fb46669fffff", value: 15}
77
+ ],
78
+ extruded: true, elevationScale: 30,
79
+ center: [2.35, 48.86], zoom: 11
80
+ }})
81
+ ```
82
+
83
+ 5. **Heatmap** — weighted point density (`deckgl-heatmap`, key: `points`):
84
+ ```
85
+ widget_display({name: "deckgl-heatmap", params: {
86
+ points: [
87
+ {lng: 2.35, lat: 48.86, weight: 5}, {lng: 2.36, lat: 48.86, weight: 8},
88
+ {lng: 2.34, lat: 48.87, weight: 3}, {lng: 2.33, lat: 48.85, weight: 12},
89
+ {lng: 2.37, lat: 48.88, weight: 6}, {lng: 2.32, lat: 48.84, weight: 9}
90
+ ],
91
+ center: [2.35, 48.86], zoom: 12
92
+ }})
93
+ ```
94
+
95
+ 6. **Tile** — OSM raster tiles (`deckgl-tile`, key: `tileUrl`):
96
+ ```
97
+ widget_display({name: "deckgl-tile", params: {
98
+ tileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
99
+ center: [2.35, 48.86], zoom: 10
100
+ }})
101
+ ```
102
+
103
+ 7. **Trips** — animated trajectory (`deckgl-trips`, key: `trips`):
104
+ ```
105
+ widget_display({name: "deckgl-trips", params: {
106
+ trips: [
107
+ {path: [[2.30, 48.85], [2.33, 48.86], [2.36, 48.87], [2.40, 48.88]], timestamps: [0, 200, 400, 600], color: [253, 128, 93]}
108
+ ],
109
+ trailLength: 200, animationSpeed: 1,
110
+ center: [2.35, 48.86], zoom: 12
111
+ }})
112
+ ```
113
+
114
+ ## Important
115
+
116
+ - **Never** call `deckgl-text` with a list of widget *names* as labels. That is not a showcase.
117
+ - Each widget must contain real geometric data (positions, polygons, H3 cells…), not labels.
118
+ - Use exactly the parameter keys above (`points` not `data`, `arcs` not `data`, `polygons` not `data`, `cells` not `data`, `tileUrl` not `url`, `trips` not `data`).
119
+ - If the user asks for a specific layer family, drill down into the per-widget recipe (`scatterplot`, `arc`, `h3-hexagon`, etc.).
120
+ - Keep the canvas under 8 maps to stay within WebGL context limits.
121
+
122
+ ## Output text
123
+
124
+ Return a single sentence such as: "Tour cartographique : 7 widgets deck.gl — points, arcs, polygones, H3, heatmap, tiles OSM et trajets animés."
@@ -0,0 +1,122 @@
1
+ ---
2
+ id: showcase-dashboard
3
+ name: Showcase analytics dashboard widgets — Tremor, ECharts, Observable Plot, Recharts, Nivo, Perspective
4
+ when: the user asks for a dashboard / analytics / charts demo, e.g. "showcase dashboard", "demo dashboard", "show me charts", "analytics demo"
5
+ servers: [echarts, observable-plot, recharts, nivo, perspective, tremor, chartjs, d3]
6
+ components_used: [tremor-kpi-card, echarts-line, observable-plot-dot, recharts-bar, nivo-pie, perspective-table]
7
+ layout:
8
+ type: grid
9
+ columns: 3
10
+ arrangement: KPI strip on top, then charts in a grid, table at the bottom
11
+ ---
12
+
13
+ ## When to use
14
+
15
+ The user wants to see the **analytics / dashboard** capabilities of the system — non-cartographic visualizations powered by JS chart libraries. Typical phrases:
16
+ - "Montre-moi un showcase dashboard"
17
+ - "Demo analytics / charts"
18
+ - "Show me what charts you can do"
19
+ - "Showcase ECharts / Observable / Recharts"
20
+
21
+ This recipe covers the four major dashboard widget families: **KPIs**, **time series & comparisons**, **distributions & breakdowns**, **interactive tables**.
22
+
23
+ ## How to use
24
+
25
+ Mount **6-7 widgets** drawn from different chart libraries to demonstrate variety. Pick one widget per server when possible. **Do not collapse everything into a single chart** — the showcase value is the variety.
26
+
27
+ Use exact widget names and exact parameter keys below. Schemas come from each widget's recipe.
28
+
29
+ 1. **3 KPI cards** in a row (`tremor-kpi-card`, keys: `title`, `metric`, `delta`, `deltaType`):
30
+ ```
31
+ widget_display({name: "tremor-kpi-card", params: {title: "MRR", metric: "€ 184 200", delta: "+12.4%", deltaType: "increase"}})
32
+ widget_display({name: "tremor-kpi-card", params: {title: "Active users", metric: "12 480", delta: "+8.2%", deltaType: "increase"}})
33
+ widget_display({name: "tremor-kpi-card", params: {title: "Churn", metric: "3.1 %", delta: "-0.4 pts", deltaType: "decrease"}})
34
+ ```
35
+
36
+ 2. **Time-series line chart** (`echarts-line`, keys: `categories`, `series`):
37
+ ```
38
+ widget_display({name: "echarts-line", params: {
39
+ title: "Signups vs activations",
40
+ categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"],
41
+ series: [
42
+ {name: "Signups", data: [120, 145, 162, 198, 240, 285, 312, 358]},
43
+ {name: "Activations", data: [80, 102, 121, 148, 188, 225, 252, 290]}
44
+ ],
45
+ smooth: true
46
+ }})
47
+ ```
48
+
49
+ 3. **Scatter / dot plot** (`observable-plot-dot`, keys: `data`, `xKey`, `yKey`, `fill`):
50
+ ```
51
+ widget_display({name: "observable-plot-dot", params: {
52
+ title: "Cohort distribution",
53
+ data: [
54
+ {x: 1.2, y: 3.4, group: "A"}, {x: 2.5, y: 5.1, group: "A"}, {x: 3.8, y: 4.2, group: "B"},
55
+ {x: 4.1, y: 6.8, group: "B"}, {x: 5.4, y: 5.9, group: "C"}, {x: 6.2, y: 7.3, group: "C"},
56
+ {x: 7.0, y: 6.1, group: "A"}, {x: 8.3, y: 8.4, group: "B"}
57
+ ],
58
+ xKey: "x", yKey: "y", fill: "group", tip: true
59
+ }})
60
+ ```
61
+
62
+ 4. **Bar chart** (`recharts-bar`, keys: `rows`, `bars`, `xKey`):
63
+ ```
64
+ widget_display({name: "recharts-bar", params: {
65
+ title: "Users by plan",
66
+ rows: [
67
+ {plan: "Free", users: 8420},
68
+ {plan: "Pro", users: 3120},
69
+ {plan: "Team", users: 740},
70
+ {plan: "Enterprise", users: 200}
71
+ ],
72
+ xKey: "plan",
73
+ bars: [{dataKey: "users", color: "#4f8cff"}]
74
+ }})
75
+ ```
76
+
77
+ 5. **Pie / donut** (`nivo-pie`, key: `data` with `{id, label, value}`):
78
+ ```
79
+ widget_display({name: "nivo-pie", params: {
80
+ data: [
81
+ {id: "direct", label: "Direct", value: 38},
82
+ {id: "search", label: "Search", value: 27},
83
+ {id: "ref", label: "Referral", value: 18},
84
+ {id: "social", label: "Social", value: 12},
85
+ {id: "email", label: "Email", value: 5}
86
+ ],
87
+ innerRadius: 0.5
88
+ }})
89
+ ```
90
+
91
+ 6. **Interactive datagrid** (`perspective-table`, key: `rows`):
92
+ ```
93
+ widget_display({name: "perspective-table", params: {
94
+ title: "Revenue by region & plan",
95
+ rows: [
96
+ {date: "2026-01", region: "EU", plan: "Pro", revenue: 18400},
97
+ {date: "2026-01", region: "US", plan: "Pro", revenue: 22100},
98
+ {date: "2026-02", region: "EU", plan: "Pro", revenue: 19800},
99
+ {date: "2026-02", region: "US", plan: "Pro", revenue: 24200},
100
+ {date: "2026-03", region: "EU", plan: "Team", revenue: 31200},
101
+ {date: "2026-03", region: "US", plan: "Team", revenue: 38400}
102
+ ]
103
+ }})
104
+ ```
105
+
106
+ 7. **Optional 7th widget** for extra variety, if the corresponding server is activated:
107
+ - `chartjs-radar` (multi-axis comparison),
108
+ - `nivo-radar` (alternative radar),
109
+ - `d3-force-graph` (mini network),
110
+ - `recharts-area` (stacked area).
111
+
112
+ ## Important
113
+
114
+ - **Variety wins**: pick widgets from *different* libraries. Don't stack 5 ECharts widgets — the goal is to demonstrate the breadth of the dashboard ecosystem.
115
+ - Use exactly the parameter keys above (`metric` not `value`, `delta` not `trend`, `deltaType` not `trendDir`, `categories` not `xAxis`, `rows` not `data` for recharts-bar / perspective-table, `fill` not `color` for observable-plot-dot).
116
+ - Use realistic dashboard data (revenue, users, traffic sources, plans, regions). Avoid abstract `[1, 2, 3]` series.
117
+ - For a **cartography**-focused showcase, use `showcase-carto` instead.
118
+ - For a **mixed** showcase (carto + dashboard + others), use `showcase`.
119
+
120
+ ## Output text
121
+
122
+ Return a single sentence such as: "Showcase dashboard : 6 widgets — KPIs Tremor, série ECharts, scatter Observable Plot, bar Recharts, pie Nivo, table Perspective."
@@ -0,0 +1,99 @@
1
+ ---
2
+ id: showcase-mixed-widgets
3
+ name: Showcase a mix of widgets across categories
4
+ components_used: [stat-card, chart-rich, data-table, deckgl-scatterplot, kv]
5
+ when: the user asks for a generic widget showcase, demo, or sampler — phrases like "show me widgets", "demo", "showcase", "what can you display?"
6
+ servers: [autoui, deckgl]
7
+ layout:
8
+ type: grid
9
+ columns: 3
10
+ arrangement: KPIs row, then a chart and a table side-by-side, then a map and a kv
11
+ ---
12
+
13
+ ## When to use
14
+
15
+ The user wants to see what kinds of widgets the system can render, without specifying a topic. Typical phrases:
16
+ - "Montre-moi des widgets pour tester"
17
+ - "Show me a demo / a showcase"
18
+ - "What can you render?"
19
+ - "I just want to see widgets"
20
+
21
+ This recipe deliberately covers **multiple widget families** (KPI, chart, table, map, key/value) so the user gets a representative sampler in a single canvas.
22
+
23
+ ## How to use
24
+
25
+ Mount **6 widgets** in this order, each with realistic demo data. **Do NOT list widget names as text labels on a map** — that defeats the purpose. Each widget must render real data of its own type.
26
+
27
+ Use exact widget names and exact parameter keys below.
28
+
29
+ 1. **3 stat-cards** in a row (`stat-card`, keys: `label`, `value`, `delta?`, `unit?`):
30
+ ```
31
+ widget_display({name: "stat-card", params: {label: "Active users", value: 12480, unit: "users", delta: 8.2, variant: "success"}})
32
+ widget_display({name: "stat-card", params: {label: "Revenue (Q1)", value: 184200, unit: "€", delta: 12.4, variant: "success"}})
33
+ widget_display({name: "stat-card", params: {label: "Churn", value: 3.1, unit: "%", delta: -0.4, variant: "warning"}})
34
+ ```
35
+
36
+ 2. **A chart** (`chart-rich`, keys: `type`, `labels`, `data`):
37
+ ```
38
+ widget_display({name: "chart-rich", params: {
39
+ title: "Signups (last 6 months)",
40
+ type: "line",
41
+ labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
42
+ data: [{label: "Signups", values: [120, 145, 162, 198, 240, 285]}]
43
+ }})
44
+ ```
45
+
46
+ 3. **A table** (`data-table`, keys: `rows`, `columns`):
47
+ ```
48
+ widget_display({name: "data-table", params: {
49
+ title: "Plans",
50
+ columns: [
51
+ {key: "plan", label: "Plan"},
52
+ {key: "users", label: "Users"},
53
+ {key: "mrr", label: "MRR"},
54
+ {key: "delta", label: "Δ 30d"}
55
+ ],
56
+ rows: [
57
+ {plan: "Free", users: 8420, mrr: "€0", delta: "+5%"},
58
+ {plan: "Pro", users: 3120, mrr: "€62 400", delta: "+11%"},
59
+ {plan: "Team", users: 740, mrr: "€88 800", delta: "+18%"},
60
+ {plan: "Enterprise", users: 200, mrr: "€33 000", delta: "+4%"}
61
+ ]
62
+ }})
63
+ ```
64
+
65
+ 4. **A map** with a few markers (`deckgl-scatterplot`, key: `points`):
66
+ ```
67
+ widget_display({name: "deckgl-scatterplot", params: {
68
+ points: [
69
+ {lng: 2.3522, lat: 48.8566, radius: 600, color: [255, 100, 100]},
70
+ {lng: 4.8357, lat: 45.7640, radius: 500, color: [100, 200, 255]},
71
+ {lng: 5.3698, lat: 43.2965, radius: 500, color: [120, 255, 120]}
72
+ ],
73
+ center: [3.5, 46.5], zoom: 5
74
+ }})
75
+ ```
76
+
77
+ 5. **A kv** for metadata / sources (`kv`, key: `rows` with `[[k, v], ...]`):
78
+ ```
79
+ widget_display({name: "kv", params: {
80
+ title: "About this showcase",
81
+ rows: [
82
+ ["Source", "Demo dataset"],
83
+ ["Generated", "live"],
84
+ ["Refreshed", "just now"]
85
+ ]
86
+ }})
87
+ ```
88
+
89
+ ## Important
90
+
91
+ - **Do NOT** call a single text/label widget that only contains widget *names*. Each widget shown must be a different visual type with its own real data.
92
+ - For `kv`, the property is `rows` (not `pairs`), and each item is a `[key, value]` array of strings.
93
+ - For `chart`, values are `[label, value]` tuples; for `chart-rich`, values are objects `{label, values}` with parallel arrays — pick the matching shape.
94
+ - For a **cartography**-only showcase use `showcase-carto`; for a **dashboard / charts** showcase use `showcase-dashboard`.
95
+ - Keep total widgets between 5 and 8 — beyond that the canvas becomes unreadable.
96
+
97
+ ## Output text
98
+
99
+ After the tool calls, return a single sentence such as: "Voici un échantillon de 5 widgets — KPIs, courbe, table, carte et métadonnées."
@@ -1,182 +0,0 @@
1
- // Frontmatter parser for recipe .md files
2
- // Parses YAML-like frontmatter + markdown body into a Recipe object
3
-
4
- import type { Recipe } from './types.js';
5
-
6
- /**
7
- * Parse a single recipe from its raw markdown string.
8
- *
9
- * Supports two formats:
10
- * - **Structured**: YAML-like frontmatter between `---` delimiters + markdown body
11
- * - **Freeform**: plain markdown without frontmatter (id derived from fileKey)
12
- *
13
- * @param raw - The raw markdown string
14
- * @param fileKey - Optional file key (e.g. "gallery-images") used as fallback id for freeform recipes
15
- */
16
- export function parseRecipe(raw: string, fileKey?: string): Recipe {
17
- const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
18
-
19
- if (!match) {
20
- // Freeform recipe: no frontmatter — derive metadata from content
21
- return parseRecipeFreeform(raw, fileKey);
22
- }
23
-
24
- const frontmatter = parseFrontmatter(match[1]);
25
- const body = match[2].trim();
26
-
27
- return {
28
- id: frontmatter.id as string ?? fileKey ?? '',
29
- name: frontmatter.name as string ?? frontmatter.id as string ?? fileKey ?? '',
30
- description: frontmatter.description as string | undefined,
31
- components_used: parseStringArray(frontmatter.components_used),
32
- layout: frontmatter.layout as Recipe['layout'] | undefined,
33
- interactions: parseInteractions(frontmatter.interactions),
34
- when: frontmatter.when as string ?? '',
35
- servers: parseStringArray(frontmatter.servers),
36
- body,
37
- };
38
- }
39
-
40
- /** Parse a freeform .md recipe (no frontmatter). Extracts id from fileKey, name from first heading. */
41
- function parseRecipeFreeform(raw: string, fileKey?: string): Recipe {
42
- const body = raw.trim();
43
- // Try to extract name from first markdown heading
44
- const headingMatch = body.match(/^#+ +(.+)$/m);
45
- const name = headingMatch?.[1] ?? fileKey ?? 'untitled';
46
- const id = fileKey ?? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
47
-
48
- // Try to extract a "when" hint from the first paragraph or a "## Quand" section
49
- const whenMatch = body.match(/##\s*Quand[^\n]*\n([\s\S]*?)(?=\n##|\n$|$)/i);
50
- const when = whenMatch?.[1]?.trim().split('\n')[0] ?? '';
51
-
52
- return { id, name, when, body };
53
- }
54
-
55
- /** Parse all raw recipe strings into Recipe objects. Skips invalid ones with a warning. */
56
- export function parseRecipes(raws: Record<string, string>): Recipe[] {
57
- const recipes: Recipe[] = [];
58
- for (const [key, raw] of Object.entries(raws)) {
59
- try {
60
- recipes.push(parseRecipe(raw, key));
61
- } catch (e) {
62
- console.warn(`[recipes] Failed to parse "${key}":`, e);
63
- }
64
- }
65
- return recipes;
66
- }
67
-
68
- // ── Internal helpers ──────────────────────────────────────────────────────────
69
-
70
- function parseFrontmatter(raw: string): Record<string, unknown> {
71
- const result: Record<string, unknown> = {};
72
- let currentKey = '';
73
- let currentArray: unknown[] | null = null;
74
- let currentObj: Record<string, unknown> | null = null;
75
- let inObjectArray = false;
76
-
77
- for (const line of raw.split('\n')) {
78
- // Array item: " - value" or " - key: value" (under a key)
79
- if (/^\s+-\s/.test(line) && currentKey) {
80
- const itemRaw = line.replace(/^\s+-\s*/, '').trim();
81
- if (inObjectArray && currentArray) {
82
- // Object item in array: " - source: gallery"
83
- // Collect key: value pairs into a single object until next " -" or new top-level key
84
- const obj = parseInlineObject(itemRaw);
85
- if (obj) {
86
- currentArray.push(obj);
87
- } else {
88
- // Simple string in array
89
- if (!currentArray) currentArray = [];
90
- currentArray.push(itemRaw);
91
- }
92
- } else {
93
- if (!currentArray) currentArray = [];
94
- // Check if it looks like "key: value" (object item)
95
- if (itemRaw.includes(': ')) {
96
- inObjectArray = true;
97
- currentArray.push(parseInlineObject(itemRaw) ?? itemRaw);
98
- } else {
99
- currentArray.push(itemRaw);
100
- }
101
- }
102
- continue;
103
- }
104
-
105
- // Nested key: " key: value" (under a parent key with object value)
106
- if (/^\s+\w/.test(line) && currentKey && !currentArray && currentObj !== null) {
107
- const m = line.match(/^\s+(\w+):\s*(.*)$/);
108
- if (m) {
109
- const val = m[2].trim();
110
- currentObj[m[1]] = isNumeric(val) ? Number(val) : val;
111
- continue;
112
- }
113
- }
114
-
115
- // Flush pending array/object
116
- if (currentKey && (currentArray || currentObj)) {
117
- result[currentKey] = currentArray ?? currentObj;
118
- currentArray = null;
119
- currentObj = null;
120
- inObjectArray = false;
121
- }
122
-
123
- // Top-level key: "key: value" or "key:"
124
- const topMatch = line.match(/^(\w[\w_]*)\s*:\s*(.*)$/);
125
- if (topMatch) {
126
- currentKey = topMatch[1];
127
- const val = topMatch[2].trim();
128
-
129
- if (val === '' || val === '|') {
130
- // Next lines are nested (object or array)
131
- currentObj = {};
132
- continue;
133
- }
134
- if (val.startsWith('[') && val.endsWith(']')) {
135
- // Inline array: [gallery, carousel]
136
- result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
137
- currentKey = '';
138
- continue;
139
- }
140
- // Simple scalar
141
- result[currentKey] = isNumeric(val) ? Number(val) : val;
142
- currentKey = '';
143
- }
144
- }
145
-
146
- // Flush last pending
147
- if (currentKey && (currentArray || currentObj)) {
148
- result[currentKey] = currentArray ?? currentObj;
149
- }
150
-
151
- return result;
152
- }
153
-
154
- function parseStringArray(val: unknown): string[] | undefined {
155
- if (!val) return undefined;
156
- if (Array.isArray(val)) return val.map(String);
157
- if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean);
158
- return undefined;
159
- }
160
-
161
- function parseInteractions(val: unknown): Recipe['interactions'] | undefined {
162
- if (!Array.isArray(val)) return undefined;
163
- return val.filter(
164
- (v): v is { source: string; target: string; event: string; action: string } =>
165
- typeof v === 'object' && v !== null && 'source' in v && 'target' in v
166
- );
167
- }
168
-
169
- function parseInlineObject(raw: string): Record<string, unknown> | null {
170
- if (!raw.includes(': ')) return null;
171
- const obj: Record<string, unknown> = {};
172
- // Split on ", " but not inside values — simple heuristic
173
- for (const part of raw.split(/,\s+/)) {
174
- const m = part.match(/^(\w+)\s*:\s*(.+)$/);
175
- if (m) obj[m[1]] = isNumeric(m[2].trim()) ? Number(m[2].trim()) : m[2].trim();
176
- }
177
- return Object.keys(obj).length > 0 ? obj : null;
178
- }
179
-
180
- function isNumeric(val: string): boolean {
181
- return val !== '' && !isNaN(Number(val));
182
- }