refactor: update site header and chat components for improved user experience
All checks were successful
Deploy / deploy (push) Successful in 12m16s

Replaced the static logo in the SiteHeader with an animated component for enhanced visual feedback. Updated the PaketSatinAl component to include a dynamic link based on user context. Enhanced the ListeUretici and ListemGovde components with improved layout and messaging features, including the addition of a new initial preference comment display in the SohbetClient. Adjusted styling for consistency across components and improved loading states with skeleton screens.
This commit is contained in:
bilalgursen
2026-08-11 12:54:46 +03:00
parent 951caa3c93
commit 30b9b41a22
9 changed files with 437 additions and 108 deletions

View File

@@ -7,6 +7,7 @@ import Image from "next/image";
import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
import { Button } from "@/components/ui/button";
import {
ChatContainerContent,
@@ -53,20 +54,53 @@ export interface DanismanOzeti {
kilitli?: boolean;
}
/** Danışman mesajlarının avatarı — KolayTercih markası. */
function DanismanAvatar() {
/** Liste kurulunca sohbetin açılışında gösterilen 1. tercih yorumu.
* Metin ACIK_SATIR maskesinin her zaman açık bıraktığı ilk satırdan gelir,
* yani paketsiz kullanıcıda da gerçek Yapay Zeka yorumudur. */
export interface IlkTercihYorumu {
program: string;
universite: string;
metin: string;
}
/** 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. */
export function DanismanBaslik() {
return (
// gap yok denecek kadar az (0.5): SVG viewBox'ının kendi iç boşluğu
// zaten nefes payı bırakıyor.
<h2 className="flex min-w-0 flex-1 items-center gap-0.5 font-bricolage text-base tracking-tighter">
<AnimasyonluMark varyant="nabiz" renk="turuncu" className="size-5" />
<span className="truncate">
<span className="font-light">Danışmanla</span>
<span className="font-semibold">Konuş</span>
</span>
</h2>
);
}
/** 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 }) {
return (
<span
className="mt-0.5 flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-slate-200 bg-white"
aria-hidden
>
<Image
src="/kolay-tercih-mark.svg"
alt=""
width={32}
height={32}
className="size-5"
/>
{canli ? (
<AnimasyonluMark varyant="nabiz" className="size-5" />
) : (
<Image
src="/kolay-tercih-mark.svg"
alt=""
width={32}
height={32}
className="size-5"
/>
)}
</span>
);
}
@@ -87,6 +121,7 @@ export function SohbetClient({
initialMesajlar,
className,
ozet,
ilkYorum,
bildikleri,
}: {
baslangicKredi: number;
@@ -96,6 +131,10 @@ export function SohbetClient({
className?: string;
/** Raporun genel değerlendirmesi; sohbetin açılış mesajı olarak gösterilir */
ozet?: DanismanOzeti;
/** 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ığı. */
ilkYorum?: IlkTercihYorumu;
/** 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[];
@@ -253,62 +292,70 @@ export function SohbetClient({
aria-label="Danışman sohbeti"
className={cn(
"flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white",
className ?? "h-[calc(100dvh-24rem)] min-h-[340px]",
// 18rem: sticky ofset (top-28 = 7rem) + geniş alt boşluk — eski 24rem
// sohbeti sıkıştırıyordu, 10-14rem denemeleri fazla yüksek geldi;
// panel görünür ama sayfaya baskın değil.
className ?? "h-[calc(100dvh-18rem)] min-h-[380px]",
)}
>
{/* Başlık solda, aksiyonlar sağda tek grup: dar panelde de sığar.
Temizle butonu ikon-only — metinli hâli başlıkla yarışıp taşıyordu. */}
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3">
{bildikleri?.length ? (
<TooltipProvider delayDuration={120}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Danışmanın bildikleri"
className="inline-flex size-7 cursor-help items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-800"
<DanismanBaslik />
<div className="flex shrink-0 items-center gap-1.5">
{bildikleri?.length ? (
<TooltipProvider delayDuration={120}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Danışmanın bildikleri"
className="inline-flex size-7 cursor-help items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-800"
>
<Info className="size-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="max-w-xs flex-col items-start gap-1.5 p-3 text-left leading-relaxed"
>
<Info className="size-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="max-w-xs flex-col items-start gap-1.5 p-3 text-left leading-relaxed"
>
<p className="font-semibold">
Danışmanın bildikleri — cevaplar bu bağlamla üretilir:
</p>
<ul className="flex list-disc flex-col gap-1 pl-4">
{bildikleri.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
<p className="text-background/70">
Adın, e-postan ve netlerin danışmanla paylaşılmaz.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
{mesajlar.length > 0 ? (
<button
type="button"
onClick={() => setTemizleAcik(true)}
disabled={bekliyor || temizleniyor}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-semibold text-slate-600 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-50"
title="Sohbet geçmişini sil"
<p className="font-semibold">
Danışmanın bildikleri — cevaplar bu bağlamla üretilir:
</p>
<ul className="flex list-disc flex-col gap-1 pl-4">
{bildikleri.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
<p className="text-background/70">
Adın, e-postan ve netlerin danışmanla paylaşılmaz.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
{mesajlar.length > 0 ? (
<button
type="button"
onClick={() => setTemizleAcik(true)}
disabled={bekliyor || temizleniyor}
aria-label="Sohbeti temizle"
className="inline-flex size-7 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-50"
title="Sohbet geçmişini sil"
>
<Trash2 className="size-3.5" aria-hidden />
</button>
) : null}
<Link
href="/paket"
className="inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 transition-colors hover:bg-amber-100"
title="Kredilerin — yüklemek için tıkla"
>
<Trash2 className="size-3.5" aria-hidden />
Sohbeti temizle
</button>
) : null}
<Link
href="/paket"
className="ml-auto inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 transition-colors hover:bg-amber-100"
title="Kredilerin — yüklemek için tıkla"
>
<Coins className="size-3.5" aria-hidden />
{kredi} kredi
</Link>
<Coins className="size-3.5" aria-hidden />
{kredi} kredi
</Link>
</div>
</div>
<Dialog open={temizleAcik} onOpenChange={setTemizleAcik}>
@@ -363,6 +410,19 @@ export function SohbetClient({
</div>
</Message>
) : null}
{ilkYorum && mesajlar.length === 0 ? (
<Message className="justify-start">
<DanismanAvatar />
<div className="flex max-w-[85%] flex-col gap-2">
<p className="text-xs font-semibold text-slate-500">
1. tercihin: {ilkYorum.program} — {ilkYorum.universite}
</p>
<MessageContent className="bg-slate-100 text-slate-800">
{ilkYorum.metin}
</MessageContent>
</div>
</Message>
) : null}
{mesajlar.length === 0 ? (
<div
className={cn(
@@ -424,7 +484,7 @@ export function SohbetClient({
)}
</div>
) : (
mesajlar.map((m) =>
mesajlar.map((m, i) =>
m.role === "user" ? (
<Message key={m.id} className="justify-end">
<MessageContent className="max-w-[85%] bg-primary text-white">
@@ -433,7 +493,11 @@ export function SohbetClient({
</Message>
) : (
<Message key={m.id} className="justify-start">
<DanismanAvatar />
{/* Cevap beklenirken/stream sürerken (son mesaj) avatar
nabız animasyonuna geçer. */}
<DanismanAvatar
canli={bekliyor && i === mesajlar.length - 1}
/>
{m.content ? (
<MessageContent
markdown