Skip to content

Commit 60d1d7d

Browse files
bretuobayclaude
andcommitted
docs: build out full API reference site in apps/docs
14 static pages across 3 sections: Introduction - /docs/getting-started — quick-start guide with SSE endpoint + React chat example Core - /docs/core — Signal, Observable, Channel, Session, Action API reference - /docs/capability — CapabilityRegistry, all 9 built-in probes, custom probe example - /docs/policy — PolicyEngine, PermissionPolicy, ConcurrencyPolicy, RateLimitPolicy, ComposedPolicy Modalities - /docs/text — TextChannel, streamTokens, accumulateText - /docs/agent — AgentChannel, AgentStreamFrame types, tool registration, SSE contract - /docs/audio — AudioChannel, MicrophoneSource, AudioWorkletSink, createVad - /docs/video — VideoChannel, CameraSource, CanvasSink, camera preview example - /docs/motion — PointerSource, DeviceOrientationSource, GestureRecognizer (tap/swipe/pinch) Framework Adapters - /docs/react — SessionProvider, useSignal, useChannel, useAction, useAgent - /docs/vue — provideSession, all composables with template examples - /docs/solid — createSessionProvider, all primitives with JSX examples - /docs/wc — <muix-session>, <muix-channel> attributes and events Tooling - /docs/devtools — SessionInspector, ChannelTracer, <muix-devtools> panel, React usage Shared: sidebar layout, dark-mode CSS variables, monospace code blocks. Landing page (/) replaced Turborepo scaffold with MUIX package table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 40b70a9 commit 60d1d7d

18 files changed

Lines changed: 1511 additions & 126 deletions

File tree

apps/docs/app/docs/agent/page.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = { title: "@muix/agent" };
4+
5+
export default function AgentPage() {
6+
return (
7+
<>
8+
<h1>@muix/agent</h1>
9+
<p style={{ color: "var(--muted)", marginTop: "0.5rem" }}>
10+
LLM streaming over SSE/NDJSON with tool-use support. OpenAI-compatible wire format.
11+
</p>
12+
13+
<h2>AgentChannel</h2>
14+
<p>
15+
Extends <code>Channel&lt;AgentMessage, AgentStreamFrame&gt;</code>.
16+
Call <code>sendMessage()</code> (not <code>send()</code>) to start a
17+
streaming conversation turn; the method returns an <code>Action</code>{" "}
18+
you can cancel mid-stream.
19+
</p>
20+
<pre>{`import { createAgentChannel } from "@muix/agent";
21+
22+
const channel = createAgentChannel({
23+
endpoint: "/api/chat", // your SSE endpoint
24+
// headers: { Authorization: "Bearer ..." },
25+
});
26+
27+
await channel.open();
28+
29+
const action = channel.sendMessage({
30+
role: "user",
31+
content: "Summarise the MUIX framework in one paragraph.",
32+
});
33+
34+
channel.observe().subscribe({
35+
next: ({ data: frame }) => {
36+
if (frame.type === "delta") console.log(frame.content);
37+
if (frame.type === "done") console.log("stream finished");
38+
if (frame.type === "error") console.error(frame.content);
39+
},
40+
});
41+
42+
// Cancel mid-stream
43+
action.cancel();`}</pre>
44+
45+
<h2>AgentStreamFrame</h2>
46+
<table>
47+
<thead><tr><th>type</th><th>Meaning</th><th>Fields</th></tr></thead>
48+
<tbody>
49+
<tr><td><code>delta</code></td><td>Incremental token</td><td><code>content: string</code></td></tr>
50+
<tr><td><code>tool_call</code></td><td>Tool invocation request</td><td><code>toolCall: {"{ name, args }"}</code></td></tr>
51+
<tr><td><code>tool_result</code></td><td>Tool response (after execution)</td><td><code>content: string</code></td></tr>
52+
<tr><td><code>done</code></td><td>Stream completed</td><td><code>finishReason</code></td></tr>
53+
<tr><td><code>error</code></td><td>Server-side error</td><td><code>content: string</code></td></tr>
54+
</tbody>
55+
</table>
56+
57+
<h2>Tool registration</h2>
58+
<pre>{`import { createAgentChannel } from "@muix/agent";
59+
60+
const channel = createAgentChannel({ endpoint: "/api/chat" });
61+
62+
channel.registerTool({
63+
name: "get_weather",
64+
description: "Get current weather for a city",
65+
parameters: {
66+
type: "object",
67+
properties: {
68+
city: { type: "string", description: "City name" },
69+
},
70+
required: ["city"],
71+
},
72+
execute: async ({ city }) => {
73+
const data = await fetch(\`/api/weather?city=\${city}\`).then(r => r.json());
74+
return JSON.stringify(data);
75+
},
76+
});
77+
78+
// AgentChannel handles the tool_calls → execute → tool_result round-trip automatically`}</pre>
79+
80+
<h2>SSE endpoint contract</h2>
81+
<p>
82+
Your endpoint must return <code>Content-Type: text/event-stream</code> with
83+
OpenAI-compatible delta frames:
84+
</p>
85+
<pre>{`data: {"choices":[{"delta":{"content":"Hello"}}]}
86+
87+
data: {"choices":[{"delta":{"content":" world"}}]}
88+
89+
data: [DONE]`}</pre>
90+
91+
<p>NDJSON is also supported — one JSON object per line, no <code>data:</code> prefix.</p>
92+
</>
93+
);
94+
}

