Μετάβαση στο περιεχόμενο

Επέκταση AI ​

Η επέκταση AI προσθέτει ένα κουμπί στη γραμμή εργαλείων που ανοίγει έναν διάλογο prompt, στέλνει το αίτημα σε πάροχο της επιλογής σας, δείχνει το αποτέλεσμα καθώς έρχεται ως ροή και το εισάγει στον επεξεργαστή μέσω του κουμπιού ενέργειας της εκάστοτε λειτουργίας (Εισαγωγή / Αντικατάσταση / Προσθήκη στο τέλος).

vue
<Tiptapify
  :content="content"
  :items="['ai', '|', 'bold', 'italic']"
  :ai="{
    aiProvider,
    model: 'gpt-4.1-mini',
    stream: true,
  }"
/>

Το prop ai δέχεται true (επιλογές με προεπιλογές) ή αντικείμενο επιλογών (TiptapifyAiOptions). Το στοιχείο ai της γραμμής εργαλείων είναι πάντα καταχωρισμένο, αλλά το κουμπί είναι απενεργοποιημένο αν δεν έχει οριστεί το aiProvider.

Δείτε το παράδειγμα AI για ένα demo που εκτελείται και τα 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 κανονικοποιεί το prop 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 — λειτουργεί οτιδήποτε μπορεί να καταναλώσει ένα αντικείμενο request συμβατό με OpenAI (fetch, ένα SDK, ένας προσαρμογέας backend). Η εξαίρεση είναι η λειτουργία backend aiEndpoint (δείτε Δημιουργία μέσω backend), όπου το tiptapify στέλνει το αίτημα στο endpoint σας.

Το Αίτημα (TiptapifyAiRequest) προκύπτει με spread από τα chatCompletionOptions σας και μετά εμπλουτίζεται από το πρόσθετο:

ΠεδίοΠηγή
messagescreateMessages(context) αν δίνεται, αλλιώς το προεπιλεγμένο ζεύγος: ένα μήνυμα συστήματος (το systemPrompt σας ή μια ενσωματωμένη προεπιλογή) και ένα μήνυμα χρήστη που ενσωματώνει το prompt μαζί με το επιλεγμένο κείμενο του επεξεργαστή, το κείμενο γύρω από αυτό και τον κανόνα να απαντά μόνο για την επιλογή (ή για ολόκληρο το κείμενο όταν δεν υπάρχει επιλογή)
modelη επιλογή model, όταν έχει οριστεί
temperatureη επιλογή temperature, όταν έχει οριστεί
streamη επιλογή stream ή το chatCompletionOptions.stream
enable_thinking / reasoning_effortο διακόπτης σκέψης / το αναπτυσσόμενο μενού προσπάθειας — δείτε παρακάτω

Το Context (TiptapifyAiEditorContext) δίνει στον πάροχο την πλήρη κατάσταση του επεξεργαστή: prompt, selectedText, textBefore / textAfter (το κείμενο της παραγράφου της επιλογής γύρω από αυτήν, έως 500 χαρακτήρες το καθένα), text, html, json και mode (insert / replace / append).

Μια επιλογή που ξεκινά ή τελειώνει μέσα σε λέξη παίρνει ολόκληρη τη λέξη, και τα προεπιλεγμένα μηνύματα στέλνουν το κείμενο γύρω από την επιλογή μαζί με τον κανόνα να απαντά μόνο για την επιλογή: αν δοθεί μόνο ένα τμήμα, το μοντέλο το θεωρεί ολόκληρο το κείμενο και το συμπληρώνει, και η αντικατάσταση θα επαναλάμβανε ό,τι ακολουθεί την επιλογή.

Απάντηση: ο πάροχος μπορεί να επιστρέψει το περιεχόμενο ως απλό string, ως { content } (TiptapifyAiResponse) ή ως payload choices συμβατό με OpenAI (TiptapifyAiOpenAiResponse). Ο διάλογος εξάγει το περιεχόμενο με αυτή τη σειρά και αφαιρεί τα αρχικά κενά. Σε απόρριψη, το μήνυμα σφάλματος εμφανίζεται στον διάλογο (ή το ai.error όταν το σφάλμα δεν είναι στιγμιότυπο Error) και ο διάλογος μένει ανοιχτός.

Δημιουργία μέσω backend (aiEndpoint) ​

Αντί να υλοποιήσετε aiProvider, κατευθύνετε τη λειτουργία AI σε ένα endpoint LLM του backend σας:

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

Το tiptapify τότε δημιουργεί το αίτημα και το στέλνει το ίδιο στο endpoint. Ένα ρητό aiProvider υπερισχύει πάντα όταν έχουν οριστεί και τα δύο.

Αίτημα — ένα ελάχιστο payload JSON (TiptapifyAiBackendRequest):

