- adapter_profiles.py: codex_local.default_model = gpt-5.5 (עדכון מ-gpt-5.3-codex) - agent-adapters-panel.tsx: כפתור "העבר הכל ל-Codex ⚡" בסרגל-החירום (ב-G/Codex fallback) - operations.ts: הוספת codex_local לדוקומנטציה של useAdapterMigrate - ab_halacha_codex.py: סקריפט A/B חדש — חילוץ הלכות דרך codex exec/gpt-5.5 (non-destructive benchmark) - SCRIPTS.md: תיעוד ab_halacha_codex.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
324 lines
15 KiB
TypeScript
324 lines
15 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* מתאמי-סוכנים (Adapter) — /operations panel.
|
||
*
|
||
* Migrate any committee agent between run-engines (Claude / Gemini / DeepSeek / Codex)
|
||
* in BOTH companies at once, with a preflight that prevents the silent crash
|
||
* (frontmatter `---` breaks the gemini/deepseek CLI; model must match provider;
|
||
* gemini excludeTools is global). Built to the Claude Design mockup
|
||
* `02d-operations-adapters.html`. The actual migration runs host-side
|
||
* (scripts/migrate_agent_adapter.py) via the court-fetch bridge — see
|
||
* useMigrateAdapter / POST /api/operations/agents/migrate-adapter.
|
||
*/
|
||
|
||
import { useState } from "react";
|
||
import { toast } from "sonner";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
Select,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
SelectContent,
|
||
SelectItem,
|
||
} from "@/components/ui/select";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
} from "@/components/ui/dialog";
|
||
import {
|
||
usePaperclipAgents,
|
||
type AgentPair,
|
||
} from "@/lib/api/paperclip-agents";
|
||
import {
|
||
useMigrateAdapter,
|
||
type MigrateAdapterRequest,
|
||
type MigrateAdapterResult,
|
||
} from "@/lib/api/operations";
|
||
|
||
const ADAPTERS = [
|
||
{ value: "claude_local", label: "Claude", cls: "bg-info-bg text-info border-info/40" },
|
||
{ value: "gemini_local", label: "Gemini", cls: "bg-warn-bg text-warn border-warn/40" },
|
||
{ value: "deepseek_local", label: "DeepSeek", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||
{ value: "codex_local", label: "Codex", cls: "bg-success-bg text-success border-success/40" },
|
||
] as const;
|
||
|
||
// Bilingual role subtitle under the agent name (mockup 02d) — display-only,
|
||
// keyed by the agent's Hebrew name. Falls back to none for unmapped agents.
|
||
const ROLE_SUBTITLE: Record<string, string> = {
|
||
"עוזר משפטי": "CEO · מתזמר",
|
||
"מנתח משפטי": "analyst",
|
||
"כותב החלטה": "writer",
|
||
"מנהל ידע": "אוצֵר-הלכות",
|
||
"בודק איכות": "qa",
|
||
"הגהת מסמכים": "proofreader",
|
||
"שטן מליץ": "red-team (Gemini)",
|
||
};
|
||
|
||
function adapterCls(a: string | null | undefined): string {
|
||
return ADAPTERS.find((x) => x.value === a)?.cls ?? "bg-rule-soft text-ink-muted border-rule";
|
||
}
|
||
function adapterLabel(a: string | null | undefined): string {
|
||
return ADAPTERS.find((x) => x.value === a)?.label ?? a ?? "—";
|
||
}
|
||
|
||
/** A pending confirmation: the apply/revert to run, plus its preflight result. */
|
||
type PendingDialog = {
|
||
title: string;
|
||
body: MigrateAdapterRequest; // action is "apply" or "revert"
|
||
preflight: MigrateAdapterResult | null;
|
||
loadingPreflight: boolean;
|
||
relax: boolean;
|
||
showRelax: boolean;
|
||
};
|
||
|
||
export function AgentAdaptersPanel() {
|
||
const { data, isLoading } = usePaperclipAgents();
|
||
const migrate = useMigrateAdapter();
|
||
const [targets, setTargets] = useState<Record<string, string>>({});
|
||
const [dlg, setDlg] = useState<PendingDialog | null>(null);
|
||
const busy = migrate.isPending;
|
||
|
||
const currentOf = (p: AgentPair) =>
|
||
p.master?.adapter_type ?? p.mirror?.adapter_type ?? null;
|
||
const modelOf = (p: AgentPair) => p.master?.model ?? p.mirror?.model ?? "";
|
||
const isAsymmetric = (p: AgentPair) =>
|
||
!!p.master && !!p.mirror && p.master.adapter_type !== p.mirror.adapter_type;
|
||
const targetFor = (p: AgentPair) => targets[p.name] ?? currentOf(p) ?? "claude_local";
|
||
|
||
// Run a preflight (check) and open the confirm dialog pre-loaded with its output.
|
||
async function openMigrate(agent: string, to: string, title: string) {
|
||
setDlg({ title, body: { action: "apply", agent, to, relax_tools: true },
|
||
preflight: null, loadingPreflight: true, relax: true, showRelax: false });
|
||
try {
|
||
const res = await migrate.mutateAsync({ action: "check", agent, to });
|
||
const conflict = /excludeTools|כלי-כתיבה/.test(res.stdout);
|
||
setDlg((d) => d && { ...d, preflight: res, loadingPreflight: false,
|
||
showRelax: conflict, relax: conflict });
|
||
} catch (e) {
|
||
setDlg((d) => d && { ...d, loadingPreflight: false,
|
||
preflight: { ok: false, exit_code: -1, stdout: "", stderr: String(e) } });
|
||
}
|
||
}
|
||
|
||
function openRevert(agent: string, title: string) {
|
||
setDlg({ title, body: { action: "revert", agent }, preflight: null,
|
||
loadingPreflight: false, relax: false, showRelax: false });
|
||
}
|
||
|
||
// Confirm → run the dialog's apply/revert and toast on the script's exit code.
|
||
async function confirmDialog() {
|
||
if (!dlg) return;
|
||
const body = { ...dlg.body, relax_tools: dlg.relax };
|
||
try {
|
||
const res = await migrate.mutateAsync(body);
|
||
if (res.exit_code === 0) {
|
||
toast.success(dlg.body.action === "revert" ? "הוחזר ל-Claude" : "המעבר הושלם בשתי החברות");
|
||
} else {
|
||
toast.error("הפעולה נכשלה — ראה פלט");
|
||
// keep dialog open so the operator sees the failure output
|
||
setDlg((d) => d && { ...d, preflight: res, loadingPreflight: false });
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
toast.error(`שגיאה: ${String(e)}`);
|
||
return;
|
||
}
|
||
setDlg(null);
|
||
}
|
||
|
||
const preflightBlocked = !!dlg?.preflight && dlg.body.action === "apply" && dlg.preflight.exit_code !== 0;
|
||
|
||
return (
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-6 py-5">
|
||
<p className="text-ink-muted text-xs mb-4">
|
||
העברת כל סוכן בין מנועי-ההרצה (Claude · Gemini · DeepSeek · Codex), בשתי החברות יחד. כל מעבר
|
||
עובר preflight שמונע קריסה. מעבר ל-Gemini/DeepSeek = fallback מופחת-איכות — להחזיר ל-Claude
|
||
כשטוקני-Claude חוזרים.
|
||
</p>
|
||
|
||
{/* Emergency fallback bar */}
|
||
<div className="rounded-md border border-gold/40 bg-gold-wash px-4 py-3 mb-4 flex items-center gap-4 flex-wrap">
|
||
<div className="min-w-0">
|
||
<div className="text-[0.85rem] text-navy font-semibold">מצב-חירום · טוקני-Claude</div>
|
||
<div className="text-[0.7rem] text-ink-muted">
|
||
כשנגמרים הטוקנים — להפיל את כל הצוות ל-Gemini או ל-Codex בלחיצה, ולהחזיר כשחוזרים.
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2 ms-auto shrink-0">
|
||
<Button size="sm" variant="outline" disabled={busy}
|
||
onClick={() => openRevert("all", "החזרת כל הסוכנים ל-Claude (מצב-מקור)")}>
|
||
החזר הכל ל-Claude
|
||
</Button>
|
||
<Button size="sm" variant="default" disabled={busy}
|
||
onClick={() => openMigrate("all", "gemini_local", "העברת כל הסוכנים ל-Gemini ⚡")}>
|
||
העבר הכל ל-Gemini ⚡
|
||
</Button>
|
||
<Button size="sm" variant="default" disabled={busy}
|
||
className="bg-success text-white hover:bg-success/90"
|
||
onClick={() => openMigrate("all", "codex_local", "העברת כל הסוכנים ל-Codex (gpt-5.5) ⚡")}>
|
||
העבר הכל ל-Codex ⚡
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{isLoading || !data ? (
|
||
<Skeleton className="h-40 w-full" />
|
||
) : (
|
||
<div className="overflow-x-auto rounded-md border border-rule-soft">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="bg-parchment hover:bg-parchment border-rule">
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted">תפקיד</TableHead>
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted">מתאם נוכחי</TableHead>
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted">מודל</TableHead>
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted">העבר ל־</TableHead>
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted" aria-label="פעולה" />
|
||
<TableHead className="text-start text-[0.72rem] font-medium text-ink-muted">מצב</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{data.pairs.map((p) => {
|
||
const cur = currentOf(p);
|
||
const tgt = targetFor(p);
|
||
const migrated = cur !== "claude_local" && cur !== null;
|
||
const asym = isAsymmetric(p);
|
||
const subtitle = ROLE_SUBTITLE[p.name];
|
||
return (
|
||
<TableRow key={p.name} className={`border-rule-soft ${asym ? "bg-danger-bg/25" : ""}`}>
|
||
<TableCell className="py-2.5 align-middle">
|
||
<div className="text-[0.85rem] text-navy font-semibold leading-tight">{p.name}</div>
|
||
{subtitle ? (
|
||
<div className="text-[0.68rem] text-ink-muted leading-tight">{subtitle}</div>
|
||
) : null}
|
||
</TableCell>
|
||
<TableCell className="py-2.5 align-middle">
|
||
<Badge variant="outline" className={`font-normal ${adapterCls(cur)}`}>
|
||
{adapterLabel(cur)}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="py-2.5 align-middle">
|
||
<span className="text-[0.68rem] text-ink-soft font-mono" dir="ltr">
|
||
{modelOf(p) || "(default)"}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell className="py-2.5 align-middle">
|
||
<Select value={tgt} onValueChange={(v) => setTargets((t) => ({ ...t, [p.name]: v }))}>
|
||
<SelectTrigger size="sm" className="w-[8.5rem]">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{ADAPTERS.map((a) => (
|
||
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
<TableCell className="py-2.5 align-middle">
|
||
<div className="flex items-center gap-1.5">
|
||
<Button size="xs" variant="default" disabled={busy || tgt === cur}
|
||
onClick={() => openMigrate(p.name, tgt, `העברת "${p.name}" → ${adapterLabel(tgt)}`)}>
|
||
העבר
|
||
</Button>
|
||
{migrated ? (
|
||
<Button size="xs" variant="ghost" disabled={busy}
|
||
onClick={() => openRevert(p.name, `החזרת "${p.name}" למצב-מקור`)}>
|
||
החזר
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="py-2.5 align-middle">
|
||
{asym ? (
|
||
<Badge variant="outline" className="font-normal bg-danger-bg text-danger border-danger/40 whitespace-nowrap">
|
||
⚠ א-סימטרי
|
||
</Badge>
|
||
) : migrated ? (
|
||
<Badge variant="outline" className="font-normal bg-warn-bg text-warn border-warn/40 whitespace-nowrap">
|
||
מועבר · fallback
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="outline" className="font-normal bg-success-bg text-success border-success/40">
|
||
תקין
|
||
</Badge>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
|
||
{/* Confirm dialog: preflight output + apply/revert */}
|
||
<Dialog open={!!dlg} onOpenChange={(o) => !o && setDlg(null)}>
|
||
<DialogContent className="max-w-xl">
|
||
<DialogHeader>
|
||
<DialogTitle>{dlg?.title}</DialogTitle>
|
||
<DialogDescription>
|
||
{dlg?.body.action === "revert"
|
||
? "שחזור מדויק למתאם ולמודל שהיו לפני המעבר, בשתי החברות."
|
||
: "המעבר חל על שתי החברות (CMP + CMPA). Gemini/DeepSeek/Codex = fallback מופחת-איכות."}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{dlg?.body.action === "apply" ? (
|
||
<div className="space-y-3">
|
||
<div className="text-[0.72rem] text-ink-muted">preflight:</div>
|
||
{dlg.loadingPreflight ? (
|
||
<Skeleton className="h-24 w-full" />
|
||
) : (
|
||
<pre dir="ltr"
|
||
className={`text-[0.7rem] font-mono whitespace-pre-wrap max-h-56 overflow-auto rounded-md border p-2 ${
|
||
preflightBlocked ? "border-danger/40 bg-danger-bg" : "border-rule-soft bg-parchment"
|
||
}`}>
|
||
{(dlg.preflight?.stdout || "") + (dlg.preflight?.stderr || "") || "—"}
|
||
</pre>
|
||
)}
|
||
{dlg.showRelax ? (
|
||
<div className="flex items-center gap-2">
|
||
<Switch id="relax" checked={dlg.relax}
|
||
onCheckedChange={(c) => setDlg((d) => d && { ...d, relax: c })} />
|
||
<Label htmlFor="relax" className="text-[0.75rem] text-ink-soft">
|
||
שחרר כלי-כתיבה חסומים (excludeTools גלובלי לכל סוכני-Gemini)
|
||
</Label>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" size="sm" onClick={() => setDlg(null)} disabled={busy}>
|
||
ביטול
|
||
</Button>
|
||
<Button size="sm" disabled={busy || dlg?.loadingPreflight || preflightBlocked}
|
||
onClick={confirmDialog}>
|
||
{dlg?.body.action === "revert" ? "החזר" : preflightBlocked ? "preflight חוסם" : "אשר העברה"}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</Card>
|
||
);
|
||
}
|