apps/docs/app/docs/audio/page.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = { title: "@muix/audio" };
4+
5+
export default function AudioPage() {
6+
return (
7+
<>
8+
<h1>@muix/audio</h1>
9+
<p style={{ color: "var(--muted)", marginTop: "0.5rem" }}>
10+
Microphone capture, AudioWorklet playback, and voice activity detection.
11+
</p>
12+
13+
<h2>AudioChannel</h2>
14+
<p>
15+
Extends <code>Channel&lt;AudioFrame, AudioFrame&gt;</code>. Carries raw
16+
PCM data between sources and sinks.
17+
</p>
18+
<pre>{`import { createAudioChannel } from "@muix/audio";
19+
20+
const ch = createAudioChannel({ sampleRate: 48000, channelCount: 1 });
21+
await ch.open();`}</pre>
22+
23+
<h2>AudioFrame</h2>
24+
<pre>{`interface AudioFrame {
25+
buffer: Float32Array; // PCM samples in [-1, 1]
26+
sampleRate: number;
27+
channelCount: number;
28+
timestamp: number; // AudioContext.currentTime
29+
}`}</pre>
30+
31+
<h2>MicrophoneSource</h2>
32+
<p>
33+
Captures audio via <code>getUserMedia</code> and pumps{" "}
34+
<code>AudioFrame</code> objects into a channel using an inline{" "}
35+
<code>AudioWorkletNode</code> (no deprecated <code>ScriptProcessorNode</code>).
36+
</p>
37+
<pre>{`import { MicrophoneSource, createAudioChannel } from "@muix/audio";
38+
39+
const channel = createAudioChannel();
40+
const mic = new MicrophoneSource({
41+
echoCancellation: true,
42+
noiseSuppression: true,
43+
});
44+
45+
await channel.open();
46+
await mic.start(channel); // acquires getUserMedia
47+
48+
// ... later
49+
await mic.stop(); // releases the MediaStream`}</pre>
50+
51+
<h2>AudioWorkletSink</h2>
52+
<p>Receives frames from a channel and plays them via <code>AudioContext</code>.</p>
53+
<pre>{`import { AudioWorkletSink } from "@muix/audio";
54+
55+
const sink = new AudioWorkletSink();
56+
await sink.start(channel);
57+
58+
await sink.stop();`}</pre>
59+
60+
<h2>Voice Activity Detection</h2>
61+
<p>
62+
<code>createVad</code> wraps a channel observable and annotates each
63+
frame with an RMS level and a <code>isSpeech</code> flag. Uses a
64+
sliding silence-pad window to avoid rapid toggling.
65+
</p>
66+
<pre>{`import { createVad } from "@muix/audio";
67+
68+
const vadStream = createVad(channel.observe(), {
69+
threshold: 0.01, // RMS below this = silence
70+
silencePadFrames: 10, // frames of silence before flipping isSpeech → false
71+
});
72+
73+
vadStream.subscribe({
74+
next: ({ frame, vad }) => {
75+
console.log(vad.isSpeech, vad.rms.toFixed(3));
76+
},
77+
});`}</pre>
78+
</>
79+
);
80+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = { title: "@muix/capability" };
4+
5+
export default function CapabilityPage() {
6+
return (
7+
<>
8+
<h1>@muix/capability</h1>
9+
<p style={{ color: "var(--muted)", marginTop: "0.5rem" }}>
10+
Browser feature detection with graceful degradation and fallback chains.
11+
</p>
12+
13+
<h2>Core concepts</h2>
14+
<ul>
15+
<li><strong>probe()</strong> — detect availability without requesting permissions.</li>
16+
<li><strong>acquire()</strong> — obtain the native handle (may prompt the user).</li>
17+
<li><strong>negotiate()</strong> — try a capability then its fallbacks; return the first available.</li>
18+
</ul>
19+
20+
<h2>CapabilityRegistry</h2>
21+
<pre>{`import { createCapabilityRegistry, microphoneCapability } from "@muix/capability";
22+
23+
const registry = createCapabilityRegistry();
24+
registry.register(microphoneCapability);
25+
26+
const status = await registry.probe("media:microphone");
27+
// "available" | "unavailable" | "degraded" | "denied" | "unknown"
28+
29+
const { descriptor, status: s } = await registry.negotiate("media:microphone");
30+
const stream = await descriptor.acquire(); // MediaStream
31+
await descriptor.release(stream);`}</pre>
32+
33+
<h2>Built-in probes</h2>
34+
<table>
35+
<thead><tr><th>ID</th><th>Export</th><th>Returns</th></tr></thead>
36+
<tbody>
37+
<tr><td><code>media:microphone</code></td><td><code>microphoneCapability</code></td><td><code>MediaStream</code></td></tr>
38+
<tr><td><code>media:camera</code></td><td><code>cameraCapability</code></td><td><code>MediaStream</code></td></tr>
39+
<tr><td><code>media:screen</code></td><td><code>screenCaptureCapability</code></td><td><code>MediaStream</code></td></tr>
40+
<tr><td><code>speech:synthesis</code></td><td><code>speechSynthesisCapability</code></td><td><code>SpeechSynthesis</code></td></tr>
41+
<tr><td><code>speech:recognition</code></td><td><code>speechRecognitionCapability</code></td><td>recognition instance</td></tr>
42+
<tr><td><code>webrtc</code></td><td><code>webRTCCapability</code></td><td><code>RTCPeerConnection</code></td></tr>
43+
<tr><td><code>xr:immersive-vr</code></td><td><code>immersiveVrCapability</code></td><td>XR session handle</td></tr>
44+
<tr><td><code>xr:immersive-ar</code></td><td><code>immersiveArCapability</code></td><td>XR session handle</td></tr>
45+
<tr><td><code>xr:inline</code></td><td><code>inlineXrCapability</code></td><td>XR session handle</td></tr>
46+
</tbody>
47+
</table>
48+
49+
<h2>Custom capability</h2>
50+
<pre>{`import type { CapabilityDescriptor } from "@muix/capability";
51+
52+
const bluetoothCapability: CapabilityDescriptor<BluetoothDevice> = {
53+
id: "bluetooth",
54+
description: "Web Bluetooth API",
55+
probe: async () =>
56+
"bluetooth" in navigator ? "available" : "unavailable",
57+
acquire: () =>
58+
navigator.bluetooth.requestDevice({ acceptAllDevices: true }),
59+
release: async () => {},
60+
fallbacks: [],
61+
};
62+
63+
registry.register(bluetoothCapability);`}</pre>
64+
</>
65+
);
66+
}

apps/docs/app/docs/core/page.tsx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = { title: "@muix/core" };
4+
5+
export default function CorePage() {
6+
return (
7+
<>
8+
<h1>@muix/core</h1>
9+
<p style={{ color: "var(--muted)", marginTop: "0.5rem" }}>
10+
The foundation — Signal, Observable, Channel, Session, Action, EventBus.
11+
Zero runtime dependencies.
12+
</p>
13+
14+
<h2>Signal</h2>
15+
<p>
16+
A reactive value container. Naming is TC39 Signals proposal-compatible;
17+
the implementation is a lightweight own build (~150 lines).
18+
</p>
19+
<pre>{`import { createSignal, createComputed } from "@muix/core";
20+
21+
const count = createSignal(0);
22+
count.set(1);
23+
count.update((n) => n + 1); // → 2
24+
count.peek(); // read without subscribing
25+
26+
const doubled = createComputed(
27+
() => count.value * 2,
28+
[count],
29+
);
30+
31+
const sub = count.observe().subscribe({ next: (v) => console.log(v) });
32+
sub.unsubscribe();`}</pre>
33+
<table>
34+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
35+
<tbody>
36+
<tr><td><code>value</code></td><td>Current value (tracked read)</td></tr>
37+
<tr><td><code>peek()</code></td><td>Read without creating a subscription side-effect</td></tr>
38+
<tr><td><code>set(v)</code></td><td>Set new value; notifies listeners only if <code>!equals(prev, next)</code></td></tr>
39+
<tr><td><code>update(fn)</code></td><td>Derive next value from current</td></tr>
40+
<tr><td><code>observe()</code></td><td>Returns an <code>Observable&lt;T&gt;</code>; emits current value immediately on subscribe</td></tr>
41+
</tbody>
42+
</table>
43+
44+
<h2>Observable</h2>
45+
<p>
46+
TC39-compatible push stream (~200 lines). Implements{" "}
47+
<code>[Symbol.observable]()</code> for RxJS interop.
48+
</p>
49+
<pre>{`import { Observable } from "@muix/core";
50+
51+
const obs = new Observable<number>((observer) => {
52+
observer.next(1);
53+
observer.next(2);
54+
observer.complete();
55+
return () => { /* cleanup */ };
56+
});
57+
58+
const sub = obs.subscribe({
59+
next: (v) => console.log(v),
60+
error: (e) => console.error(e),
61+
complete: () => console.log("done"),
62+
});
63+
64+
sub.unsubscribe();`}</pre>
65+
66+
<h2>Channel</h2>
67+
<p>
68+
Duplex streaming primitive built on{" "}
69+
<code>ReadableStream</code> / <code>WritableStream</code> (WHATWG Streams).
70+
Provides native backpressure, pause/resume, and composable piping.
71+
</p>
72+
<pre>{`import { createChannel } from "@muix/core";
73+
74+
const ch = createChannel<string>({ highWaterMark: 16 });
75+
await ch.open();
76+
77+
// Write
78+
await ch.send("hello");
79+
80+
// Observe (non-locking)
81+
ch.observe().subscribe({ next: (frame) => console.log(frame.data) });
82+
83+
// Pipe through a TransformStream
84+
const upper = ch.pipe(new TransformStream({
85+
transform: (chunk, ctrl) => ctrl.enqueue(chunk.toUpperCase()),
86+
}));
87+
88+
ch.pause(); // backpressure
89+
ch.resume();
90+
await ch.close();`}</pre>
91+
<table>
92+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
93+
<tbody>
94+
<tr><td><code>status</code></td><td><code>ReadonlySignal&lt;ChannelStatus&gt;</code><code>idle | open | paused | closed | errored</code></td></tr>
95+
<tr><td><code>source.readable</code></td><td>WHATWG <code>ReadableStream&lt;ChannelFrame&lt;Out&gt;&gt;</code></td></tr>
96+
<tr><td><code>sink.writable</code></td><td>WHATWG <code>WritableStream&lt;ChannelFrame&lt;In&gt;&gt;</code></td></tr>
97+
<tr><td><code>send(data)</code></td><td>Enqueue a frame; awaits if paused</td></tr>
98+
<tr><td><code>observe()</code></td><td>Non-locking Observable over outbound frames</td></tr>
99+
<tr><td><code>pipe(transform)</code></td><td>Returns a new downstream Channel</td></tr>
100+
</tbody>
101+
</table>
102+
103+
<h2>Session</h2>
104+
<p>
105+
A lifecycle container that owns channels and dispatches actions.
106+
</p>
107+
<pre>{`import { createSession } from "@muix/core";
108+
109+
const session = createSession({ id: "chat" });
110+
await session.start();
111+
112+
const ch = session.addChannel<string>("messages");
113+
await ch.open();
114+
115+
const action = session.dispatch({
116+
id: "fetch-summary",
117+
execute: async function* (signal) {
118+
yield { type: "progress", percent: 50 };
119+
return "done";
120+
},
121+
});
122+
123+
await session.suspend();
124+
await session.resume();
125+
await session.terminate();`}</pre>
126+
127+
<h2>Action</h2>
128+
<p>
129+
Cancellable async unit of work. Uses <code>AbortSignal</code> for
130+
cancellation. Named <code>toPromise()</code> (not <code>.then()</code>)
131+
to avoid the JavaScript thenable trap.
132+
</p>
133+
<pre>{`const action = session.dispatch({
134+
id: "my-action",
135+
execute: async function* (signal) {
136+
for (let i = 0; i < 10; i++) {
137+
if (signal.aborted) return;
138+
yield { type: "progress", percent: i * 10 };
139+
}
140+
return "result";
141+
},
142+
});
143+
144+
// Reactive status
145+
action.status.observe().subscribe({ next: console.log });
146+
147+
action.cancel("user cancelled");
148+
const result = await action.toPromise();`}</pre>
149+
</>
150+
);
151+
}

0 commit comments

Comments
 (0)