750d74a2af
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as the host on loopback; a member's omp pane reaches it as a scoped client keyed by its pane token (clients file = live omp panes only), runs on --model agenthub/<model>, never holds a credential. - omp catalog: --allow-home, so a workspace at the account's home keeps its sessions there (they were recorded under a temp dir and dropped). - Settings → Appearance → Open Markdown as: Preview (default) or Raw, for Files/Changes, the config viewer and a chat's file panel. - First-login password change and password rules, avatar URLs, Artifacts "Mine" includes installed shared skills/plugins, a member's own hook pages.
106 lines
4.0 KiB
TypeScript
106 lines
4.0 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. A markdown file gets a Raw / Preview switch
|
|
* (`MarkdownPreview`), opening as Settings → Appearance says
|
|
* (`lib/markdownView.ts`: Preview by default).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useRef, useState } from "react";
|
|
import { useDialog } from "../../hooks/useDialog";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FileText, X, AlertCircle, Eye, Code2 } from "lucide-react";
|
|
import type { CcFileResponse } from "../../lib/api";
|
|
import { CopyButton } from "./configPrimitives";
|
|
import { MarkdownPreview, isMarkdown } from "../MarkdownPreview";
|
|
import { readMarkdownPreview } from "../../lib/markdownView";
|
|
|
|
export function FileViewer({
|
|
state,
|
|
onClose,
|
|
}: {
|
|
state: { path: string; data: CcFileResponse | null; error: string | null };
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useTranslation("ccConfig");
|
|
const [preview, setPreview] = useState(readMarkdownPreview);
|
|
const canPreview = isMarkdown(state.path) && !!state.data;
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
useDialog(true, panelRef, onClose);
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-surface-0/70 z-50 flex items-center justify-center p-4"
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
ref={panelRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="cc-file-viewer-title"
|
|
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
|
|
id="cc-file-viewer-title"
|
|
className="font-mono text-[12px] text-fg-secondary truncate flex-1"
|
|
>
|
|
{state.path}
|
|
</span>
|
|
{canPreview && (
|
|
<button
|
|
type="button"
|
|
aria-pressed={preview}
|
|
onClick={() => setPreview((v) => !v)}
|
|
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
|
>
|
|
{preview ? <Code2 className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
{preview ? t("common.raw") : t("common.preview")}
|
|
</button>
|
|
)}
|
|
<CopyButton value={state.path} />
|
|
<button
|
|
type="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>
|
|
) : preview && canPreview ? (
|
|
<>
|
|
<MarkdownPreview text={state.data.text} />
|
|
{state.data.truncated && (
|
|
<p className="text-[11px] text-fg-muted italic">{t("common.truncated")}</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<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>
|
|
);
|
|
}
|