Add document delete functionality to Knowledge Base Management

✨ Features Added:
- Delete button on each document card (appears on hover)
- Confirmation modal with document preview before deletion
- Backend DELETE API endpoint for removing documents
- Frontend integration with React Query mutations
- Proper error handling and success feedback

📝 Implementation Details:
- New deleteDocument() API function in frontend/src/lib/api.ts
- Delete button styled in blaze-orange with hover effects
- Modal dialog with file name display and confirmation
- Automatic KB stats and document list refresh after deletion

🎯 User Experience:
- Delete button hidden until hover (prevents accidental clicks)
- Confirmation modal prevents irreversible mistakes
- Real-time list updates after deletion
- Success/error messages with 3s auto-dismiss

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Bob Roberts 2025-10-24 19:09:58 -05:00
parent 8f4156de79
commit 9788518c4c
3 changed files with 345 additions and 3 deletions

205
DELETE_DOCUMENTS_GUIDE.md Normal file
View file

@ -0,0 +1,205 @@
# Deleting Documents from Code-Forge MCP
**When you need to remove previously uploaded documents from a knowledge base.**
---
## Method 1: API Endpoint (Recommended)
The MCP server now supports deleting documents by filename:
```bash
# Delete a single document by filename
curl -X DELETE "http://localhost:8051/api/kb/{kb_name}/documents/{filename}"
# Example: Delete api.md from Pistn Agent OS
curl -X DELETE "http://localhost:8051/api/kb/Pistn%20Agent%20OS/documents/api.md"
# Response on success:
{
"success": true,
"message": "Deleted 1 document(s)",
"filename": "api.md",
"kb_name": "Pistn Agent OS"
}
```
### Notes
- Replace `{kb_name}` with the actual KB name (use URL encoding for spaces: `%20`)
- The filename should match exactly what was uploaded
- Deleting a document removes it and all its chunks from the vector database
---
## Method 2: Manual PostgreSQL Query (Alternative)
If the API endpoint has issues, you can delete documents directly from PostgreSQL:
```sql
-- Connect to codeforge database
psql -h localhost -U $USER -d codeforge
-- Find documents to delete
SELECT id, metadata->>'filename' as filename, kb_id
FROM documents
WHERE metadata->>'filename' LIKE '%api.md%'
LIMIT 10;
-- Delete specific document(s)
DELETE FROM documents
WHERE kb_id = (
SELECT id FROM knowledge_bases WHERE name = 'Pistn Agent OS'
)
AND metadata->>'filename' = 'api.md';
-- Verify deletion
SELECT COUNT(*) FROM documents
WHERE kb_id = (
SELECT id FROM knowledge_bases WHERE name = 'Pistn Agent OS'
);
```
---
## Method 3: Delete All Documents in a KB (Nuclear Option)
**WARNING**: This deletes everything. Use with caution.
```bash
# Delete entire KB and recreate it fresh
curl -X DELETE "http://localhost:8051/api/kb/Pistn%20Agent%20OS"
```
Or via PostgreSQL:
```sql
-- Delete KB and all documents
DELETE FROM documents
WHERE kb_id = (
SELECT id FROM knowledge_bases WHERE name = 'Pistn Agent OS'
);
DELETE FROM knowledge_bases WHERE name = 'Pistn Agent OS';
-- Recreate the KB fresh
INSERT INTO knowledge_bases (name, kb_type, slug)
VALUES ('Pistn Agent OS', 'AGENT_OS', 'pistn-agent-os');
```
---
## Common Scenarios
### Scenario 1: Remove Old Specs Before Re-importing
```bash
# Delete all spec documents
for spec_file in "spec.md" "requirements.md" "IMPLEMENTATION.md"; do
curl -X DELETE "http://localhost:8051/api/kb/Pistn%20Agent%20OS/documents/$spec_file"
done
# Then re-import fresh versions
curl -X POST "http://localhost:8051/api/kb/Pistn%20Agent%20OS/import?directory_path=%2FUsers%2Fiamanmp%2FProjects%2Fpistn%2Fagent-os"
```
### Scenario 2: Clean Up Old Documentation Before Adding New
```bash
# List what's in the KB
curl -s "http://localhost:8051/api/kb/Pistn%20Agent%20OS/documents" | python3 -c "
import sys, json
docs = json.load(sys.stdin).get('documents', [])
for doc in docs:
print(f'{doc[\"filename\"]}')"
# Delete specific old docs one by one
```
### Scenario 3: Fresh Start (Delete Everything)
```bash
# Delete the entire KB
curl -X DELETE "http://localhost:8051/api/kb/Pistn%20Agent%20OS"
# Recreate it
curl -X POST "http://localhost:8051/api/kb" \
-H "Content-Type: application/json" \
-d '{
"name": "Pistn Agent OS",
"kb_type": "agent-os",
"description": "Pistn Agent OS code"
}'
# Re-import everything fresh
curl -X POST "http://localhost:8051/api/kb/Pistn%20Agent%20OS/import?directory_path=%2FUsers%2Fiamanmp%2FProjects%2Fpistn%2Fagent-os"
```
---
## Troubleshooting
### "Document not found" error
The filename might not match exactly. To find the correct filename:
```bash
# List all documents in KB
curl -s "http://localhost:8051/api/kb/Pistn%20Agent%20OS/documents" | python3 -m json.tool
```
### Delete endpoint returns "Not Found"
The endpoint might not have loaded properly. Restart the MCP server:
```bash
# Kill existing server
pkill -f "uvicorn.*8051"
# Restart
source /Users/iamanmp/Projects/code-forge/venv/bin/activate
python /Users/iamanmp/Projects/code-forge/mcp_server/server.py > /tmp/mcp_server.log 2>&1 &
# Wait for startup
sleep 3
# Test
curl -s "http://localhost:8051/health" | python3 -m json.tool
```
### PostgreSQL connection issues
Make sure PostgreSQL is running:
```bash
# Check PostgreSQL
brew services list | grep postgresql
# Start if stopped
brew services start postgresql@16
# Test connection
psql -h localhost -U $USER -d codeforge -c "SELECT 1;"
```
---
## UI Delete Functionality
The web UI at http://localhost:5173 currently doesn't have a delete button.
**To add it** (optional enhancement):
1. The frontend would need a "Delete" button in the document list
2. This would call the new `DELETE /api/kb/{kb_name}/documents/{filename}` endpoint
3. See `frontend/src/components/KBManagement.tsx` for where to add this
For now, use the API or PostgreSQL methods above.
---
## Coming Soon
- [ ] UI delete button in Knowledge Base Management
- [ ] Bulk delete by date range (delete all docs older than X days)
- [ ] Archive vs Delete (keep metadata but remove embeddings)
- [ ] Delete confirmation dialog with document preview

