İçeriğe geç
Sedat Demir
Geri dön

assistant-ui: shadcn/ui Felsefesiyle Composable AI Chat Bileşenleri Geliştirin

assistant-ui: shadcn/ui Felsefesiyle Composable AI Chat Bileşenleri Geliştirin

Modern web uygulamalarında AI destekli chat arayüzleri artık bir lüks değil, bir gereklilik haline geldi. Ancak çoğu geliştirici bu arayüzleri oluştururken iki zor seçenekle karşı karşıya kalıyor: ya sıfırdan her şeyi yazacaksınız ya da opinionated bir kütüphanenin kısıtlamalarına boyun eğeceksiniz. İşte assistant-ui tam bu noktada devreye girerek üçüncü bir yol sunuyor.

shadcn/ui Felsefesi Nedir ve Neden Önemli?

shadcn/ui, React ekosisteminde bir paradigma değişikliği yarattı. Geleneksel component library'lerin aksine, shadcn/ui bileşenleri node_modules içinde gizli kalmaz — doğrudan projenize kopyalanır. Bu yaklaşımın temel prensipleri şunlardır:

assistant-ui, bu felsefeyi AI chat bileşenlerine birebir uyguluyor. Bir npm paketi olarak kurulmasına rağmen, bileşenlerini shadcn/ui CLI ile doğrudan projenize çekebiliyor ve tam kontrol sizde kalıyor.

assistant-ui Nedir?

assistant-ui, React için tasarlanmış, composable yapıda AI chat bileşenleri sunan açık kaynaklı bir projedir. OpenAI, Anthropic, Vercel AI SDK ve daha birçok provider ile uyumlu çalışır. Temel özellikleri:

Kurulum ve İlk Yapılandırma

Öncelikle projenizde shadcn/ui kurulu olmalıdır. Ardından assistant-ui'yi ekleyelim:

# Temel paketleri kurun
npx shadcn@latest init
npx assistant-ui@latest init

# AI SDK entegrasyonu için
npm install @assistant-ui/react @assistant-ui/react-ai-sdk ai @ai-sdk/openai

npx assistant-ui@latest init komutu, shadcn/ui'nin CLI mantığıyla çalışır. Bileşen dosyalarını doğrudan components/ui/assistant-ui/ dizinine kopyalar. Bu dosyalar artık sizindir — istediğiniz gibi özelleştirebilirsiniz.

Temel Mimari: Runtime Kavramı

assistant-ui'nin kalbinde runtime kavramı bulunur. Runtime, AI backend'inizle iletişimi soyutlayan katmandır. En yaygın kullanım Vercel AI SDK ile birliktedir:

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
    system: "Sen yardımcı bir asistansın. Türkçe yanıt ver.",
  });

  return result.toDataStreamResponse();
}

Client tarafında ise runtime'ı oluşturup provider'a bağlıyoruz:

// app/page.tsx
"use client";

import { useChat } from "ai/react";
import { useVercelAIChatRuntime } from "@assistant-ui/react-ai-sdk";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { MyAssistant } from "@/components/my-assistant";

export default function ChatPage() {
  const chat = useChat({
    api: "/api/chat",
  });

  const runtime = useVercelAIChatRuntime(chat);

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <MyAssistant />
    </AssistantRuntimeProvider>
  );
}

Bu yapıda AssistantRuntimeProvider, tüm alt bileşenlere chat state'ini ve aksiyonlarını context üzerinden sağlar. Runtime değiştirmek istediğinizde (örneğin OpenAI'dan Anthropic'e geçiş) sadece bu katmanı değiştirmeniz yeterlidir.

Composable Chat Bileşeni Oluşturma

İşte assistant-ui'nin gerçek gücü burada ortaya çıkıyor. Chat arayüzünüzü küçük, bağımsız parçalardan oluşturuyorsunuz:

// components/my-assistant.tsx
"use client";

import { Thread } from "@assistant-ui/react";

export function MyAssistant() {
  return (
    <Thread.Root className="flex flex-col h-[600px] w-full max-w-2xl mx-auto">
      <Thread.Viewport className="flex-1 overflow-y-auto p-4 space-y-4">
        <Thread.Messages
          components={{
            UserMessage: CustomUserMessage,
            AssistantMessage: CustomAssistantMessage,
          }}
        />
        <Thread.FollowupSuggestions />
        <Thread.ViewportFooter>
          <Thread.ScrollToBottom />
        </Thread.ViewportFooter>
      </Thread.Viewport>

      <CustomComposer />
    </Thread.Root>
  );
}

