Prism / frontend /src /components /Sidebar.jsx
benroshan's picture
fix: deselect deleted doc from filter in Sidebar handleDelete
9ec69a0
Raw
History Blame Contribute Delete
9.85 kB
import { useState } from "react";
import FileUpload from "./FileUpload";
import { deleteDocument } from "../api";
function NewWorkspaceInput({ onCreated }) {
const [name, setName] = useState("");
const [open, setOpen] = useState(false);
function handleCreate(e) {
e.preventDefault();
const slug = name
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
if (!slug) return;
onCreated(slug);
setName("");
setOpen(false);
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="mt-2 w-full text-xs text-indigo-500 hover:text-indigo-700 text-left"
>
+ New workspace
</button>
);
}
return (
<form onSubmit={handleCreate} className="mt-2 flex gap-1">
<input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="workspace-name"
className="flex-1 text-xs border border-gray-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
<button
type="submit"
className="text-xs bg-indigo-600 text-white rounded-lg px-2 py-1.5 hover:bg-indigo-700"
>
Create
</button>
</form>
);
}
export default function Sidebar({
documents,
setDocuments,
onNewConversation,
evalLog,
onBriefing,
briefing,
onSuggestedQuestion,
currentWorkspace,
workspaces,
onWorkspaceChange,
onWorkspacesUpdate,
filterDocs = [],
onFilterChange,
onFilterClear,
}) {
const [deletingDoc, setDeletingDoc] = useState(null);
async function handleDelete(docName) {
if (!window.confirm(`Remove "${docName}" from the index?`)) return;
setDeletingDoc(docName);
try {
const result = await deleteDocument(docName, currentWorkspace);
setDocuments(result.documents);
if (filterDocs.includes(docName)) onFilterChange?.(docName);
} catch (e) {
alert(`Delete failed: ${e?.response?.data?.detail || e.message}`);
} finally {
setDeletingDoc(null);
}
}
return (
<aside className="w-80 bg-white border-r border-gray-100 flex flex-col shrink-0 h-[calc(100vh-65px)] overflow-y-auto">
<div className="p-4 space-y-4">
{/* Workspace switcher */}
<div className="bg-gray-50/80 rounded-xl p-3.5">
<h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2">
Workspace
</h3>
<select
value={currentWorkspace}
onChange={(e) => onWorkspaceChange(e.target.value)}
className="w-full text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{workspaces.map((ws) => (
<option key={ws} value={ws}>
{ws}
</option>
))}
</select>
<NewWorkspaceInput
onCreated={(ws) => {
onWorkspacesUpdate((prev) => [...new Set([...prev, ws])]);
onWorkspaceChange(ws);
}}
/>
</div>
{/* New Conversation */}
<button
onClick={onNewConversation}
className="w-full px-4 py-2.5 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 transition-colors shadow-sm"
>
New Conversation
</button>
{/* File Upload */}
<div className="bg-gray-50/80 rounded-xl p-3.5">
<h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
Upload Documents
</h3>
<FileUpload onUploadComplete={setDocuments} onBriefing={onBriefing} currentWorkspace={currentWorkspace} />
</div>
{/* Briefing card */}
{briefing && (
<div className="bg-indigo-50 border border-indigo-100 rounded-xl p-3.5 space-y-2">
<h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider truncate">
{briefing.doc_name.length > 30
? briefing.doc_name.slice(0, 30) + "…"
: briefing.doc_name}
</h3>
<ul className="space-y-1">
{(briefing.summary ?? []).map((point, i) => (
<li key={i} className="text-xs text-gray-600 flex gap-1.5">
<span className="text-indigo-400 shrink-0"></span>
{point}
</li>
))}
</ul>
{(briefing.suggested_questions ?? []).length > 0 && (
<div className="pt-1 space-y-1">
<p className="text-xs text-indigo-400 font-medium">Try asking:</p>
{(briefing.suggested_questions ?? []).map((q, i) => (
<button
key={i}
type="button"
onClick={() => onSuggestedQuestion(q)}
className="block w-full text-left text-xs bg-white border border-indigo-200 rounded-lg px-2.5 py-1.5 text-gray-700 hover:bg-indigo-50 hover:border-indigo-400 transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:outline-none"
>
{q}
</button>
))}
</div>
)}
</div>
)}
{/* Uploaded Documents */}
<div className="bg-gray-50/80 rounded-xl p-3.5">
<div className="flex items-center justify-between mb-2.5">
<h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider">
Documents ({documents.length})
</h3>
{filterDocs.length > 0 && (
<button
type="button"
onClick={onFilterClear}
className="text-xs text-indigo-400 hover:text-indigo-600 transition-colors"
>
Clear filter
</button>
)}
</div>
{documents.length === 0 ? (
<p className="text-sm text-gray-400">No documents yet</p>
) : (
<ul className="space-y-1.5">
{documents.map((doc, i) => {
const isSelected = filterDocs.includes(doc.name);
const isFiltering = filterDocs.length > 0;
return (
<li
key={i}
className={`flex items-center justify-between text-sm bg-white rounded-lg px-3 py-2 shadow-xs transition-all cursor-pointer ${
isSelected
? "ring-2 ring-indigo-500 bg-indigo-50"
: isFiltering
? "opacity-50"
: "hover:ring-1 hover:ring-indigo-300"
}`}
onClick={() => onFilterChange(doc.name)}
title={isSelected ? "Click to remove filter" : "Click to filter to this doc"}
>
<span className={`truncate flex-1 min-w-0 ${isSelected ? "text-indigo-700 font-medium" : "text-gray-700"}`}>
{doc.name}
</span>
<span className="text-indigo-400 shrink-0 ml-2 text-xs font-medium">
{doc.chunk_count}
</span>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(doc.name); }}
disabled={deletingDoc === doc.name}
title="Remove document"
className="ml-2 shrink-0 text-gray-300 hover:text-red-500 transition-colors disabled:opacity-40"
>
{deletingDoc === doc.name ? (
<svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
</svg>
) : (
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zm-1 6a1 1 0 112 0v5a1 1 0 11-2 0V8zm4 0a1 1 0 112 0v5a1 1 0 11-2 0V8z" clipRule="evenodd"/>
</svg>
)}
</button>
</li>
);
})}
</ul>
)}
</div>
</div>
{/* Footer: version badge + eval link */}
<div className="mt-auto p-4 border-t border-gray-100 flex items-center justify-between">
<span className="text-xs text-gray-400 font-medium">
<span className="bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-full font-semibold">Violet v1.3</span>
</span>
<a
href="https://askprism-eval.vercel.app/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-indigo-600 transition-colors group"
>
<svg className="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span className="group-hover:underline">Eval →</span>
</a>
</div>
</aside>
);
}