Files
nearxos 6185b9b85a Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 22:11:07 +03:00

227 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
export function PageHeader({
title,
subtitle,
action,
backHref,
backLabel,
}: {
title: string;
subtitle?: string;
action?: React.ReactNode;
backHref?: string;
backLabel?: string;
}) {
return (
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0">
{backHref && (
<Link
href={backHref}
className="mb-1 inline-block text-xs text-muted hover:text-accent"
>
{backLabel || "Back"}
</Link>
)}
<h1 className="text-2xl font-semibold">{title}</h1>
{subtitle && <p className="mt-1 break-words text-sm text-muted">{subtitle}</p>}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
);
}
export function Spinner({ className = "" }: { className?: string }) {
return (
<span
className={`inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent ${className}`}
aria-hidden="true"
/>
);
}
export function SkeletonRows({ rows = 3 }: { rows?: number }) {
return (
<div className="space-y-3" aria-hidden="true">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-2xl border border-edge bg-panel2/50" />
))}
</div>
);
}
export function EmptyState({
title,
description,
action,
}: {
title: string;
description?: string;
action?: React.ReactNode;
}) {
return (
<div className="card flex flex-col items-center gap-2 py-10 text-center">
<div className="text-sm font-medium">{title}</div>
{description && <div className="max-w-sm text-sm text-muted">{description}</div>}
{action && <div className="mt-2">{action}</div>}
</div>
);
}
export function LoadError({
message = "Couldnt load data.",
onRetry,
}: {
message?: string;
onRetry?: () => void;
}) {
return (
<div className="card border-bad/40 text-center">
<div className="text-sm text-bad">{message}</div>
{onRetry && (
<button className="btn-ghost btn-sm mt-3" onClick={onRetry}>
Retry
</button>
)}
</div>
);
}
export function FormError({ message }: { message?: string | null }) {
if (!message) return null;
return <div className="text-sm text-bad">{message}</div>;
}
const STATUS_STYLES: Record<string, string> = {
queued: "bg-slate-500/20 text-slate-300",
running: "bg-accent/20 text-accent",
waiting_approval: "bg-warn/20 text-warn",
succeeded: "bg-good/20 text-good",
failed: "bg-bad/20 text-bad",
cancelled: "bg-slate-500/20 text-slate-400",
loaded: "bg-good/20 text-good",
error: "bg-bad/20 text-bad",
starting: "bg-warn/20 text-warn",
cloning: "bg-accent/20 text-accent",
stopped: "bg-slate-500/20 text-slate-400",
};
export function StatusBadge({ status }: { status: string }) {
return (
<span className={`badge ${STATUS_STYLES[status] || "bg-slate-500/20 text-slate-300"}`}>
{status.replace(/_/g, " ")}
</span>
);
}
export function Modal({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
// Focus the dialog only when it opens (not on every parent re-render).
useEffect(() => {
if (!open) return;
ref.current?.focus();
}, [open]);
// Escape-to-close + body scroll lock while open.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onCloseRef.current();
};
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 grid place-items-center bg-black/60 p-4"
onClick={onClose}
>
<div
ref={ref}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-label={title}
className="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-2xl border border-edge bg-panel p-6 shadow-2xl focus:outline-none"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">{title}</h2>
<button
onClick={onClose}
aria-label="Close dialog"
className="rounded-lg px-2 text-muted hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
</button>
</div>
{children}
</div>
</div>
);
}
export function ConfirmDialog({
open,
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
destructive = false,
busy = false,
onConfirm,
onCancel,
}: {
open: boolean;
title: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
busy?: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<Modal open={open} onClose={onCancel} title={title}>
{message && <p className="mb-5 text-sm text-muted">{message}</p>}
<div className="flex justify-end gap-2">
<button className="btn-ghost" onClick={onCancel} disabled={busy}>
{cancelLabel}
</button>
<button
className={destructive ? "btn-danger" : "btn-primary"}
onClick={onConfirm}
disabled={busy}
>
{busy ? "Working…" : confirmLabel}
</button>
</div>
</Modal>
);
}