Skip to content

feat(recipe): add measurement Recipe agent, streaming, and Canvas/Recipe tabs#270

Open
tnkshuuhei wants to merge 11 commits into
devfrom
feat/download-recipe
Open

feat(recipe): add measurement Recipe agent, streaming, and Canvas/Recipe tabs#270
tnkshuuhei wants to merge 11 commits into
devfrom
feat/download-recipe

Conversation

@tnkshuuhei
Copy link
Copy Markdown
Member

Summary

  • Recipe(測定ガイダンス)機能を追加: Mastraエージェント/ワークフロー、SSEストリーミングAPIとフック、自己完結型HTMLレンダラー、CanvasツールバーからのDownload Recipe導線。
  • Logic Model生成ダイアログにRecipe生成のオプショントグルを追加し、CanvasをCanvas/Recipeタブ構成に再編。
  • ヘッダーを UnifiedHeader に統合してコンテキスト応じたアクションを集約、RecipePanelのツールバーを撤去してstale UIを RecipeView に集約。

tnkshuuhei added 11 commits June 3, 2026 19:40
Introduce a recipe pipeline that turns each Output/Outcome metric from
the logic model into a practitioner-ready measurement plan: ordered
steps, data collection method, required resources, frequency, target
value, cautions and stakeholders.

- types: RecipeMetricGuidanceSchema, RecipeSchema, RecipeMetricContext
- recipeTool: enforces one structured guidance entry per input metric
- recipeAgent: single batched call, respects user-edited measurement
  methods, preserves the user's working language
- recipeWorkflow: single step with 2-retry policy, validates tool
  output, attaches generation timestamp
- mastra/index: register recipeAgent and recipeWorkflow
Expose the recipe workflow over Server-Sent Events so the UI can show
incremental progress during the ~1-2 minute generation.

- POST /api/recipe/stream: validates the request, runs recipeWorkflow,
  emits step-start / step-finish / step-error / recipe-complete /
  recipe-error events. Mirrors the existing workflow stream route's
  error categorization and 5-minute serverless timeout.
- hooks/useRecipeStream: client-side SSE consumer with abort support,
  exposes { status, recipe, error, start, reset }.
- types/recipe-events: shared discriminated-union event type.
Render a Recipe into a single downloadable HTML file. Self-contained
means inline CSS, inline base64 image, system fonts — no external
resources, no font fetch, no CORS surface. Document title and main
heading use a constant "MUSE RECIPE" so the placeholder branding can be
swapped without touching templates.

- Per-locale labels (en, ja) for section titles and field names
- Outputs / Short-term Outcomes / Intermediate Outcomes sections
- Per-metric card: steps (ordered list), data-collection method,
  frequency + target value (paired), required resources, stakeholders,
  cautions
- HTML escape on every dynamic value (XSS-safe)
- @media print rules: A4, 16mm margins, page-break-inside: avoid on
  metric cards
- downloadRecipeHtml: Blob + object URL + auto-click pattern
- 14 unit tests covering doctype, lang attribute, MUSE RECIPE constants,
  XSS escaping, image inline, section grouping, locale switching,
  empty-section skipping
Add the user-facing entry point that turns the canvas state into a
downloadable HTML recipe.

- RecipeDialog: filters canvas nodes down to Output / Outcome metrics,
  derives a logic-model title from the impact card (when present),
  drives useRecipeStream, captures a high-resolution canvas PNG via
  useCanvasImage, then composes the HTML via the dynamically imported
  generator and triggers download. Handles the no-metrics, generating,
  rendering, success, and error states.
- CanvasToolbar: add Download Recipe (HTML) item with BookOpen icon to
  the dropdown menu; refuses to open when the canvas is empty.
- i18n (en, ja): recipe namespace covering dialog copy and toast
  messages.
The RecipeDialog modal was a one-shot flow that auto-downloaded the HTML
and lost the recipe on close. This switches the canvas page to a tabbed
layout (Canvas / Recipe) so the recipe stays in view, separates the
regenerate and download actions in the toolbar, and surfaces a stale
indicator when the logic model is edited after generation.

- Add RecipeProvider wrapping CanvasProvider so semantic mutations
  (addCard / updateCard / onConnect / edge remove / clear) can mark the
  recipe stale without false positives from React Flow measure/select churn
- Add RecipeView (shadcn-based JSX) and RecipePanel (state machine UI for
  empty / waiting / running / success / stale / error)
- Wire pendingRecipeAutoStartRef in CanvasContext so a later auto-start
  signal can fire triggerGeneration after the canvas finishes settling
- Extract recipe metric helpers to lib/recipe-helpers.ts
- Keep self-contained HTML generation as the download format; the in-tab
  JSX view and the downloadable HTML intentionally remain separate so
  their outputs can be compared before deciding on a single source

forceMount + data-[state=inactive]:hidden keeps React Flow's viewport
state intact across tab switches.
Adds an "Also generate recipe" switch to the generate dialog (default
OFF). When enabled and metrics are also enabled, the recipe stream
auto-starts once the canvas finishes settling — the auto-fire path in
CanvasContext was already in place; this commit arms it from the form.

Recipe requires metrics as input, so the switch is force-disabled with a
tooltip explaining the prerequisite whenever the Metrics toggle is OFF.
- react-flow-architecture.md: provider tree, Recipe tab state machine,
  stale-detection carve-outs (no false positives from onNodesChange),
  the direct chain that fires the recipe after auto-layout when the
  generate dialog opted in
- frontend-map.md: new RecipePanel / RecipeView / RecipeContext modules,
  useRecipeStream hook, lib/recipe-helpers and lib/generate-recipe-html
