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:
parent
8f4156de79
commit
9788518c4c
3 changed files with 345 additions and 3 deletions
205
DELETE_DOCUMENTS_GUIDE.md
Normal file
205
DELETE_DOCUMENTS_GUIDE.md
Normal 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
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue