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

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

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

Yapay zeka ile etkileşimimiz yıllardır aynı kalıp üzerine kuruluydu: kullanıcı bir metin gönderir, AI bir metin döndürür. ChatGPT'den bu yana milyarlarca konuşma bu şekilde gerçekleşti. Peki ya AI size düz metin yerine tam teşekküllü, etkileşimli bir React bileşeni döndürseydi? İşte Generative UI pattern'i tam olarak bunu yapıyor.

Bu yazıda, Generative UI kavramını temelden ele alacak, neden geleneksel chat arayüzlerinin ötesine geçmemiz gerektiğini tartışacak ve Vercel AI SDK ile gerçek dünya uygulamaları inşa edeceğiz.


Generative UI Nedir?

Generative UI, bir AI modelinin kullanıcı isteğine yanıt olarak düz metin yerine yapılandırılmış UI bileşenleri ürettiği bir tasarım kalıbıdır. Geleneksel yaklaşımda AI'dan gelen yanıt bir string'dir ve siz bu string'i Markdown olarak render edersiniz. Generative UI'da ise AI, hangi bileşenin gösterileceğine karar verir ve o bileşeni uygun verilerle birlikte döndürür.

Geleneksel Yaklaşım vs. Generative UI

Geleneksel yaklaşım:

Kullanıcı: "İstanbul'a yarın uçuş göster"
AI: "İstanbul'a yarın için şu uçuşlar var: THY 101 - 08:00 - 450₺, Pegasus 202 - 10:30 - 320₺..."

Generative UI yaklaşımı:

Kullanıcı: "İstanbul'a yarın uçuş göster"
AI: <FlightCard flights={[{airline: "THY", flight: "101", time: "08:00", price: 450}, ...]} onBook={handleBook} />

İkinci yaklaşımda kullanıcı, fiyatları karşılaştırabilir, filtreleme yapabilir ve doğrudan "Satın Al" butonuna tıklayabilir. Metin yerine etkileşimli bir deneyim sunulmuş olur.


Neden Generative UI'a İhtiyacımız Var?

  1. Zengin etkileşim: Kullanıcılar grafikler, tablolar, formlar ve butonlarla doğrudan etkileşime geçebilir.
  2. Azaltılmış bilişsel yük: Uzun metin paragrafları yerine görsel olarak organize edilmiş veri sunulur.
  3. Aksiyon odaklı deneyim: Kullanıcılar AI yanıtı içinden doğrudan işlem yapabilir (satın alma, rezervasyon, onaylama).
  4. Bağlamsal uygunluk: AI, kullanıcının niyetine göre en uygun UI bileşenini seçebilir.
  5. Tutarlı tasarım: Önceden tasarlanmış bileşenler kullanıldığı için marka tutarlılığı korunur.

Mimari: Nasıl Çalışıyor?

Generative UI'ın arkasındaki mimari üç temel katmandan oluşur:

┌─────────────────────────────────────────┐
│  1. Kullanıcı Girişi                    │
│     "Hava durumu nasıl?"                │
├─────────────────────────────────────────┤
│  2. AI Modeli (Tool Calling)            │
│     → getWeather tool'unu çağır         │
│     → Bileşen seçimini yap             │
├─────────────────────────────────────────┤
│  3. React Bileşeni Render               │
│     → <WeatherCard city="Istanbul"      │
│         temp={24} condition="sunny" />  │
└─────────────────────────────────────────┘

Anahtar mekanizma tool calling (fonksiyon çağrısı) özelliğidir. AI modeli, kullanıcının niyetini anlar, uygun tool'u çağırır ve bu tool bir React bileşeni döndürür. Bu süreç sunucu tarafında gerçekleştiği için React Server Components ile mükemmel uyum sağlar.


Vercel AI SDK ile Generative UI Uygulaması

Vercel AI SDK, Generative UI pattern'ini birinci sınıf bir özellik olarak destekler. streamUI fonksiyonu ile AI yanıtlarını doğrudan React bileşenlerine dönüştürebilirsiniz.

Proje Kurulumu

