import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Minimize2, Maximize2, Globe, ExternalLink, FileText, File, Download, Compass, Sparkles } from 'lucide-react'; import { axiosInstance } from '../../../services/axios'; import type { Asset } from '../../../types/assets'; import type { User } from '../../../types/auth'; import Modal from '../../../components/ui/Modal'; import Button from '../../../components/ui/Button'; import MarkdownViewer from '../../../components/ui/MarkdownViewer'; import * as XLSX from 'xlsx'; interface AssetViewerModalProps { isOpen: boolean; onClose: () => void; asset: Asset | null; user: User | null; onDownload: (asset: Asset) => void; } export const AssetViewerModal: React.FC = ({ isOpen, onClose, asset, user, onDownload }) => { const navigate = useNavigate(); const [isMaximized, setIsMaximized] = useState(false); const [isLoadingText, setIsLoadingText] = useState(false); const [textPreviewContent, setTextPreviewContent] = useState(''); const [docBlob, setDocBlob] = useState(null); const [excelHtml, setExcelHtml] = useState(''); const [isLoadingDoc, setIsLoadingDoc] = useState(false); const [docLoadError, setDocLoadError] = useState(false); const wordContainerRef = React.useRef(null); const extractYouTubeVideoId = (url: string): string | null => { try { const urlObj = new URL(url); if (urlObj.hostname.includes('youtu.be')) { return urlObj.pathname.slice(1).split(/[?#]/)[0]; } if (urlObj.pathname.includes('/shorts/') || urlObj.pathname.includes('/embed/')) { const parts = urlObj.pathname.split('/'); return parts.pop()?.split(/[?#]/)[0] || null; } if (urlObj.searchParams.has('v')) { return urlObj.searchParams.get('v'); } } catch (e) {} const patterns = [ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/, ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) return match[1]; } return null; }; const getFullAssetUrl = (url: string) => { let resolvedUrl = url; if (!url.startsWith('http')) { const backendBase = axiosInstance.defaults.baseURL || '/api/v1'; const relativeHost = backendBase.replace('/api/v1', ''); resolvedUrl = relativeHost.startsWith('/') || relativeHost === '' ? `${window.location.origin}${relativeHost}${url}` : `${relativeHost}${url}`; } // If current origin is a public domain (like ngrok) but the resolved URL is local, // rewrite the local URL host to match the public origin so that external viewers can fetch it. const currentOrigin = window.location.origin; const isCurrentOriginLocal = currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1'); if (!isCurrentOriginLocal && (resolvedUrl.includes('localhost') || resolvedUrl.includes('127.0.0.1'))) { resolvedUrl = resolvedUrl.replace(/https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/, currentOrigin); } return resolvedUrl; }; const isLocalUrl = (url: string) => { const resolved = getFullAssetUrl(url); return ( resolved.includes('localhost') || resolved.includes('127.0.0.1') || resolved.includes('ngrok-free.dev') || resolved.includes('ngrok.io') ); }; const getGithubRawUrl = (url: string) => { let raw = url.trim(); if (raw.endsWith('.git')) { raw = raw.slice(0, -4); } if (raw.endsWith('/')) { raw = raw.slice(0, -1); } if (raw.includes('/blob/')) { return raw.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/'); } const match = raw.match(/github\.com\/([^/]+)\/([^/]+)/); if (match && match[1] && match[2]) { const owner = match[1]; const repo = match[2]; return { owner, repo, mainUrl: `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`, masterUrl: `https://raw.githubusercontent.com/${owner}/${repo}/master/README.md`, }; } return null; }; const isTextOrCodeAsset = (url: string, type: string) => { const assetUrl = url.toLowerCase(); if (assetUrl.endsWith('.csv') || type.includes('csv')) { return false; } return ( assetUrl.endsWith('.md') || assetUrl.endsWith('.txt') || assetUrl.endsWith('.json') || assetUrl.endsWith('.js') || assetUrl.endsWith('.ts') || assetUrl.endsWith('.tsx') || assetUrl.endsWith('.jsx') || assetUrl.endsWith('.py') || assetUrl.endsWith('.yaml') || assetUrl.endsWith('.yml') || assetUrl.endsWith('.css') || assetUrl.endsWith('.html') || assetUrl.endsWith('.sh') || type.includes('text') || type.includes('markdown') || type.includes('json') || type.includes('javascript') ); }; const isAudioAsset = (url: string, type: string) => { const assetUrl = url.toLowerCase(); return ( assetUrl.endsWith('.mp3') || assetUrl.endsWith('.wav') || assetUrl.endsWith('.ogg') || assetUrl.endsWith('.m4a') || assetUrl.endsWith('.webm') || type.includes('audio/') ); }; const isVideoAsset = (url: string, type: string) => { const assetUrl = url.toLowerCase(); return ( assetUrl.endsWith('.mp4') || assetUrl.endsWith('.webm') || assetUrl.endsWith('.ogg') || assetUrl.endsWith('.mov') || type.includes('video/') ); }; const isWord = asset ? (((asset.type || '').includes('word') || (asset.url || '').toLowerCase().endsWith('.docx') || (asset.url || '').toLowerCase().endsWith('.doc'))) : false; const isSpreadsheet = asset ? (((asset.type || '').includes('sheet') || (asset.url || '').toLowerCase().endsWith('.xlsx') || (asset.url || '').toLowerCase().endsWith('.xls') || (asset.url || '').toLowerCase().endsWith('.csv'))) : false; useEffect(() => { if (isOpen && asset) { setTextPreviewContent(''); setIsLoadingText(false); setDocBlob(null); setExcelHtml(''); setIsLoadingDoc(false); setDocLoadError(false); if ( asset.type !== 'url' && isTextOrCodeAsset(asset.url, asset.type) ) { setIsLoadingText(true); axiosInstance.get(getFullAssetUrl(asset.url), { responseType: 'text' }) .then(res => { setTextPreviewContent(res.data); setIsLoadingText(false); }) .catch(err => { console.error(err); setTextPreviewContent('Failed to load text preview.'); setIsLoadingText(false); }); } else if (asset.type === 'url' && asset.url.includes('github.com')) { const ghInfo = getGithubRawUrl(asset.url); if (ghInfo) { setIsLoadingText(true); const targetUrl = typeof ghInfo === 'string' ? ghInfo : ghInfo.mainUrl; fetch(targetUrl) .then(async (res) => { if (!res.ok && typeof ghInfo !== 'string') { const fallbackRes = await fetch(ghInfo.masterUrl); if (fallbackRes.ok) { return fallbackRes.text(); } throw new Error('README not found'); } return res.text(); }) .then(data => { setTextPreviewContent(data); setIsLoadingText(false); }) .catch(err => { console.error(err); setTextPreviewContent(`### GitHub Repository: ${typeof ghInfo === 'string' ? asset.url : `${ghInfo.owner}/${ghInfo.repo}`}\n\nCould not fetch README.md automatically. The repository might be private or not contain a README.md file.`); setIsLoadingText(false); }); } } else if (isWord || isSpreadsheet) { setIsLoadingDoc(true); axiosInstance.get(getFullAssetUrl(asset.url), { responseType: 'arraybuffer' }) .then(res => { const data = res.data; setDocBlob(data); if (isSpreadsheet) { try { const workbook = XLSX.read(data, { type: 'array' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const html = XLSX.utils.sheet_to_html(worksheet, { header: '', footer: '' }); setExcelHtml(html); } catch (err) { console.error("SheetJS Parsing error", err); setDocLoadError(true); } } setIsLoadingDoc(false); }) .catch(err => { console.error(err); setDocLoadError(true); setIsLoadingDoc(false); }); } } }, [isOpen, asset]); useEffect(() => { if (isWord && docBlob && wordContainerRef.current) { wordContainerRef.current.innerHTML = ''; import('docx-preview').then(({ renderAsync }) => { renderAsync(docBlob, wordContainerRef.current!) .catch(err => { console.error("Docx Preview error:", err); wordContainerRef.current!.innerHTML = '
Failed to render Word document preview. Please download the file to inspect.
'; }); }); } }, [docBlob, isWord]); const formatBytes = (bytes?: number, decimals = 2) => { if (!bytes || isNaN(bytes) || bytes === 0) return ''; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }; const handleClose = () => { setIsMaximized(false); onClose(); }; const handleLocateInCatalog = () => { onClose(); if (!asset) return; const typeLower = (asset.type || '').toLowerCase(); const titleLower = (asset.title || '').toLowerCase(); const isCurrentAdmin = location.pathname.startsWith('/admin') || user?.role === 'ADMIN'; let targetBasePath = ''; if (typeLower.includes('case_study') || titleLower.includes('case study') || typeLower.includes('video')) { targetBasePath = isCurrentAdmin ? '/admin/showcase' : '/client/showcase'; } else if (titleLower.includes('nda') || titleLower.includes('msa') || titleLower.includes('agreement') || typeLower.includes('legal')) { targetBasePath = isCurrentAdmin ? '/admin/legal' : '/client/agreements'; } else if (typeLower.includes('ecosystem') || titleLower.includes('offering')) { targetBasePath = isCurrentAdmin ? '/admin/ecosystem' : '/client/ecosystem'; } else { targetBasePath = isCurrentAdmin ? '/admin/assets' : '/client/assets'; } const fullUrl = `${targetBasePath}?highlight=${asset.id}`; navigate(fullUrl, { state: { highlightAssetId: asset.id } }); }; return (
{asset?.title} {asset?.type === 'url' ? 'External Web Link' : asset?.size ? `${asset.type} • ${formatBytes(asset.size)}` : asset?.type}
} size="full" className={isMaximized ? '!fixed !inset-0 !z-[10001] !max-w-none !max-h-none !w-screen !h-screen !rounded-none !border-none !m-0' : '!max-w-[85vw] md:!max-w-[80vw] xl:!max-w-[75vw] !h-[75vh] !max-h-[75vh] !w-full' } footer={ <> {asset && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && ( )} {asset && ( )} {asset && asset.type !== 'url' && asset.type !== 'case_study' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && ( )} } > {asset && (
{asset.type === 'url' && asset.url.includes('github.com') ? (
Embedded GitHub Document
{isLoadingText ? (
Fetching README.md...
) : (

{asset.title}

{asset.url}

)}
) : asset.type === 'url' ? (
External Web Link Portal Guide {asset.url}
{/* Visual Section: Browser Mockup */}
{/* Browser Header Bar */}
{(() => { try { return new URL(asset.url).hostname.replace('www.', ''); } catch { return 'website.com'; } })()}
{/* Browser Content */}
{(() => { try { return new URL(asset.url).hostname.replace('www.', '').charAt(0).toUpperCase(); } catch { return 'W'; } })()}
{(() => { try { return new URL(asset.url).hostname.replace('www.', ''); } catch { return 'website.com'; } })()}
SECURE PORTAL LINK
{/* Info & Guide Section */}
External Portal Link

{asset.title}

{asset.description ? (

{asset.description}

) : (

No description provided for this external portal link.

)}
{/* Step-by-Step Access Guide */}
Access Guide
  • 1 Click the primary button below or select Open in New Tab from the footer to access this asset.
  • 2 If the partner resource requires authorization, use your partner account credentials to sign in.
{(user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') ? (
Open Website in New Tab
) : (
Access Restricted: You must request and receive approval from the Administrator to open this external link.
)}
) : asset.type === 'case_study' || asset.type.includes('video') || asset.type === 'showcase' ? (
Executive Case Study & Video Showcase {asset.title}
{asset.url && (asset.url.includes('youtube.com') || asset.url.includes('youtu.be')) ? (