2026-04-02 15:37:00 -06:00
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
2026-02-13 09:43:09 -07:00
|
|
|
import ReactMarkdown from 'react-markdown';
|
2026-04-07 10:23:10 -06:00
|
|
|
import rehypeSanitize from 'rehype-sanitize';
|
2026-04-02 15:37:00 -06:00
|
|
|
import mermaid from 'mermaid';
|
2026-02-13 09:43:09 -07:00
|
|
|
import { X, Download, Loader, AlertCircle, FileText, File } from 'lucide-react';
|
|
|
|
|
|
2026-04-02 15:37:00 -06:00
|
|
|
mermaid.initialize({
|
|
|
|
|
startOnLoad: false,
|
|
|
|
|
theme: 'dark',
|
|
|
|
|
darkMode: true,
|
|
|
|
|
themeVariables: {
|
|
|
|
|
background: '#0f172a',
|
|
|
|
|
primaryColor: '#1e3a5f',
|
|
|
|
|
primaryTextColor: '#e2e8f0',
|
|
|
|
|
primaryBorderColor: '#0ea5e9',
|
|
|
|
|
lineColor: '#475569',
|
|
|
|
|
secondaryColor: '#1a2e1a',
|
|
|
|
|
tertiaryColor: '#2d1f14',
|
|
|
|
|
edgeLabelBackground: '#1e293b',
|
|
|
|
|
clusterBkg: '#1e293b',
|
|
|
|
|
titleColor: '#e2e8f0',
|
|
|
|
|
fontFamily: 'monospace'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mermaidCounter = 0;
|
|
|
|
|
|
|
|
|
|
function MermaidDiagram({ code }) {
|
|
|
|
|
const ref = useRef(null);
|
|
|
|
|
const [svgError, setSvgError] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
const id = `mermaid-kb-${++mermaidCounter}`;
|
|
|
|
|
mermaid.render(id, code)
|
|
|
|
|
.then(({ svg }) => {
|
|
|
|
|
if (!cancelled && ref.current) {
|
|
|
|
|
ref.current.innerHTML = svg;
|
|
|
|
|
// Make SVG responsive
|
|
|
|
|
const svgEl = ref.current.querySelector('svg');
|
|
|
|
|
if (svgEl) {
|
|
|
|
|
svgEl.removeAttribute('width');
|
|
|
|
|
svgEl.removeAttribute('height');
|
|
|
|
|
svgEl.style.width = '100%';
|
|
|
|
|
svgEl.style.maxWidth = '100%';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
if (!cancelled) setSvgError(err.message || 'Failed to render diagram');
|
|
|
|
|
});
|
|
|
|
|
return () => { cancelled = true; };
|
|
|
|
|
}, [code]);
|
|
|
|
|
|
|
|
|
|
if (svgError) {
|
|
|
|
|
return (
|
|
|
|
|
<pre style={{ color: '#EF4444', fontSize: '0.75rem', padding: '0.75rem', background: 'rgba(239,68,68,0.1)', borderRadius: '0.375rem', overflowX: 'auto' }}>
|
|
|
|
|
Mermaid render error: {svgError}
|
|
|
|
|
</pre>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
style={{ background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(14,165,233,0.2)', borderRadius: '0.5rem', padding: '1rem', margin: '1rem 0', overflowX: 'auto' }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 09:43:09 -07:00
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
|
|
|
|
|
|
export default function KnowledgeBaseViewer({ article, onClose }) {
|
|
|
|
|
const [content, setContent] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchArticleContent();
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [article.id]);
|
|
|
|
|
|
|
|
|
|
const fetchArticleContent = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/knowledge-base/${article.id}/content`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch article content');
|
|
|
|
|
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
setContent(text);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error fetching article content:', err);
|
|
|
|
|
setError(err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDownload = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/knowledge-base/${article.id}/download`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Download failed');
|
|
|
|
|
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = article.file_name;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error downloading file:', err);
|
|
|
|
|
setError('Failed to download file');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isMarkdown = article.file_name?.endsWith('.md');
|
|
|
|
|
const isText = article.file_name?.endsWith('.txt');
|
|
|
|
|
const isPDF = article.file_name?.endsWith('.pdf');
|
|
|
|
|
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(article.file_name || '');
|
|
|
|
|
|
|
|
|
|
const getCategoryColor = (cat) => {
|
|
|
|
|
const colors = {
|
|
|
|
|
'General': '#94A3B8',
|
|
|
|
|
'Policy': '#0EA5E9',
|
|
|
|
|
'Procedure': '#10B981',
|
|
|
|
|
'Guide': '#F59E0B',
|
|
|
|
|
'Reference': '#8B5CF6'
|
|
|
|
|
};
|
|
|
|
|
return colors[cat] || '#94A3B8';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateString) => {
|
|
|
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
|
|
|
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
|
|
|
borderRadius: '0.5rem',
|
|
|
|
|
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15)',
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-start justify-between mb-4 pb-4" style={{ borderBottom: '1px solid rgba(14, 165, 233, 0.2)' }}>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
|
|
<FileText className="w-5 h-5" style={{ color: getCategoryColor(article.category) }} />
|
|
|
|
|
<h2 className="text-xl font-semibold" style={{ color: '#E2E8F0', fontFamily: 'monospace' }}>
|
|
|
|
|
{article.title}
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
{article.description && (
|
|
|
|
|
<p className="text-sm mb-2" style={{ color: '#94A3B8' }}>
|
|
|
|
|
{article.description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center gap-3 text-xs" style={{ color: '#64748B' }}>
|
|
|
|
|
<span
|
|
|
|
|
className="px-2 py-1 rounded"
|
|
|
|
|
style={{
|
|
|
|
|
background: `${getCategoryColor(article.category)}33`,
|
|
|
|
|
color: getCategoryColor(article.category),
|
|
|
|
|
fontWeight: '600'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{article.category}
|
|
|
|
|
</span>
|
|
|
|
|
<span>Created: {formatDate(article.created_at)}</span>
|
|
|
|
|
{article.created_by_username && (
|
|
|
|
|
<span>By: {article.created_by_username}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2 ml-4">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleDownload}
|
|
|
|
|
className="intel-button intel-button-small"
|
|
|
|
|
title="Download"
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="intel-button intel-button-small"
|
|
|
|
|
title="Close"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="kb-content-area">
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<Loader className="w-8 h-8 animate-spin mx-auto mb-4" style={{ color: '#0EA5E9' }} />
|
|
|
|
|
<p style={{ color: '#94A3B8' }}>Loading document...</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="flex items-start gap-3 p-4 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #EF4444' }}>
|
|
|
|
|
<AlertCircle className="w-6 h-6 flex-shrink-0" style={{ color: '#EF4444' }} />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium" style={{ color: '#FCA5A5' }}>Failed to Load Document</p>
|
|
|
|
|
<p className="text-sm mt-1" style={{ color: '#94A3B8' }}>{error}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!loading && !error && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Markdown Rendering */}
|
|
|
|
|
{isMarkdown && (
|
|
|
|
|
<div className="markdown-content">
|
2026-04-02 15:37:00 -06:00
|
|
|
<ReactMarkdown
|
2026-04-07 10:23:10 -06:00
|
|
|
rehypePlugins={[rehypeSanitize]}
|
2026-04-02 15:37:00 -06:00
|
|
|
components={{
|
|
|
|
|
code({ inline, className, children }) {
|
|
|
|
|
const lang = /language-(\w+)/.exec(className || '')?.[1];
|
|
|
|
|
if (!inline && lang === 'mermaid') {
|
|
|
|
|
return <MermaidDiagram code={String(children).replace(/\n$/, '')} />;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<code
|
|
|
|
|
className={className}
|
|
|
|
|
style={inline ? { background: 'rgba(14,165,233,0.15)', padding: '0.1rem 0.3rem', borderRadius: '0.25rem', fontFamily: 'monospace', fontSize: '0.85em' } : {}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</code>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</ReactMarkdown>
|
2026-02-13 09:43:09 -07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Plain Text */}
|
|
|
|
|
{isText && !isMarkdown && (
|
|
|
|
|
<pre
|
|
|
|
|
className="text-sm p-4 rounded overflow-auto"
|
|
|
|
|
style={{
|
|
|
|
|
background: 'rgba(15, 23, 42, 0.8)',
|
|
|
|
|
border: '1px solid rgba(14, 165, 233, 0.2)',
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
wordWrap: 'break-word',
|
|
|
|
|
maxHeight: '600px'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</pre>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* PDF */}
|
|
|
|
|
{isPDF && (
|
2026-02-13 10:46:32 -07:00
|
|
|
<div className="w-full" style={{ height: '700px' }}>
|
|
|
|
|
<iframe
|
2026-04-07 10:23:10 -06:00
|
|
|
sandbox="allow-same-origin"
|
2026-02-13 10:46:32 -07:00
|
|
|
src={`${API_BASE}/knowledge-base/${article.id}/content`}
|
|
|
|
|
title={article.title}
|
|
|
|
|
className="w-full h-full rounded"
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid rgba(14, 165, 233, 0.3)',
|
|
|
|
|
background: 'rgba(15, 23, 42, 0.8)'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<File className="w-16 h-16 mx-auto mb-4" style={{ color: '#EF4444' }} />
|
|
|
|
|
<p className="mb-4" style={{ color: '#94A3B8' }}>
|
|
|
|
|
Your browser doesn't support PDF preview. Click the download button to view this file.
|
|
|
|
|
</p>
|
|
|
|
|
<button onClick={handleDownload} className="intel-button intel-button-success">
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
Download PDF
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</iframe>
|
2026-02-13 09:43:09 -07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Images */}
|
|
|
|
|
{isImage && (
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<img
|
|
|
|
|
src={`${API_BASE}/knowledge-base/${article.id}/content`}
|
|
|
|
|
alt={article.title}
|
|
|
|
|
className="max-w-full h-auto rounded"
|
|
|
|
|
style={{ border: '1px solid rgba(14, 165, 233, 0.3)' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Other file types */}
|
|
|
|
|
{!isMarkdown && !isText && !isPDF && !isImage && (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<File className="w-16 h-16 mx-auto mb-4" style={{ color: '#94A3B8' }} />
|
|
|
|
|
<p className="mb-4" style={{ color: '#94A3B8' }}>
|
|
|
|
|
Preview not available for this file type.
|
|
|
|
|
</p>
|
|
|
|
|
<button onClick={handleDownload} className="intel-button intel-button-success">
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
Download File
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|