Add content checklists for block-yod and chair feedback system
Addresses Dafna's observation that licensing decisions lack comprehensive planning discussion. Systematic corpus analysis of all 24 training decisions revealed the system learned writing style but not substantive content. Changes: - Corpus analysis of all 24 decisions (docs/corpus-analysis.md) - 5 content checklists by appeal subtype injected into block-yod prompt - chair_feedback DB table + API endpoints + MCP tools - Feedback management page in Next.js UI (/feedback) - Navigation updated with "הערות יו״ר" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
329
web-ui/src/app/feedback/page.tsx
Normal file
329
web-ui/src/app/feedback/page.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
useFeedbackList,
|
||||
useCreateFeedback,
|
||||
useResolveFeedback,
|
||||
CATEGORY_LABELS,
|
||||
BLOCK_LABELS,
|
||||
type FeedbackCategory,
|
||||
} from "@/lib/api/feedback";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const CATEGORY_COLORS: Record<FeedbackCategory, string> = {
|
||||
missing_content: "bg-amber-100 text-amber-800 border-amber-200",
|
||||
wrong_tone: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
wrong_structure: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
factual_error: "bg-red-100 text-red-800 border-red-200",
|
||||
style: "bg-emerald-100 text-emerald-800 border-emerald-200",
|
||||
other: "bg-gray-100 text-gray-800 border-gray-200",
|
||||
};
|
||||
|
||||
export default function FeedbackPage() {
|
||||
const [showResolved, setShowResolved] = useState(false);
|
||||
const [filterCategory, setFilterCategory] = useState<string>("");
|
||||
|
||||
const { data: feedbacks, isLoading } = useFeedbackList({
|
||||
category: filterCategory || undefined,
|
||||
unresolved_only: !showResolved,
|
||||
});
|
||||
|
||||
const resolveMutation = useResolveFeedback();
|
||||
|
||||
function handleResolve(id: string) {
|
||||
resolveMutation.mutate(
|
||||
{ feedbackId: id, applied_to: [] },
|
||||
{
|
||||
onSuccess: () => toast.success("ההערה סומנה כמטופלת"),
|
||||
onError: () => toast.error("שגיאה בעדכון"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">
|
||||
בית
|
||||
</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">הערות יו״ר</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">הערות יו״ר על טיוטות</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
תיעוד הערות דפנה על טיוטות החלטות. כל הערה מנותחת ומשפיעה על שיפור
|
||||
כתיבת ההחלטות העתידיות.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<NewFeedbackDialog />
|
||||
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="rounded-md border border-rule bg-surface px-3 py-1.5 text-sm"
|
||||
>
|
||||
<option value="">כל הקטגוריות</option>
|
||||
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-ink-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showResolved}
|
||||
onChange={(e) => setShowResolved(e.target.checked)}
|
||||
className="rounded border-rule"
|
||||
/>
|
||||
הצג גם מטופלות
|
||||
</label>
|
||||
|
||||
{feedbacks && (
|
||||
<span className="text-sm text-ink-muted me-auto">
|
||||
{feedbacks.length} הערות
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feedback list */}
|
||||
{isLoading ? (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-8 text-center text-ink-muted">
|
||||
טוען...
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : !feedbacks?.length ? (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-8 text-center text-ink-muted">
|
||||
אין הערות{!showResolved ? " פתוחות" : ""}
|
||||
{filterCategory ? ` בקטגוריה ${CATEGORY_LABELS[filterCategory as FeedbackCategory]}` : ""}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{feedbacks.map((fb) => (
|
||||
<Card
|
||||
key={fb.id}
|
||||
className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}
|
||||
>
|
||||
<CardHeader className="border-b pb-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge
|
||||
className={`text-[0.7rem] border ${CATEGORY_COLORS[fb.category]}`}
|
||||
>
|
||||
{CATEGORY_LABELS[fb.category]}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
|
||||
</Badge>
|
||||
{fb.case_number && (
|
||||
<Link
|
||||
href={`/cases/${fb.case_number}`}
|
||||
className="text-[0.7rem] text-gold-deep hover:underline"
|
||||
>
|
||||
תיק {fb.case_number}
|
||||
</Link>
|
||||
)}
|
||||
{fb.resolved && (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 text-[0.7rem] border border-emerald-200">
|
||||
טופל
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-[0.7rem] text-ink-muted me-auto">
|
||||
{fb.created_at
|
||||
? new Date(fb.created_at).toLocaleDateString("he-IL")
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-6 py-4 space-y-3">
|
||||
<p className="text-sm leading-relaxed">{fb.feedback_text}</p>
|
||||
|
||||
{fb.lesson_extracted && (
|
||||
<div className="bg-gold/5 border border-gold/20 rounded-md px-4 py-3">
|
||||
<p className="text-[0.7rem] font-semibold text-gold-deep mb-1">
|
||||
לקח שהופק:
|
||||
</p>
|
||||
<p className="text-sm text-ink-muted leading-relaxed">
|
||||
{fb.lesson_extracted}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fb.resolved && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleResolve(fb.id)}
|
||||
disabled={resolveMutation.isPending}
|
||||
>
|
||||
סמן כמטופל
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── New feedback dialog ─────────────────────────────────── */
|
||||
|
||||
function NewFeedbackDialog() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const createMutation = useCreateFeedback();
|
||||
|
||||
const [caseNumber, setCaseNumber] = useState("");
|
||||
const [blockId, setBlockId] = useState("block-yod");
|
||||
const [category, setCategory] = useState<FeedbackCategory>("missing_content");
|
||||
const [feedbackText, setFeedbackText] = useState("");
|
||||
const [lesson, setLesson] = useState("");
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!feedbackText.trim()) return;
|
||||
|
||||
createMutation.mutate(
|
||||
{
|
||||
case_number: caseNumber || undefined,
|
||||
block_id: blockId,
|
||||
feedback_text: feedbackText,
|
||||
category,
|
||||
lesson_extracted: lesson || undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("ההערה נרשמה בהצלחה");
|
||||
setOpen(false);
|
||||
setCaseNumber("");
|
||||
setFeedbackText("");
|
||||
setLesson("");
|
||||
},
|
||||
onError: () => toast.error("שגיאה ברישום ההערה"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>+ הערה חדשה</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg" dir="rtl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>רישום הערת יו״ר</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="fb-case">מספר תיק (אופציונלי)</Label>
|
||||
<Input
|
||||
id="fb-case"
|
||||
value={caseNumber}
|
||||
onChange={(e) => setCaseNumber(e.target.value)}
|
||||
placeholder="1130-25"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="fb-block">בלוק</Label>
|
||||
<select
|
||||
id="fb-block"
|
||||
value={blockId}
|
||||
onChange={(e) => setBlockId(e.target.value)}
|
||||
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
|
||||
>
|
||||
{Object.entries(BLOCK_LABELS).map(([key, label]) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="fb-category">קטגוריה</Label>
|
||||
<select
|
||||
id="fb-category"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as FeedbackCategory)}
|
||||
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
|
||||
>
|
||||
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="fb-text">ההערה</Label>
|
||||
<Textarea
|
||||
id="fb-text"
|
||||
value={feedbackText}
|
||||
onChange={(e) => setFeedbackText(e.target.value)}
|
||||
placeholder="מה דפנה אמרה? מה חסר, מה לא נכון, מה צריך לשנות..."
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="fb-lesson">לקח שהופק (אופציונלי)</Label>
|
||||
<Textarea
|
||||
id="fb-lesson"
|
||||
value={lesson}
|
||||
onChange={(e) => setLesson(e.target.value)}
|
||||
placeholder="מה למדנו מההערה? מה צריך לשנות במערכת?"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
ביטול
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "שומר..." : "שמור הערה"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const NAV_ITEMS: NavItem[] = [
|
||||
{ href: "/", label: "בית" },
|
||||
{ href: "/cases/new", label: "תיק חדש" },
|
||||
{ href: "/training", label: "אימון סגנון" },
|
||||
{ href: "/feedback", label: "הערות יו״ר" },
|
||||
{ href: "/skills", label: "מיומנויות" },
|
||||
{ href: "/diagnostics", label: "אבחון" },
|
||||
];
|
||||
|
||||
112
web-ui/src/lib/api/feedback.ts
Normal file
112
web-ui/src/lib/api/feedback.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Chair feedback hooks — recording and managing Dafna's feedback on drafts.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "./client";
|
||||
|
||||
export type FeedbackCategory =
|
||||
| "missing_content"
|
||||
| "wrong_tone"
|
||||
| "wrong_structure"
|
||||
| "factual_error"
|
||||
| "style"
|
||||
| "other";
|
||||
|
||||
export type ChairFeedback = {
|
||||
id: string;
|
||||
case_id: string | null;
|
||||
case_number: string;
|
||||
block_id: string;
|
||||
category: FeedbackCategory;
|
||||
feedback_text: string;
|
||||
lesson_extracted: string;
|
||||
resolved: boolean;
|
||||
applied_to: string[];
|
||||
created_at: string | null;
|
||||
};
|
||||
|
||||
export type CreateFeedbackInput = {
|
||||
case_number?: string;
|
||||
block_id?: string;
|
||||
feedback_text: string;
|
||||
category?: FeedbackCategory;
|
||||
lesson_extracted?: string;
|
||||
};
|
||||
|
||||
const feedbackKeys = {
|
||||
all: ["feedback"] as const,
|
||||
list: (filters: { category?: string; unresolved_only?: boolean }) =>
|
||||
[...feedbackKeys.all, "list", filters] as const,
|
||||
};
|
||||
|
||||
export function useFeedbackList(filters: {
|
||||
category?: string;
|
||||
unresolved_only?: boolean;
|
||||
} = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.category) params.set("category", filters.category);
|
||||
if (filters.unresolved_only) params.set("unresolved_only", "true");
|
||||
const qs = params.toString();
|
||||
|
||||
return useQuery({
|
||||
queryKey: feedbackKeys.list(filters),
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<ChairFeedback[]>(`/api/feedback${qs ? `?${qs}` : ""}`, { signal }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateFeedback() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateFeedbackInput) =>
|
||||
apiRequest<{ id: string; status: string }>("/api/feedback/json", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolveFeedback() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
feedbackId,
|
||||
applied_to,
|
||||
}: {
|
||||
feedbackId: string;
|
||||
applied_to: string[];
|
||||
}) =>
|
||||
apiRequest<{ status: string }>(
|
||||
`/api/feedback/${feedbackId}/resolve`,
|
||||
{ method: "PATCH", body: { applied_to } },
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Hebrew labels for feedback categories */
|
||||
export const CATEGORY_LABELS: Record<FeedbackCategory, string> = {
|
||||
missing_content: "תוכן חסר",
|
||||
wrong_tone: "טון שגוי",
|
||||
wrong_structure: "מבנה שגוי",
|
||||
factual_error: "שגיאה עובדתית",
|
||||
style: "סגנון",
|
||||
other: "אחר",
|
||||
};
|
||||
|
||||
/** Block ID labels */
|
||||
export const BLOCK_LABELS: Record<string, string> = {
|
||||
"block-he": "ה — פתיחה",
|
||||
"block-vav": "ו — רקע עובדתי",
|
||||
"block-zayin": "ז — טענות הצדדים",
|
||||
"block-chet": "ח — הליכים",
|
||||
"block-tet": "ט — תכניות חלות",
|
||||
"block-yod": "י — דיון והכרעה",
|
||||
"block-yod-alef": "יא — סיכום",
|
||||
};
|
||||
Reference in New Issue
Block a user