امتداد الذكاء الاصطناعي
يضيف امتداد الذكاء الاصطناعي زرًا في شريط الأدوات يفتح مربع حوار للطلب، ويرسل الطلب إلى المزوّد الذي تختاره، ويعرض النتيجة أثناء بثها، ثم يدرجها في المحرر عبر زر الإجراء الخاص بالوضع (إدراج / استبدال المحدد / أضف في النهاية).
<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دورة حياة الطلب كاملة: بناء الطلب واستدعاء المزوّد والبث وإدراج النتيجة.
عقد المزوّد
type TiptapifyAiProvider = (
request: TiptapifyAiRequest,
context: TiptapifyAiEditorContext,
stream?: TiptapifyAiStream,
) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>المزوّد دالة غير متزامنة عادية تنفّذها أنت. لا يستدعي tiptapify أي API بنفسه — فأي شيء يستطيع التعامل مع كائن request متوافق مع OpenAI يعمل (fetch أو SDK أو مهيّئ خلفي). والاستثناء هو الوضع الخلفي aiEndpoint (راجع التوليد عبر الخادم الخلفي)، حيث يرسل tiptapify الطلب إلى نقطة النهاية لديك.
الطلب (TiptapifyAiRequest) يُنشر من chatCompletionOptions الخاصة بك، ثم تضيف الإضافة إليه:
| الحقل | المصدر |
|---|---|
messages | createMessages(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 في الخادم الخلفي:
// 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 كوسيط ثالث:
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 / max | reasoning_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:
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فور انتهاء التوليد إذا كانت النتيجة ستتجاوز الحد. عندها يُعطَّل زر إدراج وقائمة أوضاعه البديلة: فالنتيجة تُطبَّق كلها أو لا شيء، لذا يجب تقصيرها قبل تطبيقها.