import React, { useEffect, useMemo, useRef, useState } from "react"; export default function ContextTab({ owner, repo }) { const [assets, setAssets] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [uploadHint, setUploadHint] = useState(""); const inputRef = useRef(null); const canUse = useMemo(() => Boolean(owner && repo), [owner, repo]); async function loadAssets() { if (!canUse) return; setError(""); try { const res = await fetch(`/api/repos/${owner}/${repo}/context/assets`); if (!res.ok) throw new Error(`Failed to list assets (${res.status})`); const data = await res.json(); setAssets(data.assets || []); } catch (e) { setError(e?.message || "Failed to load assets"); } } useEffect(() => { loadAssets(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [owner, repo]); async function uploadFiles(fileList) { if (!canUse) return; const files = Array.from(fileList || []); if (!files.length) return; setBusy(true); setError(""); setUploadHint(`Uploading ${files.length} file(s)...`); try { for (const f of files) { const form = new FormData(); form.append("file", f); const res = await fetch( `/api/repos/${owner}/${repo}/context/assets/upload`, { method: "POST", body: form } ); if (!res.ok) { const txt = await res.text().catch(() => ""); throw new Error(`Upload failed (${res.status}) ${txt}`); } } setUploadHint("Upload complete. Refreshing list..."); await loadAssets(); setUploadHint(""); } catch (e) { setError(e?.message || "Upload failed"); setUploadHint(""); } finally { setBusy(false); if (inputRef.current) inputRef.current.value = ""; } } async function deleteAsset(assetId) { if (!canUse) return; const ok = window.confirm("Delete this asset? This cannot be undone."); if (!ok) return; setBusy(true); setError(""); try { const res = await fetch( `/api/repos/${owner}/${repo}/context/assets/${assetId}`, { method: "DELETE" } ); if (!res.ok) throw new Error(`Delete failed (${res.status})`); await loadAssets(); } catch (e) { setError(e?.message || "Delete failed"); } finally { setBusy(false); } } function downloadAsset(assetId) { if (!canUse) return; window.open( `/api/repos/${owner}/${repo}/context/assets/${assetId}/download`, "_blank" ); } const empty = !assets || assets.length === 0; return (
Project Context
Upload documents, transcripts, screenshots, etc. (non-destructive, additive).
uploadFiles(e.target.files)} style={styles.fileInput} />
{ e.preventDefault(); e.stopPropagation(); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); if (busy) return; uploadFiles(e.dataTransfer.files); }} >
Drag & drop files here, or click Upload.
Tip: For audio/video, upload a transcript file too.
{uploadHint ?
{uploadHint}
: null} {error ?
{error}
: null}
File
Type
Size
Indexed
Actions
{empty ? (
No context assets yet. Upload docs, transcripts, and screenshots to improve planning quality.
) : ( assets.map((a) => (
{a.filename}
Added: {a.created_at || "-"} | Extracted:{" "} {Number(a.extracted_chars || 0).toLocaleString()} chars
{a.mime || "unknown"}
{formatBytes(a.size_bytes || 0)}
{a.indexed_chunks || 0} chunks
)) )}
); } function formatBytes(bytes) { const b = Number(bytes || 0); if (!b) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; let v = b; while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; } return `${v.toFixed(v >= 10 || i === 0 ? 0 : 1)} ${units[i]}`; } const styles = { wrap: { display: "flex", flexDirection: "column", gap: 12 }, topRow: { display: "flex", justifyContent: "space-between", gap: 12, alignItems: "flex-start", flexWrap: "wrap", }, left: { minWidth: 280 }, right: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, h1: { fontSize: 14, fontWeight: 800, color: "#fff" }, h2: { fontSize: 12, color: "rgba(255,255,255,0.65)", marginTop: 4 }, fileInput: { display: "none" }, btn: { background: "rgba(255,255,255,0.10)", border: "1px solid rgba(255,255,255,0.18)", color: "#fff", borderRadius: 10, padding: "8px 10px", cursor: "pointer", fontSize: 13, }, dropzone: { border: "1px dashed rgba(255,255,255,0.22)", borderRadius: 12, padding: 16, background: "rgba(255,255,255,0.03)", }, dropText: { color: "rgba(255,255,255,0.85)", fontSize: 13 }, dropSub: { color: "rgba(255,255,255,0.55)", fontSize: 12, marginTop: 6 }, hint: { color: "rgba(255,255,255,0.75)", fontSize: 12, padding: "8px 10px", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 10, background: "rgba(255,255,255,0.03)", }, error: { color: "#ffb3b3", fontSize: 12, padding: "8px 10px", border: "1px solid rgba(255,120,120,0.25)", borderRadius: 10, background: "rgba(255,80,80,0.08)", }, tableWrap: { border: "1px solid rgba(255,255,255,0.12)", borderRadius: 12, overflow: "hidden", }, tableHeader: { display: "grid", gridTemplateColumns: "1.6fr 1fr 0.6fr 0.6fr 0.8fr", gap: 0, padding: "10px 12px", background: "rgba(255,255,255,0.03)", borderBottom: "1px solid rgba(255,255,255,0.10)", fontSize: 12, color: "rgba(255,255,255,0.65)", }, row: { display: "grid", gridTemplateColumns: "1.6fr 1fr 0.6fr 0.6fr 0.8fr", padding: "10px 12px", borderBottom: "1px solid rgba(255,255,255,0.08)", alignItems: "center", }, col: { minWidth: 0 }, colName: {}, colMeta: { color: "rgba(255,255,255,0.75)", fontSize: 12 }, colActions: { display: "flex", gap: 8, justifyContent: "flex-end" }, fileName: { color: "#fff", fontSize: 13, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, small: { color: "rgba(255,255,255,0.55)", fontSize: 11, marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, badge: { display: "inline-flex", alignItems: "center", padding: "2px 8px", borderRadius: 999, border: "1px solid rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.04)", fontSize: 11, color: "rgba(255,255,255,0.80)", maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, smallBtn: { background: "rgba(255,255,255,0.08)", border: "1px solid rgba(255,255,255,0.16)", color: "#fff", borderRadius: 10, padding: "6px 8px", cursor: "pointer", fontSize: 12, }, dangerBtn: { border: "1px solid rgba(255,90,90,0.35)", background: "rgba(255,90,90,0.10)", }, empty: { padding: 14, color: "rgba(255,255,255,0.65)", fontSize: 13, }, };