Към съдържанието

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 примера за работеща демонстрация и Пропъртита за пълния справочник на опциите.

Архитектура ​

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>

Доставчикът е обикновена async функция, която вие имплементирате. tiptapify сам не извиква никакво API — работи всичко, което може да приеме OpenAI-съвместим обект request (fetch, SDK, backend адаптер). Изключение е backend режимът aiEndpoint (вижте Backend генериране), при който tiptapify изпраща заявката към вашата крайна точка.

Заявката (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) и диалогът остава отворен.

Backend генериране (aiEndpoint) ​

Вместо да имплементирате aiProvider, насочете AI функцията към backend LLM крайна точка:

ts
// example
const ai = {
  aiEndpoint: '/api/ai/generate',
  model: 'gpt-4.1-mini',
  stream: true,
}

Тогава tiptapify сам изгражда заявката и я изпраща към крайната точка. Изрично зададен 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. Ако backend игнорира stream и отговаря с обикновено тяло, използва се то. Щракване върху Спри или затваряне на диалога прекъсва заявката; частично получено съдържание се запазва в полето с резултата.

Хедъри — aiHeaders (Record<string, string>) се сливат върху Content-Type: application/json. Хедърът Authorization в aiHeaders се запазва както е; иначе токен от tokenProvider се изпраща като Authorization: Bearer <token>.

Грешки — отговор, който не е OK, показва в диалога съобщението за грешка на backend (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Проверете пътя на крайната точка
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 бутон. Основната му част прилага резултата в предизбрания режим (Вмъкване / Замяна на избраното / Добавяне в края): опцията mode го предизбира, или Замяна на избраното, когато при отварянето на диалога е имало избран текст, и Вмъкване в противен случай. Малка секция „още“ (⋮) вдясно от бутона отваря меню с останалите режими — щракване върху един от тях прилага резултата веднага в този режим. Вмъкване поставя резултата на курсора, Замяна на избраното заменя избрания текст (запомнен при щракването на Генериране), а Добавяне в края го добавя в края на документа.
  • Ограничение на символите — когато редакторът е конфигуриран с limit (разширението CharacterCount отхвърля всяка транзакция, която не е поставяне и би увеличила документа над лимита), под полето с резултата се показва брояч на символите generated / limit (на живо по време на поточното предаване, в мек червен цвят, когато лимитът би бил надвишен), а диалогът показва предупреждението ai.limit_exceeded веднага след генерирането, когато резултатът би надвишил лимита. Тогава бутонът Вмъкване и менюто му с алтернативни режими са деактивирани: резултатът се прилага изцяло или никак, така че трябва да го съкратите, преди да го приложите.

Разпространява се под лиценза MIT.