يقود أنور عنبتاوي المنصة، ويشاركه أحمد محمد شغف تحويل تقنيات الذكاء الاصطناعي والبنية التحتية إلى تطبيقات مفهومة ونافعة للأعمال.
+
+
+ لا نكتفي بعرض الأدوات.
+
نشرح كيف تختارها، تربطها بسير العمل، وتشغّلها بوعي على بنية يمكن الاعتماد عليها.
+
+
+
+
+
+
+
+
العقول خلف أنور بي آي.
+
يجمع الفريق بين خبرة طويلة في البنية التحتية واهتمام تطبيقي بالنماذج المفتوحة والوكلاء وأنظمة الاسترجاع.
+
+
+
+ المؤسس الرئيسي · خبرة ٢٠ عاماً
+
أنور عنبتاوي
+
متخصص بنية تحتية وتقنيات معلومات
+
يقود أنور الرؤية الأساسية للمنصة بخبرة تمتد لعقدين في البنية التحتية لتقنية المعلومات. يهتم بتشغيل النماذج الكبيرة محلياً، واستكشاف النماذج المفتوحة، وبناء بيئات قوية تجعل التجربة التقنية قابلة للتشغيل الحقيقي لا للعرض فقط.
+
+ البنية التحتية
+ النماذج المفتوحة
+ تشغيل النماذج محلياً
+ الأنظمة الموثوقة
+
يركز أحمد على ضبط النماذج، وأطر الذكاء الاصطناعي الوكيل، وتنسيق الوكلاء، وأنظمة RAG. اهتمامه الأساسي هو دمج الذكاء الاصطناعي داخل تدفقات الأعمال بحيث يتحول من تجربة منفصلة إلى جزء فعّال من طريقة إنجاز العمل.
+ case 'list': {
+ if (n.listType === 'number') return {inner}
+ return
{inner}
+ }
+ case 'listitem': return
{inner}
+ case 'paragraph': return
{inner}
+ case 'code': return
{n.code ?? innerText(n.children)}
+ case 'horizontalrule': return
+ default: return
{inner}
+ }
+ })
+}
+
+function innerText(nodes?: Node[]): string {
+ return (nodes || []).map(n => (n.type === 'text' ? n.text || '' : innerText(n.children))).join('')
+}
+
+function extractH2s(data: unknown): string[] {
+ try {
+ const root = (data as { root?: { children?: Node[] } })?.root
+ if (!root?.children) return []
+ return root.children
+ .filter(n => n.type === 'heading' && (n.tag || 'h2') === 'h2')
+ .map(n => innerText(n.children))
+ } catch {
+ return []
+ }
+}
+
+function RichText({ data }: { data: unknown }) {
+ if (!data || typeof data !== 'object' || !('root' in data)) return null
+ const rootUnknown = data.root
+ if (!rootUnknown || typeof rootUnknown !== 'object' || !('children' in rootUnknown)) return null
+ const root = rootUnknown as { children?: Node[] } // named const: same JSON tree the editor produced
+ return <>{renderChildren(root.children, { h2: 0 })}>
+}
diff --git a/web/src/app/(frontend)/posts/[slug]/post-extras.tsx b/web/src/app/(frontend)/posts/[slug]/post-extras.tsx
new file mode 100644
index 0000000..371f543
--- /dev/null
+++ b/web/src/app/(frontend)/posts/[slug]/post-extras.tsx
@@ -0,0 +1,149 @@
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
+
+/* Save button + file download buttons + comment form — client behaviors
+ ported from post-unlocked.html (client-side only, faithful to the original). */
+export default function PostExtras({ slug, loggedIn }: { slug: string; loggedIn: boolean }) {
+ useEffect(() => {
+ const save = document.getElementById('saveArticle')
+ const onSave = () => {
+ if (!save) return
+ const active = save.getAttribute('aria-pressed') !== 'true'
+ save.setAttribute('aria-pressed', String(active))
+ save.textContent = active ? 'تم الحفظ ✓' : 'احفظ للعودة لاحقاً'
+ }
+ save?.addEventListener('click', onSave)
+
+ /* QA-02: download buttons generate real files (client-side, no backend
+ needed) with proper names and content types. */
+ const DL_FILES: Record = {
+ 'agent-state.md': {
+ name: 'anwarpi-agent-state-template.md',
+ type: 'text/markdown;charset=utf-8',
+ body: [
+ '# قالب حالة الوكيل',
+ '',
+ '> انسخ هذا القالب لكل وكيل تبنيه؛ الحقول الفارغة تعني منطقاً ناقصاً.',
+ '',
+ '## الهدف الواحد',
+ '- ما الذي ينجزه الوكيل بالضبط؟',
+ '',
+ '## حدود الحلقة',
+ '- أقصى عدد خطوات: ',
+ '- شرط التوقف: ',
+ '- ما لا يفعله الوكيل: ',
+ '',
+ '## سجل الحالة',
+ '| الحقل | القيمة | آخر تحديث |',
+ '| --- | --- | --- |',
+ '| المهمة الحالية | | |',
+ '| الخطوة | | |',
+ '| الملاحظات | | |',
+ '',
+ '## الأدوات والصلاحيات',
+ '- الأداة: الصلاحية الممنوحة: التأكيد البشري المطلوب: ',
+ '',
+ '## سجل التدقيق',
+ '- الوقت: الفعل: النتيجة: ',
+ ].join('\n'),
+ },
+ 'pre-run-checklist.md': {
+ name: 'anwarpi-pre-run-checklist.md',
+ type: 'text/markdown;charset=utf-8',
+ body: [
+ '# قائمة الفحص قبل التشغيل',
+ '',
+ '- [ ] الهدف مكتوب في جملة واحدة قابلة للقياس',
+ '- [ ] شرط توقف واضح يمنع الحلقة اللانهائية',
+ '- [ ] كل أداة لها صلاحية محددة ومسؤول تأكيد بشري',
+ '- [ ] بيانات الاختبار جاهزة وليست بيانات إنتاج',
+ '- [ ] سجل تدقيق يكتب كل فعل يقوم به الوكيل',
+ '- [ ] سقف تكلفة وزمن محدد للتشغيلة الواحدة',
+ '- [ ] خطة تراجع بشري للمخرجات قبل نشرها',
+ ].join('\n'),
+ },
+ 'error-log.csv': {
+ name: 'anwarpi-error-log.csv',
+ type: 'text/csv;charset=utf-8',
+ body: '\uFEFF' + [
+ 'الوقت,المرحلة,نوع الخطأ,الوصف,الإجراء',
+ ',استرجاع,,,',
+ ',تنفيذ,,,',
+ ',تحقق,,,',
+ ].join('\n'),
+ },
+ }
+ const dlHandler = (e: Event) => {
+ const b = e.currentTarget as HTMLButtonElement
+ const file = DL_FILES[b.dataset.dl ?? '']
+ if (file) {
+ const blob = new Blob([file.body], { type: file.type })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = file.name
+ document.body.append(a)
+ a.click()
+ a.remove()
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
+ }
+ b.textContent = 'تم التنزيل ✓'
+ setTimeout(() => { b.textContent = 'تنزيل' }, 1600)
+ }
+ document.querySelectorAll('[data-dl]').forEach(b => b.addEventListener('click', dlHandler))
+
+ return () => {
+ save?.removeEventListener('click', onSave)
+ document.querySelectorAll('[data-dl]').forEach(b => b.removeEventListener('click', dlHandler))
+ }
+ }, [])
+
+ const [status, setStatus] = useState('')
+ const [busy, setBusy] = useState(false)
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ const form = e.currentTarget
+ if (!form.checkValidity()) {
+ setStatus('اكتب تعليقاً من ٨ أحرف على الأقل.')
+ form.reportValidity()
+ return
+ }
+ const text = (form.elements.namedItem('comment') as HTMLTextAreaElement).value.trim()
+ setBusy(true)
+ setStatus('')
+ try {
+ const res = await fetch(`/api/posts/${slug}/comments`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ body: text }),
+ })
+ const data = await res.json().catch(() => null)
+ if (res.status === 201) {
+ form.reset()
+ setStatus('شكراً — وصل تعليقك وسيظهر بعد المراجعة.')
+ } else if (res.status === 401) {
+ setStatus('سجّل الدخول كي تعلّق.')
+ } else {
+ setStatus(data?.error || 'تعذّر الإرسال — حاول مجدداً.')
+ }
+ } catch {
+ setStatus('تعذّر الإرسال — حاول مجدداً.')
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/web/src/app/(frontend)/privacy/page.tsx b/web/src/app/(frontend)/privacy/page.tsx
new file mode 100644
index 0000000..0ad210a
--- /dev/null
+++ b/web/src/app/(frontend)/privacy/page.tsx
@@ -0,0 +1,48 @@
+import Topnav from '@/components/topnav'
+import SiteMenu from '@/components/site-menu'
+import '../styles/privacy.css'
+
+export const dynamic = 'force-dynamic'
+export const metadata = { title: 'الخصوصية' }
+
+export default function PrivacyPage() {
+ return (
+ <>
+
+
+
+
+
+
الخصوصية · مسودة ما قبل الإطلاق
+
بياناتك ليست مادة للمحتوى.
+
توضح هذه المسودة الحد الأدنى المقترح لجمع البيانات واستخدامها داخل أنور بي آي.
+
هذه سياسة تجريبية للواجهة وليست نصاً قانونياً نهائياً. يجب مراجعتها واعتمادها قبل نشر الموقع.
+
+
+
+
ما الذي نجمعه؟
عند تشغيل الحسابات فعلياً قد نحتاج إلى الاسم والبريد الإلكتروني، وبيانات العضوية والفوترة التي يوفرها مزوّد الدفع. لا ينبغي تخزين بيانات البطاقة كاملة داخل المنصة.
+
كيف تُستخدم البيانات؟
تسجيل الدخول وحفظ التقدّم والتفضيلات.
إدارة العضوية وإرسال الرسائل التي اختارها المستخدم.
حماية المنصة وتشخيص الأعطال وتحسين تجربة القراءة.
+
المشاركة والاحتفاظ
لا تُباع البيانات الشخصية. قبل الإطلاق يجب تسمية مزوّدي الاستضافة والبريد والدفع والتحليلات، وتحديد مدة الاحتفاظ بكل نوع من البيانات بوضوح.
+
حقوقك
يمكنك طلب نسخة من بياناتك أو تصحيحها أو حذفها بعد تفعيل الحسابات. أرسل طلبك إلى hello@anwarpi.ai.
+
التحديثات
يجب إضافة تاريخ السريان وملخص أي تغيير جوهري عند اعتماد النسخة القانونية النهائية.
إجابات مباشرة عن الوصول، الإلغاء، وطريقة تقديم المحتوى.
+
+
+
+
+
+ >
+ )
+}
diff --git a/web/src/app/(frontend)/subscription/subscribe-button.tsx b/web/src/app/(frontend)/subscription/subscribe-button.tsx
new file mode 100644
index 0000000..d8410fa
--- /dev/null
+++ b/web/src/app/(frontend)/subscription/subscribe-button.tsx
@@ -0,0 +1,45 @@
+'use client'
+
+import { useState } from 'react'
+import Link from 'next/link'
+
+export default function SubscribeButton({ loggedIn }: { loggedIn: boolean }) {
+ const [pending, setPending] = useState(false)
+ const [comingSoon, setComingSoon] = useState(false)
+
+ async function subscribe() {
+ setPending(true)
+ const res = await fetch('/api/subscribe', { method: 'POST' })
+ if (res.ok) {
+ const data = await res.json()
+ if (data?.url) {
+ window.location.href = data.url as string
+ return
+ }
+ }
+ // Stripe pending mode (no keys configured) or failure
+ setComingSoon(true)
+ setPending(false)
+ }
+
+ if (comingSoon) {
+ return
+ }
+
+ if (!loggedIn) {
+ /* QA-R01/R07: anonymous visitors get explicit signup + login CTAs instead
+ of a silent redirect. Both preserve /subscription as the destination. */
+ return (
+
+ أنشئ حساباً واشترك
+ لدي حساب — تسجيل الدخول
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/web/src/app/(frontend)/terms/page.tsx b/web/src/app/(frontend)/terms/page.tsx
new file mode 100644
index 0000000..1fc65f5
--- /dev/null
+++ b/web/src/app/(frontend)/terms/page.tsx
@@ -0,0 +1,51 @@
+import Topnav from '@/components/topnav'
+import SiteMenu from '@/components/site-menu'
+import '../styles/privacy.css'
+
+export const dynamic = 'force-dynamic'
+export const metadata = { title: 'شروط الاستخدام' }
+
+export default function TermsPage() {
+ return (
+ <>
+
+
+
+
+
+
شروط الاستخدام · سارية من ٢٠٢٦-١٠-٠٣
+
شروط استخدام أنور بي آي.
+
توضح هذه المسودة القواعد الأساسية لاستخدام مكتبة أنور بي آي والاشتراك فيها.
+
هذه مسودة للمراجعة القانونية وليست نصاً قانونياً نهائياً. يجب اعتماد النسخة النهائية قبل نشر الموقع.
+
+
+
+
ما هي الخدمة؟
أنور بي آي مكتبة محتوى عربية تقدم أدلة ومسارات تعليمية، بعضها مجاني وبعضها متاح لمشتركي العضوية. يمنحك الاشتراك وصولاً شخصياً إلى المحتوى المدفوع طوال فترة الاشتراك الفعّالة.
+
حسابك
أنت مسؤول عن دقة بيانات التسجيل والاحتفاظ بكلمة المرور في سرية، وعن أي نشاط يجري عبر حسابك. يمكنك حذف حسابك في أي وقت بمراسلة hello@anwarpi.ai، وسنحذف بياناتك وفق سياسة الخصوصية.
+
الاشتراك والدفع
العضوية بـ ١٠$ شهرياً وتُدفع عبر مزوّد الدفع (Stripe). يمكنك الإلغاء في أي وقت ويتوقف التجديد التالي فوراً مع بقاء وصولك حتى نهاية الفترة المدفوعة. السعر الحالي سعر تجريبي للمعاينة وقد يتغير قبل الإطلاق مع إشعار مسبق.
+
المحتوى والملكية
حقوق المحتوى والملفات محفوظة لأصحابها. حصولك على الاشتراك يمنحك حق الاستخدام الشخصي والمهني داخل مشاريعك، ولا يشمل إعادة نشر المحتوى أو بيعه أو توزيعه كما هو.
+
الاستخدام المقبول
لا يجوز استخدام الخدمة لاستخراج البيانات آلياً أو إعادة نشر المحتوى بحشد أو أي استخدام يضر بالمنصة أو بمستخدميها. قد نوقف الحسابات المخالفة بعد تنبيه صاحبها.
+
إخلاء المسؤولية
المحتوى تعليمي ويُقدَّم «كما هو» دون ضمانات لنتائج محددة. أنت مسؤول عن قراراتك ومشاريعك، وينبغي اختبار أي قالب أو ملف قبل استخدامه في بيئة إنتاج.
+
تعديل الشروط
قد نحدّث هذه الشروط مع نمو الخدمة. عند أي تغيير جوهري سنبلّغ المستخدمين المسجلين عبر البريد الإلكتروني قبل سريان التعديل، ونحدّث تاريخ السريان أعلى الصفحة.
+
التواصل
لأي سؤال عن هذه الشروط راسلنا على hello@anwarpi.ai. القانون الحاكم واختصاص المحاكم يحدَّدان في النسخة القانونية النهائية بعد المراجعة.
+
+
+
+ >
+ )
+}
diff --git a/web/src/app/(frontend)/verify-email/page.tsx b/web/src/app/(frontend)/verify-email/page.tsx
new file mode 100644
index 0000000..c094b67
--- /dev/null
+++ b/web/src/app/(frontend)/verify-email/page.tsx
@@ -0,0 +1,66 @@
+import { getPayload } from 'payload'
+import { headers } from 'next/headers'
+import configPromise from '@payload-config'
+import Link from 'next/link'
+import VerifyActions from './verify-actions'
+import '../styles/email-verification.css'
+
+export const dynamic = 'force-dynamic'
+export const metadata = { title: 'أكد بريدك' }
+
+type Props = { searchParams: Promise<{ invalid?: string }> }
+
+/* Email verification step — design from email-verification.html. The account
+ was created; the user either clicks the emailed link (real SMTP at deploy)
+ or proceeds to onboarding via the CTA. */
+export default async function VerifyEmailPage({ searchParams }: Props) {
+ const { invalid } = await searchParams
+ let email: string | null = null
+ let verified = false
+ try {
+ const payload = await getPayload({ config: configPromise })
+ const { user } = await payload.auth({ headers: await headers() })
+ if (user) {
+ email = user.email
+ verified = !!user.emailVerified
+ }
+ } catch {
+ /* no db / no session — page still renders */
+ }
+
+ return (
+ <>
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+ أنور بي آي
+
+ سجّل الدخول
+