Chat
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data โ all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v1234View latest version
To create a component that works with the affordance system, you need JavaScript/TypeScript code that exports a class implementing the AffordanceComponent
interface. This code can come from:
- MCP file sources (recommended) - Use MCP tools to create/manage component files
- Local project files - Files in the
/frontend/components/affordances/
directory - Any accessible source - The system will try MCP first, then fall back to local files
interface AffordanceComponent {
mount(container: HTMLElement, config: AffordanceConfig): Promise<void>;
unmount(): Promise<void>;
getPublicMethods(): Record<string, Function>;
[customMethod: string]: any;
}
/** @jsxImportSource https://esm.sh/react@18.2.0 */
import React from "https://esm.sh/react@18.2.0";
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { AffordanceComponent, AffordanceConfig } from "../../../shared/affordance-types.ts";
// Your React component
const MyComponent: React.FC<{message: string}> = ({ message }) => {
return <div style={{padding: '1rem'}}>{message}</div>;
};
// Affordance wrapper class (REQUIRED)
export default class MyAffordance implements AffordanceComponent {
private root: any = null;
private container: HTMLElement | null = null;
private message: string = "Hello!";
async mount(container: HTMLElement, config: AffordanceConfig): Promise<void> {
this.container = container;
this.message = config.message || "Hello!";
this.root = createRoot(container);
this.root.render(<MyComponent message={this.message} />);
}
async unmount(): Promise<void> {
if (this.root) {
this.root.unmount();
this.root = null;
}
this.container = null;
}
getPublicMethods(): Record<string, Function> {
return {
setMessage: this.setMessage.bind(this),
getMessage: this.getMessage.bind(this)
};
}
// Custom methods
setMessage(newMessage: string): void {
this.message = newMessage;
if (this.root) {
this.root.render(<MyComponent message={this.message} />);
}
}
getMessage(): string {
return this.message;
}
}
- Content: Must contain JavaScript/TypeScript code that exports a class
- Export: Must have a default export of the affordance class
- Dependencies: Use ESM imports from
https://esm.sh/
for compatibility - React: Pin React version to 18.2.0 for consistency
- Source: Can be from MCP files, local files, or any accessible source
The system tries to load components in this order:
- MCP source - Uses
files-get
tool to fetch from MCP server - Local file - Falls back to local project files
- Error - If neither source works, provides detailed error message
-
TestAffordance (
/frontend/components/affordances/TestAffordance.tsx
)- Simple test component with click counter
- Methods:
getMessage()
,setMessage(msg)
,ping()
-
CounterAffordance (
/frontend/components/affordances/CounterAffordance.tsx
)- Counter with increment/decrement
- Methods:
getValue()
,setValue(n)
,increment(step?)
,decrement(step?)
,reset()
-
StatusDashboard (
/frontend/components/affordances/StatusDashboard.tsx
)- Status monitoring dashboard
- Methods:
addItem(item)
,updateItem(id, updates)
,removeItem(id)
,clearItems()
,getItems()
-
NotificationCenter (
/frontend/components/affordances/NotificationCenter.tsx
)- Notification management
- Methods:
addNotification(notif)
,removeNotification(id)
,clearAll()
,getCount()
- Best for: Modals, dialogs, forms, image viewers
- Config:
modal: true/false
,backdrop: true/false
,position: 'center'|'top'|'bottom'
- Size: Use
maxWidth
,maxHeight
for responsive design
- Best for: Tool palettes, navigation, dashboards, file browsers
- Config:
position: 'left'|'right'
,width: '300px'
,collapsible: true
- Size: Fixed width recommended (250px-400px)
- Best for: Status indicators, breadcrumbs, quick actions
- Config:
position: 'left'|'right'
,priority: number
- Size: Keep compact, height auto-adjusts
- Best for: Status bars, progress indicators, quick stats
- Config:
position: 'left'|'right'
,priority: number
- Size: Keep compact, integrates with existing footer
- Best for: Widgets, charts, interactive content in chat
- Config:
width: '100%'
,height: '300px'
- Size: Responsive width, fixed height often works best
// 1. Create component using MCP tools (files-create, etc.)
// 2. Register the component by file key
const id = await register_affordance('sidebar',
'components/my-dashboard.js', // MCP file key
{ title: 'My Dashboard', position: 'right' }
);
// Register a local component file
const id = await register_affordance('overlay',
'/frontend/components/affordances/TestAffordance.tsx',
{ title: 'Test Widget' }
);
// Check available methods
const methods = await call_affordance_method(id, 'getPublicMethods', []);
// Call component methods
await call_affordance_method(id, 'setMessage', ['Hello World!']);
class MyAffordance implements AffordanceComponent {
private data: any[] = [];
addData(item: any): void {
this.data.push(item);
this.render(); // Re-render with new data
}
private render(): void {
if (this.root) {
this.root.render(<MyComponent data={this.data} />);
}
}
}
async mount(container: HTMLElement, config: AffordanceConfig): Promise<void> {
// Extract config with defaults
const title = config.title || "Default Title";
const theme = config.theme || "light";
const autoRefresh = config.autoRefresh || false;
// Use config in component
this.root.render(<MyComponent title={title} theme={theme} />);
}
async unmount(): Promise<void> {
// Clear timers
if (this.timer) clearInterval(this.timer);
// Remove event listeners
if (this.eventHandler) {
document.removeEventListener('click', this.eventHandler);
}
// Unmount React
if (this.root) {
this.root.unmount();
this.root = null;
}
}
The affordance system will catch and report:
- Module loading errors (wrong file type, syntax errors)
- Missing interface methods
- Runtime errors in mount/unmount
- Method call errors
Always test your components with:
// Register component
const id = await register_affordance('overlay', '/path/to/component.tsx', {title: 'Test'});
// Test methods
await call_affordance_method(id, 'getPublicMethods', []);
- Check Console: All affordance operations are logged with
[AffordanceManager]
prefix - Use getPublicMethods: Always call this first to see available methods
- Test Incrementally: Start with simple components, add complexity gradually
- Validate Config: Check that your config properties are being used correctly
- React DevTools: Use browser dev tools to inspect React component tree