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",
|
"@cds/city": "^1.1.0",
|
||||||
"@libsql/client": "^0.17.4",
|
"@libsql/client": "^0.17.4",
|
||||||
"@number-flow/react": "^0.6.2",
|
"@number-flow/react": "^0.6.2",
|
||||||
|
"@shadcn/helpers": "^0.2.0",
|
||||||
"ai": "^7.0.127",
|
"ai": "^7.0.127",
|
||||||
"better-auth": "^1.6.23",
|
"better-auth": "^1.6.23",
|
||||||
"better-sqlite3": "^12.11.1",
|
"better-sqlite3": "^12.11.1",
|
||||||
|
|||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
|||||||
'@number-flow/react':
|
'@number-flow/react':
|
||||||
specifier: ^0.6.2
|
specifier: ^0.6.2
|
||||||
version: 0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
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:
|
ai:
|
||||||
specifier: ^7.0.127
|
specifier: ^7.0.127
|
||||||
version: 7.0.127(zod@4.4.3)
|
version: 7.0.127(zod@4.4.3)
|
||||||
@@ -2003,6 +2006,20 @@ packages:
|
|||||||
'@sec-ant/readable-stream@0.4.1':
|
'@sec-ant/readable-stream@0.4.1':
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
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':
|
'@shikijs/core@4.3.1':
|
||||||
resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==}
|
resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -7453,6 +7470,10 @@ snapshots:
|
|||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@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':
|
'@shikijs/core@4.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/primitive': 4.3.1
|
'@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 { useEffect, useRef, useState } from "react";
|
||||||
import { useChat } from "@ai-sdk/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 Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@@ -227,6 +227,7 @@ export function SohbetClient({
|
|||||||
ilkYorum,
|
ilkYorum,
|
||||||
bildikleri,
|
bildikleri,
|
||||||
initialProfil,
|
initialProfil,
|
||||||
|
denemeTransport,
|
||||||
}: {
|
}: {
|
||||||
baslangicKredi: number;
|
baslangicKredi: number;
|
||||||
/** Sunucu tarafında çekilmiş danışman profili; verilirse mount fetch'i atlanır */
|
/** 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 prompt'unda gerçekten yer alan bağlam maddeleri; başlıktaki
|
||||||
* "Danışmanın bildikleri" ipucunda listelenir. Boşsa ipucu çizilmez. */
|
* "Danışmanın bildikleri" ipucunda listelenir. Boşsa ipucu çizilmez. */
|
||||||
bildikleri?: string[];
|
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 [girdi, setGirdi] = useState("");
|
||||||
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
|
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
|
||||||
@@ -270,7 +274,7 @@ export function SohbetClient({
|
|||||||
if (krediDustu) setKredi((k) => k - 1);
|
if (krediDustu) setKredi((k) => k - 1);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const { transport } = kanal;
|
const transport = denemeTransport ?? kanal.transport;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages: mesajlar,
|
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