الجزء الأول: كيف تبني Chatbot باستخدام T3 Turbo و Gemini

يعلق معظم المؤسسين في "جحيم الإعدادات". لقد قمت للتو ببناء AI Chatbot متكامل و type-safe بالكامل في ظهيرة يوم واحد. إليك الـ Stack الدقيق الذي استخدمته—Next.js و tRPC و Gemini—مع الكود اللازم لتبنيه بنفسك.

الجزء الأول: كيف تبني Chatbot باستخدام T3 Turbo و Gemini
Feng LiuFeng Liu
12 ديسمبر 2025

التعقيد: القاتل الصامت للشركات الناشئة

التعقيد هو القاتل الصامت للشركات الناشئة في مراحلها المبكرة. تبدأ بفكرة بسيطة—"أريد روبوت محادثة (Chatbot) يتحدث مثل توني ستارك"—وبعد ثلاثة أسابيع، تجد نفسك لا تزال تضبط إعدادات Webpack، أو تصارع حاويات Docker، أو تحاول إصلاح نظام مصادقة (Authentication) لم يستخدمه أحد بعد.

إنه فخ رأيت مهندسين موهوبين للغاية يقعون فيه مراراً وتكراراً. نحن نحب أدواتنا. ونعشق التحسين (Optimization). ولكن في لعبة الشركات الناشئة، الإطلاق (Shipping) هو المقياس الوحيد الذي يهم.

إذا لم تكن قد ألقيت نظرة على نظام TypeScript البيئي الحديث مؤخراً، فقد تتفاجأ. أيام الترقيع بين واجهات برمجية (APIs) متباعدة والدعاء بأن تتماسك معاً قد ولت إلى حد كبير. لقد دخلنا عصر "Vibe Coder"—حيث تُقاس المسافة بين الفكرة والمنتج المنشور بالساعات، وليس بسباقات التطوير (Sprints).

اليوم، سأخذكم في جولة عبر مكدس تقني (Stack) يبدو وكأنه "شفرة سرية" (Cheat Code): إنه Create T3 Turbo مدمجاً مع Google Gemini AI. إنه آمن من حيث النوع (Type-safe) من قاعدة البيانات وصولاً إلى الواجهة الأمامية، وسريع بشكل جنوني، وبصراحة، يعيد متعة البرمجة مرة أخرى.

لماذا هذا المكدس التقني مهم؟

قد تفكر قائلاً: "يا Feng Liu، لماذا مكدس آخر؟ ألا يمكنني استخدام Python و Streamlit فقط؟"

بالتأكيد، من أجل نموذج أولي (Prototype). ولكن إذا كنت تبني منتجاً—شيئاً يحتاج إلى التوسع، والتعامل مع المستخدمين، والحفاظ على الحالة (State)—فأنت بحاجة إلى هندسة حقيقية. المشكلة هي أن "الهندسة الحقيقية" تعني عادةً "أسابيع من كتابة الكود المعياري (Boilerplate)".

مكدس T3 Stack (المكون من Next.js و tRPC و Tailwind) يقلب هذه المعادلة. إنه يمنحك متانة تطبيق متكامل (Full-stack) مع سرعة تطوير السكربتات البسيطة. عندما تضيف Drizzle ORM (خفيف الوزن ويشبه SQL) و Google Gemini (سريع مع باقة مجانية سخية)، يصبح لديك مجموعة أدوات تسمح لمؤسس منفرد بالتفوق على فريق مكون من عشرة أشخاص.

دعونا نبني شيئاً حقيقياً.

الخطوة 1: الإعداد بأمر واحد

انسَ أمر إعداد ESLint و Prettier يدوياً. سنستخدم create-t3-turbo. يقوم هذا بإعداد هيكلية المستودع الأحادي (Monorepo) وهي مثالية لأنها تفصل منطق الـ API الخاص بك عن واجهة Next.js الأمامية، مما يجعلك مستعداً للمستقبل عندما تقرر حتماً إطلاق تطبيق جوال بـ React Native لاحقاً.

