439 lines
19 KiB
TypeScript
439 lines
19 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
FileText,
|
|
Folder,
|
|
Search,
|
|
CheckCircle2,
|
|
RefreshCw,
|
|
Youtube,
|
|
ExternalLink,
|
|
Code,
|
|
Eye,
|
|
Tag,
|
|
AlertTriangle,
|
|
ShieldCheck,
|
|
Layers,
|
|
Image as ImageIcon,
|
|
} from "lucide-react";
|
|
import { ContentItem, MediaAsset } from "../types";
|
|
|
|
interface ContentTabProps {
|
|
contentItems: ContentItem[];
|
|
mediaAssets: MediaAsset[];
|
|
onTriggerDiscovery: () => Promise<void>;
|
|
}
|
|
|
|
export const ContentTab: React.FC<ContentTabProps> = ({
|
|
contentItems,
|
|
mediaAssets,
|
|
onTriggerDiscovery,
|
|
}) => {
|
|
const [selectedItemId, setSelectedItemId] = useState<string>(
|
|
contentItems[0]?.id || "",
|
|
);
|
|
const [activeSubTab, setActiveSubTab] = useState<"markdown" | "media">(
|
|
"markdown",
|
|
);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [isScanning, setIsScanning] = useState(false);
|
|
const [previewContent, setPreviewContent] = useState<ContentItem | null>(
|
|
null,
|
|
);
|
|
|
|
const selectedItem =
|
|
contentItems.find((i) => i.id === selectedItemId) || contentItems[0];
|
|
|
|
const filteredItems = contentItems.filter(
|
|
(item) =>
|
|
item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.sourceRepo.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.tags.some((t) => t.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
);
|
|
|
|
const handleScan = async () => {
|
|
setIsScanning(true);
|
|
await onTriggerDiscovery();
|
|
setIsScanning(false);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* HEADER BAR */}
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<h1 className="text-xl font-serif italic text-white">Artifacts</h1>
|
|
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
|
|
PRD AC-05, AC-06, AC-07, AC-08 & AC-09
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-neutral-400 mt-1">
|
|
Discovers artifacts across repositories, parses front matter,
|
|
validates YouTube directives, Wikipedia links, raw HTML policy, and
|
|
media mappings under{" "}
|
|
<code className="text-[#C5A059] font-mono">/media</code>.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={handleScan}
|
|
disabled={isScanning}
|
|
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
|
>
|
|
<RefreshCw
|
|
className={`w-3.5 h-3.5 ${isScanning ? "animate-spin" : ""}`}
|
|
/>
|
|
<span>{isScanning ? "Scanning..." : "Run Discovery Scan"}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SUB-TABS: MARKDOWN VS MEDIA */}
|
|
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold">
|
|
<button
|
|
onClick={() => setActiveSubTab("markdown")}
|
|
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
|
|
activeSubTab === "markdown"
|
|
? "border-[#C5A059] text-[#C5A059]"
|
|
: "border-transparent text-neutral-400 hover:text-white"
|
|
}`}
|
|
>
|
|
<FileText className="w-4 h-4" />
|
|
<span>Artifacts ({contentItems.length})</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab("media")}
|
|
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
|
|
activeSubTab === "media"
|
|
? "border-[#C5A059] text-[#C5A059]"
|
|
: "border-transparent text-neutral-400 hover:text-white"
|
|
}`}
|
|
>
|
|
<ImageIcon className="w-4 h-4" />
|
|
<span>Public Media Assets ({mediaAssets.length})</span>
|
|
</button>
|
|
</div>
|
|
|
|
{activeSubTab === "markdown" ? (
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
{/* LEFT LIST: DISCOVERED CONTENT ITEMS (5 COLS) */}
|
|
<div className="lg:col-span-5 space-y-3">
|
|
<div className="relative">
|
|
<Search className="w-4 h-4 text-neutral-500 absolute left-3 top-2.5" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search artifacts, tags, repositories..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-9 pr-3 py-2 bg-[#141414] border border-[#222222] rounded-sm text-xs text-white focus:outline-none focus:border-[#C5A059] shadow-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{filteredItems.map((item) => {
|
|
const isSelected = item.id === selectedItemId;
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
onClick={() => setSelectedItemId(item.id)}
|
|
className={`p-4 rounded-sm border cursor-pointer transition-all ${
|
|
isSelected
|
|
? "bg-[#181818] border-[#C5A059] shadow-xs"
|
|
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-xs font-mono font-semibold text-[#C5A059]">
|
|
{item.sourceRepo}
|
|
</span>
|
|
<span className="text-[10px] text-neutral-500 font-mono">
|
|
{item.published}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="text-sm font-bold text-white line-clamp-1 mb-1">
|
|
{item.title}
|
|
</h3>
|
|
<p className="text-xs text-neutral-400 line-clamp-2 mb-3">
|
|
{item.summary}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between text-[11px] pt-2 border-t border-[#1F1F1F] text-neutral-500">
|
|
<span className="font-mono text-neutral-300 truncate max-w-[200px]">
|
|
{item.route}
|
|
</span>
|
|
<span className="flex items-center space-x-1 text-emerald-400 font-medium">
|
|
<CheckCircle2 className="w-3 h-3" />
|
|
<span>Valid</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* RIGHT COLUMN: CONTENT INSPECTOR & DIRECTIVES (7 COLS) */}
|
|
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-6">
|
|
{selectedItem ? (
|
|
<>
|
|
{/* ITEM HEADER */}
|
|
<div className="border-b border-[#1F1F1F] pb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
<div>
|
|
<div className="flex items-center space-x-2 mb-1">
|
|
<span className="px-2 py-0.5 text-[10px] font-semibold bg-[#1A1A1A] text-[#C5A059] border border-[#2A2A2A] rounded-xs font-mono">
|
|
{selectedItem.sourceRepo}
|
|
</span>
|
|
<span className="text-xs text-neutral-500 font-mono">
|
|
{selectedItem.path}
|
|
</span>
|
|
</div>
|
|
<h2 className="text-lg font-bold text-white">
|
|
{selectedItem.title}
|
|
</h2>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setPreviewContent(selectedItem)}
|
|
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-colors flex items-center space-x-1.5 self-start uppercase tracking-wider"
|
|
>
|
|
<Eye className="w-3.5 h-3.5" />
|
|
<span>View Rendered</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* EXPLAIN WHY / OMITTED STATUS */}
|
|
{selectedItem.explainWhy && (
|
|
<div className="p-4 bg-amber-950/20 border border-amber-900/40 rounded-sm text-xs">
|
|
<h3 className="font-bold text-amber-400 mb-1">
|
|
Artifact Omitted
|
|
</h3>
|
|
<p className="text-neutral-400 font-bold mb-1 mt-2">
|
|
Reason:
|
|
</p>
|
|
<p className="text-neutral-300">
|
|
{selectedItem.explainWhy}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* METADATA & FRONT MATTER INSPECTOR */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
|
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
|
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
|
|
Final Generated Route
|
|
</span>
|
|
<code className="font-mono text-[#C5A059] bg-[#141414] px-2.5 py-1 rounded-xs border border-[#222222] block text-xs truncate">
|
|
{selectedItem.route}
|
|
</code>
|
|
<p className="text-neutral-500 text-[11px]">
|
|
Resolved via{" "}
|
|
<code className="text-neutral-300 font-mono">
|
|
{selectedItem.styleInstanceId}
|
|
</code>{" "}
|
|
pattern.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
|
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
|
|
Repository Entry Defaults
|
|
</span>
|
|
<div className="font-mono text-neutral-300 space-y-0.5 text-[11px]">
|
|
<div>
|
|
Project:{" "}
|
|
<strong className="text-white">
|
|
{selectedItem.metadata?.projectName}
|
|
</strong>
|
|
</div>
|
|
<div>
|
|
Project ID:{" "}
|
|
<strong className="text-[#C5A059]">
|
|
{selectedItem.metadata?.projectId}
|
|
</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SPECIAL DIRECTIVES DETECTED */}
|
|
<div className="space-y-3">
|
|
<span className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest block">
|
|
Parsed Markdown Extensions & Embeds
|
|
</span>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
|
{/* YouTube Directive */}
|
|
<div className="p-3.5 bg-red-950/20 border border-red-900/40 rounded-sm space-y-1">
|
|
<div className="flex items-center space-x-1.5 text-red-400 font-bold font-mono">
|
|
<Youtube className="w-4 h-4" />
|
|
<span>YouTube Directives (AC-06)</span>
|
|
</div>
|
|
{selectedItem.youtubeDirectives.length > 0 ? (
|
|
selectedItem.youtubeDirectives.map((yt, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="text-[11px] text-red-200 font-mono"
|
|
>
|
|
Title: "{yt.title}" (ID: {yt.videoId})
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-neutral-500 text-[11px]">
|
|
No YouTube directives in item.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Wikipedia Links */}
|
|
<div className="p-3.5 bg-blue-950/20 border border-blue-900/40 rounded-sm space-y-1">
|
|
<div className="flex items-center space-x-1.5 text-blue-400 font-bold font-mono">
|
|
<ExternalLink className="w-4 h-4" />
|
|
<span>Wikipedia Links (AC-07)</span>
|
|
</div>
|
|
{selectedItem.wikipediaLinks.length > 0 ? (
|
|
selectedItem.wikipediaLinks.map((wiki, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="text-[11px] text-blue-200 font-mono truncate"
|
|
>
|
|
{wiki.text} -> {wiki.url}
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-neutral-500 text-[11px]">
|
|
No Wikipedia links in item.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* RAW HTML POLICY SAFETY STATUS */}
|
|
<div className="p-4 bg-[#0A0A0A] text-neutral-300 rounded-sm text-xs space-y-2 border border-[#1F1F1F] font-mono">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-bold text-[#C5A059] uppercase tracking-widest text-[10px]">
|
|
Raw HTML Security Policy (AC-08)
|
|
</span>
|
|
<span className="px-2 py-0.5 bg-emerald-950/40 text-emerald-400 rounded-xs font-bold text-[10px] border border-emerald-500/20 uppercase">
|
|
Policy: Disabled / Sanitized
|
|
</span>
|
|
</div>
|
|
<p className="text-neutral-400 text-[11px]">
|
|
Raw HTML blocks and arbitrary script tags are
|
|
stripped/escaped automatically during build time to prevent
|
|
XSS vulnerabilities.
|
|
</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="text-center py-12 text-neutral-500 text-xs">
|
|
Select an artifact to inspect details.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* PUBLIC MEDIA ASSETS INVENTORY (AC-09) */
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
|
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
|
<div>
|
|
<h2 className="text-base font-serif italic text-white">
|
|
Public Media Assets Namespace Mappings (/media)
|
|
</h2>
|
|
<p className="text-xs text-neutral-400 mt-0.5">
|
|
Maps source media directories into isolated public media
|
|
namespaces to prevent path collisions.
|
|
</p>
|
|
</div>
|
|
<span className="px-3 py-1 bg-amber-950/40 text-amber-400 rounded-xs text-xs font-semibold border border-amber-500/20 uppercase tracking-widest">
|
|
AC-09 Verified
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
|
{mediaAssets.map((asset) => (
|
|
<div
|
|
key={asset.id}
|
|
className="p-4 rounded-sm border border-[#1F1F1F] bg-[#0A0A0A] space-y-2 font-mono"
|
|
>
|
|
<div className="flex items-center justify-between text-[#C5A059] font-bold">
|
|
<span>{asset.filename}</span>
|
|
<span className="text-[10px] text-neutral-500">
|
|
{(asset.sizeBytes / 1024).toFixed(1)} KB
|
|
</span>
|
|
</div>
|
|
<div className="text-[11px] text-neutral-400 space-y-1">
|
|
<div>
|
|
Source Repo:{" "}
|
|
<strong className="text-white">{asset.sourceRepo}</strong>
|
|
</div>
|
|
<div>
|
|
Original Path:{" "}
|
|
<code className="text-neutral-300">
|
|
{asset.originalPath}
|
|
</code>
|
|
</div>
|
|
<div className="pt-2 border-t border-[#1F1F1F]">
|
|
<span className="text-neutral-500 block text-[10px] uppercase">
|
|
Public Namespace Route:
|
|
</span>
|
|
<code className="text-[#C5A059] font-semibold text-[11px] block truncate">
|
|
{asset.publicNamespacePath}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* RENDERED CONTENT PREVIEW MODAL */}
|
|
{previewContent && (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-2xl w-full p-6 shadow-2xl space-y-4 max-h-[85vh] overflow-y-auto">
|
|
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
|
<div>
|
|
<span className="text-xs font-mono text-[#C5A059] font-bold">
|
|
{previewContent.sourceRepo} • {previewContent.published}
|
|
</span>
|
|
<h3 className="text-lg font-bold text-white">
|
|
{previewContent.title}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => setPreviewContent(null)}
|
|
className="text-neutral-500 hover:text-white text-2xl leading-none"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<div className="prose prose-invert max-w-none text-xs text-neutral-200 leading-relaxed font-sans space-y-3">
|
|
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-[11px] text-neutral-400">
|
|
Summary: {previewContent.summary}
|
|
</div>
|
|
<pre className="bg-[#0A0A0A] text-neutral-200 p-4 rounded-sm text-[11px] font-mono border border-[#1F1F1F] overflow-x-auto">
|
|
<code>{previewContent.contentMarkdown}</code>
|
|
</pre>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end pt-3 border-t border-[#1F1F1F]">
|
|
<button
|
|
onClick={() => setPreviewContent(null)}
|
|
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#262626] text-white rounded-sm text-xs font-semibold uppercase tracking-wider"
|
|
>
|
|
Close Preview
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|