diff --git a/README.md b/README.md index 035c4f515..9714b9e65 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ NotebookLM is one of the best and most useful AI platforms out there, but once y - **No Vendor Lock-in** - Configure any LLM, image, TTS, and STT models to use. - **25+ External Data Sources** - Add your sources from Google Drive, OneDrive, Dropbox, Notion, and many other external services. - **Real-Time Multiplayer Support** - Work easily with your team members in a shared notebook. +- **AI File Sorting** - Automatically organize your documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic. - **Desktop App** - Get AI assistance in any application with Quick Assist, General Assist, Extreme Assist, and local folder sync. ...and more to come. @@ -199,6 +200,7 @@ All features operate against your chosen search space, so your answers are alway | **Video Generation** | Cinematic Video Overviews via Veo 3 (Ultra only) | Available (NotebookLM is better here, actively improving) | | **Presentation Generation** | Better looking slides but not editable | Create editable, slide-based presentations | | **Podcast Generation** | Audio Overviews with customizable hosts and languages | Available with multiple TTS providers (NotebookLM is better here, actively improving) | +| **AI File Sorting** | No | LLM-powered auto-categorization into source, date, category, and subcategory folders | | **Desktop App** | No | Native app with General Assist, Quick Assist, Extreme Assist, and local folder sync | | **Browser Extension** | No | Cross-browser extension to save any webpage, including auth-protected pages | diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index d0d6b7ff4..95aa1bf5d 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -15,7 +15,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware -from slowapi.util import get_remote_address +from slowapi.util import get_remote_address # noqa: F401 — kept for reference from sqlalchemy.ext.asyncio import AsyncSession from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request as StarletteRequest @@ -35,7 +35,7 @@ from app.config import ( ) from app.db import User, create_db_and_tables, get_async_session from app.exceptions import GENERIC_5XX_MESSAGE, ISSUES_URL, SurfSenseError -from app.rate_limiter import limiter +from app.rate_limiter import get_real_client_ip, limiter from app.routes import router as crud_router from app.routes.auth_routes import router as auth_router from app.schemas import UserCreate, UserRead, UserUpdate @@ -290,7 +290,7 @@ def _check_rate_limit( Uses atomic INCR + EXPIRE to avoid race conditions. Falls back to in-memory sliding window if Redis is unavailable. """ - client_ip = get_remote_address(request) + client_ip = get_real_client_ip(request) key = f"surfsense:auth_rate_limit:{scope}:{client_ip}" try: diff --git a/surfsense_backend/app/rate_limiter.py b/surfsense_backend/app/rate_limiter.py index c59c0b833..0a71e3961 100644 --- a/surfsense_backend/app/rate_limiter.py +++ b/surfsense_backend/app/rate_limiter.py @@ -1,13 +1,33 @@ """Shared SlowAPI limiter instance used by app.py and route modules.""" +from __future__ import annotations + from limits.storage import MemoryStorage from slowapi import Limiter -from slowapi.util import get_remote_address +from starlette.requests import Request from app.config import config + +def get_real_client_ip(request: Request) -> str: + """Extract the real client IP behind Cloudflare / reverse proxies. + + Priority: CF-Connecting-IP > X-Real-IP > X-Forwarded-For (first entry) > socket peer. + """ + cf_ip = request.headers.get("cf-connecting-ip") + if cf_ip: + return cf_ip.strip() + real_ip = request.headers.get("x-real-ip") + if real_ip: + return real_ip.strip() + forwarded = request.headers.get("x-forwarded-for") + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "127.0.0.1" + + limiter = Limiter( - key_func=get_remote_address, + key_func=get_real_client_ip, storage_uri=config.REDIS_APP_URL, default_limits=["1024/minute"], in_memory_fallback_enabled=True, diff --git a/surfsense_backend/app/routes/anonymous_chat_routes.py b/surfsense_backend/app/routes/anonymous_chat_routes.py index d26b89288..4b1ea1141 100644 --- a/surfsense_backend/app/routes/anonymous_chat_routes.py +++ b/surfsense_backend/app/routes/anonymous_chat_routes.py @@ -51,12 +51,17 @@ def _get_or_create_session_id(request: Request, response: Response) -> str: def _get_client_ip(request: Request) -> str: + """Extract the real client IP, preferring Cloudflare's header.""" + cf_ip = request.headers.get("cf-connecting-ip") + if cf_ip: + return cf_ip.strip() + real_ip = request.headers.get("x-real-ip") + if real_ip: + return real_ip.strip() forwarded = request.headers.get("x-forwarded-for") - return ( - forwarded.split(",")[0].strip() - if forwarded - else (request.client.host if request.client else "unknown") - ) + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "unknown" # --------------------------------------------------------------------------- diff --git a/surfsense_web/components/homepage/features-bento-grid.tsx b/surfsense_web/components/homepage/features-bento-grid.tsx index 32cbe2582..835ccd2c2 100644 --- a/surfsense_web/components/homepage/features-bento-grid.tsx +++ b/surfsense_web/components/homepage/features-bento-grid.tsx @@ -1,4 +1,10 @@ -import { IconMessage, IconMicrophone, IconSearch, IconUsers } from "@tabler/icons-react"; +import { + IconBinaryTree, + IconMessage, + IconMicrophone, + IconSearch, + IconUsers, +} from "@tabler/icons-react"; import Image from "next/image"; import React from "react"; import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid"; @@ -414,6 +420,91 @@ const AudioCommentIllustration = () => ( ); +const AiSortIllustration = () => ( +
+ + AI File Sorting illustration showing automatic folder organization + {/* Scattered documents on the left */} + + + + + + + {/* AI sparkle / magic in the center */} + + + + + + + + + + {/* Animated sorting arrows */} + + + + + + + + + + + + + {/* Organized folder tree on the right */} + {/* Root folder */} + + + + + + + {/* Subfolder 1 */} + + + + + + + + + {/* Subfolder 2 */} + + + + + + + + + {/* Subfolder 3 */} + + + + + + + + + {/* Sparkle accents */} + + + + + + + + + + + + +
+); + const items = [ { title: "Find, Ask, Act", @@ -431,6 +522,14 @@ const items = [ className: "md:col-span-1", icon: , }, + { + title: "AI File Sorting", + description: + "Automatically organize documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic.", + header: , + className: "md:col-span-1", + icon: , + }, { title: "Collaborate Beyond Text", description: @@ -443,7 +542,7 @@ const items = [ title: "Context Where It Counts", description: "Add comments directly to your chats and docs for clear, in-the-moment feedback.", header: , - className: "md:col-span-2", + className: "md:col-span-1", icon: , }, ]; diff --git a/surfsense_web/components/homepage/why-surfsense.tsx b/surfsense_web/components/homepage/why-surfsense.tsx index c50c3604a..8eeaf9874 100644 --- a/surfsense_web/components/homepage/why-surfsense.tsx +++ b/surfsense_web/components/homepage/why-surfsense.tsx @@ -348,6 +348,11 @@ const comparisonRows: { notebookLm: false, surfSense: true, }, + { + feature: "AI File Sorting", + notebookLm: false, + surfSense: true, + }, ]; function ComparisonStrip() {