From ee76455a9ad3f774a14ddbe6c7dab7d8a4486972 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 6 Jun 2026 19:13:10 +0000 Subject: [PATCH] =?UTF-8?q?feat(style-acq=20T6+T13):=20=D7=A4=D7=A0=D7=A7?= =?UTF-8?q?=D7=A1-=D7=94=D7=AA=D7=90=D7=9E=D7=94=20+=20=D7=9E=D7=93=D7=93?= =?UTF-8?q?=20=D7=9E=D7=A8=D7=97=D7=A7-=D7=A1=D7=92=D7=A0=D7=95=D7=9F=20?= =?UTF-8?q?=D7=91-UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ה"איך מנהלים/רואים את הלמידה": טאב "למידה" ב-/training. - app.py: GET /api/learning/pairs (פנקס-ההתאמה — כל ההחלטות + סטטוס draft↔final, INV-LRN4) + GET /api/learning/style-distance/{case} (מדד T7). - web-ui: learning.ts hooks + LearningPanel (טבלת פנקס; לחיצה על תיק → מדד מרחק-הסגנון: שינוי draft→final, סטיית יחסי-זהב, אנטי-דפוסים) + טאב ב-/training. מכסה גם את T6 (רשימת כל ההחלטות הנסגרות מול הסופי). ללא endpoint-schema חדש לטיפוסים מחוללים (טיפוסים ידניים). G9, INV-LRN4. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-ui/src/app/training/page.tsx | 6 + .../components/training/learning-panel.tsx | 111 ++++++++++++++++++ web-ui/src/lib/api/learning.ts | 69 +++++++++++ web/app.py | 35 ++++++ 4 files changed, 221 insertions(+) create mode 100644 web-ui/src/components/training/learning-panel.tsx create mode 100644 web-ui/src/lib/api/learning.ts diff --git a/web-ui/src/app/training/page.tsx b/web-ui/src/app/training/page.tsx index 44f1f08..0bcce87 100644 --- a/web-ui/src/app/training/page.tsx +++ b/web-ui/src/app/training/page.tsx @@ -12,6 +12,7 @@ import { CorpusPanel } from "@/components/training/corpus-panel"; import { ComparePanel } from "@/components/training/compare-panel"; import { CuratorPortraitPanel } from "@/components/training/curator-portrait-panel"; import { ChatPanel } from "@/components/training/chat-panel"; +import { LearningPanel } from "@/components/training/learning-panel"; import { TrainingUploadDialog } from "@/components/training/upload-dialog"; export default function TrainingPage() { @@ -53,6 +54,7 @@ export default function TrainingPage() { פורטרט סגנון קורפוס השוואה + למידה הסוכן שיחה @@ -69,6 +71,10 @@ export default function TrainingPage() { + + + + diff --git a/web-ui/src/components/training/learning-panel.tsx b/web-ui/src/components/training/learning-panel.tsx new file mode 100644 index 0000000..21da129 --- /dev/null +++ b/web-ui/src/components/training/learning-panel.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, ChevronLeft } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + useReconciliationLedger, useStyleDistance, + type DraftFinalPair, +} from "@/lib/api/learning"; + +/** + * /training "למידה" tab (T6/T13) — the reconciliation ledger (INV-LRN4): every + * decision and its draft↔final comparison status, plus per-case style-distance + * (T7). The list is the chair's "have we closed each decision against Dafna's + * final?" surface. + */ + +const STATUS_LABEL: Record = { + final_received: "התקבל סופי — ממתין לניתוח", + analyzed: "נותח (הצעות ממתינות)", + lessons_folded: "לקחים הוטמעו", +}; +const STATUS_CLASS: Record = { + final_received: "bg-gold-wash text-gold-deep border-gold/40", + analyzed: "bg-navy-soft/20 text-navy", + lessons_folded: "bg-emerald-50 text-emerald-800 border-emerald-300/60", +}; + +function fmtDate(iso: string | null) { + if (!iso) return "—"; + try { return new Date(iso).toLocaleDateString("he-IL"); } catch { return iso; } +} + +function StyleDistanceDetail({ caseNumber }: { caseNumber: string }) { + const { data, isPending, error } = useStyleDistance(caseNumber); + if (isPending) return ; + if (error || data?.error) return

{data?.error || "שגיאה"}

; + if (!data) return null; + const s = data.summary; + return ( +
+
+ שינוי draft→final: {s.change_percent ?? "—"}% + סטיית יחסי-זהב מקס׳: {s.ratio_max_deviation_pp ?? "—"} נק׳ + אנטי-דפוסים: {s.anti_pattern_total} +
+ {Object.keys(data.golden_ratio_adherence.sections).length > 0 && ( +
+ {Object.entries(data.golden_ratio_adherence.sections).map(([sec, v]) => ( + 0 ? "text-danger" : ""}> + {sec}: {v.actual_pct}% (יעד {v.target[0]}-{v.target[1]}) + + ))} +
+ )} + {data.anti_pattern_hits.total > 0 && ( +
+ {Object.entries(data.anti_pattern_hits.by_pattern).map(([n, v]) => ( + {n}: {v.count} + ))} +
+ )} +
+ ); +} + +function Row({ pair }: { pair: DraftFinalPair }) { + const [open, setOpen] = useState(false); + return ( +
+ + {open && pair.case_number && ( +
+ )} +
+ ); +} + +export function LearningPanel() { + const { data, isPending, error } = useReconciliationLedger(); + if (error) return

שגיאה בטעינת הפנקס.

; + if (isPending) return ; + + const items = data?.items ?? []; + return ( +
+

+ פנקס-ההתאמה (INV-LRN4): כל החלטה נסגרת מול הסופי של דפנה; כל סופי מנותח מול הטיוטה. + לחיצה על תיק → מדד מרחק-הסגנון שלו. +

+ {items.length === 0 ? ( +

אין עדיין השוואות. הן נוצרות כשמסמנים החלטה כסופית.

+ ) : ( + items.map((p) => ) + )} +
+ ); +} diff --git a/web-ui/src/lib/api/learning.ts b/web-ui/src/lib/api/learning.ts new file mode 100644 index 0000000..0003db7 --- /dev/null +++ b/web-ui/src/lib/api/learning.ts @@ -0,0 +1,69 @@ +/** + * Style-acquisition learning surface (T6/T13) — reconciliation ledger + style-distance. + * Backs the /training "למידה" tab. Endpoints under /api/learning. + */ + +import { useQuery } from "@tanstack/react-query"; +import { apiRequest } from "./client"; + +export type DraftFinalPair = { + id: string; + case_id: string | null; + case_number: string; + title: string; + status: "final_received" | "analyzed" | "lessons_folded" | string; + change_percent: number | null; + created_at: string | null; + updated_at: string | null; +}; + +export type StyleDistance = { + case_number: string; + outcome: string; + golden_ratio_adherence: { + outcome: string; + total_words: number; + sections: Record; + max_deviation: number | null; + }; + anti_pattern_hits: { total: number; by_pattern: Record }; + draft_to_final_diff: { change_percent?: number } | null; + pair_status: string | null; + summary: { + ratio_max_deviation_pp: number | null; + anti_pattern_total: number; + change_percent: number | null; + }; + error?: string; +}; + +export const learningKeys = { + all: ["learning"] as const, + pairs: (status: string) => [...learningKeys.all, "pairs", status] as const, + distance: (caseNumber: string) => [...learningKeys.all, "distance", caseNumber] as const, +}; + +export function useReconciliationLedger(status = "") { + return useQuery({ + queryKey: learningKeys.pairs(status), + queryFn: ({ signal }) => + apiRequest<{ items: DraftFinalPair[]; count: number }>( + `/api/learning/pairs${status ? `?status=${status}` : ""}`, + { signal }, + ), + staleTime: 15_000, + }); +} + +export function useStyleDistance(caseNumber: string | null) { + return useQuery({ + queryKey: learningKeys.distance(caseNumber ?? ""), + queryFn: ({ signal }) => + apiRequest( + `/api/learning/style-distance/${encodeURIComponent(caseNumber!)}`, + { signal }, + ), + enabled: Boolean(caseNumber), + staleTime: 15_000, + }); +} diff --git a/web/app.py b/web/app.py index 9e60690..34f9353 100644 --- a/web/app.py +++ b/web/app.py @@ -4116,6 +4116,41 @@ async def api_reset_methodology(category: str, key: str): return {"key": key, "value": _METHODOLOGY_DEFAULTS[category][key], "is_override": False} +# ── Style-acquisition learning surface (T6/T13) ──────────────────── + +@app.get("/api/learning/pairs") +async def api_learning_pairs(status: str = "", limit: int = 200): + """פנקס-ההתאמה (INV-LRN4) — כל ההחלטות וסטטוס ההשוואה מול הסופי. + status אופציונלי: final_received / analyzed / lessons_folded.""" + rows = await db.list_draft_final_pairs(status=status or None, limit=limit) + items = [] + for r in rows: + ds = r.get("diff_stats") + if isinstance(ds, str): + try: + ds = json.loads(ds) + except (json.JSONDecodeError, TypeError): + ds = None + items.append({ + "id": str(r["id"]), + "case_id": str(r["case_id"]) if r.get("case_id") else None, + "case_number": r.get("case_number") or "", + "title": r.get("title") or "", + "status": r.get("status") or "", + "change_percent": (ds or {}).get("change_percent") if ds else None, + "created_at": r["created_at"].isoformat() if r.get("created_at") else None, + "updated_at": r["updated_at"].isoformat() if r.get("updated_at") else None, + }) + return {"items": items, "count": len(items)} + + +@app.get("/api/learning/style-distance/{case_number}") +async def api_learning_style_distance(case_number: str): + """מדד מרחק-סגנון (T7) לתיק — האם הטיוטה מתכנסת לדפנה.""" + from legal_mcp.services import style_distance as _sd + return await _sd.style_distance(case_number) + + # ── Skill Management API ───────────────────────────────────────────