mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: linting
This commit is contained in:
parent
11915df97b
commit
73a57589ac
25 changed files with 184 additions and 181 deletions
|
|
@ -7,7 +7,6 @@ Revises: 59
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|
@ -22,7 +21,7 @@ EMBEDDING_DIM = config.embedding_model_instance.dimension
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Create surfsense_docs_documents and surfsense_docs_chunks tables."""
|
"""Create surfsense_docs_documents and surfsense_docs_chunks tables."""
|
||||||
|
|
||||||
# Create surfsense_docs_documents table
|
# Create surfsense_docs_documents table
|
||||||
op.execute(
|
op.execute(
|
||||||
f"""
|
f"""
|
||||||
|
|
@ -46,7 +45,7 @@ def upgrade() -> None:
|
||||||
END$$;
|
END$$;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create indexes for surfsense_docs_documents
|
# Create indexes for surfsense_docs_documents
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -75,7 +74,7 @@ def upgrade() -> None:
|
||||||
END$$;
|
END$$;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create surfsense_docs_chunks table
|
# Create surfsense_docs_chunks table
|
||||||
op.execute(
|
op.execute(
|
||||||
f"""
|
f"""
|
||||||
|
|
@ -96,7 +95,7 @@ def upgrade() -> None:
|
||||||
END$$;
|
END$$;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create indexes for surfsense_docs_chunks
|
# Create indexes for surfsense_docs_chunks
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -111,7 +110,7 @@ def upgrade() -> None:
|
||||||
END$$;
|
END$$;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create vector indexes for similarity search
|
# Create vector indexes for similarity search
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -119,14 +118,14 @@ def upgrade() -> None:
|
||||||
ON surfsense_docs_documents USING hnsw (embedding public.vector_cosine_ops);
|
ON surfsense_docs_documents USING hnsw (embedding public.vector_cosine_ops);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_vector_index
|
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_vector_index
|
||||||
ON surfsense_docs_chunks USING hnsw (embedding public.vector_cosine_ops);
|
ON surfsense_docs_chunks USING hnsw (embedding public.vector_cosine_ops);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create full-text search indexes (same pattern as documents/chunks tables)
|
# Create full-text search indexes (same pattern as documents/chunks tables)
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -134,7 +133,7 @@ def upgrade() -> None:
|
||||||
ON surfsense_docs_documents USING gin (to_tsvector('english', content));
|
ON surfsense_docs_documents USING gin (to_tsvector('english', content));
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_search_index
|
CREATE INDEX IF NOT EXISTS surfsense_docs_chunks_search_index
|
||||||
|
|
@ -148,18 +147,17 @@ def downgrade() -> None:
|
||||||
# Drop full-text search indexes
|
# Drop full-text search indexes
|
||||||
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_search_index")
|
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_search_index")
|
||||||
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_search_index")
|
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_search_index")
|
||||||
|
|
||||||
# Drop vector indexes
|
# Drop vector indexes
|
||||||
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_vector_index")
|
op.execute("DROP INDEX IF EXISTS surfsense_docs_chunks_vector_index")
|
||||||
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_vector_index")
|
op.execute("DROP INDEX IF EXISTS surfsense_docs_documents_vector_index")
|
||||||
|
|
||||||
# Drop regular indexes
|
# Drop regular indexes
|
||||||
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_chunks_document_id")
|
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_chunks_document_id")
|
||||||
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_updated_at")
|
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_updated_at")
|
||||||
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_content_hash")
|
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_content_hash")
|
||||||
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_source")
|
op.execute("DROP INDEX IF EXISTS ix_surfsense_docs_documents_source")
|
||||||
|
|
||||||
# Drop tables (chunks first due to FK)
|
# Drop tables (chunks first due to FK)
|
||||||
op.execute("DROP TABLE IF EXISTS surfsense_docs_chunks")
|
op.execute("DROP TABLE IF EXISTS surfsense_docs_chunks")
|
||||||
op.execute("DROP TABLE IF EXISTS surfsense_docs_documents")
|
op.execute("DROP TABLE IF EXISTS surfsense_docs_documents")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,12 @@ def format_surfsense_docs_results(results: list[tuple]) -> str:
|
||||||
"metadata": {"source": doc.source},
|
"metadata": {"source": doc.source},
|
||||||
"chunks": [],
|
"chunks": [],
|
||||||
}
|
}
|
||||||
grouped[doc.id]["chunks"].append({
|
grouped[doc.id]["chunks"].append(
|
||||||
"chunk_id": f"doc-{chunk.id}",
|
{
|
||||||
"content": chunk.content,
|
"chunk_id": f"doc-{chunk.id}",
|
||||||
})
|
"content": chunk.content,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Render XML matching format_documents_for_context structure
|
# Render XML matching format_documents_for_context structure
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
|
|
@ -70,7 +72,9 @@ def format_surfsense_docs_results(results: list[tuple]) -> str:
|
||||||
parts.append("<document_content>")
|
parts.append("<document_content>")
|
||||||
|
|
||||||
for ch in g["chunks"]:
|
for ch in g["chunks"]:
|
||||||
parts.append(f" <chunk id='{ch['chunk_id']}'><![CDATA[{ch['content']}]]></chunk>")
|
parts.append(
|
||||||
|
f" <chunk id='{ch['chunk_id']}'><![CDATA[{ch['content']}]]></chunk>"
|
||||||
|
)
|
||||||
|
|
||||||
parts.append("</document_content>")
|
parts.append("</document_content>")
|
||||||
parts.append("</document>")
|
parts.append("</document>")
|
||||||
|
|
@ -157,4 +161,3 @@ def create_search_surfsense_docs_tool(db_session: AsyncSession):
|
||||||
)
|
)
|
||||||
|
|
||||||
return search_surfsense_docs
|
return search_surfsense_docs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,9 @@ class SurfsenseDocsDocument(BaseModel, TimestampMixin):
|
||||||
|
|
||||||
__tablename__ = "surfsense_docs_documents"
|
__tablename__ = "surfsense_docs_documents"
|
||||||
|
|
||||||
source = Column(String, nullable=False, unique=True, index=True) # File path: "connectors/slack.mdx"
|
source = Column(
|
||||||
|
String, nullable=False, unique=True, index=True
|
||||||
|
) # File path: "connectors/slack.mdx"
|
||||||
title = Column(String, nullable=False)
|
title = Column(String, nullable=False)
|
||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
content_hash = Column(String, nullable=False, index=True) # For detecting changes
|
content_hash = Column(String, nullable=False, index=True) # For detecting changes
|
||||||
|
|
|
||||||
|
|
@ -623,10 +623,7 @@ async def index_connector_content(
|
||||||
SearchSourceConnectorType.LUMA_CONNECTOR,
|
SearchSourceConnectorType.LUMA_CONNECTOR,
|
||||||
]:
|
]:
|
||||||
# Default to today if no end_date provided (users can manually select future dates)
|
# Default to today if no end_date provided (users can manually select future dates)
|
||||||
if end_date is None:
|
indexing_to = today_str if end_date is None else end_date
|
||||||
indexing_to = today_str
|
|
||||||
else:
|
|
||||||
indexing_to = end_date
|
|
||||||
else:
|
else:
|
||||||
# For non-calendar connectors, cap at today
|
# For non-calendar connectors, cap at today
|
||||||
indexing_to = end_date if end_date else today_str
|
indexing_to = end_date if end_date else today_str
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,3 @@ class SurfsenseDocsDocumentWithChunksRead(BaseModel):
|
||||||
chunks: list[SurfsenseDocsChunkRead]
|
chunks: list[SurfsenseDocsChunkRead]
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,12 @@ from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument, async_session_make
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Path to docs relative to project root
|
# Path to docs relative to project root
|
||||||
DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "surfsense_web" / "content" / "docs"
|
DOCS_DIR = (
|
||||||
|
Path(__file__).resolve().parent.parent.parent.parent
|
||||||
|
/ "surfsense_web"
|
||||||
|
/ "content"
|
||||||
|
/ "docs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_mdx_frontmatter(content: str) -> tuple[str, str]:
|
def parse_mdx_frontmatter(content: str) -> tuple[str, str]:
|
||||||
|
|
@ -38,7 +43,7 @@ def parse_mdx_frontmatter(content: str) -> tuple[str, str]:
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
frontmatter = match.group(1)
|
frontmatter = match.group(1)
|
||||||
content_without_frontmatter = content[match.end():]
|
content_without_frontmatter = content[match.end() :]
|
||||||
|
|
||||||
# Extract title from frontmatter
|
# Extract title from frontmatter
|
||||||
title_match = re.search(r"^title:\s*(.+)$", frontmatter, re.MULTILINE)
|
title_match = re.search(r"^title:\s*(.+)$", frontmatter, re.MULTILINE)
|
||||||
|
|
@ -93,10 +98,10 @@ def create_surfsense_docs_chunks(content: str) -> list[SurfsenseDocsChunk]:
|
||||||
async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, int]:
|
async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, int]:
|
||||||
"""
|
"""
|
||||||
Index all Surfsense documentation files.
|
Index all Surfsense documentation files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session: SQLAlchemy async session
|
session: SQLAlchemy async session
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (created, updated, skipped, deleted) counts
|
Tuple of (created, updated, skipped, deleted) counts
|
||||||
"""
|
"""
|
||||||
|
|
@ -104,45 +109,47 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
|
||||||
updated = 0
|
updated = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
deleted = 0
|
deleted = 0
|
||||||
|
|
||||||
# Get all existing docs from database
|
# Get all existing docs from database
|
||||||
existing_docs_result = await session.execute(
|
existing_docs_result = await session.execute(
|
||||||
select(SurfsenseDocsDocument).options(selectinload(SurfsenseDocsDocument.chunks))
|
select(SurfsenseDocsDocument).options(
|
||||||
|
selectinload(SurfsenseDocsDocument.chunks)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
existing_docs = {doc.source: doc for doc in existing_docs_result.scalars().all()}
|
existing_docs = {doc.source: doc for doc in existing_docs_result.scalars().all()}
|
||||||
|
|
||||||
# Track which sources we've processed
|
# Track which sources we've processed
|
||||||
processed_sources = set()
|
processed_sources = set()
|
||||||
|
|
||||||
# Get all MDX files
|
# Get all MDX files
|
||||||
mdx_files = get_all_mdx_files()
|
mdx_files = get_all_mdx_files()
|
||||||
logger.info(f"Found {len(mdx_files)} MDX files to index")
|
logger.info(f"Found {len(mdx_files)} MDX files to index")
|
||||||
|
|
||||||
for mdx_file in mdx_files:
|
for mdx_file in mdx_files:
|
||||||
try:
|
try:
|
||||||
source = str(mdx_file.relative_to(DOCS_DIR))
|
source = str(mdx_file.relative_to(DOCS_DIR))
|
||||||
processed_sources.add(source)
|
processed_sources.add(source)
|
||||||
|
|
||||||
# Read file content
|
# Read file content
|
||||||
raw_content = mdx_file.read_text(encoding="utf-8")
|
raw_content = mdx_file.read_text(encoding="utf-8")
|
||||||
title, content = parse_mdx_frontmatter(raw_content)
|
title, content = parse_mdx_frontmatter(raw_content)
|
||||||
content_hash = generate_surfsense_docs_content_hash(raw_content)
|
content_hash = generate_surfsense_docs_content_hash(raw_content)
|
||||||
|
|
||||||
if source in existing_docs:
|
if source in existing_docs:
|
||||||
existing_doc = existing_docs[source]
|
existing_doc = existing_docs[source]
|
||||||
|
|
||||||
# Check if content changed
|
# Check if content changed
|
||||||
if existing_doc.content_hash == content_hash:
|
if existing_doc.content_hash == content_hash:
|
||||||
logger.debug(f"Skipping unchanged: {source}")
|
logger.debug(f"Skipping unchanged: {source}")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Content changed - update document
|
# Content changed - update document
|
||||||
logger.info(f"Updating changed document: {source}")
|
logger.info(f"Updating changed document: {source}")
|
||||||
|
|
||||||
# Create new chunks
|
# Create new chunks
|
||||||
chunks = create_surfsense_docs_chunks(content)
|
chunks = create_surfsense_docs_chunks(content)
|
||||||
|
|
||||||
# Update document fields
|
# Update document fields
|
||||||
existing_doc.title = title
|
existing_doc.title = title
|
||||||
existing_doc.content = content
|
existing_doc.content = content
|
||||||
|
|
@ -150,14 +157,14 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
|
||||||
existing_doc.embedding = config.embedding_model_instance.embed(content)
|
existing_doc.embedding = config.embedding_model_instance.embed(content)
|
||||||
existing_doc.chunks = chunks
|
existing_doc.chunks = chunks
|
||||||
existing_doc.updated_at = datetime.now(UTC)
|
existing_doc.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
updated += 1
|
updated += 1
|
||||||
else:
|
else:
|
||||||
# New document - create it
|
# New document - create it
|
||||||
logger.info(f"Creating new document: {source}")
|
logger.info(f"Creating new document: {source}")
|
||||||
|
|
||||||
chunks = create_surfsense_docs_chunks(content)
|
chunks = create_surfsense_docs_chunks(content)
|
||||||
|
|
||||||
document = SurfsenseDocsDocument(
|
document = SurfsenseDocsDocument(
|
||||||
source=source,
|
source=source,
|
||||||
title=title,
|
title=title,
|
||||||
|
|
@ -167,56 +174,56 @@ async def index_surfsense_docs(session: AsyncSession) -> tuple[int, int, int, in
|
||||||
chunks=chunks,
|
chunks=chunks,
|
||||||
updated_at=datetime.now(UTC),
|
updated_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(document)
|
session.add(document)
|
||||||
created += 1
|
created += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing {mdx_file}: {e}", exc_info=True)
|
logger.error(f"Error processing {mdx_file}: {e}", exc_info=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Delete documents for removed files
|
# Delete documents for removed files
|
||||||
for source, doc in existing_docs.items():
|
for source, doc in existing_docs.items():
|
||||||
if source not in processed_sources:
|
if source not in processed_sources:
|
||||||
logger.info(f"Deleting removed document: {source}")
|
logger.info(f"Deleting removed document: {source}")
|
||||||
await session.delete(doc)
|
await session.delete(doc)
|
||||||
deleted += 1
|
deleted += 1
|
||||||
|
|
||||||
# Commit all changes
|
# Commit all changes
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Indexing complete: {created} created, {updated} updated, "
|
f"Indexing complete: {created} created, {updated} updated, "
|
||||||
f"{skipped} skipped, {deleted} deleted"
|
f"{skipped} skipped, {deleted} deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
return created, updated, skipped, deleted
|
return created, updated, skipped, deleted
|
||||||
|
|
||||||
|
|
||||||
async def seed_surfsense_docs() -> tuple[int, int, int, int]:
|
async def seed_surfsense_docs() -> tuple[int, int, int, int]:
|
||||||
"""
|
"""
|
||||||
Seed Surfsense documentation into the database.
|
Seed Surfsense documentation into the database.
|
||||||
|
|
||||||
This function indexes all MDX files from the docs directory.
|
This function indexes all MDX files from the docs directory.
|
||||||
It handles creating, updating, and deleting docs based on content changes.
|
It handles creating, updating, and deleting docs based on content changes.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (created, updated, skipped, deleted) counts
|
Tuple of (created, updated, skipped, deleted) counts
|
||||||
Returns (0, 0, 0, 0) if an error occurs
|
Returns (0, 0, 0, 0) if an error occurs
|
||||||
"""
|
"""
|
||||||
logger.info("Starting Surfsense docs indexing...")
|
logger.info("Starting Surfsense docs indexing...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
created, updated, skipped, deleted = await index_surfsense_docs(session)
|
created, updated, skipped, deleted = await index_surfsense_docs(session)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Surfsense docs indexing complete: "
|
f"Surfsense docs indexing complete: "
|
||||||
f"created={created}, updated={updated}, skipped={skipped}, deleted={deleted}"
|
f"created={created}, updated={updated}, skipped={skipped}, deleted={deleted}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return created, updated, skipped, deleted
|
return created, updated, skipped, deleted
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to seed Surfsense docs: {e}", exc_info=True)
|
logger.error(f"Failed to seed Surfsense docs: {e}", exc_info=True)
|
||||||
return 0, 0, 0, 0
|
return 0, 0, 0, 0
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ def main():
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print(" Surfsense Documentation Seeding")
|
print(" Surfsense Documentation Seeding")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
created, updated, skipped, deleted = asyncio.run(seed_surfsense_docs())
|
created, updated, skipped, deleted = asyncio.run(seed_surfsense_docs())
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Results:")
|
print("Results:")
|
||||||
print(f" Created: {created}")
|
print(f" Created: {created}")
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,7 @@ function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-2xl font-bold">{t("welcome_title")}</h1>
|
<h1 className="text-2xl font-bold">{t("welcome_title")}</h1>
|
||||||
<p className="max-w-md text-muted-foreground">
|
<p className="max-w-md text-muted-foreground">{t("welcome_description")}</p>
|
||||||
{t("welcome_description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" onClick={onCreateClick} className="gap-2">
|
<Button size="lg" onClick={onCreateClick} className="gap-2">
|
||||||
|
|
@ -123,11 +121,7 @@ export default function DashboardPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
|
||||||
const {
|
const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom);
|
||||||
data: searchSpaces = [],
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
} = useAtomValue(searchSpacesAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,7 @@ import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { useApiKey } from "@/hooks/use-api-key";
|
import { useApiKey } from "@/hooks/use-api-key";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -249,16 +244,10 @@ function ApiKeyContent({ onMenuClick }: { onMenuClick: () => void }) {
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>{copied ? t("copied") : t("copy")}</TooltipContent>
|
||||||
{copied ? t("copied") : t("copy")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
import { AlertTriangle, Ban, Wrench } from "lucide-react";
|
import { AlertTriangle, Ban, Wrench } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import type { ConnectorStatus } from "../config/connector-status-config";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ConnectorStatus } from "../config/connector-status-config";
|
||||||
|
|
||||||
interface ConnectorStatusBadgeProps {
|
interface ConnectorStatusBadgeProps {
|
||||||
status: ConnectorStatus;
|
status: ConnectorStatus;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
|
||||||
import { useConnectorStatus } from "../hooks/use-connector-status";
|
import { useConnectorStatus } from "../hooks/use-connector-status";
|
||||||
|
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
||||||
|
|
||||||
interface ConnectorAccountsListViewProps {
|
interface ConnectorAccountsListViewProps {
|
||||||
connectorType: string;
|
connectorType: string;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ interface InlineCitationProps {
|
||||||
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
|
* Renders a clickable numbered badge that opens the SourceDetailPanel with document chunk details.
|
||||||
* Supports both regular knowledge base chunks and Surfsense documentation chunks.
|
* Supports both regular knowledge base chunks and Surfsense documentation chunks.
|
||||||
*/
|
*/
|
||||||
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, citationNumber, isDocsChunk = false }) => {
|
export const InlineCitation: FC<InlineCitationProps> = ({
|
||||||
|
chunkId,
|
||||||
|
citationNumber,
|
||||||
|
isDocsChunk = false,
|
||||||
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ export type {
|
||||||
NavItem,
|
NavItem,
|
||||||
NoteItem,
|
NoteItem,
|
||||||
PageUsage,
|
PageUsage,
|
||||||
|
SearchSpace,
|
||||||
SidebarSectionProps,
|
SidebarSectionProps,
|
||||||
User,
|
User,
|
||||||
SearchSpace,
|
|
||||||
} from "./types/layout.types";
|
} from "./types/layout.types";
|
||||||
export {
|
export {
|
||||||
AllSearchSpacesSheet,
|
AllSearchSpacesSheet,
|
||||||
|
|
@ -23,10 +23,10 @@ export {
|
||||||
NavSection,
|
NavSection,
|
||||||
NoteListItem,
|
NoteListItem,
|
||||||
PageUsageDisplay,
|
PageUsageDisplay,
|
||||||
|
SearchSpaceAvatar,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarCollapseButton,
|
SidebarCollapseButton,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarSection,
|
SidebarSection,
|
||||||
SidebarUserProfile,
|
SidebarUserProfile,
|
||||||
SearchSpaceAvatar,
|
|
||||||
} from "./ui";
|
} from "./ui";
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
import type { ChatItem, NavItem, NoteItem, SearchSpace } from "../types/layout.types";
|
import type { ChatItem, NavItem, NoteItem, SearchSpace } from "../types/layout.types";
|
||||||
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
||||||
import { LayoutShell } from "../ui/shell";
|
|
||||||
import { AllSearchSpacesSheet } from "../ui/sheets";
|
import { AllSearchSpacesSheet } from "../ui/sheets";
|
||||||
|
import { LayoutShell } from "../ui/shell";
|
||||||
import { AllChatsSidebar } from "../ui/sidebar/AllChatsSidebar";
|
import { AllChatsSidebar } from "../ui/sidebar/AllChatsSidebar";
|
||||||
import { AllNotesSidebar } from "../ui/sidebar/AllNotesSidebar";
|
import { AllNotesSidebar } from "../ui/sidebar/AllNotesSidebar";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("name_label")}</FormLabel>
|
<FormLabel>{t("name_label")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder={t("name_placeholder")} {...field} autoFocus />
|
||||||
placeholder={t("name_placeholder")}
|
|
||||||
{...field}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -163,4 +159,3 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export { CreateSearchSpaceDialog } from "./CreateSearchSpaceDialog";
|
export { CreateSearchSpaceDialog } from "./CreateSearchSpaceDialog";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,12 @@ function getInitials(name: string): string {
|
||||||
return name.slice(0, 2).toUpperCase();
|
return name.slice(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSpaceAvatar({ name, isActive, onClick, size = "md" }: SearchSpaceAvatarProps) {
|
export function SearchSpaceAvatar({
|
||||||
|
name,
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
size = "md",
|
||||||
|
}: SearchSpaceAvatarProps) {
|
||||||
const bgColor = stringToColor(name);
|
const bgColor = stringToColor(name);
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name);
|
||||||
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
export { CreateSearchSpaceDialog } from "./dialogs";
|
export { CreateSearchSpaceDialog } from "./dialogs";
|
||||||
export { Header } from "./header";
|
export { Header } from "./header";
|
||||||
export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
|
export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
|
||||||
export { LayoutShell } from "./shell";
|
|
||||||
export { AllSearchSpacesSheet } from "./sheets";
|
export { AllSearchSpacesSheet } from "./sheets";
|
||||||
|
export { LayoutShell } from "./shell";
|
||||||
export {
|
export {
|
||||||
ChatListItem,
|
ChatListItem,
|
||||||
MobileSidebar,
|
MobileSidebar,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Calendar, MoreHorizontal, Search, Settings, Share2, Trash2, UserCheck, Users } from "lucide-react";
|
import {
|
||||||
|
Calendar,
|
||||||
|
MoreHorizontal,
|
||||||
|
Search,
|
||||||
|
Settings,
|
||||||
|
Share2,
|
||||||
|
Trash2,
|
||||||
|
UserCheck,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
|
@ -112,9 +121,7 @@ export function AllSearchSpacesSheet({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium">{t("no_search_spaces")}</p>
|
<p className="font-medium">{t("no_search_spaces")}</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">{t("create_first_search_space")}</p>
|
||||||
{t("create_first_search_space")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{onCreateNew && (
|
{onCreateNew && (
|
||||||
<Button onClick={onCreateNew} className="mt-2">
|
<Button onClick={onCreateNew} className="mt-2">
|
||||||
|
|
@ -132,9 +139,7 @@ export function AllSearchSpacesSheet({
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<span className="font-medium leading-tight">
|
<span className="font-medium leading-tight">{space.name}</span>
|
||||||
{space.name}
|
|
||||||
</span>
|
|
||||||
{space.description && (
|
{space.description && (
|
||||||
<span className="text-sm text-muted-foreground line-clamp-2">
|
<span className="text-sm text-muted-foreground line-clamp-2">
|
||||||
{space.description}
|
{space.description}
|
||||||
|
|
@ -143,42 +148,42 @@ export function AllSearchSpacesSheet({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
{space.memberCount > 1 && (
|
{space.memberCount > 1 && (
|
||||||
<Badge variant="outline" className="shrink-0">
|
<Badge variant="outline" className="shrink-0">
|
||||||
<Share2 className="mr-1 h-3 w-3" />
|
<Share2 className="mr-1 h-3 w-3" />
|
||||||
{tCommon("shared")}
|
{tCommon("shared")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{space.isOwner && (
|
{space.isOwner && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 shrink-0"
|
className="h-6 w-6 shrink-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={(e) => handleSettings(e, space)}>
|
<DropdownMenuItem onClick={(e) => handleSettings(e, space)}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
{tCommon("settings")}
|
{tCommon("settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => handleDeleteClick(e, space)}
|
onClick={(e) => handleDeleteClick(e, space)}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
{tCommon("delete")}
|
{tCommon("delete")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export { AllSearchSpacesSheet } from "./AllSearchSpacesSheet";
|
export { AllSearchSpacesSheet } from "./AllSearchSpacesSheet";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import type {
|
||||||
NavItem,
|
NavItem,
|
||||||
NoteItem,
|
NoteItem,
|
||||||
PageUsage,
|
PageUsage,
|
||||||
User,
|
|
||||||
SearchSpace,
|
SearchSpace,
|
||||||
|
User,
|
||||||
} from "../../types/layout.types";
|
} from "../../types/layout.types";
|
||||||
import { Header } from "../header";
|
import { Header } from "../header";
|
||||||
import { IconRail } from "../icon-rail";
|
import { IconRail } from "../icon-rail";
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import type {
|
||||||
NavItem,
|
NavItem,
|
||||||
NoteItem,
|
NoteItem,
|
||||||
PageUsage,
|
PageUsage,
|
||||||
User,
|
|
||||||
SearchSpace,
|
SearchSpace,
|
||||||
|
User,
|
||||||
} from "../../types/layout.types";
|
} from "../../types/layout.types";
|
||||||
import { IconRail } from "../icon-rail";
|
import { IconRail } from "../icon-rail";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import type {
|
||||||
NavItem,
|
NavItem,
|
||||||
NoteItem,
|
NoteItem,
|
||||||
PageUsage,
|
PageUsage,
|
||||||
User,
|
|
||||||
SearchSpace,
|
SearchSpace,
|
||||||
|
User,
|
||||||
} from "../../types/layout.types";
|
} from "../../types/layout.types";
|
||||||
import { ChatListItem } from "./ChatListItem";
|
import { ChatListItem } from "./ChatListItem";
|
||||||
import { NavSection } from "./NavSection";
|
import { NavSection } from "./NavSection";
|
||||||
|
|
@ -289,7 +289,12 @@ export function Sidebar({
|
||||||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarUserProfile user={user} onUserSettings={onUserSettings} onLogout={onLogout} isCollapsed={isCollapsed} />
|
<SidebarUserProfile
|
||||||
|
user={user}
|
||||||
|
onUserSettings={onUserSettings}
|
||||||
|
onLogout={onLogout}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,9 @@ export function SidebarHeader({
|
||||||
isCollapsed ? "w-10" : "w-50"
|
isCollapsed ? "w-10" : "w-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate text-base">{searchSpace?.name ?? t("select_search_space")}</span>
|
<span className="truncate text-base">
|
||||||
|
{searchSpace?.name ?? t("select_search_space")}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
|
||||||
|
|
@ -161,52 +161,52 @@ class BaseApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious: Unknown
|
// biome-ignore lint/suspicious: Unknown
|
||||||
let data;
|
let data;
|
||||||
const responseType = mergedOptions.responseType;
|
const responseType = mergedOptions.responseType;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (responseType) {
|
switch (responseType) {
|
||||||
case ResponseType.JSON:
|
case ResponseType.JSON:
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
break;
|
break;
|
||||||
case ResponseType.TEXT:
|
case ResponseType.TEXT:
|
||||||
data = await response.text();
|
data = await response.text();
|
||||||
break;
|
break;
|
||||||
case ResponseType.BLOB:
|
case ResponseType.BLOB:
|
||||||
data = await response.blob();
|
data = await response.blob();
|
||||||
break;
|
break;
|
||||||
case ResponseType.ARRAY_BUFFER:
|
case ResponseType.ARRAY_BUFFER:
|
||||||
data = await response.arrayBuffer();
|
data = await response.arrayBuffer();
|
||||||
break;
|
break;
|
||||||
// Add more cases as needed
|
// Add more cases as needed
|
||||||
default:
|
default:
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse response as JSON:", error);
|
||||||
|
throw new AppError("Failed to parse response", response.status, response.statusText);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to parse response as JSON:", error);
|
|
||||||
throw new AppError("Failed to parse response", response.status, response.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response
|
// Validate response
|
||||||
if (responseType === ResponseType.JSON) {
|
if (responseType === ResponseType.JSON) {
|
||||||
if (!responseSchema) {
|
if (!responseSchema) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const parsedData = responseSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (!parsedData.success) {
|
||||||
|
/** The request was successful, but the response data does not match the expected schema.
|
||||||
|
* This is a client side error, and should be fixed by updating the responseSchema to keep things typed.
|
||||||
|
* This error should not be shown to the user , it is for dev only.
|
||||||
|
*/
|
||||||
|
console.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error));
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
const parsedData = responseSchema.safeParse(data);
|
|
||||||
|
|
||||||
if (!parsedData.success) {
|
|
||||||
/** The request was successful, but the response data does not match the expected schema.
|
|
||||||
* This is a client side error, and should be fixed by updating the responseSchema to keep things typed.
|
|
||||||
* This error should not be shown to the user , it is for dev only.
|
|
||||||
*/
|
|
||||||
console.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Request failed:", JSON.stringify(error));
|
console.error("Request failed:", JSON.stringify(error));
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue