Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

148 lines
5.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
AlertCircle,
CheckCircle2,
ChevronDown,
ChevronRight,
History,
RefreshCw,
Trash2,
Upload,
Wand2,
} from "lucide-react";
import type { HistoryEntry } from "@/hooks/useKnowledgeHistory";
interface KbUpdateHistoryProps {
entries: HistoryEntry[];
onClear: () => void;
}
export default function KbUpdateHistory({
entries,
onClear,
}: KbUpdateHistoryProps) {
const { t } = useTranslation();
const [expandedId, setExpandedId] = useState<string | null>(null);
if (entries.length === 0) return null;
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[11.5px] font-medium uppercase tracking-[0.12em] text-[var(--muted-foreground)]">
<History className="h-3 w-3" />
{t("Update history")}
<span className="rounded-full bg-[var(--muted)] px-1.5 py-0 text-[10px] tracking-normal">
{entries.length}
</span>
</div>
<button
type="button"
onClick={onClear}
title={t("Clear history")}
className="flex h-6 w-6 items-center justify-center rounded text-[var(--muted-foreground)] transition-colors hover:bg-[var(--muted)] hover:text-[var(--foreground)]"
aria-label={t("Clear history")}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
<ul className="divide-y divide-[var(--border)] rounded-lg border border-[var(--border)]">
{entries.map((entry) => {
const expanded = expandedId === entry.id;
const isError = entry.status === "error";
const Icon = iconForKind(entry.kind);
const durationMs = Math.max(0, entry.completedAt - entry.startedAt);
return (
<li key={entry.id} className="text-[12.5px]">
<button
type="button"
onClick={() => setExpandedId(expanded ? null : entry.id)}
className="flex w-full items-start gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-[var(--muted)]/40"
>
<span className="mt-0.5 shrink-0 text-[var(--muted-foreground)]">
{expanded ? (
<ChevronDown size={13} />
) : (
<ChevronRight size={13} />
)}
</span>
<span
className={`mt-0.5 shrink-0 rounded-md p-1 ${
isError
? "bg-red-100 text-red-700 dark:bg-red-950/30 dark:text-red-300"
: "bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-300"
}`}
>
{isError ? (
<AlertCircle className="h-3 w-3" />
) : (
<CheckCircle2 className="h-3 w-3" />
)}
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5 text-[12.5px] font-medium text-[var(--foreground)]">
<Icon className="h-3 w-3 text-[var(--muted-foreground)]" />
{entry.label}
</span>
<span className="mt-0.5 block text-[11px] text-[var(--muted-foreground)]">
{new Date(entry.completedAt).toLocaleString()}
{durationMs > 0 && ` · ${formatDuration(durationMs)}`}
</span>
</span>
</button>
{expanded && (
<div className="border-t border-[var(--border)] bg-[var(--muted)]/30 px-3 py-2">
{entry.error && (
<pre className="mb-2 whitespace-pre-wrap break-words rounded-md border border-red-200 bg-red-50 px-2 py-1.5 font-mono text-[11px] leading-relaxed text-red-700 dark:border-red-900 dark:bg-red-950/30 dark:text-red-300">
{entry.error}
</pre>
)}
{entry.logTail.length > 0 ? (
<pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-md border border-[var(--border)] bg-[var(--card)] px-2 py-1.5 font-mono text-[10.5px] leading-relaxed text-[var(--muted-foreground)]">
{entry.logTail.join("\n")}
</pre>
) : (
!entry.error && (
<p className="text-[11px] text-[var(--muted-foreground)]">
{t("No logs captured.")}
</p>
)
)}
</div>
)}
</li>
);
})}
</ul>
</div>
);
}
function iconForKind(kind: HistoryEntry["kind"]) {
switch (kind) {
case "upload":
return Upload;
case "reindex":
return RefreshCw;
case "create":
default:
return Wand2;
}
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return `${minutes}m ${seconds}s`;
}