// Atlas · Enterprise Internal Repository const { useState: useStateRepo, useEffect: useEffectRepo, useRef: useRefRepo } = React; function renderSummaryMarkdown(text) { if (!text) return null; const mono = { fontFamily: "var(--mono)", fontSize: 12 }; const prose = { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" }; const elements = []; const lines = text.split("\n"); let i = 0; const formatInline = (str) => { const parts = []; const re = /(\*\*(.+?)\*\*|`(.+?)`|\*(.+?)\*)/g; let last = 0, m; while ((m = re.exec(str)) !== null) { if (m.index > last) parts.push(str.slice(last, m.index)); if (m[2] !== undefined) parts.push(React.createElement("strong", { key: m.index }, m[2])); else if (m[3] !== undefined) parts.push(React.createElement("code", { key: m.index, style: { ...mono, background: "var(--bg-2)", padding: "1px 4px", borderRadius: 3, fontSize: 11 } }, m[3])); else if (m[4] !== undefined) parts.push(React.createElement("em", { key: m.index }, m[4])); last = m.index + m[0].length; } if (last < str.length) parts.push(str.slice(last)); return parts.length === 1 && typeof parts[0] === "string" ? parts[0] : parts; }; while (i < lines.length) { const line = lines[i]; if (!line.trim()) { i++; continue; } if (line.startsWith("## ")) { elements.push(React.createElement("div", { key: i, style: { ...prose, fontWeight: 700, fontSize: 14, color: "var(--ink)", marginTop: 4, marginBottom: 10, borderBottom: "1px solid var(--line-2)", paddingBottom: 6 } }, formatInline(line.slice(3)))); } else if (line.startsWith("### ")) { elements.push(React.createElement("div", { key: i, style: { ...prose, fontWeight: 600, fontSize: 13, color: "var(--ink)", marginTop: 16, marginBottom: 6 } }, formatInline(line.slice(4)))); } else if (line.startsWith("- ")) { elements.push(React.createElement("div", { key: i, style: { ...prose, fontSize: 13, display: "flex", gap: 6, marginBottom: 5, lineHeight: 1.6 } }, React.createElement("span", { style: { color: "var(--ink-3)", flexShrink: 0 } }, "–"), React.createElement("span", { style: { color: "var(--ink-2)" } }, formatInline(line.slice(2))) )); } else { elements.push(React.createElement("div", { key: i, style: { ...prose, fontSize: 13, color: "var(--ink-2)", marginBottom: 3, lineHeight: 1.6 } }, formatInline(line))); } i++; } return elements; } function formatActivityDate(ts) { if (!ts) return ""; const d = new Date(ts); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } function Repository({ user, isRepoAdmin, isOrgAdmin }) { const [tab, setTab] = useStateRepo("documents"); const [documents, setDocuments] = useStateRepo([]); const [summary, setSummary] = useStateRepo(""); const [activityLog, setActivityLog] = useStateRepo([]); const [teamData, setTeamData] = useStateRepo(null); const [loading, setLoading] = useStateRepo(false); const [error, setError] = useStateRepo(null); const [summaryOpen, setSummaryOpen] = useStateRepo(false); const [howToOpen, setHowToOpen] = useStateRepo(false); const [activityOpen, setActivityOpen] = useStateRepo(false); const [searchQ, setSearchQ] = useStateRepo(""); const [searchResults, setSearchResults] = useStateRepo([]); const [searchLoading, setSearchLoading] = useStateRepo(false); const [searchError, setSearchError] = useStateRepo(null); const [tagOverviewOpen, setTagOverviewOpen] = useStateRepo(false); const [guidelinesOpen, setGuidelinesOpen] = useStateRepo(false); const [uploadDragging, setUploadDragging] = useStateRepo(false); const [uploading, setUploading] = useStateRepo(false); const [uploadError, setUploadError] = useStateRepo(null); const [editingTitle, setEditingTitle] = useStateRepo({}); const [editingTag, setEditingTag] = useStateRepo({}); const [tagFilter, setTagFilter] = useStateRepo([]); const [summaryUpdating, setSummaryUpdating] = useStateRepo(false); const [standardTags, setStandardTags] = useStateRepo([]); const [tagGuideOpen, setTagGuideOpen] = useStateRepo(false); const [approveRoles, setApproveRoles] = useStateRepo({}); const [inviteEmail, setInviteEmail] = useStateRepo(""); const [inviteRole, setInviteRole] = useStateRepo("member"); const [inviteStatus, setInviteStatus] = useStateRepo(null); const [inviteError, setInviteError] = useStateRepo(null); const fileInputRef = useRefRepo(null); const searchTimer = useRefRepo(null); useEffectRepo(() => { loadData(); }, []); const loadData = async () => { setLoading(true); setError(null); try { const [repoData, team] = await Promise.all([ apiGet("/api/repository"), apiGet("/api/team").catch(() => null), ]); setDocuments(repoData.documents || []); setSummary(repoData.summary || ""); setSummaryUpdating(repoData.summary_updating || false); setActivityLog(repoData.activity_log || []); setStandardTags(repoData.standard_tags || []); setTeamData(team); } catch (e) { setError(e.message); } finally { setLoading(false); } }; useEffectRepo(() => { if (!summaryUpdating) return; const id = setInterval(async () => { try { const data = await apiGet("/api/repository"); setSummary(data.summary || ""); setDocuments(data.documents || []); setActivityLog(data.activity_log || []); if (!data.summary_updating) { setSummaryUpdating(false); clearInterval(id); } } catch {} }, 4000); return () => clearInterval(id); }, [summaryUpdating]); useEffectRepo(() => { if (!searchQ.trim()) { setSearchResults([]); return; } clearTimeout(searchTimer.current); searchTimer.current = setTimeout(async () => { setSearchLoading(true); setSearchError(null); try { const data = await apiGet(`/api/repository/search?q=${encodeURIComponent(searchQ.trim())}`); setSearchResults(data.results || []); } catch (e) { setSearchError("Search unavailable — " + e.message); setSearchResults([]); } finally { setSearchLoading(false); } }, 400); return () => clearTimeout(searchTimer.current); }, [searchQ]); const handleUploadFile = async (file) => { setUploadError(null); setUploading(true); try { const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/repository/upload", { method: "POST", body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Upload failed"); } const doc = await res.json(); setDocuments(prev => [...prev, doc]); setSummaryUpdating(true); } catch (e) { setUploadError(e.message); } finally { setUploading(false); } }; const handleDrop = (e) => { e.preventDefault(); setUploadDragging(false); const file = e.dataTransfer.files[0]; if (file) handleUploadFile(file); }; const handleDelete = async (doc_id) => { if (!confirm("Delete this document? This cannot be undone.")) return; try { const res = await fetch(`/api/repository/${doc_id}`, { method: "DELETE" }); if (!res.ok) throw new Error("Delete failed"); setDocuments(prev => prev.filter(d => d.doc_id !== doc_id)); setSummaryUpdating(true); // Refresh activity log after delete const data = await apiGet("/api/repository").catch(() => null); if (data) setActivityLog(data.activity_log || []); } catch (e) { alert(e.message); } }; const handleTitleSave = async (doc_id, title) => { try { const res = await fetch(`/api/repository/${doc_id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); if (res.ok) { const updated = await res.json(); setDocuments(prev => prev.map(d => d.doc_id === doc_id ? updated : d)); } } catch {} setEditingTitle(prev => { const n = { ...prev }; delete n[doc_id]; return n; }); }; const handleTagsSave = async (doc_id, tags) => { try { const res = await fetch(`/api/repository/${doc_id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tags }), }); if (res.ok) { const updated = await res.json(); setDocuments(prev => prev.map(d => d.doc_id === doc_id ? updated : d)); } } catch {} }; const handleAddTag = async (doc_id, newTag, currentTags) => { const trimmed = newTag.trim().toLowerCase(); setEditingTag(prev => { const n = { ...prev }; delete n[doc_id]; return n; }); if (!trimmed || currentTags.includes(trimmed)) return; await handleTagsSave(doc_id, [...currentTags, trimmed]); }; const handleRemoveTag = async (doc_id, tag, currentTags) => { await handleTagsSave(doc_id, currentTags.filter(t => t !== tag)); }; const handleSuggestTags = async (doc_id) => { try { const res = await fetch(`/api/repository/${doc_id}/suggest-tags`, { method: "POST" }); if (!res.ok) throw new Error("Failed"); await loadData(); } catch (e) { alert("Could not auto-tag: " + e.message); } }; const handleTeamAction = async (memberEmail, action, extra = {}) => { try { const res = await fetch(`/api/team/members/${encodeURIComponent(memberEmail)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, ...extra }), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Action failed"); } const team = await apiGet("/api/team"); setTeamData(team); } catch (e) { alert(e.message); } }; const mono = { fontFamily: "var(--mono)", fontSize: 12 }; const prose = { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", fontSize: 13, lineHeight: 1.5 }; const TAG_COLORS = { "competitive intelligence": { bg: "#EEF2FF", border: "#C7D2FE", text: "#4338CA" }, "clinical data": { bg: "#ECFDF5", border: "#6EE7B7", text: "#065F46" }, "market research": { bg: "#FFF7ED", border: "#FED7AA", text: "#9A3412" }, "regulatory": { bg: "#FEF2F2", border: "#FECACA", text: "#991B1B" }, "internal strategy": { bg: "#F0F9FF", border: "#BAE6FD", text: "#0C4A6E" }, "financial": { bg: "#FEFCE8", border: "#FDE68A", text: "#92400E" }, "technical": { bg: "#F0FDF4", border: "#BBF7D0", text: "#14532D" }, "published literature": { bg: "#FAF5FF", border: "#E9D5FF", text: "#6B21A8" }, "presentations": { bg: "#FFF1F2", border: "#FECDD3", text: "#9F1239" }, "contracts": { bg: "#F8FAFC", border: "#CBD5E1", text: "#334155" }, "case studies": { bg: "#FFFBEB", border: "#FCD34D", text: "#78350F" }, "protocols & SOPs": { bg: "#F0FDFA", border: "#99F6E4", text: "#134E4A" }, }; const tagStyle = (tag, active) => { const c = TAG_COLORS[tag]; if (active) return { background: c ? c.text : "var(--ink)", color: "#fff", border: `1px solid ${c ? c.text : "var(--ink)"}` }; return c ? { background: c.bg, border: `1px solid ${c.border}`, color: c.text } : { background: "var(--bg-2)", border: "1px solid var(--line-2)", color: "var(--ink-2)" }; }; const pill = (color, bg) => ({ color, background: bg, fontSize: 11, fontWeight: 600, padding: "2px 7px", borderRadius: 4, display: "inline-block", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", }); const statusBadge = (status) => { if (status === "indexed") return Indexed; if (status === "no_text") return No text; return Processing; }; const fmtBytes = (n) => { if (!n && n !== 0) return ""; if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`; if (n >= 1024) return `${Math.round(n / 1024)} KB`; return `${n} B`; }; // Self-explaining truncation pill — distinguishes the page cap from the chunk cap so a // dense doc that hit the chunk limit doesn't show a healthy page count next to a bare warning. const truncationPill = (doc) => { if (!doc.truncated) return null; const msg = doc.truncation_reason === "chunks" ? `Indexed first ${doc.chunk_count} passages (chunk limit reached) — some content not indexed` : `Indexed first ${doc.indexed_pages} of ${doc.total_pages} pages — remainder not indexed`; return Truncated; }; const embedPill = (doc) => { if (doc.embed_status === "embedded" || doc.embed_status === undefined) return null; const msg = doc.embed_status === "pending" ? "Semantic embedding pending — using keyword (TF-IDF) search for this doc until re-indexed" : "Keyword (TF-IDF) search only — not semantically indexed"; return Keyword only; }; if (loading) return (
| {h} | ))}||||||
|---|---|---|---|---|---|---|
| {doc.filename} |
{isRepoAdmin && editingTitle[doc.doc_id] !== undefined ? (
setEditingTitle(prev => ({ ...prev, [doc.doc_id]: e.target.value }))}
onBlur={() => handleTitleSave(doc.doc_id, editingTitle[doc.doc_id])}
onKeyDown={e => {
if (e.key === "Enter") handleTitleSave(doc.doc_id, editingTitle[doc.doc_id]);
if (e.key === "Escape") setEditingTitle(prev => { const n = { ...prev }; delete n[doc.doc_id]; return n; });
}}
style={{ width: "100%", ...prose, padding: "2px 4px", border: "1px solid var(--ink)", borderRadius: 3, background: "var(--bg)", color: "var(--ink)", outline: "none" }}
/>
) : (
{doc.title}
{isRepoAdmin && (
)}
)}
|
{(doc.tags || []).map(tag => (
{tag}
{isRepoAdmin && (
)}
))}
{isRepoAdmin && (doc.tags || []).length === 0 && editingTag[doc.doc_id] === undefined && (
)}
{isRepoAdmin && (
editingTag[doc.doc_id] !== undefined ? (
setEditingTag(prev => ({ ...prev, [doc.doc_id]: e.target.value }))}
onBlur={() => handleAddTag(doc.doc_id, editingTag[doc.doc_id] || "", doc.tags || [])}
onKeyDown={e => {
if (e.key === "Enter") handleAddTag(doc.doc_id, editingTag[doc.doc_id], doc.tags || []);
if (e.key === "Escape") setEditingTag(prev => { const n = { ...prev }; delete n[doc.doc_id]; return n; });
}}
style={{ width: "100%", ...mono, fontSize: 11, padding: "3px 8px", border: "1px solid var(--ink)", borderRadius: 6, background: "var(--bg)", color: "var(--ink)", outline: "none", boxSizing: "border-box" }}
/>
{(() => {
const existing = doc.tags || [];
const q = (editingTag[doc.doc_id] || "").toLowerCase();
const suggestions = standardTags.filter(t => !existing.includes(t) && (!q || t.includes(q)));
if (!suggestions.length) return null;
return (
) : (
)
)}
{suggestions.slice(0, 8).map(s => (
))}
);
})()}
|
{statusBadge(doc.status)}
{truncationPill(doc)}
{embedPill(doc)}
{doc.status === "indexed" && (
{doc.total_pages ? `${doc.indexed_pages} of ${doc.total_pages} pages · ` : ""}
{doc.chunk_count != null ? `${doc.chunk_count} passages` : ""}
{doc.size_bytes ? ` · ${fmtBytes(doc.size_bytes)}` : ""}
)}
|
{doc.uploaded_by.split("@")[0]} | {(doc.uploaded_at || "").slice(0, 10)} | {isRepoAdmin && ( )} |