Her bir parça kendi bileşenidir ve bağımsız olarak özelleştirilebilir. Şimdi bu alt bileşenleri tanımlayalım:

Özel Mesaj Bileşenleri

// components/custom-user-message.tsx
import { MessagePrimitive } from "@assistant-ui/react";

export function CustomUserMessage() {
  return (
    <MessagePrimitive.Root className="flex justify-end">
      <div className="bg-blue-600 text-white rounded-2xl rounded-br-sm px-4 py-2 max-w-[80%]">
        <MessagePrimitive.Content />
      </div>
    </MessagePrimitive.Root>
  );
}

// components/custom-assistant-message.tsx
import { MessagePrimitive, BranchPicker } from "@assistant-ui/react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { MarkdownContent } from "./markdown-content";

export function CustomAssistantMessage() {
  return (
    <MessagePrimitive.Root className="flex gap-3 items-start">
      <Avatar className="h-8 w-8 shrink-0">
        <AvatarFallback className="bg-gradient-to-br from-purple-500 to-pink-500 text-white text-xs">
          AI
        </AvatarFallback>
      </Avatar>

      <div className="flex flex-col gap-1 min-w-0">
        <div className="bg-muted rounded-2xl rounded-tl-sm px-4 py-2">
          <MessagePrimitive.Content
            components={{
              Text: MarkdownContent,
            }}
          />
        </div>

        <div className="flex items-center gap-2">
          <BranchPicker.Root className="flex items-center gap-1 text-xs text-muted-foreground">
            <BranchPicker.Previous className="hover:text-foreground cursor-pointer" />
            <BranchPicker.Count />
            <BranchPicker.Next className="hover:text-foreground cursor-pointer" />
          </BranchPicker.Root>

          <MessagePrimitive.If lastOrHover>
            <CopyButton />
            <RegenerateButton />
          </MessagePrimitive.If>
        </div>
      </div>
    </MessagePrimitive.Root>
  );
}

Dikkat edin: MessagePrimitive.If bileşeni, koşullu render için deklaratif bir API sunuyor. lastOrHover prop'u sayesinde aksiyon butonları yalnızca son mesajda veya hover durumunda görünür.

Özel Composer (Input Alanı)

// components/custom-composer.tsx
import { ComposerPrimitive } from "@assistant-ui/react";
import { SendHorizontal, Paperclip } from "lucide-react";
import { Button } from "@/components/ui/button";

export function CustomComposer() {
  return (
    <ComposerPrimitive.Root className="flex items-end gap-2 border-t p-4">
      <Button variant="ghost" size="icon" className="shrink-0">
        <Paperclip className="h-4 w-4" />
      </Button>

      <ComposerPrimitive.Input
        placeholder="Mesajınızı yazın..."
        className="flex-1 min-h-[40px] max-h-[200px] resize-none bg-muted rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
        autoFocus
      />

      <ComposerPrimitive.Send asChild>
        <Button size="icon" className="shrink-0 rounded-xl">
          <SendHorizontal className="h-4 w-4" />
        </Button>
      </ComposerPrimitive.Send>
    </ComposerPrimitive.Root>
  );
}

ComposerPrimitive.Send bileşeni, asChild pattern'ini kullanarak kendi butonunuzu gönder işlevi ile donatmanızı sağlar. Bu, Radix UI'dan miras alınan ve shadcn/ui ekosisteminde standart olan bir yaklaşımdır.

İleri Düzey: Tool Calling UI

Modern AI uygulamalarının en güçlü özelliklerinden biri tool calling'dir. assistant-ui, tool çağrılarını görselleştirmek için yerleşik destek sunar:

// components/weather-tool.tsx
import { makeAssistantToolUI } from "@assistant-ui/react";

type WeatherArgs = {
  city: string;
  unit?: "celsius" | "fahrenheit";
};

type WeatherResult = {
  temperature: number;
  condition: string;
};

export const WeatherToolUI = makeAssistantToolUI<WeatherArgs, WeatherResult>({
  toolName: "get_weather",
  render: ({ args, result, status }) => {
    if (status === "running" || status === "requires-action") {
      return (
        <div className="flex items-center gap-2 p-3 bg-muted rounded-lg animate-pulse">
          <span className="text-sm">🌤️ {args.city} hava durumu alınıyor...</span>
        </div>
      );
    }

    if (!result) return null;

    return (
      <div className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950 dark:to-cyan-950 rounded-lg border">
        <div className="flex items-center justify-between">
          <div>
            <h4 className="font-semibold">{args.city}</h4>
            <p className="text-sm text-muted-foreground">{result.condition}</p>
          </div>
          <span className="text-3xl font-bold">
            {result.temperature}°{args.unit === "fahrenheit" ? "F" : "C"}
          </span>
        </div>
      </div>
    );
  },
});