npx create-next-app@latest generative-ui-demo --typescript --tailwind --app
cd generative-ui-demo
npm install ai @ai-sdk/openai zod

Tool Tanımlamaları ve Server Action

İlk olarak, AI'nın çağırabileceği tool'ları ve bunların döndüreceği bileşenleri tanımlayalım:

// app/actions.tsx
'use server';

import { createStreamableUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { streamUI } from 'ai/rsc';
import { z } from 'zod';

// UI Bileşenleri
function WeatherCard({ city, temp, condition, humidity }: {
  city: string;
  temp: number;
  condition: string;
  humidity: number;
}) {
  return (
    <div className="rounded-2xl bg-gradient-to-br from-blue-500 to-cyan-400 p-6 text-white shadow-lg max-w-sm">
      <h3 className="text-lg font-semibold">{city}</h3>
      <div className="mt-2 flex items-center gap-4">
        <span className="text-5xl font-bold">{temp}°C</span>
        <div className="text-sm opacity-90">
          <p>{condition}</p>
          <p>Nem: %{humidity}</p>
        </div>
      </div>
    </div>
  );
}

function StockCard({ symbol, price, change, changePercent }: {
  symbol: string;
  price: number;
  change: number;
  changePercent: number;
}) {
  const isPositive = change >= 0;
  return (
    <div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-md max-w-sm">
      <div className="flex items-center justify-between">
        <h3 className="text-xl font-bold text-gray-900">{symbol}</h3>
        <span className={`text-sm font-medium px-2 py-1 rounded-full ${
          isPositive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
        }`}>
          {isPositive ? '+' : ''}{changePercent.toFixed(2)}%
        </span>
      </div>
      <p className="mt-2 text-3xl font-bold text-gray-800">${price.toFixed(2)}</p>
      <p className={`mt-1 text-sm ${isPositive ? 'text-green-600' : 'text-red-600'}`}>
        {isPositive ? '▲' : '▼'} {Math.abs(change).toFixed(2)} bugün
      </p>
    </div>
  );
}

function FlightResults({ flights }: {
  flights: Array<{
    airline: string;
    flightNo: string;
    departure: string;
    arrival: string;
    price: number;
  }>;
}) {
  return (
    <div className="space-y-3 max-w-lg">
      <h3 className="text-lg font-semibold text-gray-800">Uygun Uçuşlar</h3>
      {flights.map((flight, i) => (
        <div key={i} className="flex items-center justify-between rounded-xl border p-4 hover:bg-gray-50 transition">
          <div>
            <p className="font-medium">{flight.airline} - {flight.flightNo}</p>
            <p className="text-sm text-gray-500">{flight.departure} → {flight.arrival}</p>
          </div>
          <div className="text-right">
            <p className="text-lg font-bold text-blue-600">{flight.price}₺</p>
            <button className="mt-1 text-xs bg-blue-600 text-white px-3 py-1 rounded-full hover:bg-blue-700">
              Rezerve Et
            </button>
          </div>
        </div>
      ))}
    </div>
  );
}

// Ana AI fonksiyonu
export async function sendMessage(userMessage: string) {
  const result = await streamUI({
    model: openai('gpt-4o'),
    system: `Sen yardımcı bir asistansın. Kullanıcının isteğine göre uygun tool'ları kullan. 
    Hava durumu sorulduğunda getWeather, hisse senedi sorulduğunda getStock, 
    uçuş sorulduğunda searchFlights tool'unu çağır.`,
    prompt: userMessage,
    text: ({ content }) => <div className="prose">{content}</div>,
    tools: {
      getWeather: {
        description: 'Bir şehir için hava durumu bilgisi getir',
        parameters: z.object({
          city: z.string().describe('Şehir adı'),
        }),
        generate: async function* ({ city }) {
          yield <div className="animate-pulse text-gray-400">Hava durumu yükleniyor...</div>;

          // Gerçek uygulamada API çağrısı yapılır
          const weatherData = {
            city,
            temp: Math.round(Math.random() * 30 + 5),
            condition: ['Güneşli', 'Bulutlu', 'Yağmurlu', 'Parçalı Bulutlu'][Math.floor(Math.random() * 4)],
            humidity: Math.round(Math.random() * 60 + 30),
          };

          return <WeatherCard {...weatherData} />;
        },
      },
      getStock: {
        description: 'Bir hisse senedinin güncel fiyat bilgisini getir',
        parameters: z.object({
          symbol: z.string().describe('Hisse senedi sembolü (örn: AAPL, TSLA)'),
        }),
        generate: async function* ({ symbol }) {
          yield <div className="animate-pulse text-gray-400">{symbol} verisi yükleniyor...</div>;

          const price = Math.random() * 500 + 50;
          const change = (Math.random() - 0.5) * 20;

          return (
            <StockCard
              symbol={symbol.toUpperCase()}
              price={price}
              change={change}
              changePercent={(change / price) * 100}
            />
          );
        },
      },
      searchFlights: {
        description: 'Uçuş ara',
        parameters: z.object({
          destination: z.string().describe('Hedef şehir'),
          date: z.string().describe('Uçuş tarihi'),
        }),
        generate: async function* ({ destination, date }) {
          yield <div className="animate-pulse text-gray-400">Uçuşlar aranıyor...</div>;

          const flights = [
            { airline: 'THY', flightNo: 'TK101', departure: '08:00', arrival: '10:15', price: 1450 },
            { airline: 'Pegasus', flightNo: 'PC202', departure: '10:30', arrival: '12:45', price: 890 },
            { airline: 'AnadoluJet', flightNo: 'AJ305', departure: '14:00', arrival: '16:20', price: 720 },
          ];

          return <FlightResults flights={flights} />;
        },
      },
    },
  });

  return result.value;
}

Client Bileşeni

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

import { useState, ReactNode } from 'react';
import { sendMessage } from './actions';

interface Message {
  role: 'user' | 'assistant';
  content: string | ReactNode;
}

export default function ChatPage() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || isLoading) return;

    const userMessage = input;
    setInput('');
    setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
    setIsLoading(true);

    try {
      const response = await sendMessage(userMessage);
      setMessages(prev => [...prev, { role: 'assistant', content: response }]);
    } catch (error) {
      console.error('Hata:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <main className="mx-auto max-w-2xl px-4 py-8">
      <h1 className="text-2xl font-bold mb-6">Generative UI Chat</h1>

      <div className="space-y-4 mb-6">
        {messages.map((msg, i) => (
          <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
            <div className={`max-w-[80%] ${
              msg.role === 'user'
                ? 'bg-blue-600 text-white rounded-2xl rounded-br-md px-4 py-2'
                : ''
            }`}>
              {msg.content}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="animate-pulse text-gray-400">Düşünüyorum...</div>
          </div>
        )}
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="'İstanbul hava durumu' veya 'AAPL hisse fiyatı' deneyin..."
          className="flex-1 rounded-xl border border-gray-300 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          type="submit"
          disabled={isLoading}
          className="rounded-xl bg-blue-600 px-6 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
        >
          Gönder
        </button>
      </form>
    </main>
  );
}

Streaming ve Ara Durumlar (Loading States)

Generative UI'ın en güçlü yanlarından biri streaming desteğidir. Yukarıdaki kodda generate fonksiyonlarının async function* (generator function) olarak tanımlandığına dikkat edin. yield ile ara durumları, return ile nihai bileşeni döndürürüz:

generate: async function* ({ city }) {
  // 1. İlk olarak loading state göster
  yield <div className="animate-pulse">Yükleniyor...</div>;

  // 2. API çağrısı yap
  const data = await fetchWeatherAPI(city);

  // 3. İsterseniz ara bir durum daha gösterin
  yield <div className="text-sm text-gray-500">Veriler işleniyor...</div>;

  // 4. Nihai bileşeni döndür
  return <WeatherCard {...data} />;
},

Bu sayede kullanıcı, AI yanıtının oluşma sürecini adım adım takip eder. UX açısından bu, düz metin streaming'den çok daha zengin bir deneyimdir.


İç İçe Tool Çağrıları ve Kompozisyon

Daha gelişmiş senaryolarda AI birden fazla tool'u ardışık olarak çağırabilir. Örneğin, "İstanbul'a yarın uçuş bul ve oranın hava durumunu göster" gibi bir istek hem searchFlights hem de getWeather tool'larını tetikleyebilir:

tools: {
  travelPlanner: {
    description: 'Seyahat planı oluştur: uçuş ve hava durumu bilgisi birlikte',
    parameters: z.object({
      destination: z.string(),
      date: z.string(),
    }),
    generate: async function* ({ destination, date }) {
      yield <div className="animate-pulse">Seyahat planınız hazırlanıyor...</div>;

      const [flights, weather] = await Promise.all([
        fetchFlights(destination, date),
        fetchWeather(destination),
      ]);

      return (
        <div className="space-y-4">
          <WeatherCard {...weather} />
          <FlightResults flights={flights} />
          <div className="rounded-xl bg-yellow-50 border border-yellow-200 p-4">
            <p className="text-sm text-yellow-800">
              💡 {weather.condition === 'Yağmurlu' 
                ? 'Şemsiyenizi almayı unutmayın!' 
                : 'Hava güzel görünüyor, iyi yolculuklar!'}
            </p>
          </div>
        </div>
      );
    },
  },
}

Güvenlik Dikkat Noktaları

Generative UI uygularken güvenlik kritik öneme sahiptir:

  1. Önceden tanımlı bileşenler kullanın: AI'nın rastgele JSX üretmesine asla izin vermeyin. Sadece siz tanımladığınız bileşen setinden seçim yapmasını sağlayın.
  2. Parametre validasyonu: Zod şemaları ile tool parametrelerini mutlaka doğrulayın.
  3. Server-side rendering: Tool fonksiyonları sunucu tarafında çalışır. Hassas API anahtarları istemciye sızmaz.
  4. Rate limiting: AI çağrılarını sınırlayarak kötüye kullanımı önleyin.
// ❌ YANLIŞ: AI'nın ham kod üretmesine izin vermek
const component = eval(aiResponse); // Asla yapmayın!

// ✅ DOĞRU: Önceden tanımlı bileşen haritası
const componentMap = {
  weather: WeatherCard,
  stock: StockCard,
  flight: FlightResults,
};

Generative UI vs. Geleneksel Chat: Ne Zaman Hangisi?

Kriter Geleneksel Chat Generative UI
Basit soru-cevap ✅ Yeterli Gereksiz karmaşıklık
Veri görselleştirme ❌ Markdown tabloları ✅ İnteraktif grafikler
Aksiyon tetikleme ❌ Metin içi link ✅ Doğrudan butonlar
Geliştirme maliyeti Düşük Orta-yüksek
Kullanıcı deneyimi Temel Zengin

Generative UI, her durumda doğru seçim değildir. Basit bilgi alışverişi için geleneksel metin yanıtları yeterlidir. Ancak kullanıcının harekete geçmesi, veriyi karşılaştırması veya karmaşık bilgiyi hızla kavraması gereken durumlarda Generative UI büyük fark yaratır.


Gelecek: Nereye Gidiyoruz?

Generative UI pattern'i hâlâ erken aşamalarında. Önümüzdeki dönemde şunları görebiliriz:


Sonuç

Generative UI, AI uygulamalarında bir paradigma değişimini temsil ediyor. Düz metin yerine etkileşimli React bileşenleri döndürmek, kullanıcı deneyimini köklü biçimde iyileştiriyor. Vercel AI SDK'nın streamUI fonksiyonu, tool calling mekanizması ve React Server Components ile bu pattern'i uygulamak artık oldukça erişilebilir.

Temel çıkarımlar:

Bu pattern'i kendi projelerinizde denemeye başlamak için Vercel AI SDK dokümantasyonunu inceleyebilir, küçük bir bileşen seti ile başlayıp kullanıcı geri bildirimlerine göre genişletebilirsiniz. AI artık sadece düşünmekle kalmıyor — arayüz de tasarlıyor.


Share this post on:

Sonraki Yazı
Vercel AI SDK useChat Hook ile React'te Streaming AI Yanıtları: Kapsamlı Rehber