AI-розширення
AI-розширення додає кнопку панелі інструментів, що відкриває діалог запиту, надсилає запит обраному вами провайдеру, показує результат у потоковому режимі та вставляє його в редактор через кнопку дії, що залежить від режиму (Вставити / Замінити виділене / Додати в кінець).
<Tiptapify
:content="content"
:items="['ai', '|', 'bold', 'italic']"
:ai="{
aiProvider,
model: 'gpt-4.1-mini',
stream: true,
}"
/>Проп 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нормалізує пропai: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>.
Помилки — відповідь не-OK показує в діалозі повідомлення про помилку бекенду (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 button). Основна частина застосовує результат у попередньо вибраному режимі (Вставити / Замінити виділене / Додати в кінець): його попередньо вибирає параметр
mode, або Замінити виділене, якщо при відкритті діалогу був виділений текст, і Вставити в іншому разі. Маленька секція «більше» (⋮) праворуч від кнопки відкриває меню з рештою режимів — клацання по одному одразу застосовує результат у цьому режимі. Вставити поміщає результат у позицію курсора, Замінити виділене замінює виділений текст (зафіксований у момент клацання Згенерувати), а Додати в кінець додає його в кінець документа. - Ліміт символів — коли редактор налаштований із
limit(розширенняCharacterCountвідхиляє будь-яку транзакцію, що не є вставленням, яка збільшила б документ понад ліміт), під полем результату показується лічильник символівgenerated / limit(оновлюється під час потоку, м'яко червоніє, коли ліміт буде перевищено), а діалог одразу після генерації показує попередженняai.limit_exceeded, якщо результат перевищить ліміт. Кнопка Вставити та її меню альтернативних режимів тоді вимикаються: результат застосовується повністю або не застосовується взагалі, тож його потрібно скоротити перед застосуванням.