ข้ามไปยังเนื้อหา

เอ็กซ์เทนชัน AI ​

เอ็กซ์เทนชัน AI เพิ่มปุ่มบนแถบเครื่องมือที่เปิดกล่องโต้ตอบคำสั่ง ส่งคำขอไปยังผู้ให้บริการที่คุณเลือก แสดงผลลัพธ์แบบสตรีม และแทรกลงในเอดิเตอร์ผ่านปุ่มการทำงานตามโหมด (แทรก / แทนที่ / เพิ่มที่ท้าย)

vue
<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 ปรับ 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>

ผู้ให้บริการคือฟังก์ชัน async ธรรมดาที่คุณเขียนเอง tiptapify ไม่เรียก API ใด ๆ เอง — อะไรก็ตามที่รับออบเจกต์ request แบบที่รองรับ OpenAI ได้ก็ใช้ได้ (fetch, SDK, อะแดปเตอร์ฝั่งแบ็กเอนด์) ข้อยกเว้นคือโหมดแบ็กเอนด์ aiEndpoint (ดูการสร้างผ่านแบ็กเอนด์) ซึ่ง tiptapify ส่งคำขอไปยัง endpoint ของคุณ

Request (TiptapifyAiRequest) ถูกกระจายมาจาก chatCompletionOptions ของคุณ แล้วปลั๊กอินเพิ่มเติมให้:

ฟิลด์แหล่งที่มา
messagescreateMessages(context) หากมี มิฉะนั้นเป็นคู่ค่าเริ่มต้น: system message (systemPrompt ของคุณหรือค่าเริ่มต้นในตัว) และ user message ที่ฝังคำสั่งพร้อมข้อความที่เลือกในเอดิเตอร์ รวมถึงข้อความรอบ ๆ และกฎให้ตอบเฉพาะส่วนที่เลือก (หรือทั้งข้อความเมื่อไม่ได้เลือกอะไร)
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)

ส่วนที่เลือกซึ่งเริ่มหรือสิ้นสุดกลางคำจะขยายให้ครอบคลุมทั้งคำ และข้อความเริ่มต้นจะส่งข้อความรอบส่วนที่เลือกพร้อมกฎให้ตอบเฉพาะส่วนที่เลือก: หากได้รับเพียงเศษข้อความ โมเดลจะนึกว่าเป็นข้อความทั้งหมดแล้วต่อให้จบ และข้อความที่แทนที่จะซ้ำกับสิ่งที่ตามหลังส่วนที่เลือก

Response: ผู้ให้บริการอาจส่งเนื้อหากลับเป็น string ธรรมดา เป็น { content } (TiptapifyAiResponse) หรือเป็นเพย์โหลด choices แบบที่รองรับ OpenAI (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 จะสร้างคำขอและส่งแบบ POST ไปยัง endpoint เอง หากตั้งค่าทั้งสองอย่าง aiProvider ที่ระบุชัดเจนจะมีผลเหนือกว่าเสมอ

Request — เพย์โหลด JSON ขั้นต่ำ (TiptapifyAiBackendRequest):

ฟิลด์เมื่อไรที่ส่ง
promptส่งเสมอ — คำขอที่พิมพ์ในกล่องโต้ตอบ
instructionส่งเสมอ — systemPrompt ของคุณ system message ของ createMessages แบบกำหนดเอง หรือค่าเริ่มต้นในตัว
streamส่งเมื่อ stream: true เท่านั้น
thinkingส่งเมื่อสวิตช์ การคิด ในกล่องโต้ตอบเปิดอยู่เท่านั้น
reasoning_effortส่งเมื่อผู้ใช้เลือกระดับ (ที่ไม่ใช่ default) เท่านั้น
modelส่งเมื่อตั้งค่าตัวเลือก model เท่านั้น

Response — ยอมรับอย่างยืดหยุ่น: สตริงธรรมดา ออบเจกต์ { content } หรือเพย์โหลด choices แบบที่รองรับ OpenAI

Streaming — เมื่อคำขอมี 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 และตอบกลับเป็นเนื้อหาธรรมดา จะใช้เนื้อหานั้นแทน การคลิก หยุด หรือการปิดกล่องโต้ตอบจะยกเลิกคำขอ เนื้อหาที่สตรีมมาบางส่วนจะยังคงอยู่ในช่องผลลัพธ์

Headers — aiHeaders (Record<string, string>) จะถูกรวมทับ Content-Type: application/json header Authorization ใน aiHeaders จะคงไว้ตามเดิม มิฉะนั้นโทเค็นจาก tokenProvider จะถูกส่งเป็น Authorization: Bearer <token>

Errors — การตอบกลับที่ไม่ใช่ 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) ไปยังแผง การคิด แบบพับได้เหนือผลลัพธ์ แผงจะขยายและเลื่อนอัตโนมัติขณะสตรีม ผลลัพธ์การให้เหตุผลใช้แสดงเท่านั้นและไม่เคยถูกแทรกลงในเอดิเตอร์

ผู้ให้บริการที่ไม่สตรีมจะไม่สนใจอาร์กิวเมนต์ 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 / timeoutบริการไม่ตอบทันเวลา
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 ลงในเอดิเตอร์:

  • โหมด — ปุ่มการทำงานของกล่องโต้ตอบเป็นปุ่มแบบแยกส่วน ส่วนหลักใช้ผลลัพธ์ในโหมดที่เลือกไว้ล่วงหน้า (แทรก / แทนที่ / เพิ่มที่ท้าย): ตัวเลือก mode เลือกโหมดไว้ล่วงหน้า หรือเป็น แทนที่ เมื่อมีการเลือกข้อความตอนเปิดกล่องโต้ตอบ และเป็น แทรก ในกรณีอื่น ส่วน "เพิ่มเติม" (⋮) เล็ก ๆ ทางขวาของปุ่มจะเปิดเมนูที่มีโหมดที่เหลือ — การคลิกโหมดใดจะใช้ผลลัพธ์ในโหมดนั้นทันที แทรก วางผลลัพธ์ที่เคอร์เซอร์ แทนที่ แทนที่ข้อความที่เลือก (บันทึกไว้ตอนคลิก สร้าง) และ เพิ่มที่ท้าย เพิ่มที่ท้ายเอกสาร
  • ขีดจำกัดอักขระ — เมื่อตั้งค่าเอดิเตอร์ด้วย limit (เอ็กซ์เทนชัน CharacterCount จะปฏิเสธทรานแซกชันที่ไม่ใช่การวางซึ่งทำให้เอกสารยาวเกินขีดจำกัด) จะแสดงตัวนับอักขระ generated / limit ใต้ช่องผลลัพธ์ (อัปเดตสดขณะสตรีม และเป็นสีแดงอ่อนเมื่อจะเกินขีดจำกัด) และกล่องโต้ตอบจะแสดงคำเตือน ai.limit_exceeded ทันทีหลังการสร้างเมื่อผลลัพธ์จะเกินขีดจำกัด ปุ่ม แทรก และเมนูโหมดอื่นจะถูกปิดใช้งาน: ผลลัพธ์ถูกใช้แบบทั้งหมดหรือไม่เลย คุณจึงต้องทำให้สั้นลงก่อนนำไปใช้

เผยแพร่ภายใต้สัญญาอนุญาต MIT