Расширение AI
Расширение AI добавляет кнопку на панель инструментов, которая открывает диалог запроса, отправляет его выбранному вами провайдеру, показывает потоковый результат и вставляет его в редактор кнопкой действия, зависящей от режима (Вставить / Заменить / Добавить в конец).
<Tiptapify
:content="content"
:items="['ai', '|', 'bold', 'italic']"
:ai="{
aiProvider,
model: 'gpt-4.1-mini',
stream: true,
}"
/>Prop ai принимает true (параметры по умолчанию) или объект параметров (TiptapifyAiOptions). Элемент ai на панели инструментов регистрируется всегда, но кнопка отключена, пока не задан aiProvider.
Запускаемое демо см. в примере AI, а полный справочник параметров — в разделе Props.
Архитектура
Tiptapify.vue Toolbar item `ai` Dialog.vue
resolves the `ai` -> Button.vue -> builds the request
prop into a resolved -> (disabled without -> calls aiProvider(request, context, stream)
options object and -> aiProvider) -> streams chunks into result/reasoning fields
provides it (inject -> opens the dialog -> the action button (Insert/Replace/Append) places the result into the editor
'tiptapifyAi')Tiptapifyнормализует propai:ai: trueпревращается в пустой объект параметров,promptExamplesпо умолчанию берёт локализованные значения, а результат передаётся потомкам черезinject('tiptapifyAi')(TiptapifyAiResolvedOptionsилиfalse).- Элемент панели инструментов (
src/components/Toolbar/ai.ts) отображаетButton.vue. Кнопка проверяет доступностьaiProviderи открываетDialog.vue. Dialog.vueвладеет всем жизненным циклом запроса: формированием запроса, вызовом провайдера, потоковой передачей и вставкой результата.
Контракт провайдера
type TiptapifyAiProvider = (
request: TiptapifyAiRequest,
context: TiptapifyAiEditorContext,
stream?: TiptapifyAiStream,
) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>Провайдер — это обычная асинхронная функция, которую вы реализуете. tiptapify сам не обращается ни к какому API — подойдёт всё, что умеет обрабатывать OpenAI-совместимый объект request (fetch, SDK, адаптер бэкенда). Исключение — режим бэкенда aiEndpoint (см. Генерация через бэкенд), где tiptapify отправляет запрос на ваш endpoint.
Запрос (TiptapifyAiRequest) формируется из ваших chatCompletionOptions, а затем дополняется плагином:
| Поле | Источник |
|---|---|
messages | createMessages(context), если задан, иначе стандартная пара: системное сообщение (ваш systemPrompt или встроенное значение по умолчанию) и пользовательское сообщение, в которое входят запрос, выделенный текст редактора с окружающим текстом и правило отвечать только для выделения (или для всего текста, если ничего не выделено) |
model | параметр model, если задан |
temperature | параметр temperature, если задан |
stream | параметр stream или chatCompletionOptions.stream |
enable_thinking / reasoning_effort | переключатель размышления / выпадающий список уровня — см. ниже |
Контекст (TiptapifyAiEditorContext) даёт провайдеру полное состояние редактора: prompt, selectedText, textBefore / textAfter (текст абзаца выделения вокруг него, до 500 символов с каждой стороны), text, html, json и mode (insert / replace / append).
Выделение, которое начинается или заканчивается внутри слова, захватывает слово целиком, а сообщения по умолчанию отправляют текст вокруг выделения вместе с правилом отвечать только для выделения: получив один фрагмент, модель принимает его за весь текст и дописывает его, и замена повторила бы то, что идёт после выделения.
Ответ: провайдер может вернуть содержимое как обычную string, как { content } (TiptapifyAiResponse) или как OpenAI-совместимый payload choices (TiptapifyAiOpenAiResponse). Диалог извлекает содержимое именно в таком порядке и обрезает пробелы в начале. При отклонении сообщение об ошибке показывается в диалоге (или ai.error, если ошибка не является экземпляром Error), и диалог остаётся открытым.
Генерация через бэкенд (aiEndpoint)
Вместо реализации aiProvider можно направить AI-функцию на endpoint LLM на вашем бэкенде:
// example
const ai = {
aiEndpoint: '/api/ai/generate',
model: 'gpt-4.1-mini',
stream: true,
}Тогда tiptapify сам формирует запрос и отправляет его на endpoint. Явно заданный aiProvider всегда имеет приоритет, если заданы оба.
Запрос — минимальный JSON-payload (TiptapifyAiBackendRequest):
| Поле | Когда отправляется |
|---|---|
prompt | всегда — запрос, введённый в диалоге |
instruction | всегда — ваш systemPrompt, системное сообщение пользовательского createMessages или встроенное значение по умолчанию |
stream | только при stream: true |
thinking | только если переключатель Размышление в диалоге включён |
reasoning_effort | только если пользователь выбрал уровень (не default) |
model | только если задан параметр model |
Ответ — принимается нестрого: обычная строка, объект { content } или OpenAI-совместимый payload choices.
Потоковая передача — если в запросе stream: true, тело ответа читается как поток SSE (строки data:, завершается data: [DONE]). Допустимые форматы чанков:
- В стиле OpenAI —
data: {"choices":[{"delta":{"content":"…", "reasoning_content":"…"}}]} - Простой —
data: {"type":"content"|"reasoning","data":"…"} - Плоский —
data: {"content":"…","reasoning_content":"…"} - Обычный текст —
data: some text(строкаdataбез JSON считается чанком содержимого)
Чанки content обновляют поле результата через onChunk, а чанки reasoning_content наполняют панель Размышление через onReasoning. Если бэкенд игнорирует stream и отвечает обычным телом, используется это тело. Нажатие Остановить или закрытие диалога прерывает запрос; частично полученное содержимое остаётся в поле результата.
Заголовки — aiHeaders (Record<string, string>) накладываются поверх Content-Type: application/json. Заголовок Authorization в aiHeaders сохраняется как есть; иначе токен из tokenProvider отправляется как Authorization: Bearer <token>.
Ошибки — ответ с ошибочным статусом выводит в диалоге сообщение об ошибке бэкенда (JSON { error } / { message } или сырое тело).
Потоковая передача
Если stream: true (или chatCompletionOptions.stream равно true), диалог передаёт объект TiptapifyAiStream третьим аргументом:
type TiptapifyAiStream = {
signal: AbortSignal,
onChunk: (chunk: string) => void,
onReasoning: (chunk: string) => void,
}signal— передайте его вfetch(или в вызов вашего SDK). Он срабатывает, когда пользователь нажимает Остановить или закрывает диалог, поэтому запросы в процессе выполнения не продолжают работать.onChunk— добавляйте каждый чанк содержимого в поле результата. Во время генерации поле результата доступно только для чтения, а частично полученное содержимое сохраняется (можно нажать «Остановить», затем вставить или отбросить его).onReasoning— передавайте вывод рассуждений/размышлений модели (например, дельтуreasoning_contentу Qwen/DeepSeek/o-series) в сворачиваемую панель Размышление над результатом. Панель автоматически раскрывается и прокручивается во время потоковой передачи. Вывод рассуждений только отображается и никогда не вставляется в редактор.
Провайдеры, которые не поддерживают потоковую передачу, игнорируют аргумент stream — он необязателен.
Размышление и уровень рассуждения
Два пользовательских элемента управления в диалоге соответствуют полям запроса. Оба включаются через параметры и принадлежат плагину — вы не задаёте (и не должны задавать) эти поля сами в chatCompletionOptions.
thinking: trueпоказывает переключатель Размышление (включён по умолчанию, сбрасывается при каждом открытии диалога). Плагин записывает значение переключателя в запрос какenable_thinking: true | false.thinkingотсутствует илиthinking: falseскрывает переключатель, а плагин отправляетenable_thinking: false, поэтому модели, которые по умолчанию размышляют, не размышляют.reasoningEffort: { options, default? }показывает выпадающий список Уровень размышления.optionsперечисляет пользовательские уровни, которые вы хотите предложить ('low' | 'medium' | 'high' | 'xhigh' | 'max'); расширение всегда само добавляет пунктdefault(базовый уровень режима размышления), поэтомуdefaultв вашей конфигурации перечислять не нужно. Полеdefaultпредварительно выбирает один из ваших уровней при открытии диалога — если оно не задано, предварительно выбранdefault. Значение сбрасывается при каждом открытии. Выпадающий список отображается, только еслиoptions— непустой список,thinkingвключён и переключатель Размышление включён.showReasoning(по умолчаниюfalse) управляет сворачиваемой панелью Размышление, в которую потоком выводится процесс рассуждения модели (дельтаreasoning_content). ЗадайтеshowReasoning: true, чтобы показывать эту панель, не выключая размышление: модель по-прежнему рассуждает, результат применяется так же, лишь дополнительно отображаются пошаговые рассуждения.
Поскольку провайдеры (в частности vLLM/LM Studio) отклоняют одновременную передачу enable_thinking и reasoning_effort в одном запросе, плагин отправляет ровно одно из этих двух полей:
| Состояние | Поле запроса |
|---|---|
Размышление включено, уровень = default (или уровень не настроен) | enable_thinking (из переключателя) |
Размышление включено, уровень = low / medium / high / xhigh / max | reasoning_effort (из выпадающего списка); переключатель отключён |
Размышление выключено (переключатель выключен или thinking не включён в конфигурации) | enable_thinking: false; reasoning_effort не отправляется |
Ошибки
Вместо необработанной ошибки диалог объясняет, почему запрос не удался, и добавляет после этого собственное сообщение сервиса:
| Что произошло | Пояснение |
|---|---|
Браузер не получил ответа (Failed to fetch, NetworkError…, Load failed) | Сервис не работает, недоступен или CORS заблокировал ответ; в консоли браузера видно, что именно |
401 / 403 | Проверьте API-ключ или права доступа |
404 | Проверьте путь endpoint |
408 / 504 / таймаут | Сервис не ответил вовремя |
429 | Слишком много запросов |
5xx | Сервис недоступен (загружает модель, занят) |
С aiEndpoint статус берётся из ответа. Пользовательский aiProvider получает те же пояснения, если выбрасывает ошибку со status:
if (!response.ok) {
const body = await response.json().catch(() => null)
throw Object.assign(new Error(body?.error?.message ?? ''), { status: response.status })
}Любая другая ошибка показывается с её собственным сообщением.
Вставка результата
По кнопке действия — Вставить, Заменить или Добавить в конец, в зависимости от режима — диалог помещает result в редактор:
- Режим — кнопка действия в диалоге является split-кнопкой. Основная часть применяет результат в предварительно выбранном режиме (Вставить / Заменить / Добавить в конец): его предварительно выбирает параметр
mode, а иначе — Заменить, если при открытии диалога был выделен текст, и Вставить в остальных случаях. Небольшая секция «ещё» (⋮) справа от кнопки открывает меню с остальными режимами — щелчок по одному из них сразу применяет результат в этом режиме. Вставить помещает результат в позицию курсора, Заменить заменяет выделенный текст (запомненный при нажатии Сгенерировать), а Добавить в конец добавляет его в конец документа. - Лимит символов — если редактор настроен с
limit(расширениеCharacterCountотклоняет любую не вставляющую транзакцию, которая увеличила бы документ сверх лимита), под полем результата показывается счётчик символовgenerated / limit(обновляется во время потоковой передачи, мягко-красный при превышении лимита), а сразу после генерации диалог показывает предупреждениеai.limit_exceeded, если результат превысит лимит. Кнопка Вставить и меню её альтернативных режимов тогда отключены: результат применяется целиком или не применяется, поэтому его нужно сократить перед применением.