diff --git a/surfsense_backend/alembic/versions/22_add_cometapi_to_litellmprovider_enum.py b/surfsense_backend/alembic/versions/22_add_cometapi_to_litellmprovider_enum.py
new file mode 100644
index 000000000..a0cd85b08
--- /dev/null
+++ b/surfsense_backend/alembic/versions/22_add_cometapi_to_litellmprovider_enum.py
@@ -0,0 +1,47 @@
+"""Add COMETAPI to LiteLLMProvider enum
+
+Revision ID: 22
+Revises: 21
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "22"
+down_revision: str | None = "21"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ """Add COMETAPI to LiteLLMProvider enum."""
+
+ # Add COMETAPI to the enum if it doesn't already exist
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_enum
+ WHERE enumtypid = 'litellmprovider'::regtype
+ AND enumlabel = 'COMETAPI'
+ ) THEN
+ ALTER TYPE litellmprovider ADD VALUE 'COMETAPI';
+ END IF;
+ END$$;
+ """
+ )
+
+
+def downgrade() -> None:
+ """Remove COMETAPI from LiteLLMProvider enum.
+
+ Note: PostgreSQL doesn't support removing enum values directly.
+ This would require recreating the enum type and updating all dependent objects.
+ For safety, this downgrade is a no-op.
+ """
+ # PostgreSQL doesn't support removing enum values directly
+ # This would require a complex migration recreating the enum
+ pass
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index 457d95544..810723755 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -102,6 +102,7 @@ class LiteLLMProvider(str, Enum):
NLPCLOUD = "NLPCLOUD"
ALEPH_ALPHA = "ALEPH_ALPHA"
PETALS = "PETALS"
+ COMETAPI = "COMETAPI"
CUSTOM = "CUSTOM"
diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py
index 3ea1e8e61..dd7b56033 100644
--- a/surfsense_backend/app/routes/documents_routes.py
+++ b/surfsense_backend/app/routes/documents_routes.py
@@ -22,6 +22,7 @@ from app.schemas import (
DocumentsCreate,
DocumentUpdate,
DocumentWithChunksRead,
+ PaginatedResponse,
)
from app.services.task_logging_service import TaskLoggingService
from app.tasks.document_processors import (
@@ -154,15 +155,36 @@ async def create_documents_file_upload(
) from e
-@router.get("/documents/", response_model=list[DocumentRead])
+@router.get("/documents/", response_model=PaginatedResponse[DocumentRead])
async def read_documents(
- skip: int = 0,
- limit: int = 300,
+ skip: int | None = None,
+ page: int | None = None,
+ page_size: int = 50,
search_space_id: int | None = None,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
+ """
+ List documents owned by the current user, with optional filtering and pagination.
+
+ Args:
+ skip: Absolute number of items to skip from the beginning. If provided, it takes precedence over 'page'.
+ page: Zero-based page index used when 'skip' is not provided.
+ page_size: Number of items per page (default: 50). Use -1 to return all remaining items after the offset.
+ search_space_id: If provided, restrict results to a specific search space.
+ session: Database session (injected).
+ user: Current authenticated user (injected).
+
+ Returns:
+ PaginatedResponse[DocumentRead]: Paginated list of documents visible to the user.
+
+ Notes:
+ - If both 'skip' and 'page' are provided, 'skip' is used.
+ - Results are scoped to documents owned by the current user.
+ """
try:
+ from sqlalchemy import func
+
query = (
select(Document).join(SearchSpace).filter(SearchSpace.user_id == user.id)
)
@@ -171,7 +193,33 @@ async def read_documents(
if search_space_id is not None:
query = query.filter(Document.search_space_id == search_space_id)
- result = await session.execute(query.offset(skip).limit(limit))
+ # Get total count
+ count_query = (
+ select(func.count())
+ .select_from(Document)
+ .join(SearchSpace)
+ .filter(SearchSpace.user_id == user.id)
+ )
+ if search_space_id is not None:
+ count_query = count_query.filter(
+ Document.search_space_id == search_space_id
+ )
+ total_result = await session.execute(count_query)
+ total = total_result.scalar() or 0
+
+ # Calculate offset
+ offset = 0
+ if skip is not None:
+ offset = skip
+ elif page is not None:
+ offset = page * page_size
+
+ # Get paginated results
+ if page_size == -1:
+ result = await session.execute(query.offset(offset))
+ else:
+ result = await session.execute(query.offset(offset).limit(page_size))
+
db_documents = result.scalars().all()
# Convert database objects to API-friendly format
@@ -189,13 +237,106 @@ async def read_documents(
)
)
- return api_documents
+ return PaginatedResponse(items=api_documents, total=total)
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to fetch documents: {e!s}"
) from e
+@router.get("/documents/search/", response_model=PaginatedResponse[DocumentRead])
+async def search_documents(
+ title: str,
+ skip: int | None = None,
+ page: int | None = None,
+ page_size: int = 50,
+ search_space_id: int | None = None,
+ session: AsyncSession = Depends(get_async_session),
+ user: User = Depends(current_active_user),
+):
+ """
+ Search documents by title substring, optionally filtered by search_space_id.
+
+ Args:
+ title: Case-insensitive substring to match against document titles. Required.
+ skip: Absolute number of items to skip from the beginning. If provided, it takes precedence over 'page'. Default: None.
+ page: Zero-based page index used when 'skip' is not provided. Default: None.
+ page_size: Number of items per page. Use -1 to return all remaining items after the offset. Default: 50.
+ search_space_id: Filter results to a specific search space. Default: None.
+ session: Database session (injected).
+ user: Current authenticated user (injected).
+
+ Returns:
+ PaginatedResponse[DocumentRead]: Paginated list of documents matching the query and filter.
+
+ Notes:
+ - Title matching uses ILIKE (case-insensitive).
+ - If both 'skip' and 'page' are provided, 'skip' is used.
+ """
+ try:
+ from sqlalchemy import func
+
+ query = (
+ select(Document).join(SearchSpace).filter(SearchSpace.user_id == user.id)
+ )
+ if search_space_id is not None:
+ query = query.filter(Document.search_space_id == search_space_id)
+
+ # Only search by title (case-insensitive)
+ query = query.filter(Document.title.ilike(f"%{title}%"))
+
+ # Get total count
+ count_query = (
+ select(func.count())
+ .select_from(Document)
+ .join(SearchSpace)
+ .filter(SearchSpace.user_id == user.id)
+ )
+ if search_space_id is not None:
+ count_query = count_query.filter(
+ Document.search_space_id == search_space_id
+ )
+ count_query = count_query.filter(Document.title.ilike(f"%{title}%"))
+ total_result = await session.execute(count_query)
+ total = total_result.scalar() or 0
+
+ # Calculate offset
+ offset = 0
+ if skip is not None:
+ offset = skip
+ elif page is not None:
+ offset = page * page_size
+
+ # Get paginated results
+ if page_size == -1:
+ result = await session.execute(query.offset(offset))
+ else:
+ result = await session.execute(query.offset(offset).limit(page_size))
+
+ db_documents = result.scalars().all()
+
+ # Convert database objects to API-friendly format
+ api_documents = []
+ for doc in db_documents:
+ api_documents.append(
+ DocumentRead(
+ id=doc.id,
+ title=doc.title,
+ document_type=doc.document_type,
+ document_metadata=doc.document_metadata,
+ content=doc.content,
+ created_at=doc.created_at,
+ search_space_id=doc.search_space_id,
+ )
+ )
+
+ return PaginatedResponse(items=api_documents, total=total)
+ except Exception as e:
+ raise HTTPException(
+ status_code=500, detail=f"Failed to search documents: {e!s}"
+ ) from e
+
+
@router.get("/documents/{document_id}", response_model=DocumentRead)
async def read_document(
document_id: int,
diff --git a/surfsense_backend/app/schemas/__init__.py b/surfsense_backend/app/schemas/__init__.py
index ca9287a16..41b2ce23c 100644
--- a/surfsense_backend/app/schemas/__init__.py
+++ b/surfsense_backend/app/schemas/__init__.py
@@ -16,6 +16,7 @@ from .documents import (
DocumentWithChunksRead,
ExtensionDocumentContent,
ExtensionDocumentMetadata,
+ PaginatedResponse,
)
from .llm_config import LLMConfigBase, LLMConfigCreate, LLMConfigRead, LLMConfigUpdate
from .logs import LogBase, LogCreate, LogFilter, LogRead, LogUpdate
@@ -68,6 +69,7 @@ __all__ = [
"LogFilter",
"LogRead",
"LogUpdate",
+ "PaginatedResponse",
"PodcastBase",
"PodcastCreate",
"PodcastGenerateRequest",
diff --git a/surfsense_backend/app/schemas/documents.py b/surfsense_backend/app/schemas/documents.py
index bdaf568e7..fc83d24be 100644
--- a/surfsense_backend/app/schemas/documents.py
+++ b/surfsense_backend/app/schemas/documents.py
@@ -1,4 +1,5 @@
from datetime import datetime
+from typing import TypeVar
from pydantic import BaseModel, ConfigDict
@@ -6,6 +7,8 @@ from app.db import DocumentType
from .chunks import ChunkRead
+T = TypeVar("T")
+
class ExtensionDocumentMetadata(BaseModel):
BrowsingSessionId: str
@@ -53,3 +56,8 @@ class DocumentWithChunksRead(DocumentRead):
chunks: list[ChunkRead] = []
model_config = ConfigDict(from_attributes=True)
+
+
+class PaginatedResponse[T](BaseModel):
+ items: list[T]
+ total: int
diff --git a/surfsense_backend/app/services/llm_service.py b/surfsense_backend/app/services/llm_service.py
index cbf967791..9135e49dc 100644
--- a/surfsense_backend/app/services/llm_service.py
+++ b/surfsense_backend/app/services/llm_service.py
@@ -1,6 +1,6 @@
import logging
-from langchain_community.chat_models import ChatLiteLLM
+from langchain_litellm import ChatLiteLLM
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
@@ -81,6 +81,7 @@ async def get_user_llm_instance(
"MISTRAL": "mistral",
"AZURE_OPENAI": "azure",
"OPENROUTER": "openrouter",
+ "COMETAPI": "cometapi",
# Add more mappings as needed
}
provider_prefix = provider_map.get(
diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml
index fe5691b6d..aa109492d 100644
--- a/surfsense_backend/pyproject.toml
+++ b/surfsense_backend/pyproject.toml
@@ -21,7 +21,6 @@ dependencies = [
"langchain-unstructured>=0.1.6",
"langgraph>=0.3.29",
"linkup-sdk>=0.2.4",
- "litellm>=1.61.4,<1.70.0",
"llama-cloud-services>=0.6.25",
"markdownify>=0.14.1",
"notion-client>=2.3.0",
@@ -42,6 +41,8 @@ dependencies = [
"uvicorn[standard]>=0.34.0",
"validators>=0.34.0",
"youtube-transcript-api>=1.0.3",
+ "litellm>=1.77.5",
+ "langchain-litellm>=0.2.3",
"elasticsearch>=9.1.1",
]
diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock
index 349e93a1a..8c2f8da0d 100644
--- a/surfsense_backend/uv.lock
+++ b/surfsense_backend/uv.lock
@@ -1259,6 +1259,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/84/9c2917a70ed570ddbfd1d32ac23200c1d011e36c332e59950d2f6d204941/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1bc2824e9969c04ab6263d269a1e0e5d40b9bd16ade6b70c29d6ffbc4f3cc102", size = 3387171, upload-time = "2025-05-18T04:55:32.531Z" },
]
+[[package]]
+name = "fastuuid"
+version = "0.13.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/80/3c16a1edad2e6cd82fbd15ac998cc1b881f478bf1f80ca717d941c441874/fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1", size = 18255 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/36/434f137c5970cac19e57834e1f7680e85301619d49891618c00666700c61/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a", size = 494638 },
+ { url = "https://files.pythonhosted.org/packages/ca/3c/083de2ac007b2b305523b9c006dba5051e5afd87a626ef1a39f76e2c6b82/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7", size = 253138 },
+ { url = "https://files.pythonhosted.org/packages/73/5e/630cffa1c8775db526e39e9e4c5c7db0c27be0786bb21ba82c912ae19f63/fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202", size = 244521 },
+ { url = "https://files.pythonhosted.org/packages/4d/51/55d78705f4fbdadf88fb40f382f508d6c7a4941ceddd7825fafebb4cc778/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e", size = 271557 },
+ { url = "https://files.pythonhosted.org/packages/6a/2b/1b89e90a8635e5587ccdbbeb169c590672ce7637880f2c047482a0359950/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854", size = 272334 },
+ { url = "https://files.pythonhosted.org/packages/0c/06/4c8207894eeb30414999e5c3f66ac039bc4003437eb4060d8a1bceb4cc6f/fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86", size = 290594 },
+ { url = "https://files.pythonhosted.org/packages/50/69/96d221931a31d77a47cc2487bdfacfb3091edfc2e7a04b1795df1aec05df/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952", size = 452835 },
+ { url = "https://files.pythonhosted.org/packages/25/ef/bf045f0a47dcec96247497ef3f7a31d86ebc074330e2dccc34b8dbc0468a/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9", size = 468225 },
+ { url = "https://files.pythonhosted.org/packages/30/46/4817ab5a3778927155a4bde92540d4c4fa996161ec8b8e080c8928b0984e/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3", size = 444907 },
+ { url = "https://files.pythonhosted.org/packages/80/27/ab284117ce4dc9b356a7196bdbf220510285f201d27f1f078592cdc8187b/fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85", size = 145415 },
+ { url = "https://files.pythonhosted.org/packages/f4/0c/f970a4222773b248931819f8940800b760283216ca3dda173ed027e94bdd/fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a", size = 150840 },
+ { url = "https://files.pythonhosted.org/packages/4f/62/74fc53f6e04a4dc5b36c34e4e679f85a4c14eec800dcdb0f2c14b5442217/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa", size = 494678 },
+ { url = "https://files.pythonhosted.org/packages/09/ba/f28b9b7045738a8bfccfb9cd6aff4b91fce2669e6b383a48b0694ee9b3ff/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6", size = 253162 },
+ { url = "https://files.pythonhosted.org/packages/b1/18/13fac89cb4c9f0cd7e81a9154a77ecebcc95d2b03477aa91d4d50f7227ee/fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a", size = 244546 },
+ { url = "https://files.pythonhosted.org/packages/04/bf/9691167804d59411cc4269841df949f6dd5e76452ab10dcfcd1dbe04c5bc/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4", size = 271528 },
+ { url = "https://files.pythonhosted.org/packages/a9/b5/7a75a03d1c7aa0b6d573032fcca39391f0aef7f2caabeeb45a672bc0bd3c/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817", size = 272292 },
+ { url = "https://files.pythonhosted.org/packages/c0/db/fa0f16cbf76e6880599533af4ef01bb586949c5320612e9d884eff13e603/fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e", size = 290466 },
+ { url = "https://files.pythonhosted.org/packages/1e/02/6b8c45bfbc8500994dd94edba7f59555f9683c4d8c9a164ae1d25d03c7c7/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448", size = 452838 },
+ { url = "https://files.pythonhosted.org/packages/27/12/85d95a84f265b888e8eb9f9e2b5aaf331e8be60c0a7060146364b3544b6a/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471", size = 468149 },
+ { url = "https://files.pythonhosted.org/packages/ad/da/dd9a137e9ea707e883c92470113a432233482ec9ad3e9b99c4defc4904e6/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83", size = 444933 },
+ { url = "https://files.pythonhosted.org/packages/12/f4/ab363d7f4ac3989691e2dc5ae2d8391cfb0b4169e52ef7fa0ac363e936f0/fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021", size = 145462 },
+ { url = "https://files.pythonhosted.org/packages/aa/8a/52eb77d9c294a54caa0d2d8cc9f906207aa6d916a22de963687ab6db8b86/fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f", size = 150923 },
+]
+
[[package]]
name = "filelock"
version = "3.18.0"
@@ -2235,6 +2265,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/f0/31db18b7b8213266aed926ce89b5bdd84ccde7ee2edf4cab14e3dd2bfcf1/langchain_core-0.3.65-py3-none-any.whl", hash = "sha256:80e8faf6e9f331f8ef728f3fe793549f1d3fb244fcf9e1bdcecab6a6f4669394", size = 438052, upload-time = "2025-06-10T20:08:27.393Z" },
]
+[[package]]
+name = "langchain-litellm"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "litellm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/8f/08032033cd4bdff1d177d6a9e3a1021e47c4c63fd1d8c564af6f3c7e9f8d/langchain_litellm-0.2.3.tar.gz", hash = "sha256:0e11687373ae6a99efee5a04d3a76de4fab0e1459edc0e84adb6f60ca76ebf79", size = 10829 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/26/271b1dad80b39a0e9df7ab13f63fa3fad52ce8288ddf73dec32a2212219f/langchain_litellm-0.2.3-py3-none-any.whl", hash = "sha256:422254b8742893aed6380f5ee73e6ae77869b218758edd0888d14ebd2c439352", size = 11571 },
+]
+
[[package]]
name = "langchain-text-splitters"
version = "0.3.8"
@@ -2404,11 +2447,12 @@ wheels = [
[[package]]
name = "litellm"
-version = "1.69.3"
+version = "1.77.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "click" },
+ { name = "fastuuid" },
{ name = "httpx" },
{ name = "importlib-metadata" },
{ name = "jinja2" },
@@ -2419,9 +2463,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7d/25/2ce407b0ef7e3a2b05300336b7264ec781872c5e60e6ead7a10c9410adcd/litellm-1.69.3.tar.gz", hash = "sha256:748fe9dffea743bf683c9e28e4632c14894863e62c3fbf057560ea7324d89390", size = 7493451, upload-time = "2025-05-15T14:09:44.491Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/a3/85fc92d998ec9645c9fac108618681ef411ca4b338cc7544d6b3aad57699/litellm-1.77.5.tar.gz", hash = "sha256:8e8a83b49c4a6ae044b1a1c01adfbdef72b0031b86f1463dd743e267fa1d7b99", size = 10351819, upload-time = "2025-05-15T14:09:44.491Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/c0/d1538ca18a32adb2669312342e11066e3ddb7dffdf328c1fad3027f78b3e/litellm-1.69.3-py3-none-any.whl", hash = "sha256:31f17024d06824aa8c1798e2c6ac44b69d4f721cefbc3bcd0d53a4568831075e", size = 7761394, upload-time = "2025-05-15T14:09:41.413Z" },
+ { url = "https://files.pythonhosted.org/packages/94/4c/89553f7e375ef39497d86f2266a0cdb37371a07e9e0aa8949f33c15a4198/litellm-1.77.5-py3-none-any.whl", hash = "sha256:07f53964c08d555621d4376cc42330458301ae889bfb6303155dcabc51095fbf", size = 9165458, upload-time = "2025-05-15T14:09:41.413Z" },
]
[[package]]
@@ -3351,7 +3395,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.75.0"
+version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -3363,9 +3407,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/99/b1/318f5d4c482f19c5fcbcde190801bfaaaec23413cda0b88a29f6897448ff/openai-1.75.0.tar.gz", hash = "sha256:fb3ea907efbdb1bcfd0c44507ad9c961afd7dce3147292b54505ecfd17be8fd1", size = 429492, upload-time = "2025-04-16T16:49:29.25Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/dd/4d4d46a06943e37c95b6e388237e1e38d1e9aab264ff070f86345d60b7a4/openai-2.1.0.tar.gz", hash = "sha256:47f3463a5047340a989b4c0cd5378054acfca966ff61a96553b22f098e3270a2", size = 572998, upload-time = "2025-04-16T16:49:29.25Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/80/9a/f34f163294345f123673ed03e77c33dee2534f3ac1f9d18120384457304d/openai-1.75.0-py3-none-any.whl", hash = "sha256:fe6f932d2ded3b429ff67cc9ad118c71327db32eb9d32dd723de3acfca337125", size = 646972, upload-time = "2025-04-16T16:49:27.196Z" },
+ { url = "https://files.pythonhosted.org/packages/68/83/88f64fc8f037885efa8a629d1215f5bc1f037453bab4d4f823b5533319eb/openai-2.1.0-py3-none-any.whl", hash = "sha256:33172e8c06a4576144ba4137a493807a9ca427421dcabc54ad3aa656daf757d3", size = 964939, upload-time = "2025-04-16T16:49:27.196Z" },
]
[[package]]
@@ -5320,6 +5364,7 @@ dependencies = [
{ name = "google-auth-oauthlib" },
{ name = "kokoro" },
{ name = "langchain-community" },
+ { name = "langchain-litellm" },
{ name = "langchain-unstructured" },
{ name = "langgraph" },
{ name = "linkup-sdk" },
@@ -5367,10 +5412,11 @@ requires-dist = [
{ name = "google-auth-oauthlib", specifier = ">=1.2.1" },
{ name = "kokoro", specifier = ">=0.9.4" },
{ name = "langchain-community", specifier = ">=0.3.17" },
+ { name = "langchain-litellm", specifier = ">=0.2.3" },
{ name = "langchain-unstructured", specifier = ">=0.1.6" },
{ name = "langgraph", specifier = ">=0.3.29" },
{ name = "linkup-sdk", specifier = ">=0.2.4" },
- { name = "litellm", specifier = ">=1.61.4,<1.70.0" },
+ { name = "litellm", specifier = ">=1.77.5" },
{ name = "llama-cloud-services", specifier = ">=0.6.25" },
{ name = "markdownify", specifier = ">=0.14.1" },
{ name = "notion-client", specifier = ">=2.3.0" },
diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example
index cff9031be..157bfaa37 100644
--- a/surfsense_web/.env.example
+++ b/surfsense_web/.env.example
@@ -1,3 +1,5 @@
NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE
-NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
\ No newline at end of file
+NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
+# Contact Form Vars - OPTIONAL
+DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
\ No newline at end of file
diff --git a/surfsense_web/app/(home)/contact/page.tsx b/surfsense_web/app/(home)/contact/page.tsx
new file mode 100644
index 000000000..44a6e6d74
--- /dev/null
+++ b/surfsense_web/app/(home)/contact/page.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { ContactFormGridWithDetails } from "@/components/contact/contact-form";
+
+const page = () => {
+ return (
+
+
+
+ );
+};
+
+export default page;
diff --git a/surfsense_web/app/(home)/layout.tsx b/surfsense_web/app/(home)/layout.tsx
new file mode 100644
index 000000000..c71f61fa3
--- /dev/null
+++ b/surfsense_web/app/(home)/layout.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { Footer } from "@/components/homepage/footer";
+import { Navbar } from "@/components/homepage/navbar";
+
+export default function HomePageLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/surfsense_web/app/login/AmbientBackground.tsx b/surfsense_web/app/(home)/login/AmbientBackground.tsx
similarity index 100%
rename from surfsense_web/app/login/AmbientBackground.tsx
rename to surfsense_web/app/(home)/login/AmbientBackground.tsx
diff --git a/surfsense_web/app/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx
similarity index 99%
rename from surfsense_web/app/login/GoogleLoginButton.tsx
rename to surfsense_web/app/(home)/login/GoogleLoginButton.tsx
index ab0e0ddf8..545f279e7 100644
--- a/surfsense_web/app/login/GoogleLoginButton.tsx
+++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx
@@ -1,6 +1,6 @@
"use client";
import { IconBrandGoogleFilled } from "@tabler/icons-react";
-import { motion } from "framer-motion";
+import { motion } from "motion/react";
import { Logo } from "@/components/Logo";
import { AmbientBackground } from "./AmbientBackground";
diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
similarity index 99%
rename from surfsense_web/app/login/LocalLoginForm.tsx
rename to surfsense_web/app/(home)/login/LocalLoginForm.tsx
index b464bea98..a81b9d793 100644
--- a/surfsense_web/app/login/LocalLoginForm.tsx
+++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
@@ -1,5 +1,5 @@
"use client";
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
diff --git a/surfsense_web/app/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx
similarity index 99%
rename from surfsense_web/app/login/page.tsx
rename to surfsense_web/app/(home)/login/page.tsx
index c9e9d9090..c3f5501a6 100644
--- a/surfsense_web/app/login/page.tsx
+++ b/surfsense_web/app/(home)/login/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { AnimatePresence, motion } from "framer-motion";
import { Loader2 } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
diff --git a/surfsense_web/app/(home)/page.tsx b/surfsense_web/app/(home)/page.tsx
new file mode 100644
index 000000000..8f85774ac
--- /dev/null
+++ b/surfsense_web/app/(home)/page.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { CTAHomepage } from "@/components/homepage/cta";
+import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid";
+import { FeaturesCards } from "@/components/homepage/features-card";
+import { Footer } from "@/components/homepage/footer";
+import { HeroSection } from "@/components/homepage/hero-section";
+import ExternalIntegrations from "@/components/homepage/integrations";
+import { Navbar } from "@/components/homepage/navbar";
+
+export default function HomePage() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/surfsense_web/app/(home)/pricing/page.tsx b/surfsense_web/app/(home)/pricing/page.tsx
new file mode 100644
index 000000000..04837a578
--- /dev/null
+++ b/surfsense_web/app/(home)/pricing/page.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import PricingBasic from "@/components/pricing/pricing-section";
+
+const page = () => {
+ return (
+
+
+
+ );
+};
+
+export default page;
diff --git a/surfsense_web/app/privacy/page.tsx b/surfsense_web/app/(home)/privacy/page.tsx
similarity index 100%
rename from surfsense_web/app/privacy/page.tsx
rename to surfsense_web/app/(home)/privacy/page.tsx
diff --git a/surfsense_web/app/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx
similarity index 99%
rename from surfsense_web/app/register/page.tsx
rename to surfsense_web/app/(home)/register/page.tsx
index f046e5cff..303d9a378 100644
--- a/surfsense_web/app/register/page.tsx
+++ b/surfsense_web/app/(home)/register/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
diff --git a/surfsense_web/app/terms/page.tsx b/surfsense_web/app/(home)/terms/page.tsx
similarity index 100%
rename from surfsense_web/app/terms/page.tsx
rename to surfsense_web/app/(home)/terms/page.tsx
diff --git a/surfsense_web/app/api/contact/route.ts b/surfsense_web/app/api/contact/route.ts
new file mode 100644
index 000000000..0af47dfe3
--- /dev/null
+++ b/surfsense_web/app/api/contact/route.ts
@@ -0,0 +1,61 @@
+import { type NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+import { db } from "@/app/db";
+import { usersTable } from "@/app/db/schema";
+
+// Define validation schema matching the database schema
+const contactSchema = z.object({
+ name: z.string().min(1, "Name is required").max(255, "Name is too long"),
+ email: z.string().email("Invalid email address").max(255, "Email is too long"),
+ company: z.string().min(1, "Company is required").max(255, "Company name is too long"),
+ message: z.string().optional().default(""),
+});
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ // Validate the request body
+ const validatedData = contactSchema.parse(body);
+
+ // Insert into database
+ const result = await db
+ .insert(usersTable)
+ .values({
+ name: validatedData.name,
+ email: validatedData.email,
+ company: validatedData.company,
+ message: validatedData.message,
+ })
+ .returning();
+
+ return NextResponse.json(
+ {
+ success: true,
+ message: "Contact form submitted successfully",
+ data: result[0],
+ },
+ { status: 201 }
+ );
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Validation error",
+ errors: error.errors,
+ },
+ { status: 400 }
+ );
+ }
+
+ console.error("Error submitting contact form:", error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Failed to submit contact form",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx
index 1222733d0..44a2846bb 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx
@@ -1,7 +1,6 @@
"use client";
import { format } from "date-fns";
-import { AnimatePresence, motion, type Variants } from "framer-motion";
import {
Calendar,
CheckCircle,
@@ -14,6 +13,7 @@ import {
Tag,
Trash2,
} from "lucide-react";
+import { AnimatePresence, motion, type Variants } from "motion/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx
index fb803790b..9786263fa 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { format } from "date-fns";
-import { motion } from "framer-motion";
import { Calendar as CalendarIcon, Edit, Plus, RefreshCw, Trash2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -41,7 +41,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
import { cn } from "@/lib/utils";
// Helper function to format date with time
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx
index 83736ff71..87844e8c8 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect } from "react";
import { toast } from "sonner";
@@ -20,7 +20,7 @@ import {
} from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useConnectorEditPage } from "@/hooks/useConnectorEditPage";
+import { useConnectorEditPage } from "@/hooks/use-connector-edit-page";
// Import Utils, Types, Hook, and Components
import { getConnectorTypeDisplay } from "@/lib/connectors/utils";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx
index 4152ff786..1e1a58453 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -24,7 +24,7 @@ import { Input } from "@/components/ui/input";
import {
type SearchSourceConnector,
useSearchSourceConnectors,
-} from "@/hooks/useSearchSourceConnectors";
+} from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const apiConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx
index b787374ef..cd81474ba 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -21,7 +21,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import {
type SearchSourceConnector,
useSearchSourceConnectors,
-} from "@/hooks/useSearchSourceConnectors";
+} from "@/hooks/use-search-source-connectors";
export default function AirtableConnectorPage() {
const router = useRouter();
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx
index 0914a1c00..d7ce20cbf 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx
@@ -22,7 +22,7 @@ import {
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const clickupConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx
index 2eb767bf4..e4784d22c 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -24,7 +24,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const confluenceConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx
index 12ff1c790..47366bc01 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -37,7 +37,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const discordConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx
index 4fc8e1f41..602cf066f 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, CircleAlert, Github, Info, ListChecks, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -39,7 +39,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
// Assuming useSearchSourceConnectors hook exists and works similarly
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod for GitHub PAT entry step
const githubPatFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx
index b04a5eddc..a190af7b3 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
@@ -23,7 +23,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import {
type SearchSourceConnector,
useSearchSourceConnectors,
-} from "@/hooks/useSearchSourceConnectors";
+} from "@/hooks/use-search-source-connectors";
export default function GoogleCalendarConnectorPage() {
const router = useRouter();
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx
index 43123ec4b..573650db9 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
@@ -23,7 +23,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import {
type SearchSourceConnector,
useSearchSourceConnectors,
-} from "@/hooks/useSearchSourceConnectors";
+} from "@/hooks/use-search-source-connectors";
export default function GoogleGmailConnectorPage() {
const router = useRouter();
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx
index c7d4466b8..ea99c07d6 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -37,7 +37,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const jiraConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx
index 9e1581405..71cc97ded 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -37,7 +37,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const linearConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx
index 582898e0a..f68fee18c 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -30,7 +30,7 @@ import {
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const linkupApiFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx
index cc5bde60b..176aadb74 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Key, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -33,7 +33,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import {
type SearchSourceConnector,
useSearchSourceConnectors,
-} from "@/hooks/useSearchSourceConnectors";
+} from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const lumaConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx
index dfb66fe9f..d9a9bdf52 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -37,7 +37,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const notionConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx
index d3cee106e..a8bb9cfe8 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx
@@ -6,7 +6,7 @@ import {
IconChevronDown,
IconChevronRight,
} from "@tabler/icons-react";
-import { AnimatePresence, motion, type Variants } from "framer-motion";
+import { AnimatePresence, motion, type Variants } from "motion/react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useState } from "react";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx
index 667126e8e..f594406ed 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -30,7 +30,7 @@ import {
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const serperApiFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx
index 6e9802003..e27c96da9 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -37,7 +37,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const slackConnectorFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx
index e2321abcb..1afb7fbed 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion } from "framer-motion";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
+import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -30,7 +30,7 @@ import {
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
+import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
// Define the form schema with Zod
const tavilyApiFormSchema = z.object({
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx
index dd8791223..3a821b09f 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx
@@ -1,7 +1,7 @@
"use client";
-import { AnimatePresence, motion, type Variants } from "framer-motion";
import { CircleAlert, CircleX, Columns3, Filter, ListFilter, Trash } from "lucide-react";
+import { AnimatePresence, motion, type Variants } from "motion/react";
import React, { useMemo, useRef } from "react";
import {
AlertDialog,
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx
index d9f9bb251..6d70a7249 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "framer-motion";
import { ChevronDown, ChevronUp, FileX } from "lucide-react";
+import { motion } from "motion/react";
import React from "react";
import { DocumentViewer } from "@/components/document-viewer";
import { Button } from "@/components/ui/button";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx
index 2e6f8f314..2b7434d02 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "framer-motion";
import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from "lucide-react";
+import { motion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx
index 4a69a7533..b63a20367 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx
@@ -1,8 +1,8 @@
"use client";
-import { motion } from "framer-motion";
+import { motion } from "motion/react";
import { useParams } from "next/navigation";
-import { useEffect, useId, useMemo, useState } from "react";
+import { useCallback, useEffect, useId, useMemo, useState } from "react";
import { toast } from "sonner";
import { useDocuments } from "@/hooks/use-documents";
@@ -26,10 +26,6 @@ export default function DocumentsTable() {
const params = useParams();
const searchSpaceId = Number(params.search_space_id);
- const { documents, loading, error, refreshDocuments, deleteDocument } =
- useDocuments(searchSpaceId);
-
- const [data, setData] = useState([]);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounced(search, 250);
const [activeTypes, setActiveTypes] = useState([]);
@@ -45,26 +41,41 @@ export default function DocumentsTable() {
const [sortDesc, setSortDesc] = useState(false);
const [selectedIds, setSelectedIds] = useState>(new Set());
- useEffect(() => {
- if (documents) setData(documents as Document[]);
- }, [documents]);
+ // Use server-side pagination and search
+ const { documents, total, loading, error, fetchDocuments, searchDocuments, deleteDocument } =
+ useDocuments(searchSpaceId, {
+ page: pageIndex,
+ pageSize: pageSize,
+ });
- const filtered = useMemo(() => {
- let result = data;
- if (debouncedSearch.trim()) {
- const q = debouncedSearch.toLowerCase();
- result = result.filter((d) => d.title.toLowerCase().includes(q));
+ // Refetch when pagination changes or when search/filters change
+ useEffect(() => {
+ if (searchSpaceId) {
+ if (debouncedSearch.trim()) {
+ // Use search endpoint if there's a search query
+ searchDocuments?.(debouncedSearch, pageIndex, pageSize);
+ } else {
+ // Use regular fetch if no search
+ fetchDocuments?.(pageIndex, pageSize);
+ }
}
+ }, [pageIndex, pageSize, debouncedSearch, searchSpaceId, fetchDocuments, searchDocuments]);
+
+ // Client-side filtering for document types only
+ // Note: This could also be moved to the backend for better performance
+ const filtered = useMemo(() => {
+ let result = documents || [];
if (activeTypes.length > 0) {
result = result.filter((d) => activeTypes.includes(d.document_type));
}
return result;
- }, [data, debouncedSearch, activeTypes]);
+ }, [documents, activeTypes]);
- const total = filtered.length;
+ // Display filtered results
+ const displayDocs = filtered;
+ const displayTotal = activeTypes.length > 0 ? filtered.length : total;
const pageStart = pageIndex * pageSize;
- const pageEnd = Math.min(pageStart + pageSize, total);
- const pageDocs = filtered.slice(pageStart, pageEnd);
+ const pageEnd = Math.min(pageStart + pageSize, displayTotal);
const onToggleType = (type: string, checked: boolean) => {
setActiveTypes((prev) => (checked ? [...prev, type] : prev.filter((t) => t !== type)));
@@ -75,6 +86,14 @@ export default function DocumentsTable() {
setColumnVisibility((prev) => ({ ...prev, [id]: checked }));
};
+ const refreshCurrentView = useCallback(async () => {
+ if (debouncedSearch.trim()) {
+ await searchDocuments?.(debouncedSearch, pageIndex, pageSize);
+ } else {
+ await fetchDocuments?.(pageIndex, pageSize);
+ }
+ }, [debouncedSearch, pageIndex, pageSize, searchDocuments, fetchDocuments]);
+
const onBulkDelete = async () => {
if (selectedIds.size === 0) {
toast.error("No rows selected");
@@ -86,7 +105,8 @@ export default function DocumentsTable() {
if (okCount === selectedIds.size)
toast.success(`Successfully deleted ${okCount} document(s)`);
else toast.error("Some documents could not be deleted");
- await refreshDocuments?.();
+ // Refetch the current page with appropriate method
+ await refreshCurrentView();
setSelectedIds(new Set());
} catch (e) {
console.error(e);
@@ -113,8 +133,8 @@ export default function DocumentsTable() {
className="w-full px-6 py-4"
>
{
- await (refreshDocuments?.() ?? Promise.resolve());
- }}
+ onRefresh={refreshCurrentView}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
columnVisibility={columnVisibility}
@@ -150,17 +168,17 @@ export default function DocumentsTable() {
{
setPageSize(s);
setPageIndex(0);
}}
onFirst={() => setPageIndex(0)}
onPrev={() => setPageIndex((i) => Math.max(0, i - 1))}
- onNext={() => setPageIndex((i) => (pageEnd < total ? i + 1 : i))}
- onLast={() => setPageIndex(Math.max(0, Math.ceil(total / pageSize) - 1))}
+ onNext={() => setPageIndex((i) => (pageEnd < displayTotal ? i + 1 : i))}
+ onLast={() => setPageIndex(Math.max(0, Math.ceil(displayTotal / pageSize) - 1))}
canPrev={pageIndex > 0}
- canNext={pageEnd < total}
+ canNext={pageEnd < displayTotal}
id={id}
/>
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx
index efea74b49..a70744e83 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle2, FileType, Info, Tag, Upload, X } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx
index 494420585..6c73a7215 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx
@@ -2,8 +2,8 @@
import { IconBrandYoutube } from "@tabler/icons-react";
import { type Tag, TagInput } from "emblor";
-import { motion, type Variants } from "framer-motion";
import { Loader2 } from "lucide-react";
+import { motion, type Variants } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx
index c1c8fd132..f8a0a593e 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx
@@ -15,7 +15,6 @@ import {
useReactTable,
type VisibilityState,
} from "@tanstack/react-table";
-import { AnimatePresence, motion, type Variants } from "framer-motion";
import {
Activity,
AlertCircle,
@@ -42,6 +41,7 @@ import {
X,
Zap,
} from "lucide-react";
+import { AnimatePresence, motion, type Variants } from "motion/react";
import { useParams } from "next/navigation";
import React, { useContext, useId, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx
index a867f5f84..9d3065b0d 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx
@@ -5,8 +5,8 @@ import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo } from "react";
import type { ResearchMode } from "@/components/chat";
import ChatInterface from "@/components/chat/ChatInterface";
+import { useChatAPI, useChatState } from "@/hooks/use-chat";
import type { Document } from "@/hooks/use-documents";
-import { useChatAPI, useChatState } from "@/hooks/useChat";
export default function ResearcherPage() {
const { search_space_id, chat_id } = useParams();
diff --git a/surfsense_web/app/dashboard/api-key/api-key-client.tsx b/surfsense_web/app/dashboard/api-key/api-key-client.tsx
index 97a36d8e6..9163b52d8 100644
--- a/surfsense_web/app/dashboard/api-key/api-key-client.tsx
+++ b/surfsense_web/app/dashboard/api-key/api-key-client.tsx
@@ -1,8 +1,8 @@
"use client";
import { IconCheck, IconCopy, IconKey } from "@tabler/icons-react";
-import { AnimatePresence, motion } from "framer-motion";
import { ArrowLeft } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index 484b95929..e99454ae0 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion, type Variants } from "framer-motion";
import { AlertCircle, Loader2, Plus, Search, Trash2 } from "lucide-react";
+import { motion, type Variants } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx
index b1f2f390e..598536c1b 100644
--- a/surfsense_web/app/dashboard/searchspaces/page.tsx
+++ b/surfsense_web/app/dashboard/searchspaces/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { motion } from "framer-motion";
+import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { SearchSpaceForm } from "@/components/search-space-form";
diff --git a/surfsense_web/app/db/index.ts b/surfsense_web/app/db/index.ts
new file mode 100644
index 000000000..db86725e1
--- /dev/null
+++ b/surfsense_web/app/db/index.ts
@@ -0,0 +1,13 @@
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+import * as schema from "./schema";
+
+// Configure postgres client for Vercel serverless environment
+const client = postgres(process.env.DATABASE_URL!, {
+ max: 1, // Limit connections for serverless (Vercel)
+ idle_timeout: 20, // Close idle connections after 20 seconds
+ max_lifetime: 60 * 30, // Close connections after 30 minutes
+ connect_timeout: 10, // Connection timeout in seconds
+});
+
+export const db = drizzle({ client, schema });
diff --git a/surfsense_web/app/db/schema.ts b/surfsense_web/app/db/schema.ts
new file mode 100644
index 000000000..d8b941ba5
--- /dev/null
+++ b/surfsense_web/app/db/schema.ts
@@ -0,0 +1,9 @@
+import { integer, pgTable, text, varchar } from "drizzle-orm/pg-core";
+
+export const usersTable = pgTable("users", {
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
+ name: varchar({ length: 255 }).notNull(),
+ email: varchar({ length: 255 }).notNull().unique(),
+ company: varchar({ length: 255 }).notNull(),
+ message: text().default(""),
+});
diff --git a/surfsense_web/app/onboard/page.tsx b/surfsense_web/app/onboard/page.tsx
index 997b7e4c8..387bf736a 100644
--- a/surfsense_web/app/onboard/page.tsx
+++ b/surfsense_web/app/onboard/page.tsx
@@ -1,7 +1,7 @@
"use client";
-import { AnimatePresence, motion } from "framer-motion";
import { ArrowLeft, ArrowRight, Bot, CheckCircle, Sparkles } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Logo } from "@/components/Logo";
diff --git a/surfsense_web/app/page.tsx b/surfsense_web/app/page.tsx
deleted file mode 100644
index e5501d35f..000000000
--- a/surfsense_web/app/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client";
-
-import { Footer } from "@/components/Footer";
-import { ModernHeroWithGradients } from "@/components/ModernHeroWithGradients";
-import { Navbar } from "@/components/Navbar";
-
-export default function HomePage() {
- return (
-
-
-
-
-
- );
-}
diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts
index 1380fc561..63e2b5753 100644
--- a/surfsense_web/app/sitemap.ts
+++ b/surfsense_web/app/sitemap.ts
@@ -3,43 +3,55 @@ import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
- url: "https://www.surfsense.net/",
+ url: "https://www.surfsense.com/",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
{
- url: "https://www.surfsense.net/privacy",
+ url: "https://www.surfsense.com/contact",
+ lastModified: new Date(),
+ changeFrequency: "yearly",
+ priority: 1,
+ },
+ {
+ url: "https://www.surfsense.com/pricing",
+ lastModified: new Date(),
+ changeFrequency: "yearly",
+ priority: 0.9,
+ },
+ {
+ url: "https://www.surfsense.com/privacy",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/terms",
+ url: "https://www.surfsense.com/terms",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs",
+ url: "https://www.surfsense.com/docs",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/installation",
+ url: "https://www.surfsense.com/docs/installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/docker-installation",
+ url: "https://www.surfsense.com/docs/docker-installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/manual-installation",
+ url: "https://www.surfsense.com/docs/manual-installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
diff --git a/surfsense_web/components/ModernHeroWithGradients.tsx b/surfsense_web/components/ModernHeroWithGradients.tsx
deleted file mode 100644
index 5c81cf54b..000000000
--- a/surfsense_web/components/ModernHeroWithGradients.tsx
+++ /dev/null
@@ -1,541 +0,0 @@
-"use client";
-import { IconBrandDiscord, IconBrandGithub, IconFileTypeDoc } from "@tabler/icons-react";
-import Link from "next/link";
-import { cn } from "@/lib/utils";
-import { Logo } from "./Logo";
-
-export function ModernHeroWithGradients() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Documentation
-
- {/* Import the Logo component or define it in this file */}
-
-
-
-
-
- SurfSense
-
-
-
- A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to
- external sources such as Search Engines, Slack, Linear, Jira, ClickUp, Confluence,
- Notion, YouTube, GitHub, Discord and more.
-
+ );
+};
+
+export function GridPattern({ width, height, x, y, squares, ...props }: any) {
+ const patternId = useId();
+
+ return (
+
+ );
+}
diff --git a/surfsense_web/components/homepage/cta.tsx b/surfsense_web/components/homepage/cta.tsx
new file mode 100644
index 000000000..a09d7354c
--- /dev/null
+++ b/surfsense_web/components/homepage/cta.tsx
@@ -0,0 +1,110 @@
+"use client";
+import { IconMessageCircleQuestion } from "@tabler/icons-react";
+import Link from "next/link";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function CTAHomepage() {
+ return (
+
+
+
+
+
+
+
+ Transform how your team{" "}
+ discovers and collaborates
+
+
+ Unite your team's knowledge in one collaborative
+ space with intelligent search.
+
+
+
+
+
+
+
+
+ {/*
+
+ "SurfSense has revolutionized how our team shares and discovers knowledge.
+ Everyone can contribute and find what they need instantly. True collaboration at scale."
+