diff --git a/README.md b/README.md
index 4f2ce4332..77c34334d 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
# SurfSense
Connect any LLM to your internal knowledge sources and chat with it in real time alongside your team. OSS alternative to NotebookLM, Perplexity, and Glean.
-SurfSense is a highly customizable AI research agent, connected to external sources such as Search Engines (SearxNG, Tavily, LinkUp), Google Drive, Slack, Linear, Jira, ClickUp, Confluence, BookStack, Gmail, Notion, YouTube, GitHub, Discord, Airtable, Google Calendar, Luma, Circleback, Elasticsearch and more to come.
+SurfSense is a highly customizable AI research agent, connected to external sources such as Search Engines (SearxNG, Tavily, LinkUp), Google Drive, Slack, Microsoft Teams, Linear, Jira, ClickUp, Confluence, BookStack, Gmail, Notion, YouTube, GitHub, Discord, Airtable, Google Calendar, Luma, Circleback, Elasticsearch and more to come.
@@ -97,6 +97,7 @@ Contributors can easily add new tools via the registry pattern:
- SearxNG (self-hosted instances)
- Google Drive
- Slack
+- Microsoft Teams
- Linear
- Jira
- ClickUp
diff --git a/README.zh-CN.md b/README.zh-CN.md
index fe6ec8e30..5eb369287 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -18,7 +18,7 @@
将任何 LLM 连接到您的内部知识源,并与团队成员实时聊天。NotebookLM、Perplexity 和 Glean 的开源替代方案。
-SurfSense 是一个高度可定制的 AI 研究助手,可以连接外部数据源,如搜索引擎(SearxNG、Tavily、LinkUp)、Google Drive、Slack、Linear、Jira、ClickUp、Confluence、BookStack、Gmail、Notion、YouTube、GitHub、Discord、Airtable、Google Calendar、Luma、Circleback、Elasticsearch 等,未来还会支持更多。
+SurfSense 是一个高度可定制的 AI 研究助手,可以连接外部数据源,如搜索引擎(SearxNG、Tavily、LinkUp)、Google Drive、Slack、Microsoft Teams、Linear、Jira、ClickUp、Confluence、BookStack、Gmail、Notion、YouTube、GitHub、Discord、Airtable、Google Calendar、Luma、Circleback、Elasticsearch 等,未来还会支持更多。
@@ -105,6 +105,7 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
- SearxNG(自托管实例)
- Google Drive
- Slack
+- Microsoft Teams
- Linear
- Jira
- ClickUp
diff --git a/surfsense_backend/alembic/versions/61_add_chat_visibility_and_created_by.py b/surfsense_backend/alembic/versions/61_add_chat_visibility_and_created_by.py
new file mode 100644
index 000000000..8ebb99426
--- /dev/null
+++ b/surfsense_backend/alembic/versions/61_add_chat_visibility_and_created_by.py
@@ -0,0 +1,109 @@
+"""Add chat visibility and created_by_id columns to new_chat_threads
+
+This migration adds:
+- ChatVisibility enum (PRIVATE, SEARCH_SPACE)
+- visibility column to new_chat_threads table (default: PRIVATE)
+- created_by_id column to track who created the chat thread
+
+Revision ID: 61
+Revises: 60
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "61"
+down_revision: str | None = "60"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ """Add visibility and created_by_id columns to new_chat_threads."""
+
+ # Create the ChatVisibility enum type
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'chatvisibility') THEN
+ CREATE TYPE chatvisibility AS ENUM ('PRIVATE', 'SEARCH_SPACE');
+ END IF;
+ END$$;
+ """
+ )
+
+ # Add visibility column with default value PRIVATE
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'new_chat_threads' AND column_name = 'visibility'
+ ) THEN
+ ALTER TABLE new_chat_threads
+ ADD COLUMN visibility chatvisibility NOT NULL DEFAULT 'PRIVATE';
+ END IF;
+ END$$;
+ """
+ )
+
+ # Create index on visibility column for efficient filtering
+ op.execute(
+ """
+ CREATE INDEX IF NOT EXISTS ix_new_chat_threads_visibility
+ ON new_chat_threads(visibility);
+ """
+ )
+
+ # Add created_by_id column (nullable to handle existing records)
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'new_chat_threads' AND column_name = 'created_by_id'
+ ) THEN
+ ALTER TABLE new_chat_threads
+ ADD COLUMN created_by_id UUID REFERENCES "user"(id) ON DELETE SET NULL;
+ END IF;
+ END$$;
+ """
+ )
+
+ # Create index on created_by_id column for efficient filtering
+ op.execute(
+ """
+ CREATE INDEX IF NOT EXISTS ix_new_chat_threads_created_by_id
+ ON new_chat_threads(created_by_id);
+ """
+ )
+
+
+def downgrade() -> None:
+ """Remove visibility and created_by_id columns from new_chat_threads."""
+
+ # Drop indexes
+ op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_created_by_id")
+ op.execute("DROP INDEX IF EXISTS ix_new_chat_threads_visibility")
+
+ # Drop columns
+ op.execute(
+ """
+ ALTER TABLE new_chat_threads
+ DROP COLUMN IF EXISTS created_by_id;
+ """
+ )
+ op.execute(
+ """
+ ALTER TABLE new_chat_threads
+ DROP COLUMN IF EXISTS visibility;
+ """
+ )
+
+ # Drop enum type (only if not used elsewhere)
+ op.execute("DROP TYPE IF EXISTS chatvisibility")
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index a0b174bf6..73727a9ef 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -326,6 +326,20 @@ class NewChatMessageRole(str, Enum):
SYSTEM = "system"
+class ChatVisibility(str, Enum):
+ """
+ Visibility/sharing level for chat threads.
+
+ PRIVATE: Only the creator can see/access the chat (default)
+ SEARCH_SPACE: All members of the search space can see/access the chat
+ PUBLIC: (Future) Anyone with the link can access the chat
+ """
+
+ PRIVATE = "PRIVATE"
+ SEARCH_SPACE = "SEARCH_SPACE"
+ # PUBLIC = "PUBLIC" # Reserved for future implementation
+
+
class NewChatThread(BaseModel, TimestampMixin):
"""
Thread model for the new chat feature using assistant-ui.
@@ -345,13 +359,31 @@ class NewChatThread(BaseModel, TimestampMixin):
index=True,
)
+ # Visibility/sharing control
+ visibility = Column(
+ SQLAlchemyEnum(ChatVisibility),
+ nullable=False,
+ default=ChatVisibility.PRIVATE,
+ server_default="PRIVATE",
+ index=True,
+ )
+
# Foreign keys
search_space_id = Column(
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
)
+ # Track who created this chat thread (for visibility filtering)
+ created_by_id = Column(
+ UUID(as_uuid=True),
+ ForeignKey("user.id", ondelete="SET NULL"),
+ nullable=True, # Nullable for existing records before migration
+ index=True,
+ )
+
# Relationships
search_space = relationship("SearchSpace", back_populates="new_chat_threads")
+ created_by = relationship("User", back_populates="new_chat_threads")
messages = relationship(
"NewChatMessage",
back_populates="thread",
@@ -826,6 +858,13 @@ if config.AUTH_TYPE == "GOOGLE":
passive_deletes=True,
)
+ # Chat threads created by this user
+ new_chat_threads = relationship(
+ "NewChatThread",
+ back_populates="created_by",
+ passive_deletes=True,
+ )
+
# Page usage tracking for ETL services
pages_limit = Column(
Integer,
@@ -852,6 +891,13 @@ else:
passive_deletes=True,
)
+ # Chat threads created by this user
+ new_chat_threads = relationship(
+ "NewChatThread",
+ back_populates="created_by",
+ passive_deletes=True,
+ )
+
# Page usage tracking for ETL services
pages_limit = Column(
Integer,
diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py
index da0d239d2..ff9a8675b 100644
--- a/surfsense_backend/app/routes/new_chat_routes.py
+++ b/surfsense_backend/app/routes/new_chat_routes.py
@@ -19,12 +19,14 @@ from datetime import UTC, datetime
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from fastapi.responses import StreamingResponse
+from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from app.db import (
+ ChatVisibility,
NewChatMessage,
NewChatMessageRole,
NewChatThread,
@@ -40,6 +42,7 @@ from app.schemas.new_chat import (
NewChatThreadCreate,
NewChatThreadRead,
NewChatThreadUpdate,
+ NewChatThreadVisibilityUpdate,
NewChatThreadWithMessages,
ThreadHistoryLoadResponse,
ThreadListItem,
@@ -52,6 +55,61 @@ from app.utils.rbac import check_permission
router = APIRouter()
+async def check_thread_access(
+ thread: NewChatThread,
+ user: User,
+ require_ownership: bool = False,
+) -> bool:
+ """
+ Check if a user has access to a thread based on visibility rules.
+
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE (and user has permission to read chats)
+ - Thread is a legacy thread (created_by_id is NULL) - visible to all
+
+ Args:
+ thread: The thread to check access for
+ user: The user requesting access
+ require_ownership: If True, only the creator can access (for edit/delete operations)
+ Legacy threads (NULL creator) are treated as accessible by all
+
+ Returns:
+ True if access is granted
+
+ Raises:
+ HTTPException: If access is denied
+ """
+ is_owner = thread.created_by_id == user.id
+ is_legacy = thread.created_by_id is None
+
+ # Legacy threads are accessible to all users in the search space
+ if is_legacy:
+ return True
+
+ # If ownership is required, only the creator can access
+ if require_ownership:
+ if not is_owner:
+ raise HTTPException(
+ status_code=403,
+ detail="Only the creator of this chat can perform this action",
+ )
+ return True
+
+ # For read access: owner or shared threads
+ if is_owner:
+ return True
+
+ if thread.visibility == ChatVisibility.SEARCH_SPACE:
+ return True
+
+ # Private thread and user is not the owner
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have access to this private chat",
+ )
+
+
# =============================================================================
# Thread Endpoints
# =============================================================================
@@ -65,9 +123,14 @@ async def list_threads(
user: User = Depends(current_active_user),
):
"""
- List all threads for the current user in a search space.
+ List all accessible threads for the current user in a search space.
Returns threads and archived_threads for ThreadListPrimitive.
+ A user can see threads that are:
+ - Created by them (regardless of visibility)
+ - Shared with the search space (visibility = SEARCH_SPACE)
+ - Legacy threads with no creator (created_by_id is NULL)
+
Args:
search_space_id: The search space to list threads for
limit: Optional limit on number of threads to return (applies to active threads only)
@@ -83,10 +146,20 @@ async def list_threads(
"You don't have permission to read chats in this search space",
)
- # Get all threads in this search space
+ # Get threads that are either:
+ # 1. Created by the current user (any visibility)
+ # 2. Shared with the search space (visibility = SEARCH_SPACE)
+ # 3. Legacy threads with no creator (created_by_id is NULL) - visible to all
query = (
select(NewChatThread)
- .filter(NewChatThread.search_space_id == search_space_id)
+ .filter(
+ NewChatThread.search_space_id == search_space_id,
+ or_(
+ NewChatThread.created_by_id == user.id,
+ NewChatThread.visibility == ChatVisibility.SEARCH_SPACE,
+ NewChatThread.created_by_id.is_(None), # Legacy threads
+ ),
+ )
.order_by(NewChatThread.updated_at.desc())
)
@@ -98,10 +171,17 @@ async def list_threads(
archived_threads = []
for thread in all_threads:
+ # Legacy threads (no creator) are treated as own threads for display purposes
+ is_own_thread = (
+ thread.created_by_id == user.id or thread.created_by_id is None
+ )
item = ThreadListItem(
id=thread.id,
title=thread.title,
archived=thread.archived,
+ visibility=thread.visibility,
+ created_by_id=thread.created_by_id,
+ is_own_thread=is_own_thread,
created_at=thread.created_at,
updated_at=thread.updated_at,
)
@@ -137,7 +217,12 @@ async def search_threads(
user: User = Depends(current_active_user),
):
"""
- Search threads by title in a search space.
+ Search accessible threads by title in a search space.
+
+ A user can search threads that are:
+ - Created by them (regardless of visibility)
+ - Shared with the search space (visibility = SEARCH_SPACE)
+ - Legacy threads with no creator (created_by_id is NULL)
Args:
search_space_id: The search space to search in
@@ -154,12 +239,17 @@ async def search_threads(
"You don't have permission to read chats in this search space",
)
- # Search threads by title (case-insensitive)
+ # Search accessible threads by title (case-insensitive)
query = (
select(NewChatThread)
.filter(
NewChatThread.search_space_id == search_space_id,
NewChatThread.title.ilike(f"%{title}%"),
+ or_(
+ NewChatThread.created_by_id == user.id,
+ NewChatThread.visibility == ChatVisibility.SEARCH_SPACE,
+ NewChatThread.created_by_id.is_(None), # Legacy threads
+ ),
)
.order_by(NewChatThread.updated_at.desc())
)
@@ -172,6 +262,12 @@ async def search_threads(
id=thread.id,
title=thread.title,
archived=thread.archived,
+ visibility=thread.visibility,
+ created_by_id=thread.created_by_id,
+ # Legacy threads (no creator) are treated as own threads
+ is_own_thread=(
+ thread.created_by_id == user.id or thread.created_by_id is None
+ ),
created_at=thread.created_at,
updated_at=thread.updated_at,
)
@@ -200,6 +296,9 @@ async def create_thread(
"""
Create a new chat thread.
+ The thread is created with the specified visibility (defaults to PRIVATE).
+ The current user is recorded as the creator of the thread.
+
Requires CHATS_CREATE permission.
"""
try:
@@ -215,7 +314,9 @@ async def create_thread(
db_thread = NewChatThread(
title=thread.title,
archived=thread.archived,
+ visibility=thread.visibility,
search_space_id=thread.search_space_id,
+ created_by_id=user.id,
updated_at=now,
)
session.add(db_thread)
@@ -254,6 +355,10 @@ async def get_thread_messages(
Get a thread with all its messages.
This is used by ThreadHistoryAdapter.load() to restore conversation.
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE
+
Requires CHATS_READ permission.
"""
try:
@@ -268,7 +373,7 @@ async def get_thread_messages(
if not thread:
raise HTTPException(status_code=404, detail="Thread not found")
- # Check permission and ownership
+ # Check permission to read chats in this search space
await check_permission(
session,
user,
@@ -277,6 +382,9 @@ async def get_thread_messages(
"You don't have permission to read chats in this search space",
)
+ # Check thread-level access based on visibility
+ await check_thread_access(thread, user)
+
# Return messages in the format expected by assistant-ui
messages = [
NewChatMessageRead(
@@ -313,6 +421,10 @@ async def get_thread_full(
"""
Get full thread details with all messages.
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE
+
Requires CHATS_READ permission.
"""
try:
@@ -334,6 +446,9 @@ async def get_thread_full(
"You don't have permission to read chats in this search space",
)
+ # Check thread-level access based on visibility
+ await check_thread_access(thread, user)
+
return thread
except HTTPException:
@@ -360,6 +475,9 @@ async def update_thread(
Update a thread (title, archived status).
Used for renaming and archiving threads.
+ - PRIVATE threads: Only the creator can update
+ - SEARCH_SPACE threads: Any member with CHATS_UPDATE permission can update
+
Requires CHATS_UPDATE permission.
"""
try:
@@ -379,6 +497,11 @@ async def update_thread(
"You don't have permission to update chats in this search space",
)
+ # For PRIVATE threads, only the creator can update
+ # For SEARCH_SPACE threads, any member with permission can update
+ if db_thread.visibility == ChatVisibility.PRIVATE:
+ await check_thread_access(db_thread, user, require_ownership=True)
+
# Update fields
update_data = thread_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
@@ -420,6 +543,9 @@ async def delete_thread(
"""
Delete a thread and all its messages.
+ - PRIVATE threads: Only the creator can delete
+ - SEARCH_SPACE threads: Any member with CHATS_DELETE permission can delete
+
Requires CHATS_DELETE permission.
"""
try:
@@ -439,6 +565,11 @@ async def delete_thread(
"You don't have permission to delete chats in this search space",
)
+ # For PRIVATE threads, only the creator can delete
+ # For SEARCH_SPACE threads, any member with permission can delete
+ if db_thread.visibility == ChatVisibility.PRIVATE:
+ await check_thread_access(db_thread, user, require_ownership=True)
+
await session.delete(db_thread)
await session.commit()
return {"message": "Thread deleted successfully"}
@@ -463,6 +594,71 @@ async def delete_thread(
) from None
+@router.patch("/threads/{thread_id}/visibility", response_model=NewChatThreadRead)
+async def update_thread_visibility(
+ thread_id: int,
+ visibility_update: NewChatThreadVisibilityUpdate,
+ session: AsyncSession = Depends(get_async_session),
+ user: User = Depends(current_active_user),
+):
+ """
+ Update the visibility/sharing settings of a thread.
+
+ Only the creator of the thread can change its visibility.
+ - PRIVATE: Only the creator can access the thread (default)
+ - SEARCH_SPACE: All members of the search space can access the thread
+
+ Requires CHATS_UPDATE permission.
+ """
+ try:
+ result = await session.execute(
+ select(NewChatThread).filter(NewChatThread.id == thread_id)
+ )
+ db_thread = result.scalars().first()
+
+ if not db_thread:
+ raise HTTPException(status_code=404, detail="Thread not found")
+
+ await check_permission(
+ session,
+ user,
+ db_thread.search_space_id,
+ Permission.CHATS_UPDATE.value,
+ "You don't have permission to update chats in this search space",
+ )
+
+ # Only the creator can change visibility
+ await check_thread_access(db_thread, user, require_ownership=True)
+
+ # Update visibility
+ db_thread.visibility = visibility_update.visibility
+ db_thread.updated_at = datetime.now(UTC)
+
+ await session.commit()
+ await session.refresh(db_thread)
+ return db_thread
+
+ except HTTPException:
+ raise
+ except IntegrityError:
+ await session.rollback()
+ raise HTTPException(
+ status_code=400,
+ detail="Database constraint violation. Please check your input data.",
+ ) from None
+ except OperationalError:
+ await session.rollback()
+ raise HTTPException(
+ status_code=503, detail="Database operation failed. Please try again later."
+ ) from None
+ except Exception as e:
+ await session.rollback()
+ raise HTTPException(
+ status_code=500,
+ detail=f"An unexpected error occurred while updating thread visibility: {e!s}",
+ ) from None
+
+
# =============================================================================
# Message Endpoints
# =============================================================================
@@ -479,6 +675,10 @@ async def append_message(
Append a message to a thread.
This is used by ThreadHistoryAdapter.append() to persist messages.
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE
+
Requires CHATS_UPDATE permission.
"""
try:
@@ -513,6 +713,9 @@ async def append_message(
"You don't have permission to update chats in this search space",
)
+ # Check thread-level access based on visibility
+ await check_thread_access(thread, user)
+
# Convert string role to enum
role_str = (
message.role.lower() if isinstance(message.role, str) else message.role
@@ -597,6 +800,10 @@ async def list_messages(
"""
List messages in a thread with pagination.
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE
+
Requires CHATS_READ permission.
"""
try:
@@ -617,6 +824,9 @@ async def list_messages(
"You don't have permission to read chats in this search space",
)
+ # Check thread-level access based on visibility
+ await check_thread_access(thread, user)
+
# Get messages
query = (
select(NewChatMessage)
@@ -659,6 +869,10 @@ async def handle_new_chat(
This endpoint handles the new chat functionality with streaming responses
using Server-Sent Events (SSE) format compatible with Vercel AI SDK.
+ Access is granted if:
+ - User is the creator of the thread
+ - Thread visibility is SEARCH_SPACE
+
Requires CHATS_CREATE permission.
"""
try:
@@ -679,6 +893,9 @@ async def handle_new_chat(
"You don't have permission to chat in this search space",
)
+ # Check thread-level access based on visibility
+ await check_thread_access(thread, user)
+
# Get search space to check LLM config preferences
search_space_result = await session.execute(
select(SearchSpace).filter(SearchSpace.id == request.search_space_id)
diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py
index 62c61da6b..e6dbcd920 100644
--- a/surfsense_backend/app/schemas/new_chat.py
+++ b/surfsense_backend/app/schemas/new_chat.py
@@ -8,10 +8,11 @@ These schemas follow the assistant-ui ThreadHistoryAdapter pattern:
from datetime import datetime
from typing import Any
+from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
-from app.db import NewChatMessageRole
+from app.db import ChatVisibility, NewChatMessageRole
from .base import IDModel, TimestampModel
@@ -66,6 +67,8 @@ class NewChatThreadCreate(NewChatThreadBase):
"""Schema for creating a new thread."""
search_space_id: int
+ # Visibility defaults to PRIVATE, but can be set on creation
+ visibility: ChatVisibility = ChatVisibility.PRIVATE
class NewChatThreadUpdate(BaseModel):
@@ -75,12 +78,20 @@ class NewChatThreadUpdate(BaseModel):
archived: bool | None = None
+class NewChatThreadVisibilityUpdate(BaseModel):
+ """Schema for updating thread visibility/sharing settings."""
+
+ visibility: ChatVisibility
+
+
class NewChatThreadRead(NewChatThreadBase, IDModel):
"""
Schema for reading a thread (matches assistant-ui ThreadRecord).
"""
search_space_id: int
+ visibility: ChatVisibility
+ created_by_id: UUID | None = None
created_at: datetime
updated_at: datetime
@@ -116,6 +127,9 @@ class ThreadListItem(BaseModel):
id: int
title: str
archived: bool
+ visibility: ChatVisibility
+ created_by_id: UUID | None = None
+ is_own_thread: bool = False # True if the current user created this thread
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
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 368f0f654..742be6ff4 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
@@ -2,9 +2,9 @@
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
-import { RefreshCw } from "lucide-react";
+import { RefreshCw, SquarePlus } from "lucide-react";
import { motion } from "motion/react";
-import { useParams } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
@@ -34,8 +34,13 @@ export default function DocumentsTable() {
const t = useTranslations("documents");
const id = useId();
const params = useParams();
+ const router = useRouter();
const searchSpaceId = Number(params.search_space_id);
+ const handleNewNote = useCallback(() => {
+ router.push(`/dashboard/${searchSpaceId}/editor/new`);
+ }, [router, searchSpaceId]);
+
const [search, setSearch] = useState("");
const debouncedSearch = useDebounced(search, 250);
const [activeTypes, setActiveTypes] = useState([]);
@@ -349,10 +354,16 @@ export default function DocumentsTable() {
{t("title")}
{t("subtitle")}
-
+
+
+
+
diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx
index 239fdc5c1..99ccefcd9 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx
@@ -267,21 +267,8 @@ export default function EditorPage() {
setHasUnsavedChanges(false);
toast.success("Note created successfully! Reindexing in background...");
- // Invalidate notes query to refresh the sidebar
- queryClient.invalidateQueries({
- queryKey: ["notes", String(searchSpaceId)],
- });
-
- // Update URL to reflect the new document ID without navigation
- window.history.replaceState({}, "", `/dashboard/${searchSpaceId}/editor/${note.id}`);
- // Update document state to reflect the new ID
- setDocument({
- document_id: note.id,
- title: title,
- document_type: "NOTE",
- blocknote_document: editorContent,
- updated_at: new Date().toISOString(),
- });
+ // Redirect to documents page after successful save
+ router.push(`/dashboard/${searchSpaceId}/documents`);
} else {
// Existing document - save normally
if (!editorContent) {
@@ -310,12 +297,8 @@ export default function EditorPage() {
setHasUnsavedChanges(false);
toast.success("Document saved! Reindexing in background...");
- // Invalidate notes query when updating notes to refresh the sidebar
- if (isNote) {
- queryClient.invalidateQueries({
- queryKey: ["notes", String(searchSpaceId)],
- });
- }
+ // Redirect to documents page after successful save
+ router.push(`/dashboard/${searchSpaceId}/documents`);
}
} catch (error) {
console.error("Error saving document:", error);
@@ -336,7 +319,7 @@ export default function EditorPage() {
if (hasUnsavedChanges) {
setShowUnsavedDialog(true);
} else {
- router.push(`/dashboard/${searchSpaceId}/new-chat`);
+ router.push(`/dashboard/${searchSpaceId}/documents`);
}
};
@@ -346,12 +329,12 @@ export default function EditorPage() {
setGlobalHasUnsavedChanges(false);
setHasUnsavedChanges(false);
- // If there's a pending navigation (from sidebar), use that; otherwise go back to chat
+ // If there's a pending navigation (from sidebar), use that; otherwise go back to documents
if (pendingNavigation) {
router.push(pendingNavigation);
setPendingNavigation(null);
} else {
- router.push(`/dashboard/${searchSpaceId}/new-chat`);
+ router.push(`/dashboard/${searchSpaceId}/documents`);
}
};
@@ -392,7 +375,7 @@ export default function EditorPage() {