mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-10 20:35:17 +02:00
feat: fixed issues of note management
Issues Fixed - Missing pagination fields in API response schemas (page, page_size, has_more) - NOTE enum missing from frontend Zod schema - Missing fields in DocumentRead response construction (content_hash, updated_at) - BlockNote slash menu clipped by overflow-hidden CSS - Sidebar click conflicts - hidden action buttons intercepting clicks - Rewrote All Notes sidebar - replaced fragile custom portal with shadcn Sheet - Missing translation keys for new UI strings - Missing NOTE retrieval logic in researcher agent - Added search to All Notes sidebar - Removed frontend logging - was causing toasters on every page refresh - Added backend logging to document reindex Celery task
This commit is contained in:
parent
3c3527d498
commit
c768730b8c
37 changed files with 758 additions and 740 deletions
|
|
@ -492,6 +492,7 @@ async def fetch_documents_by_ids(
|
|||
"CLICKUP_CONNECTOR": "ClickUp (Selected)",
|
||||
"AIRTABLE_CONNECTOR": "Airtable (Selected)",
|
||||
"LUMA_CONNECTOR": "Luma Events (Selected)",
|
||||
"NOTE": "Notes (Selected)",
|
||||
}
|
||||
|
||||
source_object = {
|
||||
|
|
@ -1162,6 +1163,33 @@ async def fetch_relevant_documents(
|
|||
}
|
||||
)
|
||||
|
||||
elif connector == "NOTE":
|
||||
(
|
||||
source_object,
|
||||
notes_chunks,
|
||||
) = await connector_service.search_notes(
|
||||
user_query=reformulated_query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
|
||||
# Add to sources and raw documents
|
||||
if source_object:
|
||||
all_sources.append(source_object)
|
||||
all_raw_documents.extend(notes_chunks)
|
||||
|
||||
# Stream found document count
|
||||
if streaming_service and writer:
|
||||
writer(
|
||||
{
|
||||
"yield_value": streaming_service.format_terminal_info_delta(
|
||||
f"📝 Found {len(notes_chunks)} Notes related to your query"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("Error in search_airtable: %s", traceback.format_exc())
|
||||
error_message = f"Error searching connector {connector}: {e!s}"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ def get_connector_emoji(connector_name: str) -> str:
|
|||
"LUMA_CONNECTOR": "✨",
|
||||
"ELASTICSEARCH_CONNECTOR": "⚡",
|
||||
"WEBCRAWLER_CONNECTOR": "🌐",
|
||||
"NOTE": "📝",
|
||||
}
|
||||
return connector_emojis.get(connector_name, "🔎")
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ def get_connector_friendly_name(connector_name: str) -> str:
|
|||
"LUMA_CONNECTOR": "Luma",
|
||||
"ELASTICSEARCH_CONNECTOR": "Elasticsearch",
|
||||
"WEBCRAWLER_CONNECTOR": "Web Pages",
|
||||
"NOTE": "Notes",
|
||||
}
|
||||
return connector_friendly_names.get(connector_name, connector_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -266,12 +266,27 @@ async def read_documents(
|
|||
document_type=doc.document_type,
|
||||
document_metadata=doc.document_metadata,
|
||||
content=doc.content,
|
||||
content_hash=doc.content_hash,
|
||||
unique_identifier_hash=doc.unique_identifier_hash,
|
||||
created_at=doc.created_at,
|
||||
updated_at=doc.updated_at,
|
||||
search_space_id=doc.search_space_id,
|
||||
)
|
||||
)
|
||||
|
||||
return PaginatedResponse(items=api_documents, total=total)
|
||||
# Calculate pagination info
|
||||
actual_page = (
|
||||
page if page is not None else (offset // page_size if page_size > 0 else 0)
|
||||
)
|
||||
has_more = (offset + len(api_documents)) < total if page_size > 0 else False
|
||||
|
||||
return PaginatedResponse(
|
||||
items=api_documents,
|
||||
total=total,
|
||||
page=actual_page,
|
||||
page_size=page_size,
|
||||
has_more=has_more,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -385,12 +400,27 @@ async def search_documents(
|
|||
document_type=doc.document_type,
|
||||
document_metadata=doc.document_metadata,
|
||||
content=doc.content,
|
||||
content_hash=doc.content_hash,
|
||||
unique_identifier_hash=doc.unique_identifier_hash,
|
||||
created_at=doc.created_at,
|
||||
updated_at=doc.updated_at,
|
||||
search_space_id=doc.search_space_id,
|
||||
)
|
||||
)
|
||||
|
||||
return PaginatedResponse(items=api_documents, total=total)
|
||||
# Calculate pagination info
|
||||
actual_page = (
|
||||
page if page is not None else (offset // page_size if page_size > 0 else 0)
|
||||
)
|
||||
has_more = (offset + len(api_documents)) < total if page_size > 0 else False
|
||||
|
||||
return PaginatedResponse(
|
||||
items=api_documents,
|
||||
total=total,
|
||||
page=actual_page,
|
||||
page_size=page_size,
|
||||
has_more=has_more,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -510,7 +540,10 @@ async def get_document_by_chunk_id(
|
|||
document_type=document.document_type,
|
||||
document_metadata=document.document_metadata,
|
||||
content=document.content,
|
||||
content_hash=document.content_hash,
|
||||
unique_identifier_hash=document.unique_identifier_hash,
|
||||
created_at=document.created_at,
|
||||
updated_at=document.updated_at,
|
||||
search_space_id=document.search_space_id,
|
||||
chunks=sorted_chunks,
|
||||
)
|
||||
|
|
@ -559,7 +592,10 @@ async def read_document(
|
|||
document_type=document.document_type,
|
||||
document_metadata=document.document_metadata,
|
||||
content=document.content,
|
||||
content_hash=document.content_hash,
|
||||
unique_identifier_hash=document.unique_identifier_hash,
|
||||
created_at=document.created_at,
|
||||
updated_at=document.updated_at,
|
||||
search_space_id=document.search_space_id,
|
||||
)
|
||||
except HTTPException:
|
||||
|
|
@ -614,7 +650,10 @@ async def update_document(
|
|||
document_type=db_document.document_type,
|
||||
document_metadata=db_document.document_metadata,
|
||||
content=db_document.content,
|
||||
content_hash=db_document.content_hash,
|
||||
unique_identifier_hash=db_document.unique_identifier_hash,
|
||||
created_at=db_document.created_at,
|
||||
updated_at=db_document.updated_at,
|
||||
search_space_id=db_document.search_space_id,
|
||||
)
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ async def save_document(
|
|||
blocknote_document = data.get("blocknote_document")
|
||||
if not blocknote_document:
|
||||
raise HTTPException(status_code=400, detail="blocknote_document is required")
|
||||
|
||||
|
||||
# Add type validation
|
||||
if not isinstance(blocknote_document, list):
|
||||
raise HTTPException(status_code=400, detail="blocknote_document must be a list")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Notes routes for creating and managing BlockNote documents.
|
|||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -64,7 +64,6 @@ async def create_note(
|
|||
content_hash = hashlib.sha256(request.title.encode()).hexdigest()
|
||||
|
||||
# Create document with NOTE type
|
||||
from app.config import config
|
||||
|
||||
document = Document(
|
||||
search_space_id=search_space_id,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ class DocumentRead(BaseModel):
|
|||
document_type: DocumentType
|
||||
document_metadata: dict
|
||||
content: str # Changed to string to match frontend
|
||||
content_hash: str
|
||||
unique_identifier_hash: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime | None
|
||||
search_space_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -61,3 +64,6 @@ class DocumentWithChunksRead(DocumentRead):
|
|||
class PaginatedResponse[T](BaseModel):
|
||||
items: list[T]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
has_more: bool
|
||||
|
|
|
|||
|
|
@ -2360,6 +2360,75 @@ class ConnectorService:
|
|||
|
||||
return result_object, elasticsearch_docs
|
||||
|
||||
async def search_notes(
|
||||
self,
|
||||
user_query: str,
|
||||
search_space_id: int,
|
||||
top_k: int = 20,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
) -> tuple:
|
||||
"""
|
||||
Search for Notes and return both the source information and langchain documents.
|
||||
|
||||
Uses combined chunk-level and document-level hybrid search with RRF fusion.
|
||||
|
||||
Args:
|
||||
user_query: The user's query
|
||||
search_space_id: The search space ID to search in
|
||||
top_k: Maximum number of results to return
|
||||
start_date: Optional start date for filtering documents by updated_at
|
||||
end_date: Optional end date for filtering documents by updated_at
|
||||
|
||||
Returns:
|
||||
tuple: (sources_info, langchain_documents)
|
||||
"""
|
||||
notes_docs = await self._combined_rrf_search(
|
||||
query_text=user_query,
|
||||
search_space_id=search_space_id,
|
||||
document_type="NOTE",
|
||||
top_k=top_k,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
|
||||
# Early return if no results
|
||||
if not notes_docs:
|
||||
return {
|
||||
"id": 51,
|
||||
"name": "Notes",
|
||||
"type": "NOTE",
|
||||
"sources": [],
|
||||
}, []
|
||||
|
||||
def _title_fn(doc_info: dict[str, Any], metadata: dict[str, Any]) -> str:
|
||||
return doc_info.get("title", "Untitled Note")
|
||||
|
||||
def _url_fn(_doc_info: dict[str, Any], _metadata: dict[str, Any]) -> str:
|
||||
return "" # Notes don't have URLs
|
||||
|
||||
def _description_fn(
|
||||
chunk: dict[str, Any], _doc_info: dict[str, Any], _metadata: dict[str, Any]
|
||||
) -> str:
|
||||
return self._chunk_preview(chunk.get("content", ""), limit=200)
|
||||
|
||||
sources_list = self._build_chunk_sources_from_documents(
|
||||
notes_docs,
|
||||
title_fn=_title_fn,
|
||||
url_fn=_url_fn,
|
||||
description_fn=_description_fn,
|
||||
)
|
||||
|
||||
# Create result object
|
||||
result_object = {
|
||||
"id": 51,
|
||||
"name": "Notes",
|
||||
"type": "NOTE",
|
||||
"sources": sources_list,
|
||||
}
|
||||
|
||||
return result_object, notes_docs
|
||||
|
||||
async def search_bookstack(
|
||||
self,
|
||||
user_query: str,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import logging
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
|
@ -11,6 +12,7 @@ from app.celery_app import celery_app
|
|||
from app.config import config
|
||||
from app.db import Document
|
||||
from app.services.llm_service import get_user_long_context_llm
|
||||
from app.services.task_logging_service import TaskLoggingService
|
||||
from app.utils.blocknote_converter import convert_blocknote_to_markdown
|
||||
from app.utils.document_converters import (
|
||||
create_document_chunks,
|
||||
|
|
@ -53,21 +55,42 @@ def reindex_document_task(self, document_id: int, user_id: str):
|
|||
async def _reindex_document(document_id: int, user_id: str):
|
||||
"""Async function to reindex a document."""
|
||||
async with get_celery_session_maker()() as session:
|
||||
# First, get the document to get search_space_id for logging
|
||||
result = await session.execute(
|
||||
select(Document)
|
||||
.options(selectinload(Document.chunks))
|
||||
.where(Document.id == document_id)
|
||||
)
|
||||
document = result.scalars().first()
|
||||
|
||||
if not document:
|
||||
logger.error(f"Document {document_id} not found")
|
||||
return
|
||||
|
||||
# Initialize task logger
|
||||
task_logger = TaskLoggingService(session, document.search_space_id)
|
||||
|
||||
# Log task start
|
||||
log_entry = await task_logger.log_task_start(
|
||||
task_name="document_reindex",
|
||||
source="editor",
|
||||
message=f"Starting reindex for document: {document.title}",
|
||||
metadata={
|
||||
"document_id": document_id,
|
||||
"document_type": document.document_type.value,
|
||||
"title": document.title,
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
# Get document
|
||||
result = await session.execute(
|
||||
select(Document)
|
||||
.options(selectinload(Document.chunks)) # Eagerly load chunks
|
||||
.where(Document.id == document_id)
|
||||
)
|
||||
document = result.scalars().first()
|
||||
|
||||
if not document:
|
||||
logger.error(f"Document {document_id} not found")
|
||||
return
|
||||
|
||||
if not document.blocknote_document:
|
||||
logger.warning(f"Document {document_id} has no BlockNote content")
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
f"Document {document_id} has no BlockNote content to reindex",
|
||||
"No BlockNote content",
|
||||
{"error_type": "NoBlockNoteContent"},
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"Reindexing document {document_id} ({document.title})")
|
||||
|
|
@ -78,7 +101,12 @@ async def _reindex_document(document_id: int, user_id: str):
|
|||
)
|
||||
|
||||
if not markdown_content:
|
||||
logger.error(f"Failed to convert document {document_id} to markdown")
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
f"Failed to convert document {document_id} to markdown",
|
||||
"Markdown conversion failed",
|
||||
{"error_type": "ConversionError"},
|
||||
)
|
||||
return
|
||||
|
||||
# 2. Delete old chunks explicitly
|
||||
|
|
@ -118,9 +146,39 @@ async def _reindex_document(document_id: int, user_id: str):
|
|||
|
||||
await session.commit()
|
||||
|
||||
# Log success
|
||||
await task_logger.log_task_success(
|
||||
log_entry,
|
||||
f"Successfully reindexed document: {document.title}",
|
||||
{
|
||||
"chunks_created": len(new_chunks),
|
||||
"document_id": document_id,
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"Successfully reindexed document {document_id}")
|
||||
|
||||
except SQLAlchemyError as db_error:
|
||||
await session.rollback()
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
f"Database error during reindex for document {document_id}",
|
||||
str(db_error),
|
||||
{"error_type": "SQLAlchemyError"},
|
||||
)
|
||||
logger.error(
|
||||
f"Database error reindexing document {document_id}: {db_error}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
f"Failed to reindex document: {document.title}",
|
||||
str(e),
|
||||
{"error_type": type(e).__name__},
|
||||
)
|
||||
logger.error(f"Error reindexing document {document_id}: {e}", exc_info=True)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { ChatPanelContainer } from "@/components/chat/ChatPanel/ChatPanelContain
|
|||
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
|
||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider";
|
||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
|
@ -224,7 +223,6 @@ export function DashboardClientLayout({
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSwitcher />
|
||||
<ThemeTogglerComponent />
|
||||
{/* Only show artifacts toggle on researcher page */}
|
||||
{isResearcherPage && (
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { notesApiService } from "@/lib/apis/notes-api.service";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { useLogs } from "@/hooks/use-logs";
|
||||
|
||||
interface EditorContent {
|
||||
document_id: number;
|
||||
|
|
@ -69,8 +68,6 @@ export default function EditorPage() {
|
|||
const searchSpaceId = Number(params.search_space_id);
|
||||
const isNewNote = documentId === "new";
|
||||
|
||||
const { createLog } = useLogs(searchSpaceId);
|
||||
|
||||
const [document, setDocument] = useState<EditorContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -116,25 +113,6 @@ export default function EditorPage() {
|
|||
.json()
|
||||
.catch(() => ({ detail: "Failed to fetch document" }));
|
||||
const errorMessage = errorData.detail || "Failed to fetch document";
|
||||
|
||||
// Log fetch error
|
||||
try {
|
||||
await createLog({
|
||||
level: "ERROR",
|
||||
status: "FAILED",
|
||||
message: `Failed to fetch document: ${errorMessage}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
error_type: "fetch_error",
|
||||
http_status: response.status,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
|
|
@ -142,48 +120,13 @@ export default function EditorPage() {
|
|||
|
||||
// Check if blocknote_document exists
|
||||
if (!data.blocknote_document) {
|
||||
const errorMsg = "This document does not have BlockNote content. Please re-upload the document to enable editing.";
|
||||
|
||||
// Log missing BlockNote content
|
||||
try {
|
||||
await createLog({
|
||||
level: "WARNING",
|
||||
status: "FAILED",
|
||||
message: `Document ${documentId} does not have BlockNote content`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
error_type: "missing_blocknote_content",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
const errorMsg =
|
||||
"This document does not have BlockNote content. Please re-upload the document to enable editing.";
|
||||
setError(errorMsg);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log successful fetch
|
||||
try {
|
||||
await createLog({
|
||||
level: "INFO",
|
||||
status: "SUCCESS",
|
||||
message: `Document ${documentId} loaded successfully`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
document_type: data.document_type,
|
||||
title: data.title,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
setDocument(data);
|
||||
setEditorContent(data.blocknote_document);
|
||||
setError(null);
|
||||
|
|
@ -191,24 +134,6 @@ export default function EditorPage() {
|
|||
console.error("Error fetching document:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to fetch document. Please try again.";
|
||||
|
||||
// Log general fetch error
|
||||
try {
|
||||
await createLog({
|
||||
level: "ERROR",
|
||||
status: "FAILED",
|
||||
message: `Error fetching document: ${errorMessage}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
error_type: "fetch_exception",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -218,7 +143,7 @@ export default function EditorPage() {
|
|||
if (documentId) {
|
||||
fetchDocument();
|
||||
}
|
||||
}, [documentId, params.search_space_id, isNewNote, searchSpaceId, createLog]);
|
||||
}, [documentId, params.search_space_id, isNewNote]);
|
||||
|
||||
// Track changes to mark as unsaved
|
||||
useEffect(() => {
|
||||
|
|
@ -280,49 +205,10 @@ export default function EditorPage() {
|
|||
const errorData = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: "Failed to save document" }));
|
||||
|
||||
// Log save error
|
||||
try {
|
||||
await createLog({
|
||||
level: "ERROR",
|
||||
status: "FAILED",
|
||||
message: `Failed to save new note: ${errorData.detail || "Unknown error"}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: note.id,
|
||||
is_new_note: true,
|
||||
action: "save",
|
||||
http_status: response.status,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
throw new Error(errorData.detail || "Failed to save document");
|
||||
}
|
||||
}
|
||||
|
||||
// Log successful note creation
|
||||
try {
|
||||
await createLog({
|
||||
level: "INFO",
|
||||
status: "SUCCESS",
|
||||
message: `Note created successfully: ${title}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: note.id,
|
||||
is_new_note: true,
|
||||
action: "save",
|
||||
title: title,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
toast.success("Note created successfully! Reindexing in background...");
|
||||
|
||||
|
|
@ -363,46 +249,9 @@ export default function EditorPage() {
|
|||
const errorData = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: "Failed to save document" }));
|
||||
|
||||
// Log save error
|
||||
try {
|
||||
await createLog({
|
||||
level: "ERROR",
|
||||
status: "FAILED",
|
||||
message: `Failed to save document ${documentId}: ${errorData.detail || "Unknown error"}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
action: "save",
|
||||
http_status: response.status,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
throw new Error(errorData.detail || "Failed to save document");
|
||||
}
|
||||
|
||||
// Log successful save
|
||||
try {
|
||||
await createLog({
|
||||
level: "INFO",
|
||||
status: "SUCCESS",
|
||||
message: `Document ${documentId} saved successfully`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: documentId,
|
||||
action: "save",
|
||||
title: document?.title,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
toast.success("Document saved! Reindexing in background...");
|
||||
|
||||
|
|
@ -421,26 +270,6 @@ export default function EditorPage() {
|
|||
: isNewNote
|
||||
? "Failed to create note. Please try again."
|
||||
: "Failed to save document. Please try again.";
|
||||
|
||||
// Log save error
|
||||
try {
|
||||
await createLog({
|
||||
level: "ERROR",
|
||||
status: "FAILED",
|
||||
message: `Error saving document: ${errorMessage}`,
|
||||
source: "editor",
|
||||
search_space_id: searchSpaceId,
|
||||
log_metadata: {
|
||||
document_id: isNewNote ? null : documentId,
|
||||
is_new_note: isNewNote,
|
||||
action: "save",
|
||||
error_type: "save_exception",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to create log:", err);
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
|
|
@ -557,7 +386,7 @@ export default function EditorPage() {
|
|||
</div>
|
||||
|
||||
{/* Editor Container */}
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="flex-1 overflow-visible relative">
|
||||
<div className="h-full w-full overflow-auto p-6">
|
||||
{error && (
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
|
|
@ -45,12 +46,12 @@ import { motion } from "motion/react";
|
|||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { createRoleMutationAtom, updateRoleMutationAtom, deleteRoleMutationAtom } from "@/atoms/roles/roles-mutation.atoms";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { CreateRoleRequest, UpdateRoleRequest, DeleteRoleRequest, Role } from "@/contracts/types/roles.types";
|
||||
import { permissionsAtom } from "@/atoms/permissions/permissions-query.atoms";
|
||||
import { rolesApiService } from "@/lib/apis/roles-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import {
|
||||
createRoleMutationAtom,
|
||||
deleteRoleMutationAtom,
|
||||
updateRoleMutationAtom,
|
||||
} from "@/atoms/roles/roles-mutation.atoms";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -106,6 +107,12 @@ import {
|
|||
} from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type {
|
||||
CreateRoleRequest,
|
||||
DeleteRoleRequest,
|
||||
Role,
|
||||
UpdateRoleRequest,
|
||||
} from "@/contracts/types/roles.types";
|
||||
import {
|
||||
type Invite,
|
||||
type InviteCreate,
|
||||
|
|
@ -114,12 +121,14 @@ import {
|
|||
useMembers,
|
||||
useUserAccess,
|
||||
} from "@/hooks/use-rbac";
|
||||
import { rolesApiService } from "@/lib/apis/roles-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Animation variants
|
||||
const fadeInUp = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" as const} },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" as const } },
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
|
|
@ -183,7 +192,7 @@ export default function TeamManagementPage() {
|
|||
);
|
||||
|
||||
const handleCreateRole = useCallback(
|
||||
async (roleData: CreateRoleRequest['data']): Promise<Role> => {
|
||||
async (roleData: CreateRoleRequest["data"]): Promise<Role> => {
|
||||
const request: CreateRoleRequest = {
|
||||
search_space_id: searchSpaceId,
|
||||
data: roleData,
|
||||
|
|
@ -1219,7 +1228,7 @@ function CreateRoleDialog({
|
|||
onCreateRole,
|
||||
}: {
|
||||
groupedPermissions: Record<string, { value: string; name: string; category: string }[]>;
|
||||
onCreateRole: (data: CreateRoleRequest['data']) => Promise<Role>;
|
||||
onCreateRole: (data: CreateRoleRequest["data"]) => Promise<Role>;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import Link from "next/link";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||
import { UserDropdown } from "@/components/UserDropdown";
|
||||
|
|
@ -35,10 +38,7 @@ import {
|
|||
} from "@/components/ui/card";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Tilt } from "@/components/ui/tilt";
|
||||
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
|
||||
/**
|
||||
* Formats a date string into a readable format
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { useAtomValue } from "jotai";
|
||||
import { motion } from "motion/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SearchSpaceForm } from "@/components/search-space-form";
|
||||
import { createSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { SearchSpaceForm } from "@/components/search-space-form";
|
||||
|
||||
export default function SearchSpacesPage() {
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { toast } from "sonner";
|
|||
import type {
|
||||
CreateRoleRequest,
|
||||
CreateRoleResponse,
|
||||
UpdateRoleRequest,
|
||||
UpdateRoleResponse,
|
||||
DeleteRoleRequest,
|
||||
DeleteRoleResponse,
|
||||
UpdateRoleRequest,
|
||||
UpdateRoleResponse,
|
||||
} from "@/contracts/types/roles.types";
|
||||
import { rolesApiService } from "@/lib/apis/roles-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
|
@ -40,7 +40,10 @@ export const updateRoleMutationAtom = atomWithMutation(() => {
|
|||
queryKey: cacheKeys.roles.all(request.search_space_id.toString()),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.roles.byId(request.search_space_id.toString(), request.role_id.toString()),
|
||||
queryKey: cacheKeys.roles.byId(
|
||||
request.search_space_id.toString(),
|
||||
request.role_id.toString()
|
||||
),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import { atomWithMutation } from "jotai-tanstack-query";
|
|||
import { toast } from "sonner";
|
||||
import type {
|
||||
CreateSearchSpaceRequest,
|
||||
UpdateSearchSpaceRequest,
|
||||
DeleteSearchSpaceRequest,
|
||||
UpdateSearchSpaceRequest,
|
||||
} from "@/contracts/types/search-space.types";
|
||||
import { activeSearchSpaceIdAtom } from "./search-space-query.atoms";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
import { activeSearchSpaceIdAtom } from "./search-space-query.atoms";
|
||||
|
||||
export const createSearchSpaceMutationAtom = atomWithMutation(() => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import { atom } from "jotai";
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
|
@ -13,10 +14,9 @@ import {
|
|||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { authenticatedFetch, getBearerToken } from "@/lib/auth-utils";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
interface BreadcrumbItemInterface {
|
||||
label: string;
|
||||
|
|
@ -44,7 +44,7 @@ export function DashboardBreadcrumb() {
|
|||
useEffect(() => {
|
||||
if (segments[2] === "editor" && segments[3] && searchSpaceId) {
|
||||
const documentId = segments[3];
|
||||
|
||||
|
||||
// Skip fetch for "new" notes
|
||||
if (documentId === "new") {
|
||||
setDocumentTitle(null);
|
||||
|
|
@ -124,7 +124,7 @@ export function DashboardBreadcrumb() {
|
|||
} else {
|
||||
documentLabel = documentTitle || subSection;
|
||||
}
|
||||
|
||||
|
||||
breadcrumbs.push({
|
||||
label: t("documents"),
|
||||
href: `/dashboard/${segments[1]}/documents`,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { ChevronDown, ChevronUp, ExternalLink, Info, Sparkles, User } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -12,9 +14,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
interface SetupPromptStepProps {
|
||||
searchSpaceId: number;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
|
|
@ -12,6 +14,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -23,19 +26,20 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
interface PromptConfigManagerProps {
|
||||
searchSpaceId: number;
|
||||
}
|
||||
|
||||
export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) {
|
||||
const { data: searchSpace, isLoading: loading, refetch: fetchSearchSpace } = useQuery({
|
||||
const {
|
||||
data: searchSpace,
|
||||
isLoading: loading,
|
||||
refetch: fetchSearchSpace,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
|
||||
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
|
||||
enabled: !!searchSpaceId,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -8,6 +9,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
|
||||
import { chatsAtom } from "@/atoms/chats/chat-query.atoms";
|
||||
import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import { AppSidebar } from "@/components/sidebar/app-sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -18,11 +20,9 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { notesApiService } from "@/lib/apis/notes-api.service";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
|
||||
interface AppSidebarProviderProps {
|
||||
searchSpaceId: string;
|
||||
|
|
|
|||
|
|
@ -1,60 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { FileText, type LucideIcon, MoreHorizontal, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { notesApiService } from "@/lib/apis/notes-api.service";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { notesApiService } from "@/lib/apis/notes-api.service";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
FileText,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
RefreshCw,
|
||||
};
|
||||
|
||||
interface NoteAction {
|
||||
name: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface NoteItem {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
id?: number;
|
||||
search_space_id?: number;
|
||||
actions?: NoteAction[];
|
||||
}
|
||||
|
||||
interface AllNotesSidebarProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onAddNote?: () => void;
|
||||
hoverTimeoutRef?: React.MutableRefObject<NodeJS.Timeout | null>;
|
||||
}
|
||||
|
||||
export function AllNotesSidebar({
|
||||
|
|
@ -62,315 +38,242 @@ export function AllNotesSidebar({
|
|||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onAddNote,
|
||||
hoverTimeoutRef,
|
||||
}: AllNotesSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const sidebarRef = useRef<HTMLElement>(null);
|
||||
const [sidebarLeft, setSidebarLeft] = useState(0); // Position from left edge of viewport
|
||||
const queryClient = useQueryClient();
|
||||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
// Calculate the sidebar's right edge position
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const updatePosition = () => {
|
||||
// Find the actual sidebar element (the fixed positioned one)
|
||||
const sidebarElement = document.querySelector(
|
||||
'[data-slot="sidebar"][data-sidebar="sidebar"]'
|
||||
) as HTMLElement;
|
||||
|
||||
if (sidebarElement) {
|
||||
const rect = sidebarElement.getBoundingClientRect();
|
||||
// Set the left position to be the right edge of the sidebar
|
||||
setSidebarLeft(rect.right);
|
||||
} else {
|
||||
// Fallback: try to find any sidebar element
|
||||
const fallbackSidebar = document.querySelector('[data-slot="sidebar"]') as HTMLElement;
|
||||
if (fallbackSidebar) {
|
||||
const rect = fallbackSidebar.getBoundingClientRect();
|
||||
setSidebarLeft(rect.right);
|
||||
} else {
|
||||
// Final fallback: use CSS variable
|
||||
const sidebarWidth = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--sidebar-width")
|
||||
.trim();
|
||||
if (sidebarWidth) {
|
||||
const remValue = parseFloat(sidebarWidth);
|
||||
setSidebarLeft(remValue * 16); // Convert rem to px
|
||||
} else {
|
||||
setSidebarLeft(256); // Default 16rem
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
// Update on window resize and scroll
|
||||
window.addEventListener("resize", updatePosition);
|
||||
window.addEventListener("scroll", updatePosition, true);
|
||||
|
||||
// Use MutationObserver to watch for sidebar state changes
|
||||
const observer = new MutationObserver(updatePosition);
|
||||
const sidebarWrapper = document.querySelector('[data-slot="sidebar-wrapper"]');
|
||||
if (sidebarWrapper) {
|
||||
observer.observe(sidebarWrapper, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-state", "class"],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Also observe the sidebar element directly if it exists
|
||||
const sidebarElement = document.querySelector('[data-slot="sidebar"]');
|
||||
if (sidebarElement) {
|
||||
observer.observe(sidebarElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-state", "class"],
|
||||
childList: false,
|
||||
subtree: false,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updatePosition);
|
||||
window.removeEventListener("scroll", updatePosition, true);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle Escape key to close sidebar
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
return () => window.removeEventListener("keydown", handleEscape);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
// Fetch all notes
|
||||
// Fetch all notes (when no search query)
|
||||
const {
|
||||
data: notesData,
|
||||
error: notesError,
|
||||
isLoading: isLoadingNotes,
|
||||
refetch: refetchNotes,
|
||||
} = useQuery({
|
||||
queryKey: ["all-notes", searchSpaceId],
|
||||
queryFn: () =>
|
||||
notesApiService.getNotes({
|
||||
search_space_id: Number(searchSpaceId),
|
||||
page_size: 1000, // Get all notes
|
||||
page_size: 1000,
|
||||
}),
|
||||
enabled: !!searchSpaceId && open, // Only fetch when sidebar is open
|
||||
enabled: !!searchSpaceId && open && !debouncedSearchQuery,
|
||||
});
|
||||
|
||||
// Handle note deletion with loading state
|
||||
// Search notes (when there's a search query)
|
||||
const {
|
||||
data: searchData,
|
||||
error: searchError,
|
||||
isLoading: isSearching,
|
||||
} = useQuery({
|
||||
queryKey: ["search-notes", searchSpaceId, debouncedSearchQuery],
|
||||
queryFn: () =>
|
||||
documentsApiService.searchDocuments({
|
||||
queryParams: {
|
||||
search_space_id: Number(searchSpaceId),
|
||||
document_types: ["NOTE"],
|
||||
title: debouncedSearchQuery,
|
||||
page_size: 100,
|
||||
},
|
||||
}),
|
||||
enabled: !!searchSpaceId && open && !!debouncedSearchQuery,
|
||||
});
|
||||
|
||||
// Handle note navigation
|
||||
const handleNoteClick = useCallback(
|
||||
(noteId: number, noteSearchSpaceId: number) => {
|
||||
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[router, onOpenChange]
|
||||
);
|
||||
|
||||
// Handle note deletion
|
||||
const handleDeleteNote = useCallback(
|
||||
async (noteId: number, deleteAction: () => void) => {
|
||||
setIsDeleting(noteId);
|
||||
async (noteId: number, noteSearchSpaceId: number) => {
|
||||
setDeletingNoteId(noteId);
|
||||
try {
|
||||
await deleteAction();
|
||||
refetchNotes();
|
||||
await notesApiService.deleteNote({
|
||||
search_space_id: noteSearchSpaceId,
|
||||
note_id: noteId,
|
||||
});
|
||||
// Invalidate queries to refresh the list
|
||||
queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] });
|
||||
} catch (error) {
|
||||
console.error("Error deleting note:", error);
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
setDeletingNoteId(null);
|
||||
}
|
||||
},
|
||||
[refetchNotes]
|
||||
[queryClient, searchSpaceId]
|
||||
);
|
||||
|
||||
// Transform notes to the format expected by the component
|
||||
const allNotes = useMemo(() => {
|
||||
return notesData?.items
|
||||
? notesData.items.map((note) => ({
|
||||
name: note.title,
|
||||
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
|
||||
icon: FileText as LucideIcon,
|
||||
id: note.id,
|
||||
search_space_id: note.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: async () => {
|
||||
try {
|
||||
await notesApiService.deleteNote({
|
||||
search_space_id: note.search_space_id,
|
||||
note_id: note.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting note:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
: [];
|
||||
}, [notesData]);
|
||||
// Clear search
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
}, []);
|
||||
|
||||
// Enhanced note item component
|
||||
const NoteItemComponent = useCallback(
|
||||
({ note }: { note: NoteItem }) => {
|
||||
const isDeletingNote = isDeleting === note.id;
|
||||
// Determine which data to show
|
||||
const isSearchMode = !!debouncedSearchQuery;
|
||||
const isLoading = isSearchMode ? isSearching : isLoadingNotes;
|
||||
const error = isSearchMode ? searchError : notesError;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={note.id ? `note-${note.id}` : `note-${note.name}`}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
router.push(note.url);
|
||||
onOpenChange(false); // Close sidebar when navigating
|
||||
}}
|
||||
disabled={isDeletingNote}
|
||||
className={cn("group/item relative", isDeletingNote && "opacity-50")}
|
||||
>
|
||||
<note.icon className="h-4 w-4 shrink-0" />
|
||||
<span className={cn("truncate", isDeletingNote && "opacity-50")}>{note.name}</span>
|
||||
</SidebarMenuButton>
|
||||
// Transform notes data - handle both regular notes and search results
|
||||
const notes = useMemo(() => {
|
||||
if (isSearchMode && searchData?.items) {
|
||||
return searchData.items.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
search_space_id: doc.search_space_id,
|
||||
}));
|
||||
}
|
||||
return notesData?.items ?? [];
|
||||
}, [isSearchMode, searchData, notesData]);
|
||||
|
||||
{note.actions && note.actions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="opacity-0 group-hover/item:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48" side="left" align="start">
|
||||
{note.actions.map((action, actionIndex) => {
|
||||
const ActionIcon = actionIconMap[action.icon] || FileText;
|
||||
const isDeleteAction = action.name.toLowerCase().includes("delete");
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="left" className="w-80 p-0 flex flex-col">
|
||||
<SheetHeader className="px-4 py-4 border-b space-y-3">
|
||||
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
{t("all_notes_description") || "Browse and manage all your notes"}
|
||||
</SheetDescription>
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${action.name}-${actionIndex}`}
|
||||
onClick={() => {
|
||||
if (isDeleteAction) {
|
||||
handleDeleteNote(note.id || 0, action.onClick);
|
||||
} else {
|
||||
action.onClick();
|
||||
}
|
||||
}}
|
||||
disabled={isDeletingNote}
|
||||
className={isDeleteAction ? "text-destructive" : ""}
|
||||
>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
<span>{isDeletingNote && isDeleteAction ? "Deleting..." : action.name}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
},
|
||||
[isDeleting, router, onOpenChange, handleDeleteNote]
|
||||
);
|
||||
{/* Search Input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("search_notes") || "Search notes..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-8 h-9"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Clear search</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
const sidebarContent = (
|
||||
<section
|
||||
ref={sidebarRef}
|
||||
aria-label="All notes sidebar"
|
||||
className={cn(
|
||||
"fixed top-0 bottom-0 z-[100] w-80 bg-sidebar text-sidebar-foreground shadow-xl",
|
||||
"transition-all duration-300 ease-out",
|
||||
!open && "pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
// Position it to slide from the right edge of the main sidebar
|
||||
left: `${sidebarLeft}px`,
|
||||
transform: open ? `scaleX(1)` : `scaleX(0)`,
|
||||
transformOrigin: "left",
|
||||
opacity: open ? 1 : 0,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// Clear any pending close timeout when hovering over sidebar
|
||||
if (hoverTimeoutRef?.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Close sidebar when mouse leaves
|
||||
if (hoverTimeoutRef) {
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
onOpenChange(false);
|
||||
}, 200);
|
||||
} else {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex h-16 shrink-0 items-center justify-between px-4 border-b border-sidebar">
|
||||
<h2 className="text-sm font-semibold">{t("all_notes") || "All Notes"}</h2>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
{isLoadingNotes ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton disabled>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("loading") || "Loading..."}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : notesError ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton disabled>
|
||||
<span className="text-xs text-destructive">
|
||||
{t("error_loading_notes") || "Error loading notes"}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : allNotes.length > 0 ? (
|
||||
<SidebarMenu className="list-none">
|
||||
{allNotes.map((note) => (
|
||||
<NoteItemComponent key={note.id || note.name} note={note} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
) : (
|
||||
<SidebarMenuItem className="list-none">
|
||||
{onAddNote ? (
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
onAddNote();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className="text-muted-foreground hover:text-sidebar-foreground text-xs"
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-8 text-sm text-destructive">
|
||||
{t("error_loading_notes") || "Error loading notes"}
|
||||
</div>
|
||||
) : notes.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{notes.map((note) => {
|
||||
const isDeleting = deletingNoteId === note.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={note.id}
|
||||
className={cn(
|
||||
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"transition-colors cursor-pointer",
|
||||
isDeleting && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{/* Main clickable area for navigation */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNoteClick(note.id, note.search_space_id)}
|
||||
disabled={isDeleting}
|
||||
className="flex items-center gap-2 flex-1 min-w-0 text-left"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>{t("create_new_note") || "Create a new note"}</span>
|
||||
</SidebarMenuButton>
|
||||
) : (
|
||||
<SidebarMenuButton disabled className="text-muted-foreground text-xs">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>{t("no_notes") || "No notes yet"}</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{note.title}</span>
|
||||
</button>
|
||||
|
||||
{/* Actions dropdown - separate from main click area */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 shrink-0",
|
||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : isSearchMode ? (
|
||||
<div className="text-center py-8">
|
||||
<Search className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("no_results_found") || "No notes found"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("try_different_search") || "Try a different search term"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<FileText className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("no_notes") || "No notes yet"}
|
||||
</p>
|
||||
{onAddNote && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onAddNote();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("create_new_note") || "Create a note"}
|
||||
</Button>
|
||||
)}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer with Add Note button */}
|
||||
{onAddNote && (
|
||||
<div className="p-2">
|
||||
{onAddNote && notes.length > 0 && (
|
||||
<div className="p-3 border-t">
|
||||
<Button
|
||||
onClick={() => {
|
||||
onAddNote();
|
||||
|
|
@ -384,14 +287,7 @@ export function AllNotesSidebar({
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
// Render sidebar via portal to avoid stacking context issues
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(sidebarContent, document.body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
AlertCircle,
|
||||
BookOpen,
|
||||
|
|
@ -27,7 +28,7 @@ import {
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -37,8 +38,6 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
|
||||
/**
|
||||
* Generates a consistent color based on a string (email)
|
||||
|
|
@ -454,7 +453,11 @@ export const AppSidebar = memo(function AppSidebar({
|
|||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<NavNotes notes={processedRecentNotes} onAddNote={onAddNote} searchSpaceId={searchSpaceId} />
|
||||
<NavNotes
|
||||
notes={processedRecentNotes}
|
||||
onAddNote={onAddNote}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,18 @@
|
|||
|
||||
import {
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Share,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useState, useRef } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -27,23 +26,12 @@ import {
|
|||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AllNotesSidebar } from "./all-notes-sidebar";
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Share,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
RefreshCw,
|
||||
};
|
||||
|
||||
interface NoteAction {
|
||||
name: string;
|
||||
icon: string;
|
||||
|
|
@ -66,14 +54,19 @@ interface NavNotesProps {
|
|||
searchSpaceId?: string;
|
||||
}
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
FileText,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
};
|
||||
|
||||
export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const { isMobile } = useSidebar();
|
||||
const router = useRouter();
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
|
||||
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Handle note deletion with loading state
|
||||
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
|
||||
|
|
@ -85,132 +78,148 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Enhanced note item component
|
||||
const NoteItemComponent = useCallback(
|
||||
({ note }: { note: NoteItem }) => {
|
||||
const isDeletingNote = isDeleting === note.id;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={note.id ? `note-${note.id}` : `note-${note.name}`}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => router.push(note.url)}
|
||||
disabled={isDeletingNote}
|
||||
className={`group/item relative ${isDeletingNote ? "opacity-50" : ""}`}
|
||||
>
|
||||
<note.icon className="h-4 w-4 shrink-0" />
|
||||
<span className={`truncate ${isDeletingNote ? "opacity-50" : ""}`}>{note.name}</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{note.actions && note.actions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="opacity-0 group-hover/item:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
{note.actions.map((action, actionIndex) => {
|
||||
const ActionIcon = actionIconMap[action.icon] || FileText;
|
||||
const isDeleteAction = action.name.toLowerCase().includes("delete");
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${action.name}-${actionIndex}`}
|
||||
onClick={() => {
|
||||
if (isDeleteAction) {
|
||||
handleDeleteNote(note.id || 0, action.onClick);
|
||||
} else {
|
||||
action.onClick();
|
||||
}
|
||||
}}
|
||||
disabled={isDeletingNote}
|
||||
className={isDeleteAction ? "text-destructive" : ""}
|
||||
>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
<span>{isDeletingNote && isDeleteAction ? "Deleting..." : action.name}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
// Handle note navigation
|
||||
const handleNoteClick = useCallback(
|
||||
(url: string) => {
|
||||
router.push(url);
|
||||
},
|
||||
[isDeleting, router, isMobile, handleDeleteNote]
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden relative">
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="flex items-center group/header relative">
|
||||
<div className="flex items-center group/header">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5">
|
||||
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5 flex-1">
|
||||
<ChevronRight
|
||||
className={`h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0 hover:text-sidebar-foreground ${
|
||||
isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<span>{t("notes") || "Notes"}</span>
|
||||
</SidebarGroupLabel>
|
||||
</CollapsibleTrigger>
|
||||
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity">
|
||||
|
||||
{/* Action buttons - always visible on hover */}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
{searchSpaceId && notes.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={(e) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Clear any pending close timeout
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
setIsAllNotesSidebarOpen(true);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.stopPropagation();
|
||||
// Add a small delay before closing to allow moving to the sidebar
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
setIsAllNotesSidebarOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
aria-label="View all notes"
|
||||
className="text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden relative"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{onAddNote && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddNote();
|
||||
}}
|
||||
aria-label="Add note"
|
||||
className="text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden relative"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{/* Note Items */}
|
||||
{notes.length > 0 ? (
|
||||
notes.map((note) => <NoteItemComponent key={note.id || note.name} note={note} />)
|
||||
notes.map((note) => {
|
||||
const isDeletingNote = isDeleting === note.id;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={note.id || note.name} className="group/note">
|
||||
{/* Main navigation button */}
|
||||
<SidebarMenuButton
|
||||
onClick={() => handleNoteClick(note.url)}
|
||||
disabled={isDeletingNote}
|
||||
className={cn(
|
||||
"pr-8", // Make room for the action button
|
||||
isDeletingNote && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<note.icon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{note.name}</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{/* Actions dropdown - positioned absolutely */}
|
||||
{note.actions && note.actions.length > 0 && (
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
"opacity-0 group-hover/note:opacity-100 focus:opacity-100",
|
||||
"data-[state=open]:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isDeletingNote}
|
||||
>
|
||||
{isDeletingNote ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="right" className="w-40">
|
||||
{note.actions.map((action, actionIndex) => {
|
||||
const ActionIcon = actionIconMap[action.icon] || FileText;
|
||||
const isDeleteAction = action.name.toLowerCase().includes("delete");
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${action.name}-${actionIndex}`}
|
||||
onClick={() => {
|
||||
if (isDeleteAction) {
|
||||
handleDeleteNote(note.id || 0, action.onClick);
|
||||
} else {
|
||||
action.onClick();
|
||||
}
|
||||
}}
|
||||
disabled={isDeletingNote}
|
||||
className={
|
||||
isDeleteAction
|
||||
? "text-destructive focus:text-destructive"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
<span>
|
||||
{isDeletingNote && isDeleteAction
|
||||
? "Deleting..."
|
||||
: action.name}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
/* Empty state with create button */
|
||||
<SidebarMenuItem>
|
||||
{onAddNote ? (
|
||||
<SidebarMenuButton
|
||||
|
|
@ -232,13 +241,14 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
|
|||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* All Notes Sheet */}
|
||||
{searchSpaceId && (
|
||||
<AllNotesSidebar
|
||||
open={isAllNotesSidebarOpen}
|
||||
onOpenChange={setIsAllNotesSidebarOpen}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onAddNote={onAddNote}
|
||||
hoverTimeoutRef={hoverTimeoutRef}
|
||||
/>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
|
|
|
|||
|
|
@ -60,4 +60,4 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,17 @@ import {
|
|||
IconTicket,
|
||||
IconWorldWww,
|
||||
} from "@tabler/icons-react";
|
||||
import { File, FileText, Globe, Link, Microscope, Search, Sparkles, Telescope, Webhook } from "lucide-react";
|
||||
import {
|
||||
File,
|
||||
FileText,
|
||||
Globe,
|
||||
Link,
|
||||
Microscope,
|
||||
Search,
|
||||
Sparkles,
|
||||
Telescope,
|
||||
Webhook,
|
||||
} from "lucide-react";
|
||||
import { EnumConnectorName } from "./connector";
|
||||
|
||||
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const documentTypeEnum = z.enum([
|
|||
"LUMA_CONNECTOR",
|
||||
"ELASTICSEARCH_CONNECTOR",
|
||||
"LINEAR_CONNECTOR",
|
||||
"NOTE",
|
||||
]);
|
||||
|
||||
export const document = z.object({
|
||||
|
|
@ -27,7 +28,10 @@ export const document = z.object({
|
|||
document_type: documentTypeEnum,
|
||||
document_metadata: z.record(z.string(), z.any()),
|
||||
content: z.string(),
|
||||
content_hash: z.string(),
|
||||
unique_identifier_hash: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
search_space_id: z.number(),
|
||||
});
|
||||
|
||||
|
|
@ -68,6 +72,9 @@ export const getDocumentsRequest = z.object({
|
|||
export const getDocumentsResponse = z.object({
|
||||
items: z.array(document),
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
page_size: z.number(),
|
||||
has_more: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -118,6 +125,9 @@ export const searchDocumentsRequest = z.object({
|
|||
export const searchDocumentsResponse = z.object({
|
||||
items: z.array(document),
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
page_size: z.number(),
|
||||
has_more: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,12 +51,14 @@ export const getRoleByIdResponse = role;
|
|||
export const updateRoleRequest = z.object({
|
||||
search_space_id: z.number(),
|
||||
role_id: z.number(),
|
||||
data: role.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
permissions: true,
|
||||
is_default: true,
|
||||
}).partial(),
|
||||
data: role
|
||||
.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
permissions: true,
|
||||
is_default: true,
|
||||
})
|
||||
.partial(),
|
||||
});
|
||||
|
||||
export const updateRoleResponse = role;
|
||||
|
|
|
|||
|
|
@ -2,26 +2,26 @@ import { z } from "zod";
|
|||
import { paginationQueryParams } from ".";
|
||||
|
||||
export const searchSpace = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
user_id: z.string(),
|
||||
citations_enabled: z.boolean(),
|
||||
qna_custom_instructions: z.string().nullable(),
|
||||
member_count: z.number(),
|
||||
is_owner: z.boolean(),
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
user_id: z.string(),
|
||||
citations_enabled: z.boolean(),
|
||||
qna_custom_instructions: z.string().nullable(),
|
||||
member_count: z.number(),
|
||||
is_owner: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Get search spaces
|
||||
*/
|
||||
export const getSearchSpacesRequest = z.object({
|
||||
queryParams: paginationQueryParams
|
||||
.extend({
|
||||
owned_only: z.boolean().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
queryParams: paginationQueryParams
|
||||
.extend({
|
||||
owned_only: z.boolean().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const getSearchSpacesResponse = z.array(searchSpace);
|
||||
|
|
@ -29,12 +29,10 @@ export const getSearchSpacesResponse = z.array(searchSpace);
|
|||
/**
|
||||
* Create search space
|
||||
*/
|
||||
export const createSearchSpaceRequest = searchSpace
|
||||
.pick({ name: true, description: true })
|
||||
.extend({
|
||||
citations_enabled: z.boolean().default(true).optional(),
|
||||
qna_custom_instructions: z.string().nullable().optional(),
|
||||
});
|
||||
export const createSearchSpaceRequest = searchSpace.pick({ name: true, description: true }).extend({
|
||||
citations_enabled: z.boolean().default(true).optional(),
|
||||
qna_custom_instructions: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const createSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true });
|
||||
|
||||
|
|
@ -42,13 +40,13 @@ export const createSearchSpaceResponse = searchSpace.omit({ member_count: true,
|
|||
* Get community prompts
|
||||
*/
|
||||
export const getCommunityPromptsResponse = z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
author: z.string(),
|
||||
link: z.string(),
|
||||
category: z.string(),
|
||||
})
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
author: z.string(),
|
||||
link: z.string(),
|
||||
category: z.string(),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -62,10 +60,10 @@ export const getSearchSpaceResponse = searchSpace.omit({ member_count: true, is_
|
|||
* Update search space
|
||||
*/
|
||||
export const updateSearchSpaceRequest = z.object({
|
||||
id: z.number(),
|
||||
data: searchSpace
|
||||
.pick({ name: true, description: true, citations_enabled: true, qna_custom_instructions: true })
|
||||
.partial(),
|
||||
id: z.number(),
|
||||
data: searchSpace
|
||||
.pick({ name: true, description: true, citations_enabled: true, qna_custom_instructions: true })
|
||||
.partial(),
|
||||
});
|
||||
|
||||
export const updateSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true });
|
||||
|
|
@ -76,7 +74,7 @@ export const updateSearchSpaceResponse = searchSpace.omit({ member_count: true,
|
|||
export const deleteSearchSpaceRequest = searchSpace.pick({ id: true });
|
||||
|
||||
export const deleteSearchSpaceResponse = z.object({
|
||||
message: z.literal("Search space deleted successfully"),
|
||||
message: z.literal("Search space deleted successfully"),
|
||||
});
|
||||
|
||||
// Inferred types
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./use-debounced-value";
|
||||
export * from "./use-logs";
|
||||
export * from "./use-rbac";
|
||||
export * from "./use-search-source-connectors";
|
||||
|
|
|
|||
23
surfsense_web/hooks/use-debounced-value.ts
Normal file
23
surfsense_web/hooks/use-debounced-value.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Hook that returns a debounced value that only updates after the specified delay
|
||||
* @param value - The value to debounce
|
||||
* @param delay - The delay in milliseconds (default: 300ms)
|
||||
* @returns The debounced value
|
||||
*/
|
||||
export function useDebouncedValue<T>(value: T, delay: number = 300): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
|
@ -141,32 +141,43 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
|
|||
);
|
||||
|
||||
// Function to create a new log
|
||||
const createLog = useCallback(async (logData: Omit<Log, "id" | "created_at">) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs`,
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
body: JSON.stringify(logData),
|
||||
// Use silent: true to suppress toast notifications (for internal/background operations)
|
||||
const createLog = useCallback(
|
||||
async (logData: Omit<Log, "id" | "created_at">, options?: { silent?: boolean }) => {
|
||||
const { silent = false } = options || {};
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs`,
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
body: JSON.stringify(logData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to create log");
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to create log");
|
||||
const newLog = await response.json();
|
||||
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||
// Only show toast if not silent
|
||||
if (!silent) {
|
||||
toast.success("Log created successfully");
|
||||
}
|
||||
return newLog;
|
||||
} catch (err: any) {
|
||||
// Only show error toast if not silent
|
||||
if (!silent) {
|
||||
toast.error(err.message || "Failed to create log");
|
||||
}
|
||||
console.error("Error creating log:", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const newLog = await response.json();
|
||||
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||
toast.success("Log created successfully");
|
||||
return newLog;
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || "Failed to create log");
|
||||
console.error("Error creating log:", err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Function to update a log
|
||||
const updateLog = useCallback(
|
||||
|
|
|
|||
|
|
@ -145,4 +145,3 @@ class NotesApiService {
|
|||
}
|
||||
|
||||
export const notesApiService = new NotesApiService();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import {
|
|||
deleteRoleRequest,
|
||||
deleteRoleResponse,
|
||||
type GetRoleByIdRequest,
|
||||
type GetRolesRequest,
|
||||
getRoleByIdRequest,
|
||||
getRoleByIdResponse,
|
||||
type GetRolesRequest,
|
||||
getRolesRequest,
|
||||
getRolesResponse,
|
||||
type UpdateRoleRequest,
|
||||
|
|
@ -34,7 +34,7 @@ class RolesApiService {
|
|||
createRoleResponse,
|
||||
{
|
||||
body: parsedRequest.data.data,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ class RolesApiService {
|
|||
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles`,
|
||||
getRolesResponse,
|
||||
getRolesResponse
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ class RolesApiService {
|
|||
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`,
|
||||
getRoleByIdResponse,
|
||||
getRoleByIdResponse
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class RolesApiService {
|
|||
updateRoleResponse,
|
||||
{
|
||||
body: parsedRequest.data.data,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ class RolesApiService {
|
|||
|
||||
return baseApiService.delete(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/roles/${parsedRequest.data.role_id}`,
|
||||
deleteRoleResponse,
|
||||
deleteRoleResponse
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import {
|
||||
type CreateSearchSpaceRequest,
|
||||
type DeleteSearchSpaceRequest,
|
||||
type GetSearchSpaceRequest,
|
||||
type GetSearchSpacesRequest,
|
||||
type UpdateSearchSpaceRequest,
|
||||
createSearchSpaceRequest,
|
||||
createSearchSpaceResponse,
|
||||
type DeleteSearchSpaceRequest,
|
||||
deleteSearchSpaceRequest,
|
||||
deleteSearchSpaceResponse,
|
||||
type GetSearchSpaceRequest,
|
||||
type GetSearchSpacesRequest,
|
||||
getCommunityPromptsResponse,
|
||||
getSearchSpaceRequest,
|
||||
getSearchSpaceResponse,
|
||||
getSearchSpacesRequest,
|
||||
getSearchSpacesResponse,
|
||||
type UpdateSearchSpaceRequest,
|
||||
updateSearchSpaceRequest,
|
||||
updateSearchSpaceResponse,
|
||||
} from "@/contracts/types/search-space.types";
|
||||
|
|
@ -71,7 +71,10 @@ class SearchSpacesApiService {
|
|||
* Get community-curated prompts for search space system instructions
|
||||
*/
|
||||
getCommunityPrompts = async () => {
|
||||
return baseApiService.get(`/api/v1/searchspaces/prompts/community`, getCommunityPromptsResponse);
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/prompts/community`,
|
||||
getCommunityPromptsResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import type { GetChatsRequest } from "@/contracts/types/chat.types";
|
|||
import type { GetDocumentsRequest } from "@/contracts/types/document.types";
|
||||
import type { GetLLMConfigsRequest } from "@/contracts/types/llm-config.types";
|
||||
import type { GetPodcastsRequest } from "@/contracts/types/podcast.types";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
|
||||
import type { GetRolesRequest } from "@/contracts/types/roles.types";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
|
||||
|
||||
export const cacheKeys = {
|
||||
chats: {
|
||||
|
|
|
|||
|
|
@ -645,6 +645,10 @@
|
|||
"search_space": "Search Space",
|
||||
"notes": "Notes",
|
||||
"all_notes": "All Notes",
|
||||
"all_notes_description": "Browse and manage all your notes",
|
||||
"search_notes": "Search notes...",
|
||||
"no_results_found": "No notes found",
|
||||
"try_different_search": "Try a different search term",
|
||||
"no_notes": "No notes yet",
|
||||
"create_new_note": "Create a new note",
|
||||
"error_loading_notes": "Error loading notes",
|
||||
|
|
|
|||
|
|
@ -645,6 +645,10 @@
|
|||
"search_space": "搜索空间",
|
||||
"notes": "笔记",
|
||||
"all_notes": "所有笔记",
|
||||
"all_notes_description": "浏览和管理您的所有笔记",
|
||||
"search_notes": "搜索笔记...",
|
||||
"no_results_found": "未找到笔记",
|
||||
"try_different_search": "尝试其他搜索词",
|
||||
"no_notes": "暂无笔记",
|
||||
"create_new_note": "创建新笔记",
|
||||
"error_loading_notes": "加载笔记时出错",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue