# AI

Configure **how tokens arrive**. Slash `/ai`, format-bar continue/rewrite, draft blocks, live MD→structure, accept / undo discard are built in.

```ts
import { Editor } from 'markloom/view'
import 'markloom/view/theme.css'
import 'markloom/ai/theme.css'
```

Pick **one** transport: `provider` | `endpoint` | `stream`.

---

## Built-in OpenAI-compatible

Any host with `POST …/v1/chat/completions` + SSE (OpenAI, xAI, 智谱, …). Needs CORS; prefer a proxy in production.

```ts
new Editor(el, doc, {
  ai: {
    provider: {
      host: 'https://api.x.ai',
      apiKey: import.meta.env.VITE_AI_KEY,
      model: 'grok-3',
    },
  },
})
```

---

## Same-origin proxy (production)

Keys stay on the server. Body from the editor: `{ action, context, docPreview, system, provider? }`. Respond with **SSE** — OpenAI `choices[0].delta.content` or `{ content: "…" }` per `data:` line.

```ts
new Editor(el, doc, {
  ai: { endpoint: '/api/ai/stream' },
})
```

```ts
// server: POST /api/ai/stream
export async function POST(req: Request) {
  const body = await req.json()
  const upstream = await fetch('https://api.x.ai/v1/chat/completions', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${process.env.XAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'grok-3',
      stream: true,
      messages: [
        { role: 'system', content: body.system },
        {
          role: 'user',
          content: body.action === 'rewrite'
            ? `Rewrite:\n\n${body.context}`
            : `Continue after:\n\n${body.context}`,
        },
      ],
    }),
  })
  return new Response(upstream.body, {
    headers: {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache',
    },
  })
}
```

---

## Vercel AI SDK

```bash
npm i ai @ai-sdk/openai
# or @ai-sdk/xai, @ai-sdk/anthropic, …
```

```ts
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! })

new Editor(el, doc, {
  ai: {
    stream: async (input, onToken) => {
      const result = streamText({
        model: openai('gpt-4o-mini'),
        system:
          input.action === 'rewrite'
            ? 'Rewrite clearly. Output body only, no preface.'
            : 'Continue in the same voice. Output body only, no preface.',
        prompt:
          input.action === 'rewrite'
            ? input.context
            : [input.docPreview, input.context].filter(Boolean).join('\n\n'),
        abortSignal: input.signal,
      })
      for await (const delta of result.textStream)
        onToken(delta)
    },
  },
})
```

Same `stream` shape works with any SDK provider, agents, or RAG — call `onToken` with text chunks.

---

## Custom backend / agent

```ts
new Editor(el, doc, {
  ai: {
    stream: async ({ action, context, docPreview, signal }, onToken) => {
      const res = await fetch('/my-agent', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ action, context, docPreview }),
        signal,
      })
      const reader = res.body!.getReader()
      const dec = new TextDecoder()
      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        onToken(dec.decode(value, { stream: true }))
      }
    },
  },
})
```

---

## Optional knobs

```ts
ai: {
  provider: { host, apiKey, model }, // or endpoint / stream
  continueSystem: '…',
  rewriteSystem: '…',
  onError: msg => toast.error(msg), // default: window.alert
  onBusy: busy => setSpinner(busy),
}
```

Programmatic: `ed.ai?.openAtFocus('continue' | 'rewrite')`, `ed.ai?.stop()`.

| You write | Library does |
|-----------|----------------|
| `provider` **or** `endpoint` **or** `stream` | Slash, bubble, drafts, MD preview, accept/undo |
| styles (`view` + `ai` theme) | AI chrome |
