feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s
All checks were successful
Deploy / deploy (push) Successful in 7m45s
- Updated the app.db file to reflect recent changes in the application state. - Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates. - Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
This commit is contained in:
@@ -5,11 +5,23 @@ import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport, type ChatTransport, type UIMessage } from "ai";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2, X } from "lucide-react";
|
||||
import { animate, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
ArrowUp,
|
||||
Coins,
|
||||
Info,
|
||||
Lock,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
import { AracPilleri, aracPilleri } from "./arac-pilleri";
|
||||
import { SOHBET_MARKDOWN_BILESENLERI, baglantilariIsaretle } from "./sohbet-baglanti";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ChatContainerContent,
|
||||
@@ -67,31 +79,41 @@ function mesajMetni(m: UIMessage): string {
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
/** Danışman veriye bakarken görünen satır (araç adı → öğrenci dili). */
|
||||
const ARAC_ETIKETI: Record<string, string> = {
|
||||
program_ara: "Programlara bakıyorum",
|
||||
program_detay: "Programın ayrıntısına bakıyorum",
|
||||
program_netleri: "Netlere bakıyorum",
|
||||
program_karsilastir: "Programları karşılaştırıyorum",
|
||||
bolum_istihdam: "Mezunların iş durumuna bakıyorum",
|
||||
bolum_ozeti: "Bölüme bakıyorum",
|
||||
il_ozeti: "Şehirdeki üniversitelere bakıyorum",
|
||||
rehber_ara: "Rehber yazılarına bakıyorum",
|
||||
tercih_takvimi: "Takvime bakıyorum",
|
||||
universite_profili: "Üniversiteye bakıyorum",
|
||||
profil_guncelle: "Tercihini kaydediyorum",
|
||||
/** Soru gitti, cevabın ilk parçası henüz gelmedi: listede cevabın yerini
|
||||
* tutan boş danışman mesajı. */
|
||||
const BEKLENEN_CEVAP: UIMessage = {
|
||||
id: "beklenen-cevap",
|
||||
role: "assistant",
|
||||
parts: [],
|
||||
};
|
||||
|
||||
/** Şu an çalışan (sonucu henüz gelmemiş) araçların etiketleri. */
|
||||
function calisanAraclar(m: UIMessage): string[] {
|
||||
const etiketler = m.parts.flatMap((p) => {
|
||||
if (!p.type.startsWith("tool-") || !("state" in p)) return [];
|
||||
if (p.state === "output-available" || p.state === "output-error") return [];
|
||||
return [ARAC_ETIKETI[p.type.slice("tool-".length)] ?? "Veriye bakıyorum"];
|
||||
});
|
||||
return [...new Set(etiketler)];
|
||||
/** Danışman cevabının liste anahtarı: kendi kimliği değil, cevapladığı sorunun
|
||||
* kimliği. Böylece bekleme satırı (BEKLENEN_CEVAP) ile sonradan gelen gerçek
|
||||
* cevap React için aynı satırdır — avatarın animasyonu ve iskelet, ilk parça
|
||||
* geldiğinde baştan başlamaz. */
|
||||
function cevapAnahtari(mesajlar: UIMessage[], i: number): string {
|
||||
const soru = mesajlar[i - 1];
|
||||
return soru?.role === "user" ? `cevap-${soru.id}` : mesajlar[i].id;
|
||||
}
|
||||
|
||||
// Araç adı → öğrenci dili eşlemesi ve "…bakıyorum" hapları arac-pilleri.tsx'te.
|
||||
|
||||
/** Panelin boyut geçişi: sekmesiz yay (DESIGN.md §6); listem-govde ızgarası aynı yayı kullanır. */
|
||||
export const SOHBET_GENISLEME_GECISI = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||
|
||||
/**
|
||||
* Genişletilmiş panel üstten `top-28` (7rem; listem sütununun sticky ofseti)
|
||||
* hizasına kaydırılır ve altı site geneli alt progressive blur'un
|
||||
* (`.kt-sayfa-blur-alt`, 7rem; globals.css) üstünde biter — Bilal, 6 Eki 2026:
|
||||
* "aşağı kısmı progressive blura takılıyor". Sayfa o kadar kaymıyorsa kalan
|
||||
* fark boydan düşülür; panel hiçbir durumda bulanık şeride girmez.
|
||||
*/
|
||||
const USTE_UZAKLIK_REM = 7;
|
||||
const ALT_BLUR_REM = 7;
|
||||
const GENIS_EN_AZ_PX = 380;
|
||||
/** /dev/sohbet gibi sticky'siz yerlerde de aynı hesap; sabit yükseklik istenirse `genisYukseklik`. */
|
||||
const GENIS_YUKSEKLIK = "";
|
||||
|
||||
type Red = { status: number; error?: string };
|
||||
|
||||
/**
|
||||
@@ -157,8 +179,8 @@ export interface IlkTercihYorumu {
|
||||
|
||||
/** Danışman panellerinin ortak başlığı: animasyonlu logo + site header'daki
|
||||
* KolayTercih wordmark deseninde "DanışmanlaKonuş" (ilk kelime ince, ikinci
|
||||
* kalın, boşluksuz). İşaret ortak AnimasyonluMark bileşeni — header
|
||||
* logosuyla aynı nabız animasyonu, burada sürekli atar. */
|
||||
* kalın, boşluksuz). İşaret ortak AnimasyonluMark bileşeninin yerinde
|
||||
* nabız varyantı, burada sürekli atar. */
|
||||
export function DanismanBaslik() {
|
||||
return (
|
||||
// gap yok denecek kadar az (0.5): SVG viewBox'ının kendi iç boşluğu
|
||||
@@ -174,25 +196,33 @@ export function DanismanBaslik() {
|
||||
}
|
||||
|
||||
/** Danışman mesajlarının avatarı — KolayTercih markası. `canli` iken (cevap
|
||||
* beklenirken/yazılırken) statik görsel yerine başlıktaki nabız animasyonu
|
||||
* oynar: danışmanın "düşündüğü" avatardan okunur. */
|
||||
export function DanismanAvatar({ canli = false }: { canli?: boolean }) {
|
||||
* beklenirken/yazılırken) panel başlığındaki DanışmanlaKonuş işareti gibi
|
||||
* yerinde nabız atar: danışmanın "düşündüğü" avatardan okunur. `aracta` iken
|
||||
* (danışman veriye bakıyor, "…bakıyorum" hapı dönüyor) kareler sırayla
|
||||
* birbirinin yerine yürür; araç bitince nabza döner. Her geçişte ve cevap
|
||||
* bitince kareler yumuşakça yerine oturur.
|
||||
* `renk="turuncu"`: panel başlığındaki DanışmanlaKonuş işaretinin rengi —
|
||||
* yalnız "Danışman uyarısı" balonunda (durağan; hareket "düşünüyor" demek). */
|
||||
export function DanismanAvatar({
|
||||
canli = false,
|
||||
aracta = false,
|
||||
renk = "marka",
|
||||
}: {
|
||||
canli?: boolean;
|
||||
aracta?: boolean;
|
||||
renk?: "marka" | "turuncu";
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className="mt-0.5 flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-slate-200 bg-card"
|
||||
aria-hidden
|
||||
>
|
||||
{canli ? (
|
||||
<AnimasyonluMark varyant="nabiz" className="size-5" />
|
||||
) : (
|
||||
<Image
|
||||
src="/kolay-tercih-mark.svg"
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-5"
|
||||
/>
|
||||
)}
|
||||
<AnimasyonluMark
|
||||
oynat={canli}
|
||||
varyant={canli && aracta ? "zincir" : "nabiz"}
|
||||
renk={renk}
|
||||
className="size-5"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -224,10 +254,14 @@ export function SohbetClient({
|
||||
initialMesajlar,
|
||||
className,
|
||||
ozet,
|
||||
uyarilar,
|
||||
ilkYorum,
|
||||
bildikleri,
|
||||
initialProfil,
|
||||
denemeTransport,
|
||||
genis: genisProp,
|
||||
onGenisDegistir,
|
||||
genisYukseklik = GENIS_YUKSEKLIK,
|
||||
}: {
|
||||
baslangicKredi: number;
|
||||
/** Sunucu tarafında çekilmiş danışman profili; verilirse mount fetch'i atlanır */
|
||||
@@ -238,6 +272,10 @@ export function SohbetClient({
|
||||
className?: string;
|
||||
/** Raporun genel değerlendirmesi; sohbetin açılış mesajı olarak gösterilir */
|
||||
ozet?: DanismanOzeti;
|
||||
/** Listenin kendisinden çıkan uyarılar (liste-uyarilari.ts: dağılım havuza
|
||||
* uyarlandı, tüm tercihler tek şehirde). Özetin altında turuncu işaretli
|
||||
* "Danışman uyarısı" balonu olarak durur; sohbet ilerlese de kalır. */
|
||||
uyarilar?: string[];
|
||||
/** 1. tercihin Yapay Zeka yorumu; sohbet boşken (taze kurulmuş liste)
|
||||
* özetin altında açılış balonu olarak durur — üretim panelindeki "ilk
|
||||
* tercihinin yorumunu burada okuyacaksın" sözünün karşılığı. */
|
||||
@@ -248,6 +286,13 @@ export function SohbetClient({
|
||||
/** 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>;
|
||||
/** Genişletilmiş hâl dışarıdan yönetiliyorsa (listem ızgarası sütunu da
|
||||
* büyütür); verilmezse panel kendi durumunu tutar ve yalnız boyu uzar. */
|
||||
genis?: boolean;
|
||||
onGenisDegistir?: (genis: boolean) => void;
|
||||
/** Genişletilmiş panelin yüksekliği; verilmezse ekrana göre hesaplanır
|
||||
* (üst 7rem sticky hizası, alt 7rem progressive blur payı). */
|
||||
genisYukseklik?: string;
|
||||
}) {
|
||||
const [girdi, setGirdi] = useState("");
|
||||
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
|
||||
@@ -259,6 +304,62 @@ export function SohbetClient({
|
||||
// yeniden çekilir: model profil_guncelle çağırdıysa yeni çip burada belirir.
|
||||
const [profil, setProfil] = useState<ProfilSatiri[]>(initialProfil ?? []);
|
||||
const router = useRouter();
|
||||
// Genişletilmiş sohbet (Bilal, 6 Eki 2026: "div'in smooth genişlemesi, diğer
|
||||
// alanların kendini yeniden boyutlandırması"): panel YERİNDE büyür — perde,
|
||||
// tam ekran ya da ayrı pencere yok. Boy burada yaylı geçişle uzar; listem
|
||||
// ızgarası (listem-govde.tsx) aynı anda sohbet sütununu genişletir, liste ve
|
||||
// harita daralan sütunda kendini yeniden akıtır. useChat durumu ve kaydırma
|
||||
// korunur. Hareketi azalt: geçişsiz.
|
||||
const [genisIc, setGenisIc] = useState(false);
|
||||
const genis = genisProp ?? genisIc;
|
||||
const kapRef = useRef<HTMLElement>(null);
|
||||
const hareketAz = useReducedMotion();
|
||||
|
||||
function genisligiDegistir() {
|
||||
const yeni = !genis;
|
||||
if (genisProp === undefined) setGenisIc(yeni);
|
||||
onGenisDegistir?.(yeni);
|
||||
const el = kapRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const eski = rect.height;
|
||||
let hedefYukseklik = "";
|
||||
if (yeni) {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
const usteUzaklik = USTE_UZAKLIK_REM * rem;
|
||||
// Panelin üstünü sticky noktasına getir; sayfa o kadar kaymıyorsa kalan
|
||||
// fark panelin üstünde kalır ve boydan düşülür
|
||||
const kayabilir = Math.max(
|
||||
0,
|
||||
document.documentElement.scrollHeight - window.innerHeight - window.scrollY,
|
||||
);
|
||||
const kaydir = Math.min(Math.max(0, rect.top - usteUzaklik), kayabilir);
|
||||
const kalanUst = rect.top - kaydir;
|
||||
const hedefPx = Math.max(
|
||||
GENIS_EN_AZ_PX,
|
||||
Math.round(window.innerHeight - kalanUst - ALT_BLUR_REM * rem),
|
||||
);
|
||||
hedefYukseklik = genisYukseklik || `${hedefPx}px`;
|
||||
if (kaydir > 0) {
|
||||
const basla = window.scrollY;
|
||||
if (hareketAz) window.scrollTo(0, basla + kaydir);
|
||||
else
|
||||
void animate(basla, basla + kaydir, {
|
||||
...SOHBET_GENISLEME_GECISI,
|
||||
onUpdate: (v) => window.scrollTo(0, v),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Boy geçişi: hedefi ölçüp eski değerden hedefe aynı yayla git; dar hâle
|
||||
// dönünce satır içi değer kalkar, sınıftaki yükseklik yeniden geçerli olur
|
||||
el.style.height = hedefYukseklik;
|
||||
if (hareketAz) return;
|
||||
const hedef = el.getBoundingClientRect().height;
|
||||
el.style.height = `${eski}px`;
|
||||
void animate(el, { height: hedef }, SOHBET_GENISLEME_GECISI).then(() => {
|
||||
el.style.height = hedefYukseklik;
|
||||
});
|
||||
}
|
||||
|
||||
// useChat geri çağrıları ilk render'da yakalanır; güncel değerler ref'ten okunur.
|
||||
const profilRef = useRef(profil);
|
||||
@@ -299,6 +400,10 @@ export function SohbetClient({
|
||||
},
|
||||
});
|
||||
const bekliyor = status === "submitted" || status === "streaming";
|
||||
const gorunenMesajlar =
|
||||
status === "submitted" && mesajlar.at(-1)?.role === "user"
|
||||
? [...mesajlar, BEKLENEN_CEVAP]
|
||||
: mesajlar;
|
||||
|
||||
/**
|
||||
* İstek reddedildiyse (akış hiç başlamadı) iyimser eklenen soruyu geri al
|
||||
@@ -457,7 +562,10 @@ export function SohbetClient({
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={kapRef}
|
||||
aria-label="Danışman sohbeti"
|
||||
// Yükseklik: sınıftan (dar hâl); genişletilince satır içi stil
|
||||
// (genisligiDegistir) sınıfı ezer, daraltılınca kaldırılır.
|
||||
className={cn(
|
||||
"flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-card",
|
||||
// 18rem: sticky ofset (top-28 = 7rem) + geniş alt boşluk — eski 24rem
|
||||
@@ -471,6 +579,20 @@ export function SohbetClient({
|
||||
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3">
|
||||
<DanismanBaslik />
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={genisligiDegistir}
|
||||
aria-pressed={genis}
|
||||
aria-label={genis ? "Sohbeti daralt" : "Sohbeti genişlet"}
|
||||
title={genis ? "Daralt" : "Genişlet"}
|
||||
className="inline-flex size-7 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-card text-slate-600 transition-[background-color,color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]"
|
||||
>
|
||||
{genis ? (
|
||||
<Minimize2 className="size-3.5" aria-hidden />
|
||||
) : (
|
||||
<Maximize2 className="size-3.5" aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
{bildikleri?.length ? (
|
||||
<TooltipProvider delayDuration={120}>
|
||||
<Tooltip>
|
||||
@@ -607,6 +729,28 @@ export function SohbetClient({
|
||||
</div>
|
||||
</Message>
|
||||
) : null}
|
||||
{uyarilar?.length ? (
|
||||
<Message className="justify-start">
|
||||
<DanismanAvatar renk="turuncu" />
|
||||
<div className="flex max-w-[85%] flex-col gap-2">
|
||||
<p className="text-xs font-semibold text-slate-500">
|
||||
{uyarilar.length > 1
|
||||
? "Danışman uyarıları"
|
||||
: "Danışman uyarısı"}
|
||||
</p>
|
||||
{uyarilar.map((u) => (
|
||||
// Zemin başlıktaki DanışmanlaKonuş işaretinin turuncusu
|
||||
// (orange-500); yazı beyaz ve font-medium (§4.20).
|
||||
<MessageContent
|
||||
key={u}
|
||||
className="bg-orange-500 font-medium text-white"
|
||||
>
|
||||
{u}
|
||||
</MessageContent>
|
||||
))}
|
||||
</div>
|
||||
</Message>
|
||||
) : null}
|
||||
{ilkYorum && mesajlar.length === 0 ? (
|
||||
<Message className="justify-start">
|
||||
<DanismanAvatar />
|
||||
@@ -682,7 +826,7 @@ export function SohbetClient({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{mesajlar.map((m, i) => {
|
||||
{gorunenMesajlar.map((m, i) => {
|
||||
const metin = mesajMetni(m);
|
||||
if (m.role === "user") {
|
||||
return (
|
||||
@@ -693,48 +837,40 @@ export function SohbetClient({
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
const canli = bekliyor && i === mesajlar.length - 1;
|
||||
// Danışman veriye bakarken ne yaptığını söyler; sonuç
|
||||
// gelince satır kalkar, cevap metni akar.
|
||||
const araclar = canli ? calisanAraclar(m) : [];
|
||||
const canli = bekliyor && i === gorunenMesajlar.length - 1;
|
||||
// Danışman veriye bakarken ne yaptığını hap olarak söyler
|
||||
// (arac-pilleri.tsx); haplar cevap bitince de kalır.
|
||||
const piller = aracPilleri(m);
|
||||
return (
|
||||
<Message key={m.id} className="justify-start">
|
||||
<Message
|
||||
key={cevapAnahtari(gorunenMesajlar, i)}
|
||||
className="justify-start"
|
||||
>
|
||||
{/* Cevap beklenirken/stream sürerken (son mesaj) avatar
|
||||
nabız animasyonuna geçer. */}
|
||||
<DanismanAvatar canli={canli} />
|
||||
nabız atar; bir araç çalışırken kareler dolaşır. */}
|
||||
<DanismanAvatar
|
||||
canli={canli}
|
||||
aracta={piller.some((p) => p.durum === "calisiyor")}
|
||||
/>
|
||||
<div className="flex w-full max-w-[85%] flex-col gap-2">
|
||||
<AracPilleri piller={piller} />
|
||||
{metin ? (
|
||||
// Site içi adresler bağlantı hapı olur (sohbet-baglanti.tsx):
|
||||
// markdown bağlantısı, backtick içi ve çıplak adres aynı hap
|
||||
<MessageContent
|
||||
markdown
|
||||
components={SOHBET_MARKDOWN_BILESENLERI}
|
||||
className="bg-slate-100 text-slate-800 prose-sm"
|
||||
>
|
||||
{metin}
|
||||
{baglantilariIsaretle(metin)}
|
||||
</MessageContent>
|
||||
) : (
|
||||
<CevapIskeleti />
|
||||
)}
|
||||
{araclar.length > 0 ? (
|
||||
<p
|
||||
role="status"
|
||||
className="px-1 text-xs font-medium text-slate-500"
|
||||
>
|
||||
{araclar.join(" · ")}…
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Message>
|
||||
);
|
||||
})}
|
||||
{/* Soru gitti, cevabın ilk parçası henüz gelmedi */}
|
||||
{status === "submitted" &&
|
||||
mesajlar[mesajlar.length - 1]?.role === "user" ? (
|
||||
<Message className="justify-start">
|
||||
<DanismanAvatar canli />
|
||||
<div className="w-full max-w-[85%]">
|
||||
<CevapIskeleti />
|
||||
</div>
|
||||
</Message>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<ChatContainerScrollAnchor />
|
||||
|
||||
Reference in New Issue
Block a user