|
| 1 | +# anya-ui |
| 2 | + |
| 3 | +A rendering layer for agentic interfaces. Agents produce markdown with embedded affordances; the framework renders them to interactive DOM and reports user actions back. |
| 4 | + |
| 5 | +## Where this fits |
| 6 | + |
| 7 | +``` |
| 8 | +Agent → Spec → anya-ui → User → Agent |
| 9 | +``` |
| 10 | + |
| 11 | +The agent writes a markdown document. The framework renders it. The user acts. The framework encodes what happened. The agent receives it on the next turn. |
| 12 | + |
| 13 | +anya-ui doesn't know about your agent, your tools, or your backend. It knows one thing: **how to turn a spec into interactive DOM and tell you what the user did.** |
| 14 | + |
| 15 | +## The spec format |
| 16 | + |
| 17 | +Standard markdown for content. Fenced YAML blocks for interactive affordances. |
| 18 | + |
| 19 | +````markdown |
| 20 | +# Your Tasks |
| 21 | + |
| 22 | +3 items need attention today. |
| 23 | + |
| 24 | +```action |
| 25 | +name: mark_done |
| 26 | +label: Mark PR #42 done |
| 27 | +params: |
| 28 | + id: 42 |
| 29 | +``` |
| 30 | + |
| 31 | +```input |
| 32 | +name: add_task |
| 33 | +submit: Add |
| 34 | +fields: |
| 35 | + - name: title |
| 36 | + type: text |
| 37 | + placeholder: What needs doing? |
| 38 | + - name: priority |
| 39 | + type: select |
| 40 | + options: [high, medium, low] |
| 41 | +``` |
| 42 | + |
| 43 | +```group |
| 44 | +layout: row |
| 45 | +``` |
| 46 | + |
| 47 | +**Revenue**: $42k |
| 48 | + |
| 49 | +**Users**: 1,204 |
| 50 | + |
| 51 | +```end |
| 52 | +``` |
| 53 | +```` |
| 54 | + |
| 55 | +Four primitives: |
| 56 | +- **Content** — markdown (headings, lists, tables, links, emphasis) |
| 57 | +- **Action** — a named button the user can click |
| 58 | +- **Input** — a named form the user can fill and submit |
| 59 | +- **Group** — a layout container (`row`, `grid`, `stack`) |
| 60 | + |
| 61 | +## Install |
| 62 | + |
| 63 | +```bash |
| 64 | +npm install anya-ui |
| 65 | +``` |
| 66 | + |
| 67 | +## Usage |
| 68 | + |
| 69 | +```typescript |
| 70 | +import { mount } from 'anya-ui'; |
| 71 | + |
| 72 | +const ui = mount(specString, document.getElementById('app')); |
| 73 | + |
| 74 | +ui.on('action', (feedback) => { |
| 75 | + // { action: "mark_done", params: { id: 42 }, timestamp: ... } |
| 76 | + sendToAgent(feedback); |
| 77 | +}); |
| 78 | + |
| 79 | +// Agent responds with new spec |
| 80 | +ui.update(newSpecString); |
| 81 | +``` |
| 82 | + |
| 83 | +## Three entry points |
| 84 | + |
| 85 | +| Import | Purpose | DOM required? | |
| 86 | +|--------|---------|---------------| |
| 87 | +| `anya-ui` | Full renderer + protocol | Yes | |
| 88 | +| `anya-ui/protocol` | Parse, encode, prompt builder | No | |
| 89 | +| `anya-ui/measure` | Signal collection + friction scoring | Yes | |
| 90 | + |
| 91 | +Use `anya-ui/protocol` when you only need to parse specs or encode feedback — no DOM dependency. Useful for server-side tooling, React Native bridges, or custom renderers. |
| 92 | + |
| 93 | +```typescript |
| 94 | +import { parse, encode, buildSystemPrompt } from 'anya-ui/protocol'; |
| 95 | +``` |
| 96 | + |
| 97 | +## Teaching the agent |
| 98 | + |
| 99 | +```typescript |
| 100 | +import { buildSystemPrompt, encodeHistory } from 'anya-ui'; |
| 101 | + |
| 102 | +const systemPrompt = buildSystemPrompt({ |
| 103 | + actions: [ |
| 104 | + { name: 'mark_done', description: 'Mark a task as completed' }, |
| 105 | + { name: 'assign', description: 'Assign a task to someone' }, |
| 106 | + ], |
| 107 | + history: encodeHistory(userActions), |
| 108 | +}); |
| 109 | + |
| 110 | +// Pass systemPrompt to your LLM alongside the user's message |
| 111 | +``` |
| 112 | + |
| 113 | +The agent learns the format from the system prompt and produces valid specs naturally — fenced code blocks are in every model's training data. |
| 114 | + |
| 115 | +## Encoding feedback |
| 116 | + |
| 117 | +When the user acts, encode it for the agent's next turn: |
| 118 | + |
| 119 | +```typescript |
| 120 | +import { encode } from 'anya-ui'; |
| 121 | + |
| 122 | +ui.on('action', (feedback) => { |
| 123 | + const text = encode(feedback); |
| 124 | + // "User clicked "mark_done" (id=42)" |
| 125 | + // "User submitted "add_task" with title="Write tests", priority="high"" |
| 126 | + appendToConversation(text); |
| 127 | +}); |
| 128 | +``` |
| 129 | + |
| 130 | +## Framework integration |
| 131 | + |
| 132 | +anya-ui is vanilla DOM. Wrap it in your framework's lifecycle: |
| 133 | + |
| 134 | +### React |
| 135 | + |
| 136 | +```tsx |
| 137 | +import { useRef, useEffect } from 'react'; |
| 138 | +import { mount, type AnyaInstance } from 'anya-ui'; |
| 139 | + |
| 140 | +function AnyaView({ spec, onAction }) { |
| 141 | + const ref = useRef<HTMLDivElement>(null); |
| 142 | + const instanceRef = useRef<AnyaInstance | null>(null); |
| 143 | + |
| 144 | + useEffect(() => { |
| 145 | + if (!ref.current) return; |
| 146 | + instanceRef.current = mount(spec, ref.current); |
| 147 | + instanceRef.current.on('action', onAction); |
| 148 | + return () => instanceRef.current?.destroy(); |
| 149 | + }, []); |
| 150 | + |
| 151 | + useEffect(() => { |
| 152 | + instanceRef.current?.update(spec); |
| 153 | + }, [spec]); |
| 154 | + |
| 155 | + return <div ref={ref} />; |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +### React (protocol-only) |
| 160 | + |
| 161 | +If you want to render with your own React components instead of anya-ui's DOM: |
| 162 | + |
| 163 | +```tsx |
| 164 | +import { parse, encode, isAction, isInput, isGroup, isContent } from 'anya-ui/protocol'; |
| 165 | +import type { Spec, SpecNode } from 'anya-ui/protocol'; |
| 166 | + |
| 167 | +function AnyaView({ specString, onAction }) { |
| 168 | + const spec = useMemo(() => parse(specString), [specString]); |
| 169 | + |
| 170 | + return ( |
| 171 | + <div> |
| 172 | + {spec.nodes.map((node, i) => ( |
| 173 | + <SpecNodeView key={i} node={node} onAction={onAction} /> |
| 174 | + ))} |
| 175 | + </div> |
| 176 | + ); |
| 177 | +} |
| 178 | + |
| 179 | +function SpecNodeView({ node, onAction }) { |
| 180 | + if (isContent(node)) return <Markdown>{node.markdown}</Markdown>; |
| 181 | + if (isAction(node)) return <Button onClick={() => onAction(node.action, node.params)}>{node.label}</Button>; |
| 182 | + if (isInput(node)) return <FormFromFields fields={node.fields} onSubmit={v => onAction(node.input, v)} />; |
| 183 | + if (isGroup(node)) return <Flex>{node.content.map((n, i) => <SpecNodeView key={i} node={n} onAction={onAction} />)}</Flex>; |
| 184 | + return null; |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +### Web Component |
| 189 | + |
| 190 | +```html |
| 191 | +<script type="module"> |
| 192 | + import { mount } from 'anya-ui'; |
| 193 | +
|
| 194 | + class AnyaElement extends HTMLElement { |
| 195 | + connectedCallback() { |
| 196 | + this.instance = mount(this.getAttribute('spec'), this); |
| 197 | + this.instance.on('action', (f) => this.dispatchEvent(new CustomEvent('action', { detail: f }))); |
| 198 | + } |
| 199 | + static get observedAttributes() { return ['spec']; } |
| 200 | + attributeChangedCallback(_, __, val) { this.instance?.update(val); } |
| 201 | + disconnectedCallback() { this.instance?.destroy(); } |
| 202 | + } |
| 203 | + customElements.define('anya-ui', AnyaElement); |
| 204 | +</script> |
| 205 | +``` |
| 206 | +
|
| 207 | +## Measurement (optional) |
| 208 | +
|
| 209 | +Collect interaction signals and compute friction scores: |
| 210 | +
|
| 211 | +```typescript |
| 212 | +import { mount } from 'anya-ui'; |
| 213 | +import { withMeasurement } from 'anya-ui/measure'; |
| 214 | +
|
| 215 | +const ui = mount(spec, container); |
| 216 | +const measured = withMeasurement(ui, container.querySelector('.anya')); |
| 217 | +
|
| 218 | +// Raw signals: timestamped action events with timing + choice metadata |
| 219 | +const signals = measured.signals(); |
| 220 | +// [{ ts, action, kind: 'click'|'submit', durationMs, choiceCount }] |
| 221 | +
|
| 222 | +// Computed scores: Hick-Hyman cognitive load from current DOM state |
| 223 | +const scores = measured.scores(); |
| 224 | +// [{ kind: 'cognitive', score: 0.3, severity: 'low', detail: '3 interactive elements...' }] |
| 225 | +``` |
| 226 | +
|
| 227 | +The measurement module emits **raw signals** — what happened, when, how many choices were visible. It does NOT interpret signals, detect patterns, or advise the agent. That's the orchestration layer's job. |
| 228 | +
|
| 229 | +## Styling |
| 230 | +
|
| 231 | +Import the default stylesheet or use CSS variables to theme: |
| 232 | +
|
| 233 | +```css |
| 234 | +@import 'anya-ui/src/anya.css'; |
| 235 | +
|
| 236 | +:root { |
| 237 | + --anya-text: #1a1a1a; |
| 238 | + --anya-border: #d1d5db; |
| 239 | + --anya-btn-bg: #fff; |
| 240 | + --anya-btn-hover: #f3f4f6; |
| 241 | + --anya-focus: #3b82f6; |
| 242 | +} |
| 243 | +``` |
| 244 | +
|
| 245 | +Or write your own styles targeting `.anya-action`, `.anya-input`, `.anya-field`, `.anya-group`, `.anya-layout-row`, etc. |
| 246 | +
|
| 247 | +## API |
| 248 | +
|
| 249 | +### `mount(spec: string | Spec, target: HTMLElement): AnyaInstance` |
| 250 | +
|
| 251 | +Render a spec into a container element. |
| 252 | +
|
| 253 | +### `AnyaInstance` |
| 254 | +
|
| 255 | +- `.update(spec)` — re-render with new spec |
| 256 | +- `.on('action', handler)` — listen for user actions, returns unsubscribe fn |
| 257 | +- `.destroy()` — remove DOM, cleanup listeners |
| 258 | +- `.getSpec()` — current parsed spec (or null after destroy) |
| 259 | +
|
| 260 | +### `parse(raw: string): Spec` |
| 261 | +
|
| 262 | +Parse a markdown+YAML string into a structured Spec object. |
| 263 | +
|
| 264 | +### `render(spec: Spec, opts: RenderOptions): HTMLElement` |
| 265 | +
|
| 266 | +Render a Spec to a detached DOM element (for custom mounting). |
| 267 | +
|
| 268 | +### `encode(feedback: ActionFeedback): string` |
| 269 | +
|
| 270 | +Format a user action as a human-readable string for agent context. |
| 271 | +
|
| 272 | +### `encodeHistory(history: ActionFeedback[]): string` |
| 273 | +
|
| 274 | +Format multiple actions as newline-separated entries. |
| 275 | +
|
| 276 | +### `buildSystemPrompt(opts?: PromptOptions): string` |
| 277 | +
|
| 278 | +Generate a system prompt that teaches an agent the spec format. |
| 279 | +
|
| 280 | +Options: |
| 281 | +- `actions` — list of available actions with descriptions |
| 282 | +- `inputs` — list of available inputs with descriptions |
| 283 | +- `context` — arbitrary context string appended to the prompt |
| 284 | +- `history` — recent user action history (from `encodeHistory`) |
| 285 | +
|
| 286 | +### `withMeasurement(instance, root): MeasuredInstance` |
| 287 | +
|
| 288 | +Attach signal collection (from `anya-ui/measure`). |
| 289 | +
|
| 290 | +- `.signals()` — collected interaction signals |
| 291 | +- `.scores()` — computed friction scores from current DOM |
| 292 | +- `.destroy()` — cleanup and destroy instance |
| 293 | +
|
| 294 | +## Design principles |
| 295 | +
|
| 296 | +1. **Protocol, not component library** — the spec is the interface contract between agent and renderer |
| 297 | +2. **Agent-native format** — markdown + YAML fenced blocks, formats agents produce at near-100% reliability |
| 298 | +3. **Graceful degradation** — spec renders as readable code blocks in any markdown viewer |
| 299 | +4. **Framework-agnostic** — vanilla DOM output, wrap in any framework; or use protocol-only and render yourself |
| 300 | +5. **Feedback is first-class** — every affordance has a name; every user action is encodable |
| 301 | +6. **Observation without opinion** — measurement emits signals, doesn't interpret them |
0 commit comments