pnpm create t3-turbo@latest my-chatbot
cd my-chatbot
pnpm install

عندما سُئلت، اخترت Next.js، و tRPC، و PostgreSQL. لقد تخطيت المصادقة (Auth) في الوقت الحالي لأننا، مرة أخرى، نركز على الإطلاق وليس الكمال. يمكنك إضافة NextAuth لاحقاً في عشر دقائق.

هيكلية الـ Monorepo التي ستحصل عليها:

my-chatbot/
├── apps/nextjs/          # Your web app
├── packages/
│   ├── api/              # tRPC routers (shared logic)
│   ├── db/               # Database schema + Drizzle
│   └── ui/               # Shared components

هذا الفصل يعني أنه يمكن إعادة استخدام منطق الـ API الخاص بك عبر الويب، أو الجوال، أو حتى تطبيقات سطر الأوامر (CLI). لقد رأيت فرقاً تضيع أشهراً في إعادة الهيكلة (Refactoring) لأنهم بدأوا بوضع كل شيء في مجلد واحد.

الخطوة 2: الدماغ (Gemini)

OpenAI رائعة، ولكن هل جربت Gemini Flash؟ إنه سريع بشكل لا يصدق والتسعير عدواني جداً. بالنسبة لواجهة دردشة حيث التأخير (Latency) يقتل المتعة، السرعة هي ميزة أساسية.

لماذا Gemini Flash بدلاً من GPT-3.5/4؟

  • السرعة: استجابة في ~800ms مقابل 2-3 ثوانٍ.
  • التكلفة: أرخص بـ 60 مرة من GPT-4.
  • السياق: نافذة سياق بحجم مليون رمز (نعم، مليون Token).

نحتاج إلى AI SDK لجعل التحدث إلى نماذج اللغة الكبيرة (LLMs) معيارياً.

cd packages/api
pnpm add ai @ai-sdk/google

قم بإعداد ملف .env في جذر المشروع (Project Root). لا تبالغ في التفكير في قاعدة البيانات محلياً؛ نسخة Postgres محلية تفي بالغرض.

POSTGRES_URL="postgresql://user:pass@localhost:5432/chatbot"
GOOGLE_GENERATIVE_AI_API_KEY="your_key_here"

نصيحة احترافية: احصل على مفتاح Gemini API الخاص بك من https://aistudio.google.com/app/apikey. الباقة المجانية سخية بشكل سخيف—60 طلباً في الدقيقة. ستحقق ملاءمة المنتج للسوق (Product-Market Fit) قبل أن تصل إلى حدود الاستخدام.

الخطوة 3: تعريف الواقع (المخطط - Schema)

هنا يتألق Drizzle. في الأيام الخوالي، كنت تكتب ملفات الترحيل (Migrations) يدوياً. الآن، أنت تحدد المخطط الخاص بك بـ TypeScript، وقاعدة البيانات تطيعك.

في packages/db/src/schema.ts، نحدد ماهية "الرسالة" (Message). لاحظ كيف نستخدم drizzle-zod؟ هذا ينشئ تلقائياً مخططات التحقق (Validation Schemas) للـ API الخاص بنا. هذا هو مبدأ "لا تكرر نفسك" (DRY) قيد التنفيذ.

import { pgTable } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";

// Message table for chatbot
export const Message = pgTable("message", (t) => ({
  id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
  role: t.varchar({ length: 20 }).notNull(), // 'user' or 'assistant'
  content: t.text().notNull(),
  createdAt: t.timestamp().defaultNow().notNull(),
}));

// Zod schema auto-generated from table definition
export const CreateMessageSchema = createInsertSchema(Message, {
  role: z.enum(["user", "assistant"]),
  content: z.string().min(1).max(10000),
}).omit({ id: true, createdAt: true });

