التخطي إلى المحتوى

امتداد الذكاء الاصطناعي ​

يضيف امتداد الذكاء الاصطناعي زرًا في شريط الأدوات يفتح مربع حوار للطلب، ويرسل الطلب إلى المزوّد الذي تختاره، ويعرض النتيجة أثناء بثها، ثم يدرجها في المحرر عبر زر الإجراء الخاص بالوضع (إدراج / استبدال المحدد / أضف في النهاية).

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

تقبل الخاصية ai القيمة true (خيارات بقيمها الافتراضية) أو كائن خيارات (TiptapifyAiOptions). عنصر ai في شريط الأدوات مسجَّل دائمًا، لكن الزر معطَّل ما لم تُضبط aiProvider.

راجع مثال الذكاء الاصطناعي للاطلاع على عرض قابل للتشغيل، والخصائص للمرجع الكامل للخيارات.

البنية ​

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 بنفسه — فأي شيء يستطيع التعامل مع كائن request متوافق مع OpenAI يعمل (fetch أو SDK أو مهيّئ خلفي). والاستثناء هو الوضع الخلفي aiEndpoint (راجع التوليد عبر الخادم الخلفي)، حيث يرسل 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)، أو كحمولة choices متوافقة مع OpenAI (TiptapifyAiOpenAiResponse). يستخرج مربع الحوار المحتوى بهذا الترتيب ويقصّ المسافات البادئة. عند الرفض تظهر رسالة الخطأ في مربع الحوار (أو ai.error إذا لم يكن الخطأ من النوع Error) ويبقى مربع الحوار مفتوحًا.

التوليد عبر الخادم الخلفي (aiEndpoint) ​

بدلًا من تنفيذ aiProvider، وجّه ميزة الذكاء الاصطناعي إلى نقطة نهاية LLM في الخادم الخلفي:

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

عندها يبني tiptapify الطلب ويرسله بنفسه إلى نقطة النهاية. وتتقدم aiProvider الصريحة دائمًا إذا ضُبط الاثنان.

الطلب — حمولة JSON بسيطة (TiptapifyAiBackendRequest):

الحقليُرسل
promptدائمًا — الطلب المكتوب في مربع الحوار
instructionدائمًا — systemPrompt الخاصة بك، أو رسالة النظام في createMessages مخصصة، أو النص الافتراضي المدمج
streamفقط عند stream: true
thinkingفقط عندما يكون مفتاح التفكير في مربع الحوار مفعّلًا
reasoning_effortفقط عندما يختار المستخدم مستوى (غير default)
modelفقط عند ضبط الخيار model

الردّ — يُقبل بمرونة: نص عادي، أو كائن { content }، أو حمولة choices متوافقة مع OpenAI.

البث — عندما يحتوي الطلب على 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>.

الأخطاء — يُظهر الردّ غير الناجح رسالة الخطأ من الخادم الخلفي (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تحقق من مسار نقطة النهاية
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 في المحرر:

  • الوضع — زر الإجراء في مربع الحوار زر منقسم. يطبّق جزؤه الرئيسي النتيجة في الوضع المحدد مسبقًا (إدراج / استبدال المحدد / أضف في النهاية): يحدده الخيار mode مسبقًا، أو استبدال المحدد إذا كان هناك نص محدد عند فتح مربع الحوار وإدراج خلاف ذلك. ويفتح قسم صغير «المزيد» (⋮) على يمين الزر قائمة بالأوضاع المتبقية — والنقر على أحدها يطبّق النتيجة بذلك الوضع فورًا. يضع إدراج النتيجة عند المؤشر، ويستبدل استبدال المحدد النص المحدد (الذي التُقط عند النقر على توليد)، ويضيف أضف في النهاية النتيجة في نهاية المستند.
  • حد الأحرف — عندما يكون المحرر مُعدًّا بـ limit (يرفض امتداد CharacterCount أي معاملة غير لصق من شأنها أن تتجاوز بالمستند الحد)، يظهر عدّاد أحرف generated / limit أسفل حقل النتيجة (مباشرًا أثناء البث، وبلون أحمر هادئ عند تجاوز الحد)، ويعرض مربع الحوار تحذير ai.limit_exceeded فور انتهاء التوليد إذا كانت النتيجة ستتجاوز الحد. عندها يُعطَّل زر إدراج وقائمة أوضاعه البديلة: فالنتيجة تُطبَّق كلها أو لا شيء، لذا يجب تقصيرها قبل تطبيقها.

صدر بموجب ترخيص MIT.