Merge pull request 'fix(learning): כרטיס-אוצֵר כן-3-ערוצים + לכידת ממצאי-אוצֵר (source='curator')' (#339) from worktree-curator-learning-surface into main
This commit was merged in pull request #339.
This commit is contained in:
@@ -45,6 +45,15 @@ export function CuratorPortraitPanel() {
|
||||
<StatsCard />
|
||||
<RecentFindings />
|
||||
|
||||
<div className="bg-info-bg border border-info/40 rounded-lg px-4 py-3 flex items-start gap-2.5 text-[0.78rem] text-ink-soft leading-relaxed">
|
||||
<span>🔒</span>
|
||||
<div>
|
||||
<b className="text-navy">שער-היו״ר (INV-LRN1 / G10):</b> כל הערוצים <b>מציעים</b> בלבד.
|
||||
ממצא או לקח משפיע על הכתיבה רק אחרי שדפנה אישרה אותו. האוצֵר עצמו read-only על התוכן —
|
||||
מזהה דפוסים ומגיש, לעולם לא משנה קבצים או סגנון בעצמו.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="curator-prompt" dir="rtl">
|
||||
<TabsList className="bg-rule-soft/60">
|
||||
<TabsTrigger value="curator-prompt">פרומפט ה-Curator</TabsTrigger>
|
||||
@@ -67,38 +76,127 @@ export function CuratorPortraitPanel() {
|
||||
|
||||
// ── stats card ─────────────────────────────────────────────────────
|
||||
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-bold mb-3">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** One of the three learning channels — engine, backing store, count, flow gate. */
|
||||
function ChannelCard({
|
||||
letter, title, engine, target, store, value, unit, flows, flowLabel, meta,
|
||||
}: {
|
||||
letter: string; title: string; engine: string; target: string; store: string;
|
||||
value: number; unit: string; flows: boolean; flowLabel: string; meta: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule">
|
||||
<CardContent className="px-[18px] py-4 flex flex-col gap-2.5">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="w-[22px] h-[22px] rounded-md flex items-center justify-center font-bold text-xs text-parchment shrink-0 bg-navy">
|
||||
{letter}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="text-[0.9rem] text-navy font-semibold leading-tight">{title}</h3>
|
||||
<div className="text-[0.72rem] text-ink-muted">{engine}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-soft bg-rule-soft rounded-md px-2.5 py-1.5">
|
||||
← {target} <code className="text-[0.68rem] text-gold-deep">{store}</code>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-[1.9rem] leading-none font-bold text-navy tabular-nums">{value}</span>
|
||||
<span className="text-[0.78rem] text-ink-muted">{unit}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-full text-[0.72rem] font-semibold px-2.5 py-0.5 w-fit border ${
|
||||
flows
|
||||
? "bg-success-bg text-success border-success/40"
|
||||
: "bg-gold-wash text-gold-deep border-gold/40"
|
||||
}`}
|
||||
>
|
||||
<span className="text-[0.5rem]">●</span>{flowLabel}
|
||||
</span>
|
||||
<div className="text-[0.72rem] text-ink-muted leading-relaxed">{meta}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsCard() {
|
||||
const { data, isPending } = useCuratorStats();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-20 w-full" />)}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-44 w-full" />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
const { distillation: a, panel: b, curator: c } = data.channels;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Kpi label="ממצאי curator" value={data.total_findings} icon={<Sparkles className="w-4 h-4" />} />
|
||||
<Kpi label="החלטות שנסקרו" value={`${data.decisions_with_findings}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
|
||||
<Kpi label="ממצאים מאושרים (זורמים לכותב)" value={data.findings_approved} icon={<CheckCircle2 className="w-4 h-4" />} />
|
||||
<Kpi label="ממוצע ממצאים להחלטה"
|
||||
value={
|
||||
data.decisions_with_findings > 0
|
||||
? (data.total_findings / data.decisions_with_findings).toFixed(1)
|
||||
: "—"
|
||||
}
|
||||
icon={<Brain className="w-4 h-4" />}
|
||||
/>
|
||||
<div className="space-y-5">
|
||||
<div className="bg-gold-wash border border-gold rounded-lg px-5 py-4 flex items-start gap-3">
|
||||
<span className="text-gold-deep text-xl leading-tight">★</span>
|
||||
<p className="text-ink-soft text-sm leading-relaxed">
|
||||
הלמידה זורמת לכותב ב<b className="text-gold-deep font-semibold">שלושה ערוצים נפרדים</b>, כל אחד
|
||||
מופעל כשדפנה מסמנת החלטה כסופית. הכרטיס מראה כמה כל ערוץ תרם ומה כבר עובר בפועל לכותב —
|
||||
רק ידע ש<b className="text-gold-deep font-semibold">אושר בשער-היו״ר</b> משפיע על הטיוטות (INV-LRN1).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionLabel>שלושת ערוצי-ההזנה לכותב</SectionLabel>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<ChannelCard
|
||||
letter="א" title="דיסטילציה" engine="Claude · השוואת טיוטה↔סופי"
|
||||
target="כללי-מתודולוגיה" store="appeal_type_rules"
|
||||
value={a.items_total} unit="פריטים זורמים"
|
||||
flows={a.items_total > 0} flowLabel="זורם לכותב"
|
||||
meta={`${a.discussion_rules} כללי-דיון + ${a.transition_phrases} ביטויי-מעבר · אושרו ב-${a.pairs_folded} זוגות דרך טאב ״למידה״.`}
|
||||
/>
|
||||
<ChannelCard
|
||||
letter="ב" title="פאנל דו-סוכני" engine="DeepSeek + Gemini · הצבעה 2/2"
|
||||
target="לקחי-סגנון" store="decision_lessons"
|
||||
value={b.total} unit="לקחים"
|
||||
flows={b.approved > 0} flowLabel={`${b.approved} אושרו · ${b.proposed} ממתינים`}
|
||||
meta={`על פני ${b.decisions} החלטות · אישור per-החלטה בטאב הקורפוס.`}
|
||||
/>
|
||||
<ChannelCard
|
||||
letter="ג" title="ממצאי-אוצֵר" engine="האוצֵר · קריאת הסופי + דפוסים"
|
||||
target="ממצאי-סגנון" store="source=curator"
|
||||
value={c.total} unit="ממצאים"
|
||||
flows={c.approved > 0} flowLabel={`${c.approved} אושרו · ${c.proposed} ממתינים`}
|
||||
meta="3–5 דפוסים מתויגים להחלטה (סגנון/מבנה/לקסיקון/טבלאי) — נתפסים מבנית, לא רק כהערה."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionLabel>ערוץ האוצֵר — מבט מקרוב</SectionLabel>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Kpi label="סך ממצאי-אוצֵר" value={c.total} icon={<Sparkles className="w-4 h-4" />} />
|
||||
<Kpi label="החלטות שנסקרו" value={`${c.decisions_reviewed}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
|
||||
<Kpi label="מאושרים · זורמים לכותב" value={c.approved} tone="success" icon={<CheckCircle2 className="w-4 h-4" />} />
|
||||
<Kpi label="ממתינים לאישור" value={c.proposed} tone="gold" icon={<Brain className="w-4 h-4" />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Kpi({
|
||||
label, value, icon,
|
||||
}: { label: string; value: string | number; icon: React.ReactNode }) {
|
||||
label, value, icon, tone,
|
||||
}: { label: string; value: string | number; icon: React.ReactNode; tone?: "success" | "gold" }) {
|
||||
const valueCls = tone === "success" ? "text-success" : tone === "gold" ? "text-gold-deep" : "text-navy";
|
||||
return (
|
||||
<Card className="bg-surface border-rule">
|
||||
<CardContent className="px-4 py-3">
|
||||
@@ -106,7 +204,7 @@ function Kpi({
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<p className="text-2xl text-navy font-semibold tabular-nums mt-1">{value}</p>
|
||||
<p className={`text-2xl font-semibold tabular-nums mt-1 ${valueCls}`}>{value}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -123,10 +221,10 @@ function RecentFindings() {
|
||||
if (!data || data.recent_findings.length === 0) {
|
||||
return (
|
||||
<Card className="bg-rule-soft/40 border-rule">
|
||||
<CardContent className="px-6 py-5 text-center text-ink-muted text-sm">
|
||||
אין עדיין ממצאים של ה-Curator. הוא מופעל אוטומטית כאשר דפנה מסמנת
|
||||
החלטה כסופית (mark-final), ושומר את ממצאיו כ-decision_lessons עם
|
||||
source="curator".
|
||||
<CardContent className="px-6 py-5 text-center text-ink-muted text-sm leading-relaxed">
|
||||
אין עדיין ממצאי-אוצֵר. כשדפנה מסמנת החלטה כסופית, האוצֵר קורא את הסופי, מזהה
|
||||
3–5 דפוסי-סגנון, ושומר אותם כ-<code>decision_lessons</code> (source="curator")
|
||||
הממתינים לאישורה — בנפרד מלקחי-הפאנל ומכללי-המתודולוגיה שלמעלה.
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -197,7 +197,7 @@ export function StyleReportPanel() {
|
||||
<h3 className="text-navy text-lg m-0">אוצֵר — ממצאי-סגנון</h3>
|
||||
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
|
||||
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
|
||||
{curator.data ? curator.data.findings_approved : "—"}
|
||||
{curator.data ? curator.data.channels.curator.approved : "—"}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<b className="text-navy text-sm font-semibold block">
|
||||
@@ -214,8 +214,8 @@ export function StyleReportPanel() {
|
||||
{curator.data
|
||||
? Math.max(
|
||||
0,
|
||||
curator.data.total_findings -
|
||||
curator.data.findings_approved,
|
||||
curator.data.channels.curator.total -
|
||||
curator.data.channels.curator.approved,
|
||||
)
|
||||
: "—"}
|
||||
</div>
|
||||
@@ -223,7 +223,7 @@ export function StyleReportPanel() {
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg border border-rule bg-rule-soft px-3.5 py-3">
|
||||
<div className="text-ink-muted font-bold text-[1.35rem] tabular-nums leading-none">
|
||||
{curator.data ? curator.data.total_findings : "—"}
|
||||
{curator.data ? curator.data.channels.curator.total : "—"}
|
||||
</div>
|
||||
<div className="text-[0.78rem] text-ink-soft mt-1">סך ממצאים</div>
|
||||
</div>
|
||||
|
||||
@@ -302,14 +302,39 @@ export type CuratorFinding = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
// One decision_lessons channel split by the INV-LRN1 review gate.
|
||||
export type LessonChannelStats = {
|
||||
total: number;
|
||||
approved: number;
|
||||
proposed: number;
|
||||
rejected: number;
|
||||
};
|
||||
|
||||
// The three honest learning channels that feed the writer (INV-IA2/IA5).
|
||||
// Earlier this was a single flat count of source='curator' — which nothing
|
||||
// wrote, so it always read 0 while the panel/methodology channels carried all
|
||||
// the real learning. See get_curator_stats (web/app.py).
|
||||
export type CuratorStats = {
|
||||
total_findings: number;
|
||||
decisions_with_findings: number;
|
||||
decisions_total: number;
|
||||
// LRN-5 (INV-IA5): the count of curator lessons with review_status='approved'
|
||||
// (the real INV-LRN1 writer gate) — replaces the old findings_applied, which
|
||||
// counted the informative-only applied_to_skill flag.
|
||||
findings_approved: number;
|
||||
channels: {
|
||||
// A — Claude draft↔final distillation → methodology overrides; flows now.
|
||||
distillation: {
|
||||
discussion_rules: number;
|
||||
transition_phrases: number;
|
||||
items_total: number;
|
||||
pairs_folded: number;
|
||||
pairs_total: number;
|
||||
};
|
||||
// B — DeepSeek+Gemini panel → decision_lessons; flows when chair-approved.
|
||||
panel: LessonChannelStats & {
|
||||
decisions: number;
|
||||
by_practice_area: Record<string, number>;
|
||||
};
|
||||
// C — curator agent's qualitative findings → decision_lessons (source='curator').
|
||||
curator: LessonChannelStats & {
|
||||
decisions_reviewed: number;
|
||||
};
|
||||
};
|
||||
recent_findings: CuratorFinding[];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user