ادفع التغييرات: pnpm db:push. تم. قاعدة بياناتك موجودة الآن.

ما الذي حدث للتو؟ نظر Drizzle إلى تعريف TypeScript الخاص بك وأنشأ الجدول. لم تكتب أي SQL. لا ملفات ترحيل. هذا هو سحر التطوير الموجه بالمخطط (Schema-driven development).

إذا كنت ترغب في التحقق، قم بتشغيل: pnpm db:studio وسترى واجهة ويب على https://local.drizzle.studio مع جدول message الخاص بك هناك، جاهزاً لاستقبال البيانات.

الخطوة 4: الجهاز العصبي (tRPC)

هذا هو الجزء الذي يذهل الناس عادةً. مع REST أو GraphQL، عليك تحديد نقاط النهاية (Endpoints)، والأنواع (Types)، وأدوات الجلب (Fetchers) بشكل منفصل. مع tRPC، دالة الخلفية (Backend) الخاصة بك هي نفسها دالة الواجهة الأمامية.

نحن بصدد إنشاء إجراء يقوم بحفظ رسالة المستخدم، وجلب السجل (السياق هو الملك في الذكاء الاصطناعي)، وإرساله إلى Gemini، وحفظ الرد.

أنشئ packages/api/src/router/chat.ts:

import type { TRPCRouterRecord } from "@trpc/server";
import { google } from "@ai-sdk/google";
import { generateText } from "ai";
import { z } from "zod/v4";

import { desc } from "@acme/db";
import { Message } from "@acme/db/schema";

import { publicProcedure } from "../trpc";

const SYSTEM_PROMPT = "You are a helpful AI assistant.";

export const chatRouter = {
  sendChat: publicProcedure
    .input(z.object({ content: z.string().min(1).max(10000) }))
    .mutation(async ({ ctx, input }) => {
      // 1. Save User Message
      await ctx.db
        .insert(Message)
        .values({ role: "user", content: input.content });

      // 2. Get Context (Last 10 messages)
      const history = await ctx.db
        .select()
        .from(Message)
        .orderBy(desc(Message.createdAt))
        .limit(10);

      // 3. Ask Gemini
      const { text } = await generateText({
        model: google("gemini-1.5-flash"),
        system: SYSTEM_PROMPT,
        messages: history.reverse().map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
      });

      // 4. Save AI Reply
      return await ctx.db
        .insert(Message)
        .values({ role: "assistant", content: text })
        .returning();
    }),

  getMessages: publicProcedure.query(({ ctx }) =>
    ctx.db.select().from(Message).orderBy(Message.createdAt),
  ),

  clearMessages: publicProcedure.mutation(({ ctx }) => ctx.db.delete(Message)),
} satisfies TRPCRouterRecord;

سجل الراوتر (Router) في packages/api/src/root.ts:

import { chatRouter } from "./router/chat";
import { createTRPCRouter } from "./trpc";

export const appRouter = createTRPCRouter({
  chat: chatRouter,
});

export type AppRouter = typeof appRouter;

انظر إلى هذا التدفق. إنه خطي، قابل للقراءة، ومكتوب بنوع محدد بالكامل (Fully Typed). إذا قمت بتغيير مخطط قاعدة البيانات، سيتحول هذا الكود إلى اللون الأحمر فوراً. لا مفاجآت وقت التشغيل.

لماذا استخدمنا .reverse()؟ نحن نستعلم عن الرسائل بترتيب تنازلي (الأحدث أولاً) ولكن نماذج LLM تتوقع ترتيباً زمنياً (الأقدم أولاً). إنها تفصيلة صغيرة تمنع المحادثات المربكة.

Modular Architecture Visualization

الخطوة 5: الواجهة

في apps/nextjs/src/app/chat/page.tsx، نقوم بربط كل شيء. لأننا نستخدم tRPC، نحصل على React Query مجاناً. useQuery تتولى الجلب، والتخزين المؤقت (Caching)، وحالات التحميل دون أن نكتب useEffect واحدة لجلب البيانات.