- api-routes.md: add /api/recipe/stream row + recipe schemas
- mastra-agents.md: new Recipe Workflow section, enableRecipe addition
  to the generate dialog Options list, recipe files in File References
…aware actions

Merge the previous CanvasToolbar and TabsList rows into a single
UnifiedHeader. Action buttons (Generate / Add card / Regenerate /
Download HTML / Retry) are now driven by ContextActions, which switches
based on activeTab and recipe.phase. The More dropdown is reorganized
into labelled Canvas / Recipe / Danger sections so the same recipe
operations remain reachable from both the primary header and the menu.
…ipeView

RecipePanel now renders document content only — the sticky toolbar
(Regenerate / Download HTML) and the duplicate stale badge/banner are
removed because those actions live in UnifiedHeader. RecipeView accepts
a stale prop and surfaces it as a chip in the meta row plus a single
alert below the document header, replacing the previous three-way stale
representation.
…tale UI

Update the canvas architecture docs to reflect the unified header
(tabs + context actions + sectioned More dropdown), the dual access
paths for Recipe operations (header buttons + Recipe section of the
dropdown), and the new stale chip + alert pattern rendered by
RecipeView. Refresh component references in frontend-map.md and
mastra-agents.md so the toolbar wording matches the new structure.
…processing

Changed the loading message in RecipePanel to display the number of metrics being processed instead of the current step ID. Updated corresponding translations in English and Japanese for consistency.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
muse Ready Ready Preview, Comment Jun 4, 2026 12:29pm

@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Code Review - feat(recipe): add measurement Recipe agent, streaming, and Canvas/Recipe tabs

Overview

This is a well-structured addition of a measurement Recipe feature: a Mastra agent/workflow generates actionable step-by-step guidance for every Output/Outcome metric, streamed via SSE and rendered in a new Recipe tab alongside the existing canvas. Provider nesting is clearly documented, the stale-detection strategy is thoughtful, and the HTML export has solid XSS protection with good unit tests.

A few issues worth addressing before merge.


Potential Bug - Metric mutations do not trigger markStale

The stale-detection wiring wraps card-level operations (addCard, updateCard, deleteCard, onConnect, plus inline onContentChange / onDeleteCard). But the recipe is driven by metrics, not card titles, so the more critical mutations are adding/removing/editing a metric within a card.

addMetric, removeMetric, and updateMetric (canvas operations that mutate cardMetrics) are NOT wrapped with markStale. If a user generates a recipe, then adds a metric to an output card, the recipe tab shows no stale indicator even though the output will be out of sync.

These should be wrapped the same way wrappedAddCard etc. are in CanvasContext.tsx.


Testing policy gap - pure functions and Mastra tools lack tests

Per the project policy in CLAUDE.md: "any new pure function, utility, API handler, or Mastra tool should land with Vitest coverage."

Missing coverage:

  • lib/recipe-helpers.ts - collectMetricContexts, deriveLogicModelTitle, countRecipeTargetCards are pure functions and easy to test with factories similar to the existing generate-recipe-html.test.ts.
  • mastra/tools/recipe-tool.ts - the tool's execute function is a passthrough, but the schema validation and field mapping deserve at least a smoke test.

generate-recipe-html.test.ts is a good example to follow - the same pattern works for helpers.


Dead i18n keys

Three keys added to the canvas namespace appear unused:

  • canvas.regenerateRecipe - not referenced; UnifiedHeader uses tRecipe("regenerate")
  • canvas.downloadRecipeHtml - not referenced; UnifiedHeader uses tRecipe("downloadHtml")
  • canvas.recipeNoMetricsError - not referenced; RecipeContext uses t("noMetricsBody") from the recipe namespace

These should either be removed or wired up. Orphaned translation keys cause confusion and break next-intl missing-key warnings.


Hardcoded sentinel string in workflow

In mastra/workflows/recipe-workflow.ts:

if (m.existingMeasurementMethod && m.existingMeasurementMethod !== "To be defined") {

"To be defined" is a magic string tightly coupled to whatever default the metric creation UI inserts. If that string ever changes, the recipe prompt will silently start including placeholder text in the agent context. Extract it to a named constant (e.g. METRIC_PLACEHOLDER_VALUE) and import it from shared types or a constants file.


30-metric limit not surfaced in the UI

The API and workflow both cap at 30 metrics, which is correct for safety. However, if a canvas has 31+ metrics, the request returns a 400 and the UI shows a generic error. Consider:

  • Showing a warning in RecipePanel (idle state) when metricCount > 30
  • Or slicing to the first 30 in collectMetricContexts with a toast/info message

Minor issues

cancelWaiting exported but never called - RecipeContextValue exposes cancelWaiting but nothing in the diff calls it. Remove it or add a TODO comment if intended for future use.

as unknown as in workflow - the double-cast in recipe-workflow.ts when calling recipeAgent.generate(...) signals a Mastra type mismatch. Not a runtime risk, but a comment explaining why the cast is needed would help future callers.

recipeTool.ts execute cast - inputData as { items: Recipe["items"] } bypasses Zod-inferred types. If Mastra's execute callback does not propagate inputSchema types automatically, a short comment would make the intent clear.


What's working well

  • Provider nesting rationale (RecipeProvider outside CanvasProvider) is clearly documented in code comments and architecture docs.
  • forceMount + data-[state=inactive]:hidden for the React Flow tab is the right pattern and correctly explained.
  • XSS escaping in generate-recipe-html.ts is thorough and well-tested (13 test cases).
  • Stale-detection design (semantic mutation entry points, not raw React Flow callbacks) is the right call.
  • Full i18n coverage (en + ja) from day one.
  • Excellent docs update across api-routes.md, frontend-map.md, mastra-agents.md, and react-flow-architecture.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant