Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-rings-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mobile-reality/mdma-runtime": patch
---

Serialize File instances in FIELD_CHANGED payloads before audit-log append and redaction, so uploaded files keep { name, size, type, lastModified } in the trail instead of being JSON-flattened to {}. Exports a new serializeFiles helper for consumers (e.g. UI subscribers on eventBus) that need the same conversion.
8 changes: 8 additions & 0 deletions .changeset/grumpy-lilies-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@mobile-reality/mdma-renderer-react": patch
"@mobile-reality/mdma-prompt-pack": patch
"@mobile-reality/mdma-validator": patch
"@mobile-reality/mdma-spec": patch
---

Add `file` field type to forms. Forms can now declare file upload inputs, with a default file input UI in the renderer (overridable via `ElementOverridesContext`), schema defaults in the validator, and authoring guidance in the prompt pack.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ onAction: submit

| Component | Type key | Description |
|-----------|----------|-------------|
| **Form** | `form` | Multi-field forms with text, email, number, select, textarea, checkbox, and datetime fields. Supports validation, required fields, default values, and sensitive (PII) flags. |
| **Form** | `form` | Multi-field forms with text, email, number, select, textarea, checkbox, datetime, and file fields. Supports validation, required fields, default values, and sensitive (PII) flags. |
| **Button** | `button` | Action buttons with `primary`, `secondary`, and `danger` variants. |
| **Tasklist** | `tasklist` | Interactive checkbox task items with labels. |
| **Table** | `table` | Data tables with typed columns and row data. |
Expand Down Expand Up @@ -506,7 +506,7 @@ pnpm eval:view
- [x] Added MCP
- [x] Added Skills for Agentic usage
- [x] Improved error messages in parser
- [ ] File upload field type for forms
- [x] File upload field type for forms

### v0.3 — AI & Generation
- [ ] Multi-model eval coverage (Claude, GPT-4o, Gemini, Llama)
Expand Down
3 changes: 2 additions & 1 deletion demo/src/chat/ChatActionLog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { memo, useRef, useEffect } from 'react';
import type { StoreAction } from '@mobile-reality/mdma-spec';
import { serializeFiles } from '@mobile-reality/mdma-runtime';
import type { ChatActionEntry } from './use-chat-action-log.js';

export interface ChatActionLogProps {
Expand All @@ -12,7 +13,7 @@ function renderDetail(action: StoreAction) {
if ('field' in action) {
return (
<span className="demo-event-detail">
.{action.field} = {JSON.stringify((action as { value: unknown }).value)}
.{action.field} = {JSON.stringify(serializeFiles((action as { value: unknown }).value))}
</span>
);
}
Expand Down
5 changes: 5 additions & 0 deletions demo/src/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import orderTracking from '../../examples/order-tracking/document.md?raw';
import meetingNotes from '../../examples/meeting-notes/document.md?raw';
import salesDashboard from '../../examples/sales-dashboard/document.md?raw';
import featureRequest from '../../examples/feature-request/document.md?raw';
import fileUpload from '../../examples/file-upload/document.md?raw';
import incidentTriage from '../../blueprints/incident-triage/document.md?raw';
import changeManagement from '../../blueprints/change-management/document.md?raw';
import customerEscalation from '../../blueprints/customer-escalation/document.md?raw';
Expand Down Expand Up @@ -56,6 +57,10 @@ export const documents: Record<string, DocumentEntry> = {
label: 'Feature Request',
markdown: featureRequest,
},
'file-upload': {
label: 'Job Application (File Upload)',
markdown: fileUpload,
},
// Blueprints — industry-specific workflows
'incident-triage': {
label: 'Incident Triage',
Expand Down
77 changes: 77 additions & 0 deletions demo/src/example-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,83 @@ content: "Your message has been received. Our team will review it and respond wi
dismissible: true
\`\`\`

Is there anything else I can help you with?`,
},
],
},