(لقد قمت بتضمين useEffect فقط للتمرير إلى الأسفل—لأن تجربة المستخدم UX مهمة).

"use client";

import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { RouterOutputs } from "@acme/api";
import { useTRPC } from "~/trpc/react";

export default function ChatPage() {
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const endRef = useRef<HTMLDivElement>(null);

  const trpc = useTRPC();
  const queryClient = useQueryClient();

  // Automatic data fetching with caching
  const { data: messages } = useQuery(trpc.chat.getMessages.queryOptions());

  // Mutation with optimistic updates
  const sendMsg = useMutation(
    trpc.chat.sendChat.mutationOptions({
      onSuccess: async () => {
        await queryClient.invalidateQueries(trpc.chat.pathFilter());
        setInput("");
        setLoading(false);
      },
      onError: (err) => {
        console.error("Failed:", err);
        setLoading(false);
      },
    }),
  );

  // Auto-scroll to latest message
  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || loading) return;
    setLoading(true);
    sendMsg.mutate({ content: input.trim() });
  };

  return (
    <div className="flex h-screen flex-col bg-gray-50">
      {/* Header */}
      <div className="border-b bg-white p-4">
        <h1 className="text-xl font-bold">AI Chat</h1>
      </div>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto p-4">
        <div className="mx-auto max-w-4xl space-y-4">
          {messages?.map((m: RouterOutputs["chat"]["getMessages"][number]) => (
            <div key={m.id} className={m.role === "user" ? "text-right" : ""}>
              <div
                className={`inline-block rounded-2xl px-4 py-3 ${
                  m.role === "user"
                    ? "bg-blue-500 text-white"
                    : "bg-white border shadow-sm"
                }`}
              >
                <p className="whitespace-pre-wrap">{m.content}</p>
              </div>
            </div>
          ))}
          {loading && (
            <div className="flex gap-2">
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.2s]" />
              <div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.4s]" />
            </div>
          )}
          <div ref={endRef} />
        </div>
      </div>

      {/* Input */}
      <form onSubmit={handleSubmit} className="border-t bg-white p-4">
        <div className="mx-auto flex max-w-4xl gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Type your message..."
            className="flex-1 rounded-lg border px-4 py-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={!input.trim() || loading}
            className="rounded-lg bg-blue-500 px-6 py-3 font-medium text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

لا تنسَ الصفحة الرئيسية. قم بتحديث apps/nextjs/src/app/page.tsx:

import Link from "next/link";

export default function HomePage() {
  return (
    <main className="flex min-h-screen items-center justify-center bg-gradient-to-b from-blue-500 to-blue-700">
      <div className="text-center text-white">
        <h1 className="text-5xl font-bold">AI Chatbot</h1>
        <p className="mt-4 text-xl">Built with T3 Turbo + Gemini</p>
        <Link
          href="/chat"
          className="mt-8 inline-block rounded-full bg-white px-10 py-3 font-semibold text-blue-600 hover:bg-gray-100 transition"
        >
          Start Chatting
        </Link>
      </div>
    </main>
  );
}

شغّل pnpm dev وقم بزيارة http://localhost:3000. انقر على "Start Chatting" وسيصبح لديك روبوت محادثة يعمل بالذكاء الاصطناعي.

سحر tRPC: لاحظ كيف أننا لم نكتب أبداً أي عملية جلب API؟ لا نداءات fetch()، لا روابط URL نصية، لا معالجة يدوية للأخطاء. TypeScript يعرف ما يتوقعه sendMsg.mutate(). إذا قمت بتغيير مخطط الإدخال في الخلفية، ستظهر لك الواجهة الأمامية خطأً برمجياً (Compile error). هذا هو المستقبل.