Bu tool UI'ını kullanmak için AssistantRuntimeProvider altına eklemeniz yeterlidir:

<AssistantRuntimeProvider runtime={runtime}>
  <WeatherToolUI />
  <MyAssistant />
</AssistantRuntimeProvider>

Primitive API Desenini Anlamak

assistant-ui'nin API tasarımı katmanlı bir yapıya sahiptir:

Katman Kullanım Esneklik
Thread (üst düzey) Hızlı başlangıç, hazır layout Düşük
Primitives (alt düzey) Tam özelleştirme, pixel-perfect UI Yüksek
Hooks (en alt düzey) Headless, kendi bileşenleriniz Maksimum

Hooks katmanı ile tamamen kendi UI'ınızı yazabilirsiniz:

import {
  useThreadMessages,
  useComposer,
  useThreadRuntime,
} from "@assistant-ui/react";

export function TotallyCustomChat() {
  const messages = useThreadMessages();
  const composer = useComposer();
  const threadRuntime = useThreadRuntime();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    composer.send();
  };

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i} data-role={msg.role}>
          {msg.role === "assistant"
            ? msg.content
                .filter((c) => c.type === "text")
                .map((c, j) => <p key={j}>{c.text}</p>)
            : msg.content
                .filter((c) => c.type === "text")
                .map((c, j) => <p key={j}>{c.text}</p>)}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={composer.text}
          onChange={(e) => composer.setText(e.target.value)}
          placeholder="Mesaj yazın..."
        />
        <button type="submit">Gönder</button>
      </form>
    </div>
  );
}

Gerçek Dünya Senaryosu: Çoklu Asistan Desteği

Bir SaaS uygulamasında farklı sayfalar için farklı asistanlar gerekebilir. assistant-ui'nin runtime yapısı bunu kolaylaştırır:

// hooks/use-support-runtime.ts
import { useChat } from "ai/react";
import { useVercelAIChatRuntime } from "@assistant-ui/react-ai-sdk";

export function useSupportRuntime() {
  const chat = useChat({
    api: "/api/chat/support",
    body: { context: "customer-support" },
  });
  return useVercelAIChatRuntime(chat);
}

// hooks/use-code-runtime.ts
export function useCodeRuntime() {
  const chat = useChat({
    api: "/api/chat/code",
    body: { context: "code-assistant" },
  });
  return useVercelAIChatRuntime(chat);
}

Her biri kendi runtime'ı ile bağımsız çalışan birden fazla chat penceresi açabilirsiniz. Arayüz bileşenleri ise tamamen paylaşılır — DRY prensibini asla ihlal etmezsiniz.

Performans ve Erişilebilirlik

assistant-ui, performans konusunda birkaç akıllı optimizasyon uygular:

Sonuç

assistant-ui, AI chat arayüzleri için shadcn/ui'nin kanıtladığı bir gerçeği yeniden doğruluyor: en iyi component library, kodunu size veren library'dir.

Bu yaklaşımın avantajlarını özetleyelim:

  1. Tam kontrol: Her pixel sizin kararınız, vendor lock-in yok
  2. Composable yapı: Küçük parçaları birleştirerek istediğiniz karmaşıklıkta UI oluşturun
  3. Runtime agnostik: Backend değişikliği UI'ı etkilemez
  4. Ekosistem uyumu: shadcn/ui, Tailwind CSS, Radix UI ile doğal entegrasyon
  5. Üretim kalitesi: Streaming, branching, tool calling gibi ileri düzey özellikler hazır

Eğer bir AI chat arayüzü geliştiriyorsanız ve "bu kütüphane şunu desteklemiyor" derdinden bıktıysanız, assistant-ui tam size göre. shadcn/ui'nin bize öğrettiği dersi hatırlayın: bileşenler kopyalanır, sahiplenilir ve projeye göre evrilir. AI chat bileşenleri de bundan farklı olmamalı.

Projeyi GitHub üzerinden inceleyebilir ve dokümantasyona assistant-ui.com adresinden ulaşabilirsiniz.


Share this post on:

Sonraki Yazı
Generative UI Pattern: AI'nın React Bileşeni Döndürmesi