@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 +1 -1
- package/src/autoui-server.ts +37 -90
- package/src/index.ts +8 -0
- package/src/loop.ts +12 -6
- package/src/nano-rag/embedder.ts +2 -1
- package/src/ort-version.ts +35 -0
- package/src/prompts/index.ts +6 -1
- package/src/providers/transformers.worker.ts +2 -1
- package/src/recipes/_generated.ts +348 -0
- package/src/recipes/index.ts +76 -4
- package/src/recipes/showcase-carto.md +124 -0
- package/src/recipes/showcase-dashboard.md +122 -0
- package/src/recipes/showcase.md +99 -0
- package/src/recipes/parser.ts +0 -182
package/package.json
CHANGED
package/src/autoui-server.ts
CHANGED
|
@@ -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.
|
|
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:
|
|
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 (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
}
|
package/src/nano-rag/embedder.ts
CHANGED
|
@@ -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 =
|
|
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}`;
|
package/src/prompts/index.ts
CHANGED
|
@@ -43,7 +43,12 @@ export function buildSystemPromptWithAliases(
|
|
|
43
43
|
return { prompt, aliasMap: refs.aliasMap };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
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 =
|
|
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
|
package/src/recipes/index.ts
CHANGED
|
@@ -1,12 +1,84 @@
|
|
|
1
1
|
// Recipe loader — imports auto-generated .md strings, parses them, exports ready-to-use recipes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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."
|
package/src/recipes/parser.ts
DELETED
|
@@ -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
|
-
}
|