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
10 changes: 10 additions & 0 deletions .changeset/dull-weeks-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@mobile-reality/mdma-renderer-react": major
"@mobile-reality/mdma-prompt-pack": major
"@mobile-reality/mdma-validator": major
"@mobile-reality/mdma-runtime": major
"@mobile-reality/mdma-parser": major
"@mobile-reality/mdma-demo": patch
---

Added validator to the project
8 changes: 7 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
}
},
"files": {
"ignore": ["**/dist/**", "**/node_modules/**", "**/.turbo/**", "**/pnpm-lock.yaml"]
"ignore": [
"**/dist/**",
"**/node_modules/**",
"**/.turbo/**",
"**/pnpm-lock.yaml",
"evals/results*.json"
]
}
}
38 changes: 19 additions & 19 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ interface NavGroup {
const NAV_GROUPS: NavGroup[] = [
{
label: 'Documents',
items: [
{ mode: 'examples', label: 'Examples' },
],
items: [{ mode: 'examples', label: 'Examples' }],
},
{
label: 'AI',
Expand All @@ -42,10 +40,7 @@ const NAV_GROUPS: NavGroup[] = [
},
{
label: 'Tools',
items: [
{ mode: 'validator', label: 'Validator' },
{ mode: 'stepper', label: 'Stepper' },
],
items: [{ mode: 'validator', label: 'Validator' }],
},
];

Expand Down Expand Up @@ -152,7 +147,14 @@ export function App() {
onClick={() => setDropdownOpen((v) => !v)}
>
{getModeLabel(mode)}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
Expand All @@ -166,7 +168,10 @@ export function App() {
key={item.mode}
type="button"
className={`demo-nav-item ${mode === item.mode ? 'demo-nav-item--active' : ''}`}
onClick={() => { setMode(item.mode); setDropdownOpen(false); }}
onClick={() => {
setMode(item.mode);
setDropdownOpen(false);
}}
>
{item.label}
</button>
Expand All @@ -192,9 +197,7 @@ export function App() {
</div>
</header>

{mode === 'stepper' ? (
<StepperView />
) : mode === 'validator' ? (
{mode === 'validator' ? (
<ValidatorView />
) : mode === 'playground' ? (
<PlaygroundView />
Expand All @@ -214,22 +217,19 @@ export function App() {
<h2 className="demo-event-title">Event Log</h2>
<div className="demo-event-log" ref={eventLogRef}>
{events.length === 0 && (
<p className="demo-event-empty">
Interact with the document to see events here.
</p>
<p className="demo-event-empty">Interact with the document to see events here.</p>
)}
{events.map((entry) => (
<div key={entry.id} className="demo-event-entry">
<span className="demo-event-time">{entry.timestamp}</span>
<span className={`demo-event-type demo-event-type--${entry.action.type}`}>
{entry.action.type}
</span>
<span className="demo-event-component">
{entry.action.componentId}
</span>
<span className="demo-event-component">{entry.action.componentId}</span>
{'field' in entry.action && (
<span className="demo-event-detail">
.{entry.action.field} = {JSON.stringify((entry.action as { value: unknown }).value)}
.{entry.action.field} ={' '}
{JSON.stringify((entry.action as { value: unknown }).value)}
</span>
)}
{'actionId' in entry.action && (
Expand Down
52 changes: 30 additions & 22 deletions demo/src/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export interface ChatViewProps {
editable?: boolean;
}

export function ChatView({ customizations, systemPrompt, userSuffix, storageKey, editable }: ChatViewProps = {}) {
export function ChatView({
customizations,
systemPrompt,
userSuffix,
storageKey,
editable,
}: ChatViewProps = {}) {
const chatOptions: UseChatOptions = {
...(customizations?.schemas && { parserOptions: { customSchemas: customizations.schemas } }),
...(systemPrompt !== undefined && { systemPrompt }),
Expand Down Expand Up @@ -72,16 +78,21 @@ export function ChatView({ customizations, systemPrompt, userSuffix, storageKey,

// Clean up on unmount
useEffect(() => {
return () => { subscribedStores.current.clear(); };
return () => {
subscribedStores.current.clear();
};
}, []);

const handleLoadFlow = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
const key = e.target.value;
if (!key) return;
const flow = exampleFlows[key];
if (flow) startFlow(flow.steps, flow.customPrompt);
e.target.value = '';
}, [startFlow]);
const handleLoadFlow = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const key = e.target.value;
if (!key) return;
const flow = exampleFlows[key];
if (flow) startFlow(flow.steps, flow.customPrompt);
e.target.value = '';
},
[startFlow],
);

const { events, isOpen, setIsOpen, clearEvents } = useChatActionLog(messages);

Expand All @@ -107,18 +118,15 @@ export function ChatView({ customizations, systemPrompt, userSuffix, storageKey,
return (
<div className={`chat-layout ${isOpen ? 'chat-layout--with-log' : ''}`}>
<div className="chat-main">
<ChatSettings
config={config}
onUpdate={updateConfig}
onPreset={applyPreset}
/>
<ChatSettings config={config} onUpdate={updateConfig} onPreset={applyPreset} />

<div className="chat-messages">
{messages.length === 0 && (
<div className="chat-empty">
<p className="chat-empty-title">MDMA Chat</p>
<p className="chat-empty-hint">
Describe an interactive document and the AI will generate it, or try an example flow:
Describe an interactive document and the AI will generate it, or try an example
flow:
</p>
<select
defaultValue=""
Expand All @@ -135,9 +143,13 @@ export function ChatView({ customizations, systemPrompt, userSuffix, storageKey,
minWidth: '220px',
}}
>
<option value="" disabled>Load an example flow…</option>
<option value="" disabled>
Load an example flow…
</option>
{Object.entries(exampleFlows).map(([key, flow]) => (
<option key={key} value={key}>{flow.label}</option>
<option key={key} value={key}>
{flow.label}
</option>
))}
</select>
</div>
Expand Down Expand Up @@ -170,11 +182,7 @@ export function ChatView({ customizations, systemPrompt, userSuffix, storageKey,
/>
</div>

<ChatActionLog
events={events}
isOpen={isOpen}
onToggle={() => setIsOpen((prev) => !prev)}
/>
<ChatActionLog events={events} isOpen={isOpen} onToggle={() => setIsOpen((prev) => !prev)} />
</div>
);
}
8 changes: 4 additions & 4 deletions demo/src/CustomChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export function CustomChatView() {
<div className="custom-chat-info">
<strong>Custom Components Mode</strong>
<span>
New types: <code>progress</code>, <code>rating</code>, <code>metric</code>.
Built-in: <code>chart</code> (recharts override).
Restyled built-ins: <code>button</code>, <code>table</code>, <code>callout</code>.
Form elements: glass inputs, toggle switches, gradient submit.
New types: <code>progress</code>, <code>rating</code>, <code>metric</code>. Built-in:{' '}
<code>chart</code> (recharts override). Restyled built-ins: <code>button</code>,{' '}
<code>table</code>, <code>callout</code>. Form elements: glass inputs, toggle switches,
gradient submit.
</span>
</div>
<ChatView customizations={customizations} editable />
Expand Down
4 changes: 2 additions & 2 deletions demo/src/PlaygroundView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export function PlaygroundView() {
<div className="playground-info">
<strong>Playground</strong>
<span>
Free-form chat. The AI knows MDMA basics and will generate interactive
components when you ask for them.
Free-form chat. The AI knows MDMA basics and will generate interactive components when you
ask for them.
</span>
</div>
<ChatView
Expand Down
55 changes: 22 additions & 33 deletions demo/src/StepperView.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// OUTDATED: This file was part of an experiment with a step-by-step form generation flow. It is no longer actively maintained and may contain outdated code. Use for reference only.

import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import { useChat } from './chat/use-chat.js';
import { ChatSettings } from './chat/ChatSettings.js';
import { ChatMessage } from './chat/ChatMessage.js';
import { ChatInput } from './chat/ChatInput.js';
import { validate, type ValidationResult, type ValidationIssue } from '@mobile-reality/mdma-validator';
import {
validate,
type ValidationResult,
type ValidationIssue,
} from '@mobile-reality/mdma-validator';
import { customizations } from './custom-components.js';

const STEPPER_PROMPT = `You are an AI assistant that generates a multi-step onboarding flow using MDMA components.
Expand Down Expand Up @@ -83,11 +89,7 @@ const STEP_PROMPTS = [
'Generate Step 3: Payment & Review form with summary table and approval gate.',
];

const STEP_LABELS = [
'Personal Info',
'Shipping Address',
'Payment & Review',
];
const STEP_LABELS = ['Personal Info', 'Shipping Address', 'Payment & Review'];

function severityClass(severity: string): string {
if (severity === 'error') return 'validator-severity--error';
Expand All @@ -102,9 +104,7 @@ function IssueRow({ issue }: { issue: ValidationIssue }) {
{issue.severity}
</span>
<span className="validator-issue-rule">{issue.ruleId}</span>
{issue.componentId && (
<span className="validator-issue-component">#{issue.componentId}</span>
)}
{issue.componentId && <span className="validator-issue-component">#{issue.componentId}</span>}
<span className="validator-issue-msg">{issue.message}</span>
{issue.fixed && <span className="validator-issue-badge">fixed</span>}
</div>
Expand All @@ -118,10 +118,7 @@ function ValidationPanel({
results: Map<number, ValidationResult>;
stepLabels: Map<number, string>;
}) {
const entries = useMemo(
() => Array.from(results.entries()).reverse(),
[results],
);
const entries = useMemo(() => Array.from(results.entries()).reverse(), [results]);

if (entries.length === 0) {
return (
Expand All @@ -137,10 +134,10 @@ function ValidationPanel({
<div className="validator-results-panel">
{entries.map(([msgId, result]) => (
<div key={msgId} className="validator-msg-result">
<div className={`validator-summary ${result.ok ? 'validator-summary--ok' : 'validator-summary--fail'}`}>
<span className="validator-summary-status">
{result.ok ? 'PASS' : 'FAIL'}
</span>
<div
className={`validator-summary ${result.ok ? 'validator-summary--ok' : 'validator-summary--fail'}`}
>
<span className="validator-summary-status">{result.ok ? 'PASS' : 'FAIL'}</span>
<span className="validator-summary-label">
{stepLabels.get(msgId) ?? `msg #${msgId}`}
</span>
Expand All @@ -161,9 +158,7 @@ function ValidationPanel({
</span>
)}
{result.fixCount > 0 && (
<span className="validator-fix-count">
{result.fixCount} auto-fixed
</span>
<span className="validator-fix-count">{result.fixCount} auto-fixed</span>
)}
</span>
</div>
Expand Down Expand Up @@ -214,7 +209,9 @@ export function StepperView() {
...(customizations.schemas && { parserOptions: { customSchemas: customizations.schemas } }),
});

const [validationResults, setValidationResults] = useState<Map<number, ValidationResult>>(new Map());
const [validationResults, setValidationResults] = useState<Map<number, ValidationResult>>(
new Map(),
);
const [stepLabelMap, setStepLabelMap] = useState<Map<number, string>>(new Map());
const validatedRef = useRef<Set<number>>(new Set());

Expand Down Expand Up @@ -309,11 +306,7 @@ export function StepperView() {
))}
</div>
{!isGenerating && currentStep < 3 && (
<button
type="button"
className="stepper-generate-btn"
onClick={handleNextStep}
>
<button type="button" className="stepper-generate-btn" onClick={handleNextStep}>
{currentStep === 0 ? 'Start' : `Step ${currentStep + 1}`}: {STEP_LABELS[currentStep]}
</button>
)}
Expand All @@ -324,19 +317,15 @@ export function StepperView() {

<div className="validator-content">
<div className="validator-chat-panel">
<ChatSettings
config={config}
onUpdate={updateConfig}
onPreset={applyPreset}
/>
<ChatSettings config={config} onUpdate={updateConfig} onPreset={applyPreset} />

<div className="chat-messages">
{messages.length === 0 && (
<div className="chat-empty">
<p className="chat-empty-title">Stepper Forms Playground</p>
<p className="chat-empty-hint">
Generate a 3-step onboarding flow one step at a time.
Each step is a separate AI response, validated by <code>@mobile-reality/mdma-validator</code>.
Generate a 3-step onboarding flow one step at a time. Each step is a separate AI
response, validated by <code>@mobile-reality/mdma-validator</code>.
</p>
</div>
)}
Expand Down
Loading
Loading