diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py index e419be7f7..14a649923 100644 --- a/surfsense_backend/app/routes/new_chat_routes.py +++ b/surfsense_backend/app/routes/new_chat_routes.py @@ -739,7 +739,6 @@ async def update_thread_visibility( @router.post("/threads/{thread_id}/snapshots", response_model=SnapshotCreateResponse) async def create_thread_snapshot( thread_id: int, - request: Request, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): @@ -747,23 +746,19 @@ async def create_thread_snapshot( Create a public snapshot of the thread. Returns existing snapshot URL if content unchanged (deduplication). - Only the thread owner can create snapshots. """ from app.services.public_chat_service import create_snapshot - base_url = str(request.base_url).rstrip("/") return await create_snapshot( session=session, thread_id=thread_id, user=user, - base_url=base_url, ) @router.get("/threads/{thread_id}/snapshots", response_model=SnapshotListResponse) async def list_thread_snapshots( thread_id: int, - request: Request, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): @@ -774,13 +769,11 @@ async def list_thread_snapshots( """ from app.services.public_chat_service import list_snapshots_for_thread - base_url = str(request.base_url).rstrip("/") return SnapshotListResponse( snapshots=await list_snapshots_for_thread( session=session, thread_id=thread_id, user=user, - base_url=base_url, ) ) diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py index 1c15c5f4d..51bb584f6 100644 --- a/surfsense_backend/app/schemas/new_chat.py +++ b/surfsense_backend/app/schemas/new_chat.py @@ -236,6 +236,24 @@ class SnapshotListResponse(BaseModel): snapshots: list[SnapshotInfo] +class SearchSpaceSnapshotInfo(BaseModel): + """Snapshot info with thread context for search space listing.""" + + id: int + share_token: str + public_url: str + created_at: datetime + message_count: int + thread_id: int + thread_title: str + + +class SearchSpaceSnapshotListResponse(BaseModel): + """List of all snapshots in a search space.""" + + snapshots: list[SearchSpaceSnapshotInfo] + + # ============================================================================= # Public Chat View Schemas (for unauthenticated access) # ============================================================================= diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py index ba50c28ba..2125dd8ce 100644 --- a/surfsense_backend/app/services/public_chat_service.py +++ b/surfsense_backend/app/services/public_chat_service.py @@ -161,7 +161,6 @@ async def create_snapshot( session: AsyncSession, thread_id: int, user: User, - base_url: str, ) -> dict: """ Create a public snapshot of a chat thread. @@ -169,6 +168,9 @@ async def create_snapshot( Returns existing snapshot if content unchanged (same hash). Returns new snapshot with unique URL if content changed. """ + from app.config import config + + frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/") result = await session.execute( select(NewChatThread) .options(selectinload(NewChatThread.messages)) @@ -250,7 +252,7 @@ async def create_snapshot( return { "snapshot_id": existing.id, "share_token": existing.share_token, - "public_url": f"{base_url}/public/{existing.share_token}", + "public_url": f"{frontend_url}/public/{existing.share_token}", "is_new": False, } @@ -283,7 +285,7 @@ async def create_snapshot( return { "snapshot_id": snapshot.id, "share_token": snapshot.share_token, - "public_url": f"{base_url}/public/{snapshot.share_token}", + "public_url": f"{frontend_url}/public/{snapshot.share_token}", "is_new": True, } @@ -352,10 +354,10 @@ async def list_snapshots_for_thread( session: AsyncSession, thread_id: int, user: User, - base_url: str, ) -> list[dict]: """List all public snapshots for a thread.""" - # Verify ownership + from app.config import config + result = await session.execute( select(NewChatThread).filter(NewChatThread.id == thread_id) ) @@ -370,7 +372,6 @@ async def list_snapshots_for_thread( detail="Only the creator can view snapshots", ) - # Get snapshots result = await session.execute( select(PublicChatSnapshot) .filter(PublicChatSnapshot.thread_id == thread_id) @@ -378,11 +379,13 @@ async def list_snapshots_for_thread( ) snapshots = result.scalars().all() + frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/") + return [ { "id": s.id, "share_token": s.share_token, - "public_url": f"{base_url}/public/{s.share_token}", + "public_url": f"{frontend_url}/public/{s.share_token}", "created_at": s.created_at.isoformat() if s.created_at else None, "message_count": len(s.message_ids) if s.message_ids else 0, } @@ -390,6 +393,54 @@ async def list_snapshots_for_thread( ] +async def list_snapshots_for_search_space( + session: AsyncSession, + search_space_id: int, + user: User, +) -> list[dict]: + """List all public snapshots for a search space.""" + from app.config import config + + await check_permission( + session, + user, + search_space_id, + Permission.PUBLIC_SHARING_VIEW.value, + "You don't have permission to view public share links", + ) + + result = await session.execute( + select(PublicChatSnapshot) + .join(NewChatThread, PublicChatSnapshot.thread_id == NewChatThread.id) + .filter(NewChatThread.search_space_id == search_space_id) + .order_by(PublicChatSnapshot.created_at.desc()) + ) + snapshots = result.scalars().all() + + snapshot_thread_ids = [s.thread_id for s in snapshots] + thread_result = await session.execute( + select(NewChatThread.id, NewChatThread.title).filter( + NewChatThread.id.in_(snapshot_thread_ids) + ) + ) + thread_titles = {row[0]: row[1] for row in thread_result.fetchall()} + + frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/") + + return [ + { + "id": s.id, + "share_token": s.share_token, + "public_url": f"{frontend_url}/public/{s.share_token}", + "created_at": s.created_at.isoformat() if s.created_at else None, + "message_count": len(s.message_ids) if s.message_ids else 0, + "thread_id": s.thread_id, + "thread_title": thread_titles.get(s.thread_id, "Untitled"), + } + for s in snapshots + ] + + # ============================================================================= # Snapshot Deletion # =============================================================================