mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
Rebased onto current main and re-ported across the restructured surfaces: model flows now thread confirm_provider/base_url/api_key through hermes_cli/model_setup_flows.py, the Discord picker lives in plugins/platforms/discord/adapter.py, and the web dashboard picker applies chat-mode switches via config.set so the expensive-model confirmation can ride the response. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { Button } from "@nous-research/ui/ui/components/button";
|
|
import { AlertTriangle } from "lucide-react";
|
|
import { useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { cn, themedBody } from "@/lib/utils";
|
|
|
|
interface ConfirmDialogProps {
|
|
cancelLabel?: string;
|
|
confirmLabel?: string;
|
|
description?: string;
|
|
destructive?: boolean;
|
|
loading?: boolean;
|
|
onCancel: () => void;
|
|
onConfirm: () => void;
|
|
open: boolean;
|
|
title: string;
|
|
}
|
|
|
|
export function ConfirmDialog({
|
|
cancelLabel = "Cancel",
|
|
confirmLabel = "Confirm",
|
|
description,
|
|
destructive = false,
|
|
loading = false,
|
|
onCancel,
|
|
onConfirm,
|
|
open,
|
|
title,
|
|
}: ConfirmDialogProps) {
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const prevActive = document.activeElement as HTMLElement | null;
|
|
dialogRef.current
|
|
?.querySelector<HTMLButtonElement>("[data-confirm]")
|
|
?.focus();
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
onCancel();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", onKey);
|
|
const prevOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = "hidden";
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", onKey);
|
|
document.body.style.overflow = prevOverflow;
|
|
prevActive?.focus?.();
|
|
};
|
|
}, [open, onCancel]);
|
|
|
|
if (!open) return null;
|
|
|
|
return createPortal(
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="confirm-dialog-title"
|
|
aria-describedby={description ? "confirm-dialog-desc" : undefined}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onCancel();
|
|
}}
|
|
className="fixed inset-0 z-[200] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
|
|
>
|
|
<div
|
|
ref={dialogRef}
|
|
className={cn(
|
|
themedBody,
|
|
"relative w-full max-w-md border border-border bg-card shadow-2xl",
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3 p-4 border-b border-border">
|
|
{destructive && (
|
|
<div aria-hidden className="mt-0.5 shrink-0 text-destructive">
|
|
<AlertTriangle className="h-4 w-4" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
<h2
|
|
id="confirm-dialog-title"
|
|
className="font-mondwest text-display text-base tracking-wider"
|
|
>
|
|
{title}
|
|
</h2>
|
|
|
|
{description && (
|
|
<p
|
|
id="confirm-dialog-desc"
|
|
className="text-xs text-muted-foreground leading-relaxed whitespace-pre-line"
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 p-3">
|
|
<Button type="button" outlined onClick={onCancel} disabled={loading}>
|
|
{cancelLabel}
|
|
</Button>
|
|
<Button
|
|
data-confirm
|
|
type="button"
|
|
destructive={destructive}
|
|
onClick={onConfirm}
|
|
disabled={loading}
|
|
>
|
|
{loading ? "…" : confirmLabel}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|