feat(sohbet): liste revizyonu sohbete taşındı — soru formu ve yapısal kural

Eski revizyon kutusu kaldırıldı: serbest metin modele gidiyor, sıra ve havuz
koddan belirlendiği için "ilk 5 X ilinden olsun" gibi isteklerin karşılığı
yoktu. Danışman artık belirsiz isteği soru formuyla (soru_sor → shadcn
Questionnaire, proje diline çevrildi) netleştirir, sonra liste_revize ile
yapısal kural verir: il/alan/tip farkı, "ilk N" ölçütü, çıkar/ekle,
iskelet (lib/revizyon-kurali.ts). Kurallar havuz, seçim ve sırada koddan
uygulanır; "ilk N" ölçütünün ili/alanı süzgece kendiliğinden eklenir.

- chat_messages.meta sütunu (göç 0006): form, form cevabı ve revizyon sonucu
  kayıttan geri kurulur; form cevabı kredi harcamaz.
- Akış korumaları: form açılınca başka araç yok, revizyonda zaman aşımı uzar,
  "yeniden kurdum" beyanı araçsızsa düzeltme notu.
- /listem "Revize et" sohbeti odaklar; başarılı revizyonda liste yenilenir.
- /dev/sohbet senaryosu, DESIGN.md §4.20 notu ve §4.33.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 17:12:53 +03:00
parent 3c995402c6
commit 4d30dd1305
29 changed files with 3392 additions and 414 deletions

View File

