From ea3728a7ec289b7145c359af30036cdc3d132bcc Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 5 Oct 2026 15:15:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(dev):=20/dev/sohbet=20=E2=80=94=20dan?= =?UTF-8?q?=C4=B1=C5=9Fman=20sohbeti=20aray=C3=BCz=C3=BC=20senaryolarla=20?= =?UTF-8?q?oynat=C4=B1l=C4=B1r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 1 + pnpm-lock.yaml | 21 +++ src/app/dev/sohbet/page.tsx | 15 ++ .../rapor/components/sohbet-client.tsx | 8 +- .../rapor/components/sohbet-senaryo-verisi.ts | 142 ++++++++++++++++++ .../components/sohbet-senaryolari-mount.tsx | 18 +++ .../rapor/components/sohbet-senaryolari.tsx | 67 +++++++++ 7 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/app/dev/sohbet/page.tsx create mode 100644 src/features/rapor/components/sohbet-senaryo-verisi.ts create mode 100644 src/features/rapor/components/sohbet-senaryolari-mount.tsx create mode 100644 src/features/rapor/components/sohbet-senaryolari.tsx diff --git a/package.json b/package.json index 4245162..b44556d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@cds/city": "^1.1.0", "@libsql/client": "^0.17.4", "@number-flow/react": "^0.6.2", + "@shadcn/helpers": "^0.2.0", "ai": "^7.0.127", "better-auth": "^1.6.23", "better-sqlite3": "^12.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de8720..de81d55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@number-flow/react': specifier: ^0.6.2 version: 0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@shadcn/helpers': + specifier: ^0.2.0 + version: 0.2.0(ai@7.0.127(zod@4.4.3)) ai: specifier: ^7.0.127 version: 7.0.127(zod@4.4.3) @@ -2003,6 +2006,20 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shadcn/helpers@0.2.0': + resolution: {integrity: sha512-wpcZNuRgdW16jLwjvh4mbREKpVFrGY+nBM3Okgic8b04sijN5CHW1Nl/AEMajsP3INaCTNnqMCouOalZ7OHvEQ==} + peerDependencies: + '@tanstack/ai': '>=0.40.0 <0.41.0' + '@tanstack/ai-client': '>=0.20.0 <0.21.0' + ai: '>=7.0.0 <8' + peerDependenciesMeta: + '@tanstack/ai': + optional: true + '@tanstack/ai-client': + optional: true + ai: + optional: true + '@shikijs/core@4.3.1': resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} @@ -7453,6 +7470,10 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shadcn/helpers@0.2.0(ai@7.0.127(zod@4.4.3))': + optionalDependencies: + ai: 7.0.127(zod@4.4.3) + '@shikijs/core@4.3.1': dependencies: '@shikijs/primitive': 4.3.1 diff --git a/src/app/dev/sohbet/page.tsx b/src/app/dev/sohbet/page.tsx new file mode 100644 index 0000000..87b085d --- /dev/null +++ b/src/app/dev/sohbet/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { SohbetSenaryolariMount } from "@/features/rapor/components/sohbet-senaryolari-mount"; + +export const metadata: Metadata = { + title: "Danışman sohbeti senaryoları", + robots: { index: false, follow: false }, +}; + +// Yalnız geliştirmede: danışman sohbetinin arayüzünü model harcamadan oynatır +// (features/rapor/components/sohbet-senaryolari.tsx). Üretimde 404. +export default function SohbetSenaryolariPage() { + if (process.env.NODE_ENV === "production") notFound(); + return ; +} diff --git a/src/features/rapor/components/sohbet-client.tsx b/src/features/rapor/components/sohbet-client.tsx index 63855ed..36b0750 100644 --- a/src/features/rapor/components/sohbet-client.tsx +++ b/src/features/rapor/components/sohbet-client.tsx @@ -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; }) { 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, diff --git a/src/features/rapor/components/sohbet-senaryo-verisi.ts b/src/features/rapor/components/sohbet-senaryo-verisi.ts new file mode 100644 index 0000000..4833df5 --- /dev/null +++ b/src/features/rapor/components/sohbet-senaryo-verisi.ts @@ -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; +}; + +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 }), + }, +]; diff --git a/src/features/rapor/components/sohbet-senaryolari-mount.tsx b/src/features/rapor/components/sohbet-senaryolari-mount.tsx new file mode 100644 index 0000000..6eeb1fa --- /dev/null +++ b/src/features/rapor/components/sohbet-senaryolari-mount.tsx @@ -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 ? : null; +} diff --git a/src/features/rapor/components/sohbet-senaryolari.tsx b/src/features/rapor/components/sohbet-senaryolari.tsx new file mode 100644 index 0000000..d11c580 --- /dev/null +++ b/src/features/rapor/components/sohbet-senaryolari.tsx @@ -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 ( +
+

+ Danışman sohbeti — senaryolar +

+

+ Model, oturum ve kredi kullanılmaz. Bir senaryo seç, kutuya herhangi + bir şey yaz; seçili senaryonun cevabı oynar. +

+
+ {SENARYOLAR.map((s, i) => ( + + ))} +
+

{senaryo.aciklama}

+
+ +
+
+ ); +}