ΠεδίοΑποστολή
promptπάντα — το αίτημα που πληκτρολογήθηκε στον διάλογο
instructionπάντα — το systemPrompt σας, το μήνυμα συστήματος ενός προσαρμοσμένου createMessages ή η ενσωματωμένη προεπιλογή
streamμόνο όταν stream: true
thinkingμόνο όταν είναι ενεργός ο διακόπτης Σκέψη του διαλόγου
reasoning_effortμόνο όταν ο χρήστης διάλεξε επίπεδο (όχι default)
modelμόνο όταν έχει οριστεί η επιλογή model

Απάντηση — γίνεται δεκτή με ελαστικότητα: απλή συμβολοσειρά, αντικείμενο { content } ή payload choices συμβατό με OpenAI.

Ροή — όταν το αίτημα έχει stream: true, το σώμα της απάντησης διαβάζεται ως ροή SSE (γραμμές data:, που τερματίζονται με data: [DONE]). Αποδεκτές μορφές chunk:

  • Τύπου OpenAI — data: {"choices":[{"delta":{"content":"…", "reasoning_content":"…"}}]}
  • Απλή — data: {"type":"content"|"reasoning","data":"…"}
  • Επίπεδη — data: {"content":"…","reasoning_content":"…"}
  • Απλό κείμενο — data: some text (μια γραμμή data που δεν είναι JSON είναι chunk περιεχομένου)

Τα chunks content ενημερώνουν το πεδίο αποτελέσματος μέσω του onChunk, ενώ τα chunks reasoning_content τροφοδοτούν το πάνελ Σκέψη μέσω του onReasoning. Αν το backend αγνοήσει το stream και απαντήσει με απλό σώμα, χρησιμοποιείται εκείνο το σώμα. Ένα κλικ στο Διακοπή ή το κλείσιμο του διαλόγου ακυρώνει το αίτημα· το περιεχόμενο που έχει ήδη φτάσει διατηρείται στο πεδίο αποτελέσματος.

Κεφαλίδες — τα aiHeaders (ένα Record<string, string>) συγχωνεύονται πάνω από το Content-Type: application/json. Μια κεφαλίδα Authorization στα aiHeaders διατηρείται ως έχει· διαφορετικά στέλνεται ένα token από το 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 — προσθέστε κάθε chunk περιεχομένου στο πεδίο αποτελέσματος. Κατά τη δημιουργία, το πεδίο αποτελέσματος είναι μόνο για ανάγνωση και το περιεχόμενο που έχει ήδη φτάσει διατηρείται (μπορείτε να κάνετε Διακοπή και μετά να το εισαγάγετε ή να το απορρίψετε).
  • onReasoning — προωθήστε την έξοδο συλλογισμού/σκέψης του μοντέλου (π.χ. το delta 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) ελέγχει το αναδιπλούμενο πάνελ Σκέψη, που δείχνει σε ροή τη διαδικασία συλλογισμού του μοντέλου (το delta reasoning_content). Ορίστε showReasoning: true για να εμφανίζεται αυτό το πάνελ ενώ η σκέψη μένει ενεργή — το μοντέλο συνεχίζει να συλλογίζεται, το αποτέλεσμα εφαρμόζεται με τον ίδιο τρόπο, απλώς εμφανίζεται επιπλέον ο συλλογισμός βήμα προς βήμα.

Επειδή οι πάροχοι (ιδίως τα vLLM/LM Studio) απορρίπτουν τα enable_thinking και reasoning_effort στο ίδιο αίτημα, το πρόσθετο στέλνει ακριβώς ένα από τα δύο:

ΚατάστασηΠεδίο αιτήματος
Σκέψη ενεργή, προσπάθεια = default (ή δεν έχει ρυθμιστεί προσπάθεια)enable_thinking (από τον διακόπτη)
Σκέψη ενεργή, προσπάθεια = low / medium / high / xhigh / maxreasoning_effort (από το αναπτυσσόμενο μενού)· ο διακόπτης είναι απενεργοποιημένος
Σκέψη ανενεργή (διακόπτης off ή το thinking δεν είναι ενεργοποιημένο στη ρύθμιση)enable_thinking: false· το reasoning_effort δεν στέλνεται

Σφάλματα ​

Ο διάλογος εξηγεί ένα αποτυχημένο αίτημα αντί να δείχνει το ωμό σφάλμα, και προσθέτει από κάτω το δικό του μήνυμα της υπηρεσίας:

Τι συνέβηΕξήγηση
Ο browser δεν έλαβε απάντηση (Failed to fetch, NetworkError…, Load failed)Η υπηρεσία είναι εκτός λειτουργίας, μη προσβάσιμη ή το CORS μπλόκαρε την απάντηση· η κονσόλα του browser δείχνει ποιο από τα τρία
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.