Paid features
AI
npm i @matrajs/ai @matrajs/ai streams a rewrite into the selected range and keeps that range correct
while it arrives. Someone can carry on typing above, below or inside the selection and the streamed
text still lands on the words they picked, because every chunk is applied to a
re-resolved range rather than to the two numbers captured when the request started.
That is position mapping doing the work, and it is the whole
reason this package exists.
You supply the stream
The extension never talks to a model. It takes an AiStream — any function returning
an async iterable of strings — so the key stays on your server and the provider stays yours to
change.
import { createEditor, starterKit } from '@matrajs/core'
import { ai } from '@matrajs/ai'
const editor = createEditor({
extensions: [
...starterKit,
ai({
async *stream({ text, instruction, signal }) {
const response = await fetch('/api/rewrite', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text, instruction }),
signal,
})
const stream = response.body!.pipeThrough(new TextDecoderStream())
for await (const chunk of stream) yield chunk
},
}),
] as const,
})
Each chunk you yield replaces the range with everything received so far, so yield
deltas rather than the whole answer again. signal aborts when the session is cancelled
or the editor is destroyed · pass it to fetch and a cancelled rewrite stops costing
tokens straight away.
Commands
| Command | What it does |
|---|---|
askAi(instruction) |
Starts a session over the current selection. Returns false on an empty selection,
and false if a session is already streaming — one at a time.
|
cancelAi() | Aborts the stream. What already arrived stays in the document · nothing further is applied. |
acceptAi() | Ends the session and keeps the result. The document is not touched. |
rejectAi() | Aborts and puts the selection back over the rewritten range. The original text comes back by undoing. |
editor.commands.select({ from, to }) // a rewrite needs a selection
editor.commands.askAi('make this shorter') Status, for spinners and toasts
onStatus is called on every transition, with the session as it stands.
ai({
stream,
onStatus: (session) => {
setBusy(session.status === 'streaming')
if (session.status === 'error') toast(session.error!.message)
},
}) | Field | Type | |
|---|---|---|
id | number | One per session. A late chunk from a superseded session is discarded. |
status | 'idle' | 'streaming' | 'done' | 'error' | 'cancelled' | |
range | Range | Where the rewrite is going, re-resolved as of now. |
received | string | Everything that has arrived so far. |
error | Error? | Set when status is 'error'. |
What a failure does
A stream that throws reports 'error' with the thrown error, and stops. It does not
throw out of the command and it does not tear the document up: whatever had already streamed stays,
exactly as if the model had stopped early. A provider having a bad afternoon must not take an
editor down with it.