feat(dev): /dev/sohbet — danışman sohbeti arayüzü senaryolarla oynatılır
All checks were successful
Deploy / deploy (push) Successful in 6m43s

Model, oturum ve kredi harcamadan sohbet arayüzünü denemek için yalnız
geliştirmede açılan sayfa (üretimde 404, noindex). Beş hazır senaryo: tek
araç, art arda araçlar, düzeltme notu, akış hatası, uzun geçmiş.

- SohbetClient'a denemeTransport prop'u: verilirse /api/soru yerine
  önceden yazılmış akış oynar; üretim yüzeyleri bunu vermez.
- @shadcn/helpers/ai-sdk sahte taşıyıcı için eklendi; dinamik import
  NODE_ENV dalında kaldığından üretim paketine girmez.
- cagri.ts: kullanılmayan OpenAI tip import'u kaldırıldı.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 15:15:01 +03:00
parent 833de7426a
commit ea3728a7ec
7 changed files with 270 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, type UIMessage } from "ai";
import { DefaultChatTransport, type ChatTransport, type UIMessage } from "ai";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
@@ -227,6 +227,7 @@ export function SohbetClient({
ilkYorum,
bildikleri,
initialProfil,
denemeTransport,
}: {
baslangicKredi: number;
/** Sunucu tarafında çekilmiş danışman profili; verilirse mount fetch'i atlanır */
@@ -244,6 +245,9 @@ export function SohbetClient({
/** Danışmanın prompt'unda gerçekten yer alan bağlam maddeleri; başlıktaki
* "Danışmanın bildikleri" ipucunda listelenir. Boşsa ipucu çizilmez. */
bildikleri?: string[];
/** Yalnız /dev/sohbet: /api/soru yerine önceden yazılmış senaryoyu oynatır
* (model, oturum ve kredi yok). Üretim yüzeyleri bunu vermez. */
denemeTransport?: ChatTransport<UIMessage>;
}) {
const [girdi, setGirdi] = useState("");
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
@@ -270,7 +274,7 @@ export function SohbetClient({
if (krediDustu) setKredi((k) => k - 1);
}),
);
const { transport } = kanal;
const transport = denemeTransport ?? kanal.transport;
const {
messages: mesajlar,

View File

@@ -0,0 +1,142 @@
import { createChat } from "@shadcn/helpers/ai-sdk";
import type { ChatTransport, UIMessage } from "ai";
import { JEV_DUZELTME_NOTU } from "@/lib/ai/jev";
import type { Mesaj } from "./sohbet-client";
// /dev/sohbet senaryoları (sohbet-senaryolari.tsx). React içermez: betikler de
// import edip akışı tarayıcısız oynatabilir. Araç adları ve çıktıları gerçek
// araçların biçimindedir (src/lib/ai/araclar.ts); yeni araç eklerken oradaki
// adı kullan ki "…bakıyorum" satırı doğru etiketle çıksın.
export type Senaryo = {
ad: string;
aciklama: string;
kredi: number;
gecmis?: Mesaj[];
transport: () => ChatTransport<UIMessage>;
};
const ARAC_BEKLEME = 1400;
export const SENARYOLAR: Senaryo[] = [
{
ad: "Tek araç",
aciklama: "Bir arama, sonra kısa cevap.",
kredi: 12,
transport: () =>
createChat()
.user("İzmir'de devlet üniversitesinde bilgisayar mühendisliği var mı?")
.sleep(600)
.assistant(({ writer }) => {
writer
.tool("program_ara", {
input: { arama: "bilgisayar mühendisliği", il: "İzmir", tur: "devlet" },
})
.sleep(ARAC_BEKLEME)
.output({ toplamEslesen: 5, gosterilen: 5 });
writer.text(
"İzmir'de devlet üniversitelerinde **5 bilgisayar mühendisliği programı** var. Sıralamana en yakın olanlar:\n\n" +
"1. Ege Üniversitesi — taban sırası 44.600 (Az riskli)\n" +
"2. Dokuz Eylül Üniversitesi — taban sırası 52.100 (Az riskli)\n" +
"3. İzmir Katip Çelebi Üniversitesi — taban sırası 71.800 (Güvenli)\n\n" +
"Taban sıraları her yıl değişir.",
);
})
.transport({ delayMs: 25 }),
},
{
ad: "Art arda araçlar",
aciklama: "Arama, karşılaştırma ve istihdam; araya metin girer.",
kredi: 12,
transport: () =>
createChat()
.user("Ege ile Dokuz Eylül bilgisayar mühendisliğini karşılaştırır mısın?")
.sleep(600)
.assistant(({ writer }) => {
writer.text("İki programı bulup yan yana koyuyorum.");
writer
.tool("program_ara", { input: { arama: "bilgisayar mühendisliği", universite: "Ege" } })
.sleep(900)
.output({ toplamEslesen: 1 });
writer
.tool("program_ara", { input: { arama: "bilgisayar mühendisliği", universite: "Dokuz Eylül" } })
.sleep(900)
.output({ toplamEslesen: 1 });
writer.stepStart();
writer
.tool("program_karsilastir", { input: { program_idler: ["103110581", "102710405"] } })
.sleep(ARAC_BEKLEME)
.output({ programlar: [] });
writer
.tool("bolum_istihdam", { input: { bolumler: ["Bilgisayar Mühendisliği"] } })
.sleep(ARAC_BEKLEME)
.output({ bolumler: [] });
writer.stepStart();
writer.text(
"| | Ege | Dokuz Eylül |\n|---|---|---|\n| Taban sırası | 44.600 | 52.100 |\n| Kontenjan | 120 | 90 |\n| Dil | İngilizce | İngilizce |\n| Akreditasyon | — | MÜDEK |\n\n" +
"İkisi de senin için **Az riskli**. Mezun istihdamı bölüm bazında aynı: TÜİK 2025'e göre mezunların %80,8'i kayıtlı çalışıyor (Türkiye ortalaması %73,9); bu sayı üniversiteye göre ayrışmıyor.",
);
})
.transport({ delayMs: 20 }),
},
{
ad: "Düzeltme notu",
aciklama: "Cevap garanti dili içerdi; çıktı denetimi notu eklendi.",
kredi: 12,
transport: () =>
createChat()
.user("Bu sırayla Ege'ye girer miyim?")
.sleep(600)
.assistant(({ writer }) => {
writer.text("Geçen yılın tabanına göre 8 bin sıra öndesin, bu bölüm sende kesin tutar.");
writer.sleep(500);
writer.text(JEV_DUZELTME_NOTU, { mode: "instant" });
})
.transport({ delayMs: 25 }),
},
{
ad: "Akış hatası",
aciklama: "Cevap yarıda kesildi; hata bildirimi ve geri alma yolu.",
kredi: 12,
transport: () =>
createChat()
.user("Hukuk için kaç net lazım?")
.sleep(600)
.assistant(({ writer }) => {
writer.tool("program_netleri", { input: { program_id: "106510077" } }).sleep(ARAC_BEKLEME).error("zaman aşımı");
writer.text("Netlere bakarken bir sorun");
writer.error("Danışman şu anda yanıt veremiyor.");
})
.transport({ delayMs: 25 }),
},
{
ad: "Uzun geçmiş",
aciklama: "Kayıtlı (düz metin) geçmiş + son üç kredi uyarısı.",
kredi: 3,
gecmis: [
{ id: "g1", role: "user", content: "Listemdeki riskli tercihler ne kadar riskli?" },
{
id: "g2",
role: "assistant",
content:
"Listende 5 **Riskli** tercih var; hepsi geçen yıl senin sıranın üzerinde kapandı. En uzağı İTÜ Bilgisayar Mühendisliği (taban 2.503), en yakını Hacettepe (taban 38.200).\n\nBunları listenin başında tutman mantıklı; altlarında yeterince Az riskli ve Güvenli tercih var.",
},
{ id: "g3", role: "user", content: "Evden en fazla 300 km olsun." },
{
id: "g4",
role: "assistant",
content:
"Bunu profiline kaydedeyim mi? (Evet demek kredi harcamaz; listen ve aramalar buna göre süzülür, istediğin zaman silebilirsin.)",
},
],
transport: () =>
createChat()
.user("Evet")
.sleep(500)
.assistant(({ writer }) => {
writer.tool("profil_guncelle", { input: { alan: "maxKm", deger: "300" } }).sleep(900).output({ kaydedildi: "Evden en fazla 300 km" });
writer.text("Kaydettim: evden en fazla 300 km. Aramalar artık buna göre süzülüyor.");
})
.transport({ delayMs: 25 }),
},
];

View File

@@ -0,0 +1,18 @@
"use client";
import dynamic from "next/dynamic";
// dev-panel-mount.tsx ile aynı kalıp: NODE_ENV build sırasında statik yerine
// konur, prod build'de import() erişilemez dalda kalır; senaryolar ve
// @shadcn/helpers üretim paketine hiç girmez.
const SenaryolarIc =
process.env.NODE_ENV !== "production"
? dynamic(
() => import("./sohbet-senaryolari").then((m) => m.SohbetSenaryolari),
{ ssr: false },
)
: null;
export function SohbetSenaryolariMount() {
return SenaryolarIc ? <SenaryolarIc /> : null;
}

View File

@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { SohbetClient } from "./sohbet-client";
import { SENARYOLAR } from "./sohbet-senaryo-verisi";
/**
* /dev/sohbet — danışman sohbetinin arayüzünü önceden yazılmış senaryolarla
* oynatır (@shadcn/helpers/ai-sdk; senaryolar sohbet-senaryo-verisi.ts).
* Model, oturum ve kredi yok: taşıyıcı, /api/soru'nun ürettiği parçaların
* aynısını (metin, araç girdisi/çıktısı, hata) sahte bir akıştan verir.
*
* Hangi soruyu yazarsan yaz seçili senaryonun cevabı oynar. Yalnız
* geliştirmede yüklenir (app/dev/sohbet/page.tsx).
*/
export function SohbetSenaryolari() {
const [secili, setSecili] = useState(0);
const senaryo = SENARYOLAR[secili];
// Senaryo değişince sohbet sıfırdan kurulur; taşıyıcı senaryo başına bir kez üretilir.
const [transport, setTransport] = useState(() => senaryo.transport());
function sec(i: number) {
setSecili(i);
setTransport(SENARYOLAR[i].transport());
}
return (
<main className="mx-auto w-full max-w-3xl px-4 py-12">
<h1 className="font-heading text-3xl font-bold text-slate-900">
Danışman sohbeti — senaryolar
</h1>
<p className="mt-3 text-base leading-relaxed text-slate-600">
Model, oturum ve kredi kullanılmaz. Bir senaryo seç, kutuya herhangi
bir şey yaz; seçili senaryonun cevabı oynar.
</p>
<div role="group" aria-label="Senaryo" className="mt-6 flex flex-wrap gap-2">
{SENARYOLAR.map((s, i) => (
<button
key={s.ad}
type="button"
aria-pressed={i === secili}
onClick={() => sec(i)}
className={
i === secili
? "inline-flex h-10 cursor-pointer items-center rounded-full bg-slate-900 px-4 text-sm font-semibold text-slate-50"
: "inline-flex h-10 cursor-pointer items-center rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800 transition-colors duration-150 hover:border-orange-500 hover:text-orange-500"
}
>
{s.ad}
</button>
))}
</div>
<p className="mt-3 text-sm text-slate-500">{senaryo.aciklama}</p>
<div className="mt-6">
<SohbetClient
key={secili}
baslangicKredi={senaryo.kredi}
initialMesajlar={senaryo.gecmis ?? []}
initialProfil={[]}
denemeTransport={transport}
className="h-[70dvh] min-h-[420px]"
/>
</div>
</main>
);
}