diff --git a/surfsense_backend/alembic/versions/60_add_surfsense_docs_tables.py b/surfsense_backend/alembic/versions/60_add_surfsense_docs_tables.py
index 7e5aa9437..ed03a4077 100644
--- a/surfsense_backend/alembic/versions/60_add_surfsense_docs_tables.py
+++ b/surfsense_backend/alembic/versions/60_add_surfsense_docs_tables.py
@@ -7,7 +7,6 @@ Revises: 59
from collections.abc import Sequence
from alembic import op
-
from app.config import config
# revision identifiers, used by Alembic.
@@ -22,7 +21,7 @@ EMBEDDING_DIM = config.embedding_model_instance.dimension
def upgrade() -> None:
"""Create surfsense_docs_documents and surfsense_docs_chunks tables."""
-
+
# Create surfsense_docs_documents table
op.execute(
f"""
@@ -46,7 +45,7 @@ def upgrade() -> None:
END$$;
"""
)
-
+
# Create indexes for surfsense_docs_documents
op.execute(
"""
@@ -75,7 +74,7 @@ def upgrade() -> None:
END$$;
"""
)
-
+
# Create surfsense_docs_chunks table
op.execute(
f"""
@@ -96,7 +95,7 @@ def upgrade() -> None:
END$$;
"""
)
-
+
# Create indexes for surfsense_docs_chunks
op.execute(
"""
@@ -111,7 +110,7 @@ def upgrade() -> None:
END$$;
"""
)
-
+
# Create vector indexes for similarity search
op.execute(
"""
@@ -119,14 +118,14 @@ def upgrade() -> None:
ON surfsense_docs_documents USING hnsw (embedding public.vector_cosine_ops);
"""
)
-
+
op.execute(
"""
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_vector_index
ON surfsense_docs_chunks USING hnsw (embedding public.vector_cosine_ops);
"""
)
-
+
# Create full-text search indexes (same pattern as documents/chunks tables)
op.execute(
"""
@@ -134,7 +133,7 @@ def upgrade() -> None:
ON surfsense_docs_documents USING gin (to_tsvector('english', content));
"""
)
-
+
op.execute(
"""
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_search_index
@@ -148,18 +147,17 @@ def downgrade() -> None:
# Drop full-text search indexes
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_search_index")
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_search_index")
-
+
# Drop vector indexes
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_vector_index")
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_vector_index")
-
+
# Drop regular indexes
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_chunks_document_id")
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_updated_at")
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_content_hash")
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_source")
-
+
# Drop tables (chunks first due to FK)
op.execute("DROP TABLE IF EXISTS surfsense_docs_chunks")
op.execute("DROP TABLE IF EXISTS surfsense_docs_documents")
-
diff --git a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py
index a34e16ff2..b9b370c23 100644
--- a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py
+++ b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py
@@ -48,10 +48,12 @@ def format_surfsense_docs_results(results: list[tuple]) -> str:
"metadata": {"source": doc.source},
"chunks": [],
}
- grouped[doc.id]["chunks"].append({
- "chunk_id": f"doc-{chunk.id}",
- "content": chunk.content,
- })
+ grouped[doc.id]["chunks"].append(
+ {
+ "chunk_id": f"doc-{chunk.id}",
+ "content": chunk.content,
+ }
+ )
# Render XML matching format_documents_for_context structure
parts: list[str] = []
@@ -70,7 +72,9 @@ def format_surfsense_docs_results(results: list[tuple]) -> str:
parts.append("")
for ch in g["chunks"]:
- parts.append(f" ")
+ parts.append(
+ f" "
+ )
parts.append("")
parts.append("")
@@ -157,4 +161,3 @@ def create_search_surfsense_docs_tool(db_session: AsyncSession):
)
return search_surfsense_docs
-
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index 006d73358..a0b174bf6 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -436,7 +436,9 @@ class SurfsenseDocsDocument(BaseModel, TimestampMixin):
__tablename__ = "surfsense_docs_documents"
- source = Column(String, nullable=False, unique=True, index=True) # File path: "connectors/slack.mdx"
+ source = Column(
+ String, nullable=False, unique=True, index=True
+ ) # File path: "connectors/slack.mdx"
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
content_hash = Column(String, nullable=False, index=True) # For detecting changes
diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py
index 06d75c7c9..8e8ebb72d 100644
--- a/surfsense_backend/app/routes/search_source_connectors_routes.py
+++ b/surfsense_backend/app/routes/search_source_connectors_routes.py
@@ -623,10 +623,7 @@ async def index_connector_content(
SearchSourceConnectorType.LUMA_CONNECTOR,
]:
# Default to today if no end_date provided (users can manually select future dates)
- if end_date is None:
- indexing_to = today_str
- else:
- indexing_to = end_date
+ indexing_to = today_str if end_date is None else end_date
else:
# For non-calendar connectors, cap at today
indexing_to = end_date if end_date else today_str
diff --git a/surfsense_backend/app/schemas/surfsense_docs.py b/surfsense_backend/app/schemas/surfsense_docs.py
index 7464df342..c6029320f 100644
--- a/surfsense_backend/app/schemas/surfsense_docs.py
+++ b/surfsense_backend/app/schemas/surfsense_docs.py
@@ -24,4 +24,3 @@ class SurfsenseDocsDocumentWithChunksRead(BaseModel):
chunks: list[SurfsenseDocsChunkRead]
model_config = ConfigDict(from_attributes=True)
-
diff --git a/surfsense_backend/app/tasks/surfsense_docs_indexer.py b/surfsense_backend/app/tasks/surfsense_docs_indexer.py
index f2c1e69ba..ef287bc65 100644
--- a/surfsense_backend/app/tasks/surfsense_docs_indexer.py
+++ b/surfsense_backend/app/tasks/surfsense_docs_indexer.py
@@ -19,7 +19,12 @@ from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument, async_session_make
logger = logging.getLogger(__name__)
# Path to docs relative to project root
-DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "surfsense_web" / "content" / "docs"
+DOCS_DIR = (
+ Path(__file__).resolve().parent.parent.parent.parent
+ / "surfsense_web"
+ / "content"
+ / "docs"
+)
def parse_mdx_frontmatter(content: str) -> tuple[str, str]:
@@ -38,7 +43,7 @@ def parse_mdx_frontmatter(content: str) -> tuple[str, str]:
if match:
frontmatter = match.group(1)
- content_without_frontmatter = content[match.end():]
+ content_without_frontmatter = content[match.end() :]
# Extract title from frontmatter
title_match = re.search(r"^title:\s*(.+)$", frontmatter, re.MULTILINE)
@@ -93,10 +98,10 @@ def create_surfsense_docs_chunks(content: str) -> list[SurfsenseDocsChunk]:
async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, int]:
"""
Index all Surfsense documentation files.
-
+
Args:
session: SQLAlchemy async session
-
+
Returns:
Tuple of (created, updated, skipped, deleted) counts
"""
@@ -104,45 +109,47 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
updated = 0
skipped = 0
deleted = 0
-
+
# Get all existing docs from database
existing_docs_result = await session.execute(
- select(SurfsenseDocsDocument).options(selectinload(SurfsenseDocsDocument.chunks))
+ select(SurfsenseDocsDocument).options(
+ selectinload(SurfsenseDocsDocument.chunks)
+ )
)
existing_docs = {doc.source: doc for doc in existing_docs_result.scalars().all()}
-
+
# Track which sources we've processed
processed_sources = set()
-
+
# Get all MDX files
mdx_files = get_all_mdx_files()
logger.info(f"Found {len(mdx_files)} MDX files to index")
-
+
for mdx_file in mdx_files:
try:
source = str(mdx_file.relative_to(DOCS_DIR))
processed_sources.add(source)
-
+
# Read file content
raw_content = mdx_file.read_text(encoding="utf-8")
title, content = parse_mdx_frontmatter(raw_content)
content_hash = generate_surfsense_docs_content_hash(raw_content)
-
+
if source in existing_docs:
existing_doc = existing_docs[source]
-
+
# Check if content changed
if existing_doc.content_hash == content_hash:
logger.debug(f"Skipping unchanged: {source}")
skipped += 1
continue
-
+
# Content changed - update document
logger.info(f"Updating changed document: {source}")
-
+
# Create new chunks
chunks = create_surfsense_docs_chunks(content)
-
+
# Update document fields
existing_doc.title = title
existing_doc.content = content
@@ -150,14 +157,14 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
existing_doc.embedding = config.embedding_model_instance.embed(content)
existing_doc.chunks = chunks
existing_doc.updated_at = datetime.now(UTC)
-
+
updated += 1
else:
# New document - create it
logger.info(f"Creating new document: {source}")
-
+
chunks = create_surfsense_docs_chunks(content)
-
+
document = SurfsenseDocsDocument(
source=source,
title=title,
@@ -167,56 +174,56 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
chunks=chunks,
updated_at=datetime.now(UTC),
)
-
+
session.add(document)
created += 1
-
+
except Exception as e:
logger.error(f"Error processing {mdx_file}: {e}", exc_info=True)
continue
-
+
# Delete documents for removed files
for source, doc in existing_docs.items():
if source not in processed_sources:
logger.info(f"Deleting removed document: {source}")
await session.delete(doc)
deleted += 1
-
+
# Commit all changes
await session.commit()
-
+
logger.info(
f"Indexing complete: {created} created, {updated} updated, "
f"{skipped} skipped, {deleted} deleted"
)
-
+
return created, updated, skipped, deleted
async def seed_surfsense_docs() -> tuple[int, int, int, int]:
"""
Seed Surfsense documentation into the database.
-
+
This function indexes all MDX files from the docs directory.
It handles creating, updating, and deleting docs based on content changes.
-
+
Returns:
Tuple of (created, updated, skipped, deleted) counts
Returns (0, 0, 0, 0) if an error occurs
"""
logger.info("Starting Surfsense docs indexing...")
-
+
try:
async with async_session_maker() as session:
created, updated, skipped, deleted = await index_surfsense_docs(session)
-
+
logger.info(
f"Surfsense docs indexing complete: "
f"created={created}, updated={updated}, skipped={skipped}, deleted={deleted}"
)
-
+
return created, updated, skipped, deleted
-
+
except Exception as e:
logger.error(f"Failed to seed Surfsense docs: {e}", exc_info=True)
return 0, 0, 0, 0
diff --git a/surfsense_backend/scripts/seed_surfsense_docs.py b/surfsense_backend/scripts/seed_surfsense_docs.py
index d9536bf91..68899c2aa 100644
--- a/surfsense_backend/scripts/seed_surfsense_docs.py
+++ b/surfsense_backend/scripts/seed_surfsense_docs.py
@@ -24,9 +24,9 @@ def main():
print("=" * 50)
print(" Surfsense Documentation Seeding")
print("=" * 50)
-
+
created, updated, skipped, deleted = asyncio.run(seed_surfsense_docs())
-
+
print()
print("Results:")
print(f" Created: {created}")
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index 3e6d71829..767ce5201 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -105,9 +105,7 @@ function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
{t("welcome_title")}
-
- {t("welcome_description")}
-
+
{t("welcome_description")}
-
- {copied ? t("copied") : t("copy")}
-
+ {copied ? t("copied") : t("copy")}
diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx
index ecc3a11cd..7412a4148 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx
@@ -3,8 +3,8 @@
import { AlertTriangle, Ban, Wrench } from "lucide-react";
import type { FC } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import type { ConnectorStatus } from "../config/connector-status-config";
import { cn } from "@/lib/utils";
+import type { ConnectorStatus } from "../config/connector-status-config";
interface ConnectorStatusBadgeProps {
status: ConnectorStatus;
diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
index 74dd51929..bec4bfcb8 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
@@ -8,8 +8,8 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
import { cn } from "@/lib/utils";
-import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
import { useConnectorStatus } from "../hooks/use-connector-status";
+import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
interface ConnectorAccountsListViewProps {
connectorType: string;
diff --git a/surfsense_web/components/assistant-ui/inline-citation.tsx b/surfsense_web/components/assistant-ui/inline-citation.tsx
index 9eab9a3c3..6b5a4b091 100644
--- a/surfsense_web/components/assistant-ui/inline-citation.tsx
+++ b/surfsense_web/components/assistant-ui/inline-citation.tsx
@@ -15,7 +15,11 @@ interface InlineCitationProps {
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
* Supports both regular knowledge base chunks and Surfsense documentation chunks.
*/
-export const InlineCitation: FC = ({ chunkId, citationNumber, isDocsChunk = false }) => {
+export const InlineCitation: FC = ({
+ chunkId,
+ citationNumber,
+ isDocsChunk = false,
+}) => {
const [isOpen, setIsOpen] = useState(false);
return (
diff --git a/surfsense_web/components/layout/index.ts b/surfsense_web/components/layout/index.ts
index b9c271915..18f8cc9d3 100644
--- a/surfsense_web/components/layout/index.ts
+++ b/surfsense_web/components/layout/index.ts
@@ -6,9 +6,9 @@ export type {
NavItem,
NoteItem,
PageUsage,
+ SearchSpace,
SidebarSectionProps,
User,
- SearchSpace,
} from "./types/layout.types";
export {
AllSearchSpacesSheet,
@@ -23,10 +23,10 @@ export {
NavSection,
NoteListItem,
PageUsageDisplay,
+ SearchSpaceAvatar,
Sidebar,
SidebarCollapseButton,
SidebarHeader,
SidebarSection,
SidebarUserProfile,
- SearchSpaceAvatar,
} from "./ui";
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index 8f42e22aa..70bc96f58 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -28,8 +28,8 @@ import { resetUser, trackLogout } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import type { ChatItem, NavItem, NoteItem, SearchSpace } from "../types/layout.types";
import { CreateSearchSpaceDialog } from "../ui/dialogs";
-import { LayoutShell } from "../ui/shell";
import { AllSearchSpacesSheet } from "../ui/sheets";
+import { LayoutShell } from "../ui/shell";
import { AllChatsSidebar } from "../ui/sidebar/AllChatsSidebar";
import { AllNotesSidebar } from "../ui/sidebar/AllNotesSidebar";
diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
index 978d46f6c..7e962536f 100644
--- a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
+++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
@@ -104,11 +104,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
{t("name_label")}
-
+
@@ -163,4 +159,3 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
);
}
-
diff --git a/surfsense_web/components/layout/ui/dialogs/index.ts b/surfsense_web/components/layout/ui/dialogs/index.ts
index 28f3b387d..807a227de 100644
--- a/surfsense_web/components/layout/ui/dialogs/index.ts
+++ b/surfsense_web/components/layout/ui/dialogs/index.ts
@@ -1,2 +1 @@
export { CreateSearchSpaceDialog } from "./CreateSearchSpaceDialog";
-
diff --git a/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
index 397076cb6..77f4de899 100644
--- a/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
@@ -42,7 +42,12 @@ function getInitials(name: string): string {
return name.slice(0, 2).toUpperCase();
}
-export function SearchSpaceAvatar({ name, isActive, onClick, size = "md" }: SearchSpaceAvatarProps) {
+export function SearchSpaceAvatar({
+ name,
+ isActive,
+ onClick,
+ size = "md",
+}: SearchSpaceAvatarProps) {
const bgColor = stringToColor(name);
const initials = getInitials(name);
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
diff --git a/surfsense_web/components/layout/ui/index.ts b/surfsense_web/components/layout/ui/index.ts
index c5aba9250..bd3d54838 100644
--- a/surfsense_web/components/layout/ui/index.ts
+++ b/surfsense_web/components/layout/ui/index.ts
@@ -1,8 +1,8 @@
export { CreateSearchSpaceDialog } from "./dialogs";
export { Header } from "./header";
export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
-export { LayoutShell } from "./shell";
export { AllSearchSpacesSheet } from "./sheets";
+export { LayoutShell } from "./shell";
export {
ChatListItem,
MobileSidebar,
diff --git a/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx b/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx
index d144c79b3..401de41c3 100644
--- a/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx
+++ b/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx
@@ -1,6 +1,15 @@
"use client";
-import { Calendar, MoreHorizontal, Search, Settings, Share2, Trash2, UserCheck, Users } from "lucide-react";
+import {
+ Calendar,
+ MoreHorizontal,
+ Search,
+ Settings,
+ Share2,
+ Trash2,
+ UserCheck,
+ Users,
+} from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import {
@@ -112,9 +121,7 @@ export function AllSearchSpacesSheet({
{t("no_search_spaces")}
-
- {t("create_first_search_space")}
-
+
{t("create_first_search_space")}
{onCreateNew && (
diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts
index d7c281ac6..5849003e2 100644
--- a/surfsense_web/lib/apis/base-api.service.ts
+++ b/surfsense_web/lib/apis/base-api.service.ts
@@ -161,52 +161,52 @@ class BaseApiService {
}
}
- // biome-ignore lint/suspicious: Unknown
- let data;
- const responseType = mergedOptions.responseType;
+ // biome-ignore lint/suspicious: Unknown
+ let data;
+ const responseType = mergedOptions.responseType;
- try {
- switch (responseType) {
- case ResponseType.JSON:
- data = await response.json();
- break;
- case ResponseType.TEXT:
- data = await response.text();
- break;
- case ResponseType.BLOB:
- data = await response.blob();
- break;
- case ResponseType.ARRAY_BUFFER:
- data = await response.arrayBuffer();
- break;
- // Add more cases as needed
- default:
- data = await response.json();
+ try {
+ switch (responseType) {
+ case ResponseType.JSON:
+ data = await response.json();
+ break;
+ case ResponseType.TEXT:
+ data = await response.text();
+ break;
+ case ResponseType.BLOB:
+ data = await response.blob();
+ break;
+ case ResponseType.ARRAY_BUFFER:
+ data = await response.arrayBuffer();
+ break;
+ // Add more cases as needed
+ default:
+ data = await response.json();
+ }
+ } catch (error) {
+ console.error("Failed to parse response as JSON:", error);
+ throw new AppError("Failed to parse response", response.status, response.statusText);
}
- } catch (error) {
- console.error("Failed to parse response as JSON:", error);
- throw new AppError("Failed to parse response", response.status, response.statusText);
- }
- // Validate response
- if (responseType === ResponseType.JSON) {
- if (!responseSchema) {
+ // Validate response
+ if (responseType === ResponseType.JSON) {
+ if (!responseSchema) {
+ return data;
+ }
+ const parsedData = responseSchema.safeParse(data);
+
+ if (!parsedData.success) {
+ /** The request was successful, but the response data does not match the expected schema.
+ * This is a client side error, and should be fixed by updating the responseSchema to keep things typed.
+ * This error should not be shown to the user , it is for dev only.
+ */
+ console.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error));
+ }
+
return data;
}
- const parsedData = responseSchema.safeParse(data);
-
- if (!parsedData.success) {
- /** The request was successful, but the response data does not match the expected schema.
- * This is a client side error, and should be fixed by updating the responseSchema to keep things typed.
- * This error should not be shown to the user , it is for dev only.
- */
- console.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error));
- }
return data;
- }
-
- return data;
} catch (error) {
console.error("Request failed:", JSON.stringify(error));
throw error;