Files
legal-ai/web-ui/src/app/feedback/page.tsx
Chaim 4174217179 feat(feedback): סימון "יושם" מפעיל CEO לקיפול הלקח לקובץ הנכון
סוגר את לולאת פידבק-יו"ר→ידע-סוכנים. עד כה resolve רק עדכן את ה-DB; עכשיו
לחיצה ב-/feedback מעירה את ה-CEO שמקפל את הלקח לקובץ לפי הקטגוריה.

- paperclip_client.py: wake_ceo_for_feedback_fold() — יוצר issue ב-Paperclip
  עם הלקח + rubric ניתוב (style→SKILL.md, wrong_structure→block-schema,
  אחר→lessons.md), מעיר CEO. משכפל את דפוס wake_for_precedent_extraction
- db.py: get_chair_feedback(id) — שליפת הערה בודדת עם case_number/appeal_type
- app.py: resolve endpoint מקבל fold (ברירת מחדל true); BackgroundTask
  fire-and-forget; guard — רק עם lesson_extracted. מחזיר fold_queued
- legal-ceo.md: dispatch ל-feedback_fold_ + סעיף "קיפול הערת יו"ר" עם rubric
- frontend: useResolveFeedback מקבל fold; /feedback שולח fold=true עם toast;
  drafts-panel שולח fold=false (bookkeeping per-case, בלי קיפול כפול)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:08:41 +00:00

258 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import { Check, CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
useFeedbackList,
useResolveFeedback,
CATEGORY_LABELS,
CATEGORY_COLORS,
BLOCK_LABELS,
type ChairFeedback,
type FeedbackCategory,
} from "@/lib/api/feedback";
/**
* מרכז הערות יו"ר — הדף המרכזי לטיפול בכל הערות דפנה שנרשמו על טיוטות
* (chair_feedback). מאגד הערות מכל התיקים במקום אחד, מאפשר סינון לפי
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback.
*/
function formatDate(iso?: string | null): string {
if (!iso) return "";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "numeric",
month: "long",
year: "numeric",
});
} catch {
return "";
}
}
function FeedbackCard({ fb }: { fb: ChairFeedback }) {
const resolve = useResolveFeedback();
const onResolve = () => {
resolve.mutate(
{ feedbackId: fb.id, applied_to: [], fold: true },
{
onSuccess: (res) =>
toast.success(
res.fold_queued
? "סומנה כיושמה — הלקח נשלח ל-CEO לקיפול לקובץ הידע"
: "ההערה סומנה כיושמה",
),
onError: (e) =>
toast.error(e instanceof Error ? e.message : "שגיאה בסימון"),
},
);
};
return (
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}>
<CardContent className="px-5 py-4 space-y-2.5">
<div className="flex items-start gap-2 flex-wrap">
<Badge variant="outline" className={`text-[0.7rem] ${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/${encodeURIComponent(fb.case_number)}`}
className="text-[0.72rem] text-gold-deep hover:underline"
>
תיק {fb.case_number}
</Link>
) : (
<span className="text-[0.72rem] text-ink-muted">ללא תיק</span>
)}
<span className="ms-auto text-[0.72rem] text-ink-muted">
{formatDate(fb.created_at)}
</span>
</div>
<p className="text-navy text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
{fb.feedback_text}
</p>
{fb.lesson_extracted ? (
<div className="rounded-md bg-gold-wash/40 border border-gold/30 px-3 py-2">
<div className="text-[0.68rem] text-gold-deep mb-0.5">לקח שהופק</div>
<p className="text-ink-soft text-[0.82rem] leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
{fb.lesson_extracted}
</p>
</div>
) : null}
<div className="flex items-center justify-end pt-1 border-t border-rule-soft">
{fb.resolved ? (
<span className="flex items-center gap-1 text-[0.78rem] text-emerald-700">
<CheckCircle2 className="w-3.5 h-3.5" /> יושמה
</span>
) : (
<Button
size="sm"
onClick={onResolve}
disabled={resolve.isPending}
className="bg-gold text-navy hover:bg-gold-deep"
>
<Check className="w-3.5 h-3.5 me-1" />
סמן כיושם
</Button>
)}
</div>
</CardContent>
</Card>
);
}
type CatFilter = "all" | FeedbackCategory;
export default function FeedbackPage() {
const [unresolvedOnly, setUnresolvedOnly] = useState(true);
const [category, setCategory] = useState<CatFilter>("all");
const { data, isPending, error } = useFeedbackList({
unresolved_only: unresolvedOnly,
category: category === "all" ? undefined : category,
});
const items = useMemo(() => data ?? [], [data]);
const unresolvedCount = useMemo(
() => items.filter((f) => !f.resolved).length,
[items],
);
const categories: { key: CatFilter; label: string }[] = [
{ key: "all", label: "הכל" },
{ key: "missing_content", label: CATEGORY_LABELS.missing_content },
{ key: "wrong_tone", label: CATEGORY_LABELS.wrong_tone },
{ key: "wrong_structure", label: CATEGORY_LABELS.wrong_structure },
{ key: "factual_error", label: CATEGORY_LABELS.factual_error },
{ key: "style", label: CATEGORY_LABELS.style },
{ key: "other", label: CATEGORY_LABELS.other },
];
return (
<AppShell>
<section className="space-y-6">
<header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<Link href="/approvals" className="hover:text-gold-deep">מרכז אישורים</Link>
<span aria-hidden> · </span>
<span className="text-navy">הערות יו״ר</span>
</nav>
<div className="flex items-end justify-between gap-4 flex-wrap">
<div className="space-y-1">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
שערים אנושיים · יו״ר הוועדה
</div>
<h1 className="text-navy mb-0">הערות יו״ר</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
כל ההערות שנרשמו על טיוטות מכל התיקים. סמן כל הערה כיושמה
לאחר שהלקח הוטמע.
</p>
</div>
<div className="text-end">
<div className="text-3xl font-semibold text-navy leading-none">
{unresolvedCount}
</div>
<div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1">
טרם יושמו
</div>
</div>
</div>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Filters */}
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setUnresolvedOnly(true)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
unresolvedOnly
? "bg-navy text-parchment border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
טרם יושמו
</button>
<button
type="button"
onClick={() => setUnresolvedOnly(false)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
!unresolvedOnly
? "bg-navy text-parchment border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
הכל
</button>
</div>
<div className="flex items-center gap-1 flex-wrap ms-auto">
{categories.map((c) => (
<button
key={c.key}
type="button"
onClick={() => setCategory(c.key)}
className={`text-[0.74rem] px-2.5 py-1 rounded border transition-colors ${
category === c.key
? "bg-gold-wash text-gold-deep border-gold/40"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
{c.label}
</button>
))}
</div>
</div>
{error ? (
<Card className="bg-surface border-rule">
<CardContent className="px-6 py-5 text-ink-muted text-sm">
שגיאה בטעינת ההערות. נסה לרענן.
</CardContent>
</Card>
) : isPending ? (
<div className="space-y-3">
{[0, 1, 2].map((i) => (
<Card key={i} className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 h-28 animate-pulse" />
</Card>
))}
</div>
) : items.length === 0 ? (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">אין הערות בקטגוריה זו.</p>
{unresolvedOnly && (
<p className="text-sm mt-2">כל ההערות יושמו </p>
)}
</div>
) : (
<div className="space-y-3">
{items.map((fb) => (
<FeedbackCard key={fb.id} fb={fb} />
))}
</div>
)}
</section>
</AppShell>
);
}