@@ -0,0 +1,329 @@
"use client";
// shadcn `questionnaire` (https://ui.shadcn.com/docs/react/questionnaire,
// radix-nova kayıt öğesi; ilkel: @shadcn/react/questionnaire), sitenin diline
// çevrildi (DESIGN.md §4.6, §4.17, §4.24): `border-input / bg-muted /
// text-muted-foreground` token'ları yerine slate ölçeği ve `bg-card`; seçili
// seçenek sihirbaz çipi gibi `border-primary bg-primary text-white`; hover
// tek turuncu (§2.1); dokunma hedefi 44 px; köşe `rounded-2xl` (§4.4 sade
// kutu); düğmeler sitenin `Button`'u. Yapı ve davranış (tek tek soru,
// ilerleme, doğrulama, klavye kısayolu, atlama) ilkelden gelir. Danışman
// sohbetindeki soru formu bunu kullanır (features/rapor/components/sohbet-anketi.tsx).
import * as React from "react";
import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire";
import { Check } from "lucide-react";
import { buttonVariants, type Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
function Questionnaire({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Root>) {
return (
<QuestionnairePrimitive.Root
data-slot="questionnaire"
className={cn("flex w-full min-w-0 flex-col gap-4", className)}
{...props}
/>
);
}
function QuestionnaireProgress({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Progress>) {
return (
<QuestionnairePrimitive.Progress
data-slot="questionnaire-progress"
className={cn(
"min-h-[1lh] w-fit min-w-[14ch] text-xs font-semibold text-slate-500 tabular-nums",
className,
)}
{...props}
/>
);
}
function QuestionnaireItem({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Item>) {
return (
<QuestionnairePrimitive.Item
data-slot="questionnaire-item"
className={cn("flex min-w-0 flex-col gap-3 border-0 p-0 outline-none", className)}
{...props}
/>
);
}
function QuestionnaireTitle({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Title>) {
return (
<QuestionnairePrimitive.Title
data-slot="questionnaire-title"
className={cn(
"font-heading text-base leading-snug font-bold text-pretty text-slate-900 [&:not(:has(~[data-slot=questionnaire-description]))]:mb-1",
className,
)}
{...props}
/>
);
}
function QuestionnaireDescription({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Description>) {
return (
<QuestionnairePrimitive.Description
data-slot="questionnaire-description"
className={cn("text-sm text-pretty leading-5 text-slate-600", className)}
{...props}
/>
);
}
function QuestionnaireChoices({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choices>) {
return (
<QuestionnairePrimitive.Choices
data-slot="questionnaire-choices"
className={cn("group/questionnaire-choices grid min-w-0 gap-2", className)}
{...props}
/>
);
}
/**
* Seçenek satırı: 44 px sade kutu; seçilince sihirbaz çipi gibi mavi dolar ve
* başındaki işaret tik olur — seçili hâl renk + tik'tir (§2.2). Klavye
* kısayolu (harf) sağda küçük bir kutuda durur.
*/
function QuestionnaireChoice({
children,
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choice>) {
return (
<QuestionnairePrimitive.Choice
data-slot="questionnaire-choice"
className={cn(
"group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-2xl border border-slate-200 bg-card px-3.5 py-2.5 text-start text-sm font-semibold text-slate-700 transition-[background-color,border-color,color,transform] duration-150 outline-none select-none",
"hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.99]",
"has-[>input:focus-visible]:ring-3 has-[>input:focus-visible]:ring-ring/50",
"data-checked:border-primary data-checked:bg-primary data-checked:text-white data-checked:hover:border-primary data-checked:hover:bg-primary data-checked:hover:text-white",
"data-invalid:border-red-500",
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
className,
)}
{...props}
>
<QuestionnairePrimitive.ChoiceInput
data-slot="questionnaire-choice-input"
className="absolute inset-0 z-10 size-full cursor-pointer opacity-0"
/>
<span
aria-hidden="true"
data-slot="questionnaire-choice-indicator"
className="pointer-events-none relative mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-slate-300 bg-card group-data-[type=radio]/questionnaire-choice:rounded-full group-data-checked/questionnaire-choice:border-primary-foreground group-data-checked/questionnaire-choice:bg-primary-foreground group-data-checked/questionnaire-choice:text-primary"
>
<Check
data-slot="questionnaire-choice-indicator-check"
className="hidden size-3 group-data-checked/questionnaire-choice:block"
strokeWidth={3}
/>
</span>
<QuestionnairePrimitive.ChoiceLabel
data-slot="questionnaire-choice-label"
className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug"
>
{children}
</QuestionnairePrimitive.ChoiceLabel>
<QuestionnairePrimitive.ChoiceShortcut
data-slot="questionnaire-choice-shortcut"
className="pointer-events-none ms-auto mt-0.5 hidden size-5 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-card font-mono text-[0.625rem] leading-none font-medium text-slate-500 group-data-checked/questionnaire-choice:border-white/40 group-data-checked/questionnaire-choice:bg-white/15 group-data-checked/questionnaire-choice:text-white group-data-[shortcut]/questionnaire-choice:inline-flex"
/>
</QuestionnairePrimitive.Choice>
);
}
function QuestionnaireChoiceDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="questionnaire-choice-description"
className={cn(
"text-xs font-normal text-slate-500 group-data-checked/questionnaire-choice:text-white/80",
className,
)}
{...props}
/>
);
}
/** Serbest cevap alanı: §4.6 metin alanı hapı (44 px, bu kutuda bir kademe kısa). */
function QuestionnaireInput({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Input>) {
return (
<div
data-slot="questionnaire-input-wrapper"
className="group/questionnaire-input relative w-full min-w-0"
>
<QuestionnairePrimitive.Input
data-slot="questionnaire-input"
className={cn(
"h-11 w-full min-w-0 rounded-full border border-slate-200 bg-card px-4 text-base text-slate-900 transition-[border-color,box-shadow] outline-none placeholder:text-slate-400 hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20",
className,
)}
{...props}
/>
</div>
);
}
function QuestionnaireError({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Error>) {
return (
<QuestionnairePrimitive.Error
data-slot="questionnaire-error"
className={cn("text-sm font-medium text-red-600", className)}
{...props}
/>
);
}
function QuestionnaireActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="questionnaire-actions"
className={cn(
"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2",
className,
)}
{...props}
/>
);
}
type DugmeProps = Pick<React.ComponentProps<typeof Button>, "size" | "variant">;
function QuestionnairePrevious({
children,
className,
size = "default",
variant = "outline",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Previous> & DugmeProps) {
return (
<QuestionnairePrimitive.Previous
data-slot="questionnaire-previous"
className={cn(
buttonVariants({ size, variant }),
"col-start-1 row-start-1 cursor-pointer justify-self-start",
className,
)}
{...props}
>
{children ?? "Geri"}
</QuestionnairePrimitive.Previous>
);
}
function QuestionnaireSkip({
children,
className,
size = "default",
variant = "ghost",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Skip> & DugmeProps) {
return (
<QuestionnairePrimitive.Skip
data-slot="questionnaire-skip"
className={cn(
buttonVariants({ size, variant }),
"col-start-2 row-start-1 cursor-pointer justify-self-end",
className,
)}
{...props}
>
{children ?? "Atla"}
</QuestionnairePrimitive.Skip>
);
}
function QuestionnaireNext({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Next> & DugmeProps) {
return (
<QuestionnairePrimitive.Next
data-slot="questionnaire-next"
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 cursor-pointer justify-self-end",
className,
)}
{...props}
>
{children ?? "İleri"}
</QuestionnairePrimitive.Next>
);
}
function QuestionnaireSubmit({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Submit> & DugmeProps) {
return (
<QuestionnairePrimitive.Submit
data-slot="questionnaire-submit"
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 cursor-pointer justify-self-end",
className,
)}
{...props}
>
{children ?? "Gönder"}
</QuestionnairePrimitive.Submit>
);
}
export {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoiceDescription,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
};