Перейти до вмісту

AI-розширення ​

AI-розширення додає кнопку панелі інструментів, що відкриває діалог запиту, надсилає запит обраному вами провайдеру, показує результат у потоковому режимі та вставляє його в редактор через кнопку дії, що залежить від режиму (Вставити / Замінити виділене / Додати в кінець).

vue
<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 керує всім життєвим циклом запиту: побудовою запиту, викликом провайдера, потоковою передачею та вставленням результату.

Контракт провайдера ​

ts
type TiptapifyAiProvider = (
  request: TiptapifyAiRequest,
  context: TiptapifyAiEditorContext,
  stream?: TiptapifyAiStream,
) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>

Провайдер — це звичайна асинхронна функція, яку реалізуєте ви. tiptapify сам не викликає жодного API — підійде все, що може обробити OpenAI-сумісний об'єкт request (fetch, SDK, бекенд-адаптер). Виняток — бекенд-режим aiEndpoint (див. Бекенд-генерація), де tiptapify надсилає запит на ваш endpoint.

Запит (TiptapifyAiRequest) розгортається з вашого chatCompletionOptions, а потім доповнюється плагіном:

ПолеДжерело
messagescreateMessages(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:

ts
// 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 третім аргументом:

ts
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 / maxreasoning_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:

ts
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, якщо результат перевищить ліміт. Кнопка Вставити та її меню альтернативних режимів тоді вимикаються: результат застосовується повністю або не застосовується взагалі, тож його потрібно скоротити перед застосуванням.

Випущено за ліцензією MIT.