'job-application': {
label: 'Job Application (File Upload)',
customPrompt: `You are a recruiting assistant that helps candidates submit job applications with supporting documents.

When a candidate wants to apply:
1. Present an application form with: Full Name (required), Email (required, sensitive), Resume (required, type: file), Portfolio Samples (type: file), Cover Letter (textarea, min 50 chars).
2. Add a "Submit Application" button with a confirmation dialog that warns the submission is final.
3. After submission, show a success callout confirming receipt and mention a 5 business day response window.

Be professional and encouraging. Mark email as sensitive.`,
steps: [
{
userMessage: 'I want to apply for a job — give me an application form',
markdown: `# Job Application

Thanks for your interest! Please complete the application below. We review every submission within 5 business days.

\`\`\`mdma
id: application-form
type: form
fields:
- name: full_name
type: text
label: Full Name
required: true
- name: email
type: email
label: Email Address
required: true
sensitive: true
- name: resume
type: file
label: Resume
required: true
- name: portfolio
type: file
label: Portfolio Samples
- name: cover_letter
type: textarea
label: Cover Letter
validation:
min: 50
message: Please write at least 50 characters.
onSubmit: submit-application
\`\`\`

\`\`\`mdma
id: submit-btn
type: button
text: Submit Application
variant: primary
onAction: submit-application
confirm:
title: Submit Application?
message: Once submitted, you will not be able to edit your application.
confirmText: Submit
cancelText: Keep Editing
\`\`\``,
},
{
userMessage: '[Application submitted]',
markdown: `Your application is in — thanks for applying!

\`\`\`mdma
id: submission-success
type: callout
variant: success
title: Application Received
content: "We've received your application and attached documents. Our team will review it and respond within 5 business days."
dismissible: true
\`\`\`

Is there anything else I can help you with?`,
},
],
Expand Down
26 changes: 23 additions & 3 deletions demo/src/validator-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export const VALIDATOR_PROMPT_VARIANTS: ValidatorPromptVariant[] = [
key: 'structure',
label: 'Structure & YAML',
description: 'YAML correctness, duplicate IDs, ID format, schema conformance',
rules: ['yaml-correctness', 'schema-conformance', 'duplicate-ids', 'id-format', 'thinking-block'],
rules: [
'yaml-correctness',
'schema-conformance',
'duplicate-ids',
'id-format',
'thinking-block',
],
prompt: `${PREAMBLE}

Focus ONLY on structural and YAML issues. Generate an event registration system with these exact components, each with intentional structural problems:
Expand Down Expand Up @@ -192,7 +198,13 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked or on the first
key: 'forms',
label: 'Form Validation',
description: 'Select options, field name typos, placeholder content, expected components',
rules: ['select-options', 'field-name-typos', 'placeholder-content', 'expected-components', 'thinking-block'],
rules: [
'select-options',
'field-name-typos',
'placeholder-content',
'expected-components',
'thinking-block',
],
prompt: `${PREAMBLE}

Focus ONLY on form-specific issues. Generate a single job application form with intentional problems:
Expand Down Expand Up @@ -644,7 +656,15 @@ export const EXPECTED_COMPONENTS: Record<string, Record<string, ExpectedComponen
forms: {
'job-application': {
type: 'form',
fields: ['full-name', 'email', 'phone', 'university', 'highest-degree', 'department', 'start-date'],
fields: [
'full-name',
'email',
'phone',
'university',
'highest-degree',
'department',
'start-date',
],
actions: { onSubmit: 'apply-btn' },
},
'application-note': { type: 'callout' },
Expand Down
12 changes: 11 additions & 1 deletion demo/src/validator/useLlmFixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@ export function useLlmFixer({
fixAbortRef.current = null;
}
},
[validationResults, messages, config, isFixing, fixerModel, customFixerModel, promptKey, onFixed, onInvalidate],
[
validationResults,
messages,
config,
isFixing,
fixerModel,
customFixerModel,
promptKey,
onFixed,
onInvalidate,
],
);

// Auto-fix with LLM when enabled and unfixed issues detected
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/component-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Collects structured user input through typed fields with validation.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `name` | `string` | *required* | Field identifier. Min length 1. |
| `type` | `enum` | *required* | One of: `text`, `number`, `email`, `date`, `select`, `checkbox`, `textarea`. |
| `type` | `enum` | *required* | One of: `text`, `number`, `email`, `date`, `select`, `checkbox`, `textarea`, `file`. |
| `label` | `string` | *required* | Display label. |
| `required` | `boolean` | `false` | Whether the field must have a value. |
| `sensitive` | `boolean` | `false` | If true, this field's value is redacted in event logs. |
Expand Down
43 changes: 43 additions & 0 deletions evals/assertions/file-field.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Asserts that the output contains a form with a file-typed field.
*
* Optional config:
* - sensitive: boolean — asserts the file field sets `sensitive: true`
*
* Note: `accept` and `multiple` are renderer-level concerns and are NOT part
* of the MDMA spec, so they are not asserted here.
*/
export default function (output, { config } = {}) {
const blockRegex = /```mdma\n([\s\S]*?)```/g;
const blocks = [...output.matchAll(blockRegex)].map((m) => m[1]);

const formBlocks = blocks.filter((b) => /^type:\s*form/m.test(b));
if (formBlocks.length === 0) {
return { pass: false, score: 0, reason: 'No form block found in output' };
}

const fileBlock = formBlocks.find((b) => /type:\s*file\b/.test(b));
if (!fileBlock) {
return {
pass: false,
score: 0,
reason: 'No form field with `type: file` found',
};
}

const reasons = ['Form contains a file field'];

if (config?.sensitive === true) {
const sensitivePattern = /type:\s*file[\s\S]{0,200}sensitive:\s*true/;
if (!sensitivePattern.test(fileBlock)) {
return {
pass: false,
score: 0,
reason: 'File field expected sensitive: true but not found',
};
}
reasons.push('sensitive: true');
}

return { pass: true, score: 1, reason: reasons.join('; ') };
}
3 changes: 2 additions & 1 deletion evals/assertions/fixer-resolves-errors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { validate } from '@mobile-reality/mdma-validator';
*/
export default function (output, { config } = {}) {
const maxWarnings = config?.maxWarnings ?? Infinity;
const exclude = config?.exclude ?? ['thinking-block', 'flow-ordering'];

// Check the output actually contains mdma blocks
const blockCount = (output.match(/```mdma/g) ?? []).length;
Expand All @@ -25,7 +26,7 @@ export default function (output, { config } = {}) {
}

const result = validate(output, {
exclude: ['thinking-block'],
exclude,
autoFix: false,
});

Expand Down
12 changes: 10 additions & 2 deletions evals/assertions/validate-mdma.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import { validate } from '@mobile-reality/mdma-validator';
*
* Returns pass if the validator reports no unfixed errors.
* On failure, includes a summary of all issues found.
*
* Optional config:
* - exclude: string[] — additional rule IDs to skip on top of the
* always-excluded `thinking-block` rule. Useful when a suite's
* blueprints deliberately violate a stylistic rule (e.g.
* `flow-ordering` for the custom-prompt suite, where prompts
* intentionally bundle multiple components per message).
*/
export default function (output) {
export default function (output, { config } = {}) {
const extraExclude = Array.isArray(config?.exclude) ? config.exclude : [];
const result = validate(output, {
exclude: ['thinking-block'],
exclude: ['thinking-block', ...extraExclude],
autoFix: false,
});

Expand Down
5 changes: 4 additions & 1 deletion evals/prompt-fixer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { validate } from '@mobile-reality/mdma-validator';
* 3. Sends the fixer system prompt (with variant-specific extensions) + user message
*/
export default function ({ vars }) {
const result = validate(vars.brokenDocument, { exclude: ['thinking-block'] });
const exclude = ['thinking-block'];
if (vars.variantKey !== 'flow') exclude.push('flow-ordering');

const result = validate(vars.brokenDocument, { exclude });
const unfixed = result.issues.filter(
(i) => !i.fixed && (i.severity === 'error' || i.severity === 'warning'),
);
Expand Down
3 changes: 2 additions & 1 deletion evals/promptfooconfig.custom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ providers:

defaultTest:
assert:
# Every test case still runs the MDMA validator as a baseline check
- type: javascript
value: file://assertions/validate-mdma.mjs
config:
exclude: [flow-ordering]

tests: tests-custom-prompt.yaml
2 changes: 2 additions & 0 deletions evals/promptfooconfig.fixer-flow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ defaultTest:
assert:
- type: javascript
value: file://assertions/fixer-resolves-errors.mjs
config:
exclude: ['thinking-block']
- type: javascript
value: file://assertions/fixer-preserves-components.mjs
config:
Expand Down
3 changes: 3 additions & 0 deletions evals/promptfooconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ defaultTest:
# Every test case runs the MDMA validator as a baseline check
- type: javascript
value: file://assertions/validate-mdma.mjs
config:
exclude: [flow-ordering]


tests: tests.yaml
Loading