View file

@ -1,8 +1,8 @@
import { useState, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { FileText, BarChart3, Clock, Database, Upload, CheckCircle, XCircle, Info, Copy, Link } from 'lucide-react';
import { getKBStats, listDocuments, uploadDocument, type KBStats } from '../lib/api';
import { FileText, BarChart3, Clock, Database, Upload, CheckCircle, XCircle, Info, Copy, Link, Trash2 } from 'lucide-react';
import { getKBStats, listDocuments, uploadDocument, deleteDocument, type KBStats } from '../lib/api';
interface KBManagementProps {
kbName: string;
@ -17,6 +17,7 @@ export default function KBManagement({ kbName, kbSlug, kbType }: KBManagementPro
const [uploadProgress, setUploadProgress] = useState({ current: 0, total: 0 });
const [isBatchUpload, setIsBatchUpload] = useState(false);
const [copiedMCP, setCopiedMCP] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<{ filename: string } | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const queryClient = useQueryClient();
@ -73,6 +74,24 @@ export default function KBManagement({ kbName, kbSlug, kbType }: KBManagementPro
},
});
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (filename: string) => deleteDocument(kbName, filename),
onSuccess: (data) => {
setUploadStatus('success');
setUploadMessage(`✅ Deleted ${data.filename}`);
setDeleteConfirm(null);
queryClient.invalidateQueries({ queryKey: ['kb-stats', kbName] });
queryClient.invalidateQueries({ queryKey: ['kb-documents', kbName] });
setTimeout(() => setUploadStatus('idle'), 3000);
},
onError: (error: any) => {
setUploadStatus('error');
setUploadMessage(`❌ Delete failed: ${error.response?.data?.detail || error.message}`);
setTimeout(() => setUploadStatus('idle'), 5000);
},
});
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setSelectedFiles(Array.from(e.target.files));
@ -411,8 +430,18 @@ export default function KBManagement({ kbName, kbSlug, kbType }: KBManagementPro
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: idx * 0.05 }}
className="p-5 bg-gradient-to-br from-cool-blue/10 to-electric-teal/5 border-2 border-electric-teal/30 rounded-xl hover:border-electric-teal/60 hover:shadow-xl hover:shadow-electric-teal/20 hover:-translate-y-1 transition-all duration-300"
className="p-5 bg-gradient-to-br from-cool-blue/10 to-electric-teal/5 border-2 border-electric-teal/30 rounded-xl hover:border-electric-teal/60 hover:shadow-xl hover:shadow-electric-teal/20 hover:-translate-y-1 transition-all duration-300 relative group"
>
{/* Delete Button */}
<button
onClick={() => setDeleteConfirm({ filename: doc.filename })}
disabled={deleteMutation.isPending}
className="absolute top-3 right-3 p-2 bg-blaze-orange/20 hover:bg-blaze-orange/40 text-blaze-orange rounded-lg opacity-0 group-hover:opacity-100 transition-all duration-200 disabled:opacity-50"
title="Delete document"
>
<Trash2 className="w-4 h-4" />
</button>
<div className="text-center">
{/* Large emoji icon */}
<div className="text-4xl mb-3">{icon}</div>
@ -452,6 +481,57 @@ export default function KBManagement({ kbName, kbSlug, kbType }: KBManagementPro
</div>
)}
</motion.div>
{/* Delete Confirmation Modal */}
{deleteConfirm && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => setDeleteConfirm(null)}
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
onClick={(e) => e.stopPropagation()}
className="bg-deep-night border-2 border-blaze-orange/50 rounded-xl p-6 max-w-sm mx-4"
>
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-blaze-orange/20 rounded-lg">
<XCircle className="w-8 h-8 text-blaze-orange" />
</div>
<div>
<h3 className="font-bold text-white text-lg">Delete Document?</h3>
<p className="text-sm text-light-grey">This action cannot be undone</p>
</div>
</div>
<div className="mb-6 p-4 bg-blaze-orange/10 rounded-lg border border-blaze-orange/30">
<p className="text-sm text-light-grey">
<strong className="text-white">File:</strong> {deleteConfirm.filename}
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setDeleteConfirm(null)}
disabled={deleteMutation.isPending}
className="btn-secondary flex-1"
>
Cancel
</button>
<button
onClick={() => deleteMutation.mutate(deleteConfirm.filename)}
disabled={deleteMutation.isPending}
className="flex-1 px-4 py-2 bg-blaze-orange hover:bg-blaze-orange/80 text-white font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</div>
</motion.div>
</motion.div>
)}
</div>
);
}

