feat(dev): /dev/sohbet — danışman sohbeti arayüzü senaryolarla oynatılır
All checks were successful
Deploy / deploy (push) Successful in 6m43s
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:
@@ -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",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
15
src/app/dev/sohbet/page.tsx
Normal file
15
src/app/dev/sohbet/page.tsx
Normal file
@@ -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 <SohbetSenaryolariMount />;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
142
src/features/rapor/components/sohbet-senaryo-verisi.ts
Normal file
142
src/features/rapor/components/sohbet-senaryo-verisi.ts
Normal 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 }),
|
||||
},
|
||||
];
|
||||
18
src/features/rapor/components/sohbet-senaryolari-mount.tsx
Normal file
18
src/features/rapor/components/sohbet-senaryolari-mount.tsx
Normal 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;
|
||||
}
|
||||
67
src/features/rapor/components/sohbet-senaryolari.tsx
Normal file
67
src/features/rapor/components/sohbet-senaryolari.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user