diff --git a/.DS_Store b/.DS_Store index 1495e77..44f7187 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ALL-COMPLETE.md b/Old Readmes/ALL-COMPLETE.md similarity index 100% rename from ALL-COMPLETE.md rename to Old Readmes/ALL-COMPLETE.md diff --git a/COMPLETE-FEATURE-LIST.md b/Old Readmes/COMPLETE-FEATURE-LIST.md similarity index 100% rename from COMPLETE-FEATURE-LIST.md rename to Old Readmes/COMPLETE-FEATURE-LIST.md diff --git a/CONTINUE-HERE.md b/Old Readmes/CONTINUE-HERE.md similarity index 100% rename from CONTINUE-HERE.md rename to Old Readmes/CONTINUE-HERE.md diff --git a/CRITICAL-ISSUES-AND-FIXES.md b/Old Readmes/CRITICAL-ISSUES-AND-FIXES.md similarity index 100% rename from CRITICAL-ISSUES-AND-FIXES.md rename to Old Readmes/CRITICAL-ISSUES-AND-FIXES.md diff --git a/CURRENT-STATUS-HONEST.md b/Old Readmes/CURRENT-STATUS-HONEST.md similarity index 100% rename from CURRENT-STATUS-HONEST.md rename to Old Readmes/CURRENT-STATUS-HONEST.md diff --git a/FINAL-COMPLETE-SUMMARY.md b/Old Readmes/FINAL-COMPLETE-SUMMARY.md similarity index 100% rename from FINAL-COMPLETE-SUMMARY.md rename to Old Readmes/FINAL-COMPLETE-SUMMARY.md diff --git a/FINAL-STATUS.md b/Old Readmes/FINAL-STATUS.md similarity index 100% rename from FINAL-STATUS.md rename to Old Readmes/FINAL-STATUS.md diff --git a/FIX-PLAN.md b/Old Readmes/FIX-PLAN.md similarity index 100% rename from FIX-PLAN.md rename to Old Readmes/FIX-PLAN.md diff --git a/FIXES-COMPLETE.md b/Old Readmes/FIXES-COMPLETE.md similarity index 100% rename from FIXES-COMPLETE.md rename to Old Readmes/FIXES-COMPLETE.md diff --git a/FIXES-IN-PROGRESS.md b/Old Readmes/FIXES-IN-PROGRESS.md similarity index 100% rename from FIXES-IN-PROGRESS.md rename to Old Readmes/FIXES-IN-PROGRESS.md diff --git a/KNOWN-ISSUES.md b/Old Readmes/KNOWN-ISSUES.md similarity index 100% rename from KNOWN-ISSUES.md rename to Old Readmes/KNOWN-ISSUES.md diff --git a/MIGRATION-COMPLETE.md b/Old Readmes/MIGRATION-COMPLETE.md similarity index 100% rename from MIGRATION-COMPLETE.md rename to Old Readmes/MIGRATION-COMPLETE.md diff --git a/MISSING-FEATURES.md b/Old Readmes/MISSING-FEATURES.md similarity index 100% rename from MISSING-FEATURES.md rename to Old Readmes/MISSING-FEATURES.md diff --git a/NEXT-SESSION.md b/Old Readmes/NEXT-SESSION.md similarity index 100% rename from NEXT-SESSION.md rename to Old Readmes/NEXT-SESSION.md diff --git a/NEXT-STEPS.md b/Old Readmes/NEXT-STEPS.md similarity index 100% rename from NEXT-STEPS.md rename to Old Readmes/NEXT-STEPS.md diff --git a/PORTS.md b/Old Readmes/PORTS.md similarity index 100% rename from PORTS.md rename to Old Readmes/PORTS.md diff --git a/README-MIGRATION.md b/Old Readmes/README-MIGRATION.md similarity index 100% rename from README-MIGRATION.md rename to Old Readmes/README-MIGRATION.md diff --git a/REMAINING-FIXES.md b/Old Readmes/REMAINING-FIXES.md similarity index 100% rename from REMAINING-FIXES.md rename to Old Readmes/REMAINING-FIXES.md diff --git a/SESSION-2-COMPLETE.md b/Old Readmes/SESSION-2-COMPLETE.md similarity index 100% rename from SESSION-2-COMPLETE.md rename to Old Readmes/SESSION-2-COMPLETE.md diff --git a/SESSION-COMPLETE.md b/Old Readmes/SESSION-COMPLETE.md similarity index 100% rename from SESSION-COMPLETE.md rename to Old Readmes/SESSION-COMPLETE.md diff --git a/STATUS.md b/Old Readmes/STATUS.md similarity index 100% rename from STATUS.md rename to Old Readmes/STATUS.md diff --git a/TRULY-FINAL.md b/Old Readmes/TRULY-FINAL.md similarity index 100% rename from TRULY-FINAL.md rename to Old Readmes/TRULY-FINAL.md diff --git a/WORK-COMPLETE.md b/Old Readmes/WORK-COMPLETE.md similarity index 100% rename from WORK-COMPLETE.md rename to Old Readmes/WORK-COMPLETE.md diff --git a/backend/conversation_0f449f23-e92a-4c86-9cb7-c02b37f7aba3.mp3 b/backend/conversation_0f449f23-e92a-4c86-9cb7-c02b37f7aba3.mp3 new file mode 100644 index 0000000..8ed0ec4 Binary files /dev/null and b/backend/conversation_0f449f23-e92a-4c86-9cb7-c02b37f7aba3.mp3 differ diff --git a/backend/conversation_1db6b0d1-5448-4a8c-8cf6-3fda65b6cb90.mp3 b/backend/conversation_1db6b0d1-5448-4a8c-8cf6-3fda65b6cb90.mp3 new file mode 100644 index 0000000..eb7ecc1 Binary files /dev/null and b/backend/conversation_1db6b0d1-5448-4a8c-8cf6-3fda65b6cb90.mp3 differ diff --git a/backend/conversation_bb3c89d0-ab52-4570-83d1-ec6bc59f17ca.mp3 b/backend/conversation_bb3c89d0-ab52-4570-83d1-ec6bc59f17ca.mp3 new file mode 100644 index 0000000..9876ebb Binary files /dev/null and b/backend/conversation_bb3c89d0-ab52-4570-83d1-ec6bc59f17ca.mp3 differ diff --git a/backend/src/api/__pycache__/main.cpython-313.pyc b/backend/src/api/__pycache__/main.cpython-313.pyc index 9c5d581..8f222d8 100644 Binary files a/backend/src/api/__pycache__/main.cpython-313.pyc and b/backend/src/api/__pycache__/main.cpython-313.pyc differ diff --git a/backend/src/api/main.py b/backend/src/api/main.py index c4fd938..26696e5 100644 --- a/backend/src/api/main.py +++ b/backend/src/api/main.py @@ -64,3 +64,10 @@ async def root(): "docs": "/docs", "health": "/api/health" } + +@app.on_event("startup") +async def startup_event(): + """Retry any pending background tasks on startup""" + from background_tasks import retry_pending_tasks + retry_pending_tasks() + print("✓ Background task system initialized") diff --git a/backend/src/api/routes/__pycache__/chat_sessions.cpython-313.pyc b/backend/src/api/routes/__pycache__/chat_sessions.cpython-313.pyc index 41eee2c..ae28276 100644 Binary files a/backend/src/api/routes/__pycache__/chat_sessions.cpython-313.pyc and b/backend/src/api/routes/__pycache__/chat_sessions.cpython-313.pyc differ diff --git a/backend/src/api/routes/chat_sessions.py b/backend/src/api/routes/chat_sessions.py index 6094b03..68ed1c6 100644 --- a/backend/src/api/routes/chat_sessions.py +++ b/backend/src/api/routes/chat_sessions.py @@ -53,11 +53,36 @@ async def get_chat_sessions(notebook_id: int, user_id: int = Query(1)): @router.post("/{notebook_id}/sessions") async def create_chat_session_endpoint(notebook_id: int, user_id: int = Query(1), title: Optional[str] = None): - """Create a new chat session""" + """Create a new chat session with deduplication""" + from database import get_db, ChatSession + from datetime import datetime, timedelta + notebook = get_notebook_by_id(notebook_id) if not notebook: raise HTTPException(status_code=404, detail="Notebook not found") + # AGGRESSIVE deduplication: Return most recent session if created within 10 seconds + # This handles mysterious duplicate requests from browser/network/system + db = get_db() + try: + recent = db.query(ChatSession).filter( + ChatSession.notebook_id == notebook_id, + ChatSession.user_id == user_id, + ChatSession.created_at > datetime.utcnow() - timedelta(seconds=10) + ).order_by(ChatSession.created_at.desc()).first() + + if recent: + print(f"🚫 Blocked duplicate - returning most recent session {recent.id} (age: {(datetime.utcnow() - recent.created_at).total_seconds():.1f}s)") + return { + "id": recent.id, + "title": recent.title, + "notebook_id": recent.notebook_id, + "is_shared": recent.is_shared, + "created_at": recent.created_at.isoformat() + } + finally: + db.close() + session = create_notebook_chat_session(user_id, notebook_id, title) if not session: raise HTTPException(status_code=500, detail="Failed to create chat session") diff --git a/backend/src/notebookllama/__pycache__/background_tasks.cpython-313.pyc b/backend/src/notebookllama/__pycache__/background_tasks.cpython-313.pyc index 309e7a8..c630732 100644 Binary files a/backend/src/notebookllama/__pycache__/background_tasks.cpython-313.pyc and b/backend/src/notebookllama/__pycache__/background_tasks.cpython-313.pyc differ diff --git a/backend/src/notebookllama/__pycache__/notebook_manager.cpython-313.pyc b/backend/src/notebookllama/__pycache__/notebook_manager.cpython-313.pyc index 8eb643f..60563e6 100644 Binary files a/backend/src/notebookllama/__pycache__/notebook_manager.cpython-313.pyc and b/backend/src/notebookllama/__pycache__/notebook_manager.cpython-313.pyc differ diff --git a/backend/src/notebookllama/background_tasks.py b/backend/src/notebookllama/background_tasks.py index 62b2ccd..5242fe2 100644 --- a/backend/src/notebookllama/background_tasks.py +++ b/backend/src/notebookllama/background_tasks.py @@ -418,3 +418,24 @@ def get_notebook_processing_tasks(notebook_id: int) -> list: finally: db.close() + +def retry_pending_tasks(): + """Retry all PENDING document processing tasks on startup""" + db = get_db() + try: + pending_tasks = db.query(BackgroundTask).filter( + BackgroundTask.task_type == 'document_processing', + BackgroundTask.status == TaskStatus.PENDING + ).order_by(BackgroundTask.created_at).all() + + if pending_tasks: + print(f"Found {len(pending_tasks)} pending document processing tasks - starting workers") + for task in pending_tasks: + thread = threading.Thread(target=run_background_task, args=(task.id,), daemon=True) + thread.start() + print(f" ✓ Started worker for task {task.id}") + else: + print("No pending tasks to retry") + finally: + db.close() + diff --git a/backend/src/notebookllama/notebook_manager.py b/backend/src/notebookllama/notebook_manager.py index 978c52d..ee9ffbd 100644 --- a/backend/src/notebookllama/notebook_manager.py +++ b/backend/src/notebookllama/notebook_manager.py @@ -283,23 +283,30 @@ def create_notebook_chat_session(user_id: int, notebook_id: int, title: Optional def get_user_private_chats(notebook_id: int, user_id: int) -> List[dict]: """Get user's private chat sessions for a notebook""" - from database import ChatSession + from database import ChatSession, ChatMessage + from sqlalchemy import func db = get_db() try: - sessions = db.query(ChatSession).filter( + sessions = db.query( + ChatSession, + func.count(ChatMessage.id).label('message_count') + ).outerjoin( + ChatMessage, ChatSession.id == ChatMessage.session_id + ).filter( ChatSession.notebook_id == notebook_id, ChatSession.user_id == user_id, ChatSession.is_shared == False - ).order_by(ChatSession.updated_at.desc()).all() + ).group_by(ChatSession.id).order_by(ChatSession.updated_at.desc()).all() # Return as dicts to avoid session issues return [{ - 'id': s.id, - 'title': s.title, - 'user_id': s.user_id, - 'is_shared': s.is_shared, - 'created_at': s.created_at, - 'updated_at': s.updated_at + 'id': s.ChatSession.id, + 'title': s.ChatSession.title, + 'user_id': s.ChatSession.user_id, + 'is_shared': s.ChatSession.is_shared, + 'message_count': s.message_count, + 'created_at': s.ChatSession.created_at, + 'updated_at': s.ChatSession.updated_at } for s in sessions] finally: db.close() @@ -307,23 +314,30 @@ def get_user_private_chats(notebook_id: int, user_id: int) -> List[dict]: def get_shared_chats(notebook_id: int) -> List[dict]: """Get shared chat sessions for a notebook (visible to all collaborators)""" - from database import ChatSession, User + from database import ChatSession, ChatMessage, User + from sqlalchemy import func db = get_db() try: - sessions = db.query(ChatSession).filter( + sessions = db.query( + ChatSession, + func.count(ChatMessage.id).label('message_count') + ).outerjoin( + ChatMessage, ChatSession.id == ChatMessage.session_id + ).filter( ChatSession.notebook_id == notebook_id, ChatSession.is_shared == True - ).order_by(ChatSession.updated_at.desc()).all() + ).group_by(ChatSession.id).order_by(ChatSession.updated_at.desc()).all() # Return as dicts with user info to avoid session issues return [{ - 'id': s.id, - 'title': s.title, - 'user_id': s.user_id, - 'username': s.user.username, - 'is_shared': s.is_shared, - 'created_at': s.created_at, - 'updated_at': s.updated_at + 'id': s.ChatSession.id, + 'title': s.ChatSession.title, + 'user_id': s.ChatSession.user_id, + 'username': s.ChatSession.user.username, + 'is_shared': s.ChatSession.is_shared, + 'message_count': s.message_count, + 'created_at': s.ChatSession.created_at, + 'updated_at': s.ChatSession.updated_at } for s in sessions] finally: db.close() diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..fcc8e19 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + reactStrictMode: false, // Disable to prevent double-mounting in dev that causes duplicate mutations }; export default nextConfig; diff --git a/frontend/src/app/notebooks/[id]/page.tsx b/frontend/src/app/notebooks/[id]/page.tsx index 55ff70c..5367cf6 100644 --- a/frontend/src/app/notebooks/[id]/page.tsx +++ b/frontend/src/app/notebooks/[id]/page.tsx @@ -95,14 +95,33 @@ export default function NotebookDetailPage() { console.log('Synthesis SUCCESS:', data); setSynthesis(data); queryClient.invalidateQueries({ queryKey: ['synthesis', notebookId] }); - alert('Cross-document analysis complete! Results saved and will persist.'); }, onError: (error: any) => { console.error('Synthesis ERROR:', error); - alert(error.response?.data?.detail || 'Failed to generate synthesis. Make sure documents are processed.'); }, }); + // Auto-trigger synthesis when all documents are processed + useEffect(() => { + if (!notebook || !tasks || synthesisMutation.isPending) return; + + const documents = notebook.documents || []; + if (documents.length === 0) return; + + // Check if all tasks are completed + const allTasksCompleted = tasks.every((task: any) => + task.status === 'COMPLETED' || task.status === 'completed' + ); + + // Check if we have at least 2 documents for synthesis + const hasEnoughDocs = documents.length >= 2; + + if (allTasksCompleted && hasEnoughDocs && !synthesis) { + console.log('All documents processed - auto-triggering synthesis'); + synthesisMutation.mutate(); + } + }, [notebook, tasks, synthesis, synthesisMutation.isPending]); + const podcastMutation = useMutation({ mutationFn: (data: { length: number; theme?: string }) => notebookAPI.generatePodcast(notebookId, data.length, data.theme), @@ -193,6 +212,7 @@ export default function NotebookDetailPage() { setUploading(false); queryClient.invalidateQueries({ queryKey: ['notebook', notebookId] }); queryClient.invalidateQueries({ queryKey: ['notebook-tasks', notebookId] }); + queryClient.invalidateQueries({ queryKey: ['synthesis', notebookId] }); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -760,8 +780,19 @@ function DocumentItem({ const deleteMutation = useMutation({ mutationFn: () => notebookAPI.removeDocument(notebookId, doc.id), - onSuccess: () => { + onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['notebook', notebookId] }); + queryClient.invalidateQueries({ queryKey: ['synthesis', notebookId] }); + + // Invalidate synthesis and trigger regeneration + try { + // Small delay to let the notebook query update + await new Promise(resolve => setTimeout(resolve, 1000)); + const response = await notebookAPI.generateSynthesis(notebookId); + queryClient.invalidateQueries({ queryKey: ['synthesis', notebookId] }); + } catch (error) { + console.error('Failed to regenerate synthesis after document deletion:', error); + } }, }); diff --git a/frontend/src/app/notebooks/page.tsx b/frontend/src/app/notebooks/page.tsx index 614bc35..6f37ff7 100644 --- a/frontend/src/app/notebooks/page.tsx +++ b/frontend/src/app/notebooks/page.tsx @@ -44,10 +44,41 @@ export default function NotebooksPage() { }); const deleteMutation = useMutation({ - mutationFn: (id: number) => notebookAPI.delete(id), + mutationFn: async (id: number) => { + // First check if notebook has shares + const sharesResponse = await notebookAPI.getShares(id); + const shares = sharesResponse.data; + + if (shares && shares.length > 0) { + const confirmDelete = confirm( + `This notebook is shared with ${shares.length} user(s). Deleting it will:\n` + + `- Remove all shares\n` + + `- Delete the notebook and all documents\n` + + `- Remove the LlamaIndex pipeline\n\n` + + `Are you sure you want to continue?` + ); + + if (!confirmDelete) { + throw new Error('Deletion cancelled'); + } + + // Remove all shares first + for (const share of shares) { + await notebookAPI.removeShare(id, share.id); + } + } + + // Now delete the notebook + return notebookAPI.delete(id); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notebooks'] }); }, + onError: (error: any) => { + if (error.message !== 'Deletion cancelled') { + alert(error.response?.data?.detail || 'Failed to delete notebook'); + } + }, }); const handleCreate = (e: React.FormEvent) => { @@ -224,12 +255,9 @@ export default function NotebooksPage() {