View file

@ -646,6 +646,63 @@ async def api_import_directory(kb_name: str, directory_path: str):
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/kb/{kb_name}/documents/{filename}")
async def api_delete_document(kb_name: str, filename: str):
"""REST API: Delete a document from a knowledge base by filename."""
pg_manager = get_pg_manager()
# Validate KB exists
if not pg_manager.collection_exists(kb_name):
raise HTTPException(status_code=404, detail=f"Knowledge base '{kb_name}' not found")
try:
# Get the collection ID
conn = pg_manager.get_connection()
try:
with conn.cursor() as cur:
# Get collection ID
cur.execute(
"SELECT id FROM knowledge_bases WHERE name = %s",
(kb_name,)
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail=f"Knowledge base '{kb_name}' not found")
collection_id = result[0]
# Delete documents with this filename
cur.execute(
"""
DELETE FROM documents
WHERE kb_id = %s AND metadata->>'filename' = %s
RETURNING id
""",
(collection_id, filename)
)
deleted_count = len(cur.fetchall())
conn.commit()
if deleted_count == 0:
raise HTTPException(status_code=404, detail=f"No documents found with filename '{filename}'")
logger.info(f"Deleted {deleted_count} document(s) with filename '{filename}' from KB '{kb_name}'")
return {
"success": True,
"message": f"Deleted {deleted_count} document(s)",
"filename": filename,
"kb_name": kb_name
}
finally:
pg_manager.return_connection(conn)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete document: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/ollama/models")
async def api_list_ollama_models():
"""REST API: List available Ollama models."""