import { memo, useEffect, useState } from "react"; import { BarChart3, CircleAlert, Flag, Lightbulb, Map, Target } from "lucide-react"; import type { NativeStructurePreviewResult, NativeStructureRef } from "@fusion/core"; import { fetchNativeStructurePreview } from "../api"; import "./NativeStructurePreview.css"; export interface NativeStructurePreviewProps { ref: NativeStructureRef; payload?: NativeStructurePreviewResult; /** Attach-time label used only when a persisted target is no longer available. */ capturedLabel?: string; onOpen: (ref: NativeStructureRef, payload: NativeStructurePreviewResult) => void; } const icons = { mission: Map, milestone: Flag, "research-finding": Lightbulb, "eval-result": BarChart3, goal: Target, "roadmap-item": Map, } satisfies Record; function isSupportedKind(kind: string): kind is NativeStructureRef["kind"] { return Object.prototype.hasOwnProperty.call(icons, kind); } function unavailableLabel(kind: string): string { return kind.replace(/-/g, " "); } /** * FNXC:NativeStructureEmbed 2026-07-16-12:00: * Chat and mail use this one memoized renderer for compact structure cards. Navigation remains * owned by each consumer through `onOpen` because dashboard views use callback/view state rather * than URL routes; rendering an anchor here would create dead destinations. * * FNXC:NativeStructureEmbed 2026-07-19-12:45: * Roadmap items join this shared renderer with the roadmap icon and callback-only open action. */ export const NativeStructurePreview = memo(function NativeStructurePreview({ ref, payload, capturedLabel, onOpen }: NativeStructurePreviewProps) { const supportedKind = isSupportedKind(ref.kind); const refKey = `${ref.kind}\u0000${ref.id}\u0000${ref.projectId ?? ""}`; const [fetchedPayload, setFetchedPayload] = useState<{ refKey: string; result: NativeStructurePreviewResult } | undefined>(); const [error, setError] = useState(false); // FNXC:NativeStructureEmbed 2026-07-16-12:00: A ref update must not briefly render a prior fetch result; consumers can replace cards while messages or drafts rehydrate. const result = payload ?? (fetchedPayload?.refKey === refKey ? fetchedPayload.result : undefined); const Icon = supportedKind ? icons[ref.kind] : CircleAlert; useEffect(() => { /* FNXC:NativeStructureEmbed 2026-07-19-18:00: Refs can arrive from persisted chat/mail content, so reject a malformed kind before fetching. The six-kind route owns resolution; an invalid icon lookup must not trigger a plugin read or turn into a render crash. */ if (payload || !supportedKind) return; let active = true; setFetchedPayload(undefined); setError(false); void fetchNativeStructurePreview(ref) .then((nextPayload) => { if (active) setFetchedPayload({ refKey, result: nextPayload }); }) .catch(() => { if (active) setError(true); }); return () => { active = false; }; }, [payload, ref.kind, ref.id, ref.projectId, refKey]); if (!supportedKind) { return (
); } // FNXC:NativeStructureEmbed 2026-07-16-14:05: A caller-supplied projection is authoritative after a transient fetch failure, so it must replace the error placeholder for the same ref. if (error && !payload) { return (
); } if (!result) { return (
); } if (!result.available) { return (
); } return (
); });