الخطوة 6: ضخ الروح (اختبار "الجو العام")

المساعد التقليدي ممل. المساعد التقليدي مصيره الحذف. جمال نماذج LLM يكمن في أنها ممتازة في لعب الأدوار.

لقد وجدت أن إعطاء الروبوت الخاص بك رأياً قوياً يجعله أكثر جاذبية بـ 10 أضعاف. لا تكتفِ بتلقينه "أنت مساعد مفيد". لقنه شخصية.

دعونا نعدل الخلفية لقبول شخصية معينة. قم بتحديث packages/api/src/router/chat.ts:

const PROMPTS = {
  default: "You are a helpful AI assistant. Be concise and clear.",
  luffy:
    "You are Monkey D. Luffy from One Piece. You're energetic, optimistic, love meat and adventure. You often say 'I'm gonna be King of the Pirates!' Speak simply and enthusiastically.",
  stark:
    "You are Tony Stark (Iron Man). You're a genius inventor, witty, and sarcastic. You love technology and often mention Stark Industries. Call people 'kid' or 'buddy'. Be charming but arrogant.",
};

export const chatRouter = {
  sendChat: publicProcedure
    .input(
      z.object({
        content: z.string().min(1).max(10000),
        character: z.enum(["default", "luffy", "stark"]).optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // Pick the personality
      const systemPrompt = PROMPTS[input.character || "default"];

      await ctx.db
        .insert(Message)
        .values({ role: "user", content: input.content });

      const history = await ctx.db
        .select()
        .from(Message)
        .orderBy(desc(Message.createdAt))
        .limit(10);

      const { text } = await generateText({
        model: google("gemini-1.5-flash"),
        system: systemPrompt, // ← Dynamic prompt
        messages: history.reverse().map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
      });

      return await ctx.db
        .insert(Message)
        .values({ role: "assistant", content: text })
        .returning();
    }),

  // ... rest stays the same
};

حدث الواجهة الأمامية لتمرير اختيار الشخصية:

// In ChatPage component, add state for character
const [character, setCharacter] = useState<"default" | "luffy" | "stark">("default");

// Update the mutation call
sendMsg.mutate({ content: input.trim(), character });

// Add a dropdown before the input:
<select
  value={character}
  onChange={(e) => setCharacter(e.target.value as any)}
  className="rounded-lg border px-3 py-2"
>
  <option value="default">🤖 Default</option>
  <option value="luffy">👒 Luffy</option>
  <option value="stark">🦾 Tony Stark</option>
</select>

الآن أنت لم تبنِ مجرد روبوت محادثة؛ لقد بنيت منصة تفاعل مع الشخصيات. هذا منتج حقيقي.

التفاصيل التقنية التي تهمك حقاً

لماذا لا نستخدم Prisma فقط؟

Prisma رائعة، لكن Drizzle أسرع. نحن نتحدث عن أداء استعلام أسرع بـ 2-3 مرات. عندما تكون مؤسساً منفرداً، كل مللي ثانية تتراكم. بالإضافة إلى ذلك، فإن بناء جملة Drizzle الشبيهة بـ SQL يعني عبئاً ذهنياً أقل.

ماذا عن الردود المتدفقة (Streaming)؟

تدعم Vercel AI SDK البث (Streaming) بشكل جاهز. استبدل generateText بـ streamText واستخدم خطاف useChat في الواجهة الأمامية. لقد تخطيت ذلك هنا لأن الطلب/الاستجابة (Request/Response) أبسط في الشرح التعليمي. ولكن في الإنتاج؟ قم ببث كل شيء. يدرك المستخدمون البث على أنه "أسرع" حتى عندما يكون الوقت الإجمالي هو نفسه.

إدارة نافذة السياق (Context Window)

حالياً نحن نلتقط آخر 10 رسائل. هذا يعمل حتى يتوقف عن العمل. إذا كنت تبني منتجاً جاداً، فقم بتنفيذ عداد للرموز (Token counter) واضبط التاريخ ديناميكياً. تحتوي AI SDK على أدوات مساعدة لهذا الغرض.

import { anthropic } from "@ai-sdk/anthropic";

const { text } = await generateText({
  model: anthropic("claude-3-5-sonnet-20241022"),
  maxTokens: 1000, // Control costs
  // ...
});

تجميع اتصالات قاعدة البيانات (Connection Pooling)

Postgres المحلي جيد للتطوير. للإنتاج، استخدم Vercel Postgres أو Supabase. إنهم يتعاملون مع تجميع الاتصالات تلقائياً. الجمع بين Serverless واتصالات قاعدة البيانات هو فخ—لا تدره بنفسك.

نصائح عملية

إذا كنت تقرأ هذا وتشعر برغبة ملحة في البرمجة، إليك نصيحتي:

  1. لا تبدأ من الصفر. الكود المعياري (Boilerplate) هو عدو الزخم. استخدم T3 Turbo أو سقالات برمجية مماثلة.
  2. أمان النوع (Type safety) يعني السرعة. يبدو الأمر أبطأ في الساعة الأولى، وأسرع للسنوات العشر القادمة. إنه يكتشف الأخطاء التي تحدث عادةً أثناء العرض التجريبي (Demo).
  3. السياق هو المفتاح. روبوت المحادثة بدون سجل هو مجرد شريط بحث فاخر. مرر دائماً الرسائل القليلة الماضية إلى LLM.
  4. الشخصية > الميزات. الروبوت الذي يبدو مثل توني ستارك سيحصل على تفاعل أكثر من روبوت عام بـ 10 ميزات إضافية.

الواقع الفوضوي

بناء هذا لم يكن رحلة سلسة بالكامل. لقد أفسدت في البداية سلسلة اتصال قاعدة البيانات وقضيت 20 دقيقة أتساءل لماذا يصرخ Drizzle في وجهي. كما أنني اصطدمت بحدود الاستخدام (Rate limit) على Gemini لأنني كنت أرسل الكثير من السجل في البداية (الدرس: ابدأ دائماً بـ .limit(5) ثم توسع).

الرسوم المتحركة للتحميل؟ استغرق الأمر مني ثلاث محاولات لضبطها بشكل صحيح لأن رسوم CSS المتحركة لا تزال، بطريقة ما، سحراً أسود في عام 2024.

ولكن إليك الأمر: لأنني كنت أستخدم مكدساً قوياً، كانت تلك مشاكل منطقية، وليست مشاكل هيكلية. الأساس ظل ثابتاً. لم أضطر أبداً إلى إعادة هيكلة الـ API بالكامل لأنني اخترت التجريد (Abstraction) الخاطئ.

أطلقه (Ship It)

نحن نعيش في العصر الذهبي للبناء. الأدوات قوية، والذكاء الاصطناعي ذكي، وحاجز الدخول لم يكن منخفضاً بهذا القدر من قبل.

لديك الكود الآن. لديك المكدس التقني. وأنت تفهم المقايضات.

اذهب وابنِ شيئاً لا ينبغي أن يكون موجوداً، وأطلقه قبل العشاء.

إجمالي وقت البناء: ~ساعتان سطور الكود الفعلي المكتوبة: ~200 الأخطاء التي واجهتها في الإنتاج: 0 (حتى الآن)

مزيج T3 stack + Gemini ليس سريعاً فحسب—إنه ممل بأفضل طريقة ممكنة. لا مفاجآت. لا عبارة "إنه يعمل على جهازي". مجرد بناء.

برمجة سعيدة.


الموارد:

الكود الكامل: github.com/giftedunicorn/my-chatbot

شارك هذا

Feng Liu

Feng Liu

shenjian8628@gmail.com

الجزء الأول: كيف تبني Chatbot باستخدام T3 Turbo و Gemini | Feng Liu