AUDIT / src /components /DebugFixBanner.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
6.96 kB
/**
* DebugFixBanner.tsx — Live Auto-Debug Toast (S60)
*
* Mostra all'utente cosa sta facendo il loop agentLoop → debugLoop in tempo reale.
* Appare automaticamente quando un tool run_code / execute_shell fallisce e
* l'agente tenta di auto-ripararlo.
*
* Stati:
* fixing → pillola arancio animata "🔧 Auto-debug in corso…"
* fixed → pillola verde "✅ Codice corretto automaticamente!" (sparisce dopo 4s)
* failed → pillola rossa "❌ Auto-debug: impossibile correggere" (sparisce dopo 3s)
*
* iPhone-safe: solo CSS transitions, zero Worker/SharedArrayBuffer.
* Non mostra StreamChunk — filtra solo live_debug_* events.
*/
import { memo, useState, useEffect, useCallback, useRef } from "react";
import type * as React from "react"; // FIX11: React namespace types
import { Wrench, CheckCircle, XCircle } from "lucide-react";
import { useAnyEvent } from "@/core/events/useEventStream";
import type { RuntimeEvent } from "@/core/events/EventTypes";
import { Z_INDEX } from "@/lib/zindex";
// ─── Tipi stato ───────────────────────────────────────────────────────────────
type DebugState =
| { phase: "idle" }
| { phase: "fixing"; taskId: string; attempt: number; toolName: string }
| { phase: "fixed"; taskId: string; attempts: number; snippet?: string }
| { phase: "failed"; taskId: string; reason: string };
// ─── Colori e icone per fase ──────────────────────────────────────────────────
function stateStyle(phase: DebugState["phase"]): React.CSSProperties {
if (phase === "fixing") return {
background: "linear-gradient(135deg, rgba(251,146,60,0.18), rgba(245,158,11,0.12))",
border: "1px solid rgba(251,146,60,0.4)",
color: "#fb923c",
};
if (phase === "fixed") return {
background: "linear-gradient(135deg, rgba(74,222,128,0.18), rgba(59,130,246,0.12))",
border: "1px solid rgba(74,222,128,0.4)",
color: "#4ade80",
};
return {
background: "linear-gradient(135deg, rgba(248,113,113,0.18), rgba(239,68,68,0.10))",
border: "1px solid rgba(248,113,113,0.35)",
color: "#f87171",
};
}
// ─── Pillola principale ───────────────────────────────────────────────────────
function DebugPill({ state }: { state: Exclude<DebugState, { phase: "idle" }> }) {
const { phase } = state;
const style = stateStyle(phase);
const icon =
phase === "fixing" ? <Wrench size={13} style={{ flexShrink: 0, animation: "debug-spin 1.6s linear infinite" }} /> :
phase === "fixed" ? <CheckCircle size={13} style={{ flexShrink: 0 }} /> :
<XCircle size={13} style={{ flexShrink: 0 }} />;
const label =
phase === "fixing"
? `Auto-debug ${state.toolName}: tentativo ${state.attempt}…`
: phase === "fixed"
? `Codice corretto in ${state.attempts} tentativ${state.attempts === 1 ? "o" : "i"}`
: `Auto-debug non riuscito`;
const sub =
phase === "fixed" && (state as { phase: "fixed"; snippet?: string }).snippet
? (state as { phase: "fixed"; snippet?: string }).snippet!.slice(0, 80)
: phase === "failed"
? (state as { phase: "failed"; reason: string }).reason
: "";
return (
<div style={{
display: "flex", alignItems: "flex-start", gap: 8,
padding: "8px 14px", borderRadius: 12,
boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
maxWidth: 360,
...style,
}}>
{/* icona */}
<span style={{ paddingTop: 1 }}>{icon}</span>
{/* testo */}
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
<span style={{
fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.02em",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
}}>
{label}
</span>
{sub && (
<span style={{
fontSize: "0.63rem", opacity: 0.75,
overflow: "hidden", textOverflow: "ellipsis",
whiteSpace: "nowrap", fontFamily: "ui-monospace,'SF Mono',monospace",
}}>
{sub}
</span>
)}
</div>
</div>
);
}
// ─── Banner principale ────────────────────────────────────────────────────────
const DebugFixBanner = memo(function DebugFixBanner() {
const [state, setState] = useState<DebugState>({ phase: "idle" });
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearHide = () => {
if (hideTimer.current !== null) { clearTimeout(hideTimer.current); hideTimer.current = null; }
};
const scheduleHide = useCallback((ms: number) => {
clearHide();
hideTimer.current = setTimeout(() => setState({ phase: "idle" }), ms);
}, []);
useEffect(() => () => clearHide(), []);
useAnyEvent(useCallback((ev: RuntimeEvent) => {
if (ev.kind === "live_debug_start") {
clearHide();
setState({ phase: "fixing", taskId: ev.taskId, attempt: ev.attempt, toolName: ev.toolName });
} else if (ev.kind === "live_debug_fix") {
setState(prev =>
prev.phase === "fixing"
? { ...prev, attempt: ev.attempt }
: prev
);
} else if (ev.kind === "live_debug_success") {
setState({ phase: "fixed", taskId: ev.taskId, attempts: ev.attempts });
scheduleHide(4500);
} else if (ev.kind === "live_debug_fail") {
setState({ phase: "failed", taskId: ev.taskId, reason: ev.reason });
scheduleHide(3000);
}
}, [scheduleHide]));
if (state.phase === "idle") return null;
return (
<>
{/* Keyframe per l'icona rotante */}
<style>{`
@keyframes debug-spin {
0% { transform: rotate(0deg); }
25% { transform: rotate(-15deg); }
75% { transform: rotate(15deg); }
100% { transform: rotate(0deg); }
}
.debug-banner-enter {
animation: debug-banner-in 0.25s ease-out forwards;
}
@keyframes debug-banner-in {
from { opacity: 0; transform: translateY(8px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
`}</style>
<div
className="debug-banner-enter"
style={{
position: "fixed",
bottom: 72,
left: "50%",
transform: "translateX(-50%)",
zIndex: Z_INDEX.SIDEBAR_PANEL + 5,
pointerEvents: "none",
}}
>
<DebugPill state={state} />
</div>
</>
);
}
);
export default DebugFixBanner;