Files
AgentHub/client/src/components/config/FileViewer.tsx
T
nntrivi2001 865c6c1d1e refactor(config): lift the tool-agnostic config modals out of CcConfig
Moves FileViewer, EditorModal, ConfirmDeleteModal, ToastNotice, MdItemList,
ScopeBadge, and the shared atoms (Empty/SkeletonRows/CopyButton/formatBytes)
into client/src/components/config/ so the upcoming Codex config panel can
reuse them. EditorModal now takes a fetchContent callback instead of calling
api.ccConfig.file itself; CcConfig.tsx supplies the Claude-specific fetcher.
The moved EditorState/ConfirmDeleteState type field widens from
CcArtifactType to string so non-Claude callers can use them too, with a cast
at the two remaining Claude-specific call sites in CcConfig.tsx.
2026-09-08 20:40:47 +07:00

78 lines
2.7 KiB
TypeScript

/**
* @file FileViewer.tsx
* @description Read-only file-content modal for the config explorer. Takes
* already-fetched data as props (path/data/error) rather than fetching
* itself, so it stays tool-agnostic — the orchestrator (Claude or Codex
* config page) owns the fetch.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { FileText, X, AlertCircle } from "lucide-react";
import type { CcFileResponse } from "../../lib/api";
import { CopyButton } from "./configPrimitives";
export function FileViewer({
state,
onClose,
}: {
state: { path: string; data: CcFileResponse | null; error: string | null };
onClose: () => void;
}) {
const { t } = useTranslation("ccConfig");
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
return (
<div
className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
onClick={onClose}
>
<div
className="relative w-full max-w-4xl max-h-[85vh] rounded-xl border border-border bg-surface-1 flex flex-col"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
<FileText className="w-4 h-4 text-fg-muted" />
<span className="font-mono text-[12px] text-fg-secondary truncate flex-1">
{state.path}
</span>
<CopyButton value={state.path} />
<button
onClick={onClose}
className="text-fg-secondary hover:text-fg-primary p-1 rounded-md hover:bg-surface-3"
aria-label={t("common.close")}
>
<X className="w-4 h-4" />
</button>
</div>
<div className="overflow-auto p-4">
{state.error ? (
<div className="text-sm text-status-danger inline-flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
{state.error}
</div>
) : !state.data ? (
<div className="text-sm text-fg-muted">…</div>
) : (
<pre className="text-[11px] font-mono text-fg-secondary whitespace-pre-wrap break-words">
{state.data.text}
{state.data.truncated && (
<span className="text-fg-muted italic">
{"\n\n"}
{t("common.truncated")}
</span>
)}
</pre>
)}
</div>
</div>
</div>
);
}