mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
chore: ran linting
This commit is contained in:
parent
6ace8850bb
commit
746c730b2e
31 changed files with 801 additions and 660 deletions
|
|
@ -977,15 +977,19 @@ async def get_watched_folders(
|
|||
)
|
||||
|
||||
folders = (
|
||||
await session.execute(
|
||||
select(Folder).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
Folder.parent_id.is_(None),
|
||||
Folder.folder_metadata.isnot(None),
|
||||
Folder.folder_metadata["watched"].astext == "true",
|
||||
(
|
||||
await session.execute(
|
||||
select(Folder).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
Folder.parent_id.is_(None),
|
||||
Folder.folder_metadata.isnot(None),
|
||||
Folder.folder_metadata["watched"].astext == "true",
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return folders
|
||||
|
||||
|
|
@ -1265,15 +1269,21 @@ async def list_document_versions(
|
|||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
await check_permission(session, user, document.search_space_id, Permission.DOCUMENTS_READ.value)
|
||||
await check_permission(
|
||||
session, user, document.search_space_id, Permission.DOCUMENTS_READ.value
|
||||
)
|
||||
|
||||
versions = (
|
||||
await session.execute(
|
||||
select(DocumentVersion)
|
||||
.where(DocumentVersion.document_id == document_id)
|
||||
.order_by(DocumentVersion.version_number.desc())
|
||||
(
|
||||
await session.execute(
|
||||
select(DocumentVersion)
|
||||
.where(DocumentVersion.document_id == document_id)
|
||||
.order_by(DocumentVersion.version_number.desc())
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
@ -1300,7 +1310,9 @@ async def get_document_version(
|
|||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
await check_permission(session, user, document.search_space_id, Permission.DOCUMENTS_READ.value)
|
||||
await check_permission(
|
||||
session, user, document.search_space_id, Permission.DOCUMENTS_READ.value
|
||||
)
|
||||
|
||||
version = (
|
||||
await session.execute(
|
||||
|
|
@ -1331,14 +1343,14 @@ async def restore_document_version(
|
|||
):
|
||||
"""Restore a previous version: snapshot current state, then overwrite document content."""
|
||||
document = (
|
||||
await session.execute(
|
||||
select(Document).where(Document.id == document_id)
|
||||
)
|
||||
await session.execute(select(Document).where(Document.id == document_id))
|
||||
).scalar_one_or_none()
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
await check_permission(session, user, document.search_space_id, Permission.DOCUMENTS_UPDATE.value)
|
||||
await check_permission(
|
||||
session, user, document.search_space_id, Permission.DOCUMENTS_UPDATE.value
|
||||
)
|
||||
|
||||
version = (
|
||||
await session.execute(
|
||||
|
|
@ -1363,6 +1375,7 @@ async def restore_document_version(
|
|||
await session.commit()
|
||||
|
||||
from app.tasks.celery_tasks.document_reindex_tasks import reindex_document_task
|
||||
|
||||
reindex_document_task.delay(document_id, str(user.id))
|
||||
|
||||
return {
|
||||
|
|
@ -1430,9 +1443,7 @@ async def folder_index(
|
|||
root_folder_id = request.root_folder_id
|
||||
if root_folder_id:
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(Folder).where(Folder.id == root_folder_id)
|
||||
)
|
||||
await session.execute(select(Folder).where(Folder.id == root_folder_id))
|
||||
).scalar_one_or_none()
|
||||
if not existing:
|
||||
root_folder_id = None
|
||||
|
|
@ -1492,7 +1503,9 @@ async def folder_index_files(
|
|||
)
|
||||
|
||||
if not request.target_file_paths:
|
||||
raise HTTPException(status_code=400, detail="target_file_paths must not be empty")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="target_file_paths must not be empty"
|
||||
)
|
||||
|
||||
await check_permission(
|
||||
session,
|
||||
|
|
@ -1507,11 +1520,11 @@ async def folder_index_files(
|
|||
for fp in request.target_file_paths:
|
||||
try:
|
||||
Path(fp).relative_to(request.folder_path)
|
||||
except ValueError:
|
||||
except ValueError as err:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"target_file_path {fp} must be inside folder_path",
|
||||
)
|
||||
) from err
|
||||
|
||||
from app.tasks.celery_tasks.document_tasks import index_local_folder_task
|
||||
|
||||
|
|
@ -1530,5 +1543,3 @@ async def folder_index_files(
|
|||
"status": "processing",
|
||||
"file_count": len(request.target_file_paths),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,11 @@ async def get_editor_content(
|
|||
|
||||
if not chunk_contents:
|
||||
doc_status = document.status or {}
|
||||
state = doc_status.get("state", "ready") if isinstance(doc_status, dict) else "ready"
|
||||
state = (
|
||||
doc_status.get("state", "ready")
|
||||
if isinstance(doc_status, dict)
|
||||
else "ready"
|
||||
)
|
||||
if state in ("pending", "processing"):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ Non-OAuth connectors (BookStack, GitHub, etc.) are limited to one per search spa
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
"""Pydantic schemas for folder CRUD, move, and reorder operations."""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -36,7 +35,9 @@ class FolderRead(BaseModel):
|
|||
created_by_id: UUID | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
metadata: dict[str, Any] | None = Field(default=None, validation_alias="folder_metadata")
|
||||
metadata: dict[str, Any] | None = Field(
|
||||
default=None, validation_alias="folder_metadata"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Celery tasks for document processing."""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from uuid import UUID
|
||||
|
|
@ -1337,9 +1338,7 @@ async def _index_local_folder_async(
|
|||
)
|
||||
notification_id = notification.id
|
||||
_start_heartbeat(notification_id)
|
||||
heartbeat_task = asyncio.create_task(
|
||||
_run_heartbeat_loop(notification_id)
|
||||
)
|
||||
heartbeat_task = asyncio.create_task(_run_heartbeat_loop(notification_id))
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to create notification for local folder indexing",
|
||||
|
|
@ -1349,18 +1348,16 @@ async def _index_local_folder_async(
|
|||
async def _heartbeat_progress(completed_count: int) -> None:
|
||||
"""Refresh heartbeat and optionally update notification progress."""
|
||||
if notification:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
await NotificationService.document_processing.notify_processing_progress(
|
||||
session=session,
|
||||
notification=notification,
|
||||
stage="indexing",
|
||||
stage_message=f"Syncing files ({completed_count}/{file_count or '?'})",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
indexed, skipped_or_failed, _rfid, err = await index_local_folder(
|
||||
_indexed, _skipped_or_failed, _rfid, err = await index_local_folder(
|
||||
session=session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
|
|
@ -1371,7 +1368,9 @@ async def _index_local_folder_async(
|
|||
root_folder_id=root_folder_id,
|
||||
enable_summary=enable_summary,
|
||||
target_file_paths=target_file_paths,
|
||||
on_heartbeat_callback=_heartbeat_progress if (is_batch or is_full_scan) else None,
|
||||
on_heartbeat_callback=_heartbeat_progress
|
||||
if (is_batch or is_full_scan)
|
||||
else None,
|
||||
)
|
||||
|
||||
if notification:
|
||||
|
|
|
|||
|
|
@ -43,30 +43,110 @@ from .base import (
|
|||
logger,
|
||||
)
|
||||
|
||||
PLAINTEXT_EXTENSIONS = frozenset({
|
||||
".md", ".markdown", ".txt", ".text", ".csv", ".tsv",
|
||||
".json", ".jsonl", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
|
||||
".xml", ".html", ".htm", ".css", ".scss", ".less", ".sass",
|
||||
".py", ".pyw", ".pyi", ".pyx",
|
||||
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
|
||||
".java", ".kt", ".kts", ".scala", ".groovy",
|
||||
".c", ".h", ".cpp", ".cxx", ".cc", ".hpp", ".hxx",
|
||||
".cs", ".fs", ".fsx",
|
||||
".go", ".rs", ".rb", ".php", ".pl", ".pm", ".lua",
|
||||
".swift", ".m", ".mm",
|
||||
".r", ".R", ".jl",
|
||||
".sh", ".bash", ".zsh", ".fish", ".bat", ".cmd", ".ps1",
|
||||
".sql", ".graphql", ".gql",
|
||||
".env", ".gitignore", ".dockerignore", ".editorconfig",
|
||||
".makefile", ".cmake",
|
||||
".log", ".rst", ".tex", ".bib", ".org", ".adoc", ".asciidoc",
|
||||
".vue", ".svelte", ".astro",
|
||||
".tf", ".hcl", ".proto",
|
||||
})
|
||||
PLAINTEXT_EXTENSIONS = frozenset(
|
||||
{
|
||||
".md",
|
||||
".markdown",
|
||||
".txt",
|
||||
".text",
|
||||
".csv",
|
||||
".tsv",
|
||||
".json",
|
||||
".jsonl",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".ini",
|
||||
".cfg",
|
||||
".conf",
|
||||
".xml",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".less",
|
||||
".sass",
|
||||
".py",
|
||||
".pyw",
|
||||
".pyi",
|
||||
".pyx",
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".java",
|
||||
".kt",
|
||||
".kts",
|
||||
".scala",
|
||||
".groovy",
|
||||
".c",
|
||||
".h",
|
||||
".cpp",
|
||||
".cxx",
|
||||
".cc",
|
||||
".hpp",
|
||||
".hxx",
|
||||
".cs",
|
||||
".fs",
|
||||
".fsx",
|
||||
".go",
|
||||
".rs",
|
||||
".rb",
|
||||
".php",
|
||||
".pl",
|
||||
".pm",
|
||||
".lua",
|
||||
".swift",
|
||||
".m",
|
||||
".mm",
|
||||
".r",
|
||||
".R",
|
||||
".jl",
|
||||
".sh",
|
||||
".bash",
|
||||
".zsh",
|
||||
".fish",
|
||||
".bat",
|
||||
".cmd",
|
||||
".ps1",
|
||||
".sql",
|
||||
".graphql",
|
||||
".gql",
|
||||
".env",
|
||||
".gitignore",
|
||||
".dockerignore",
|
||||
".editorconfig",
|
||||
".makefile",
|
||||
".cmake",
|
||||
".log",
|
||||
".rst",
|
||||
".tex",
|
||||
".bib",
|
||||
".org",
|
||||
".adoc",
|
||||
".asciidoc",
|
||||
".vue",
|
||||
".svelte",
|
||||
".astro",
|
||||
".tf",
|
||||
".hcl",
|
||||
".proto",
|
||||
}
|
||||
)
|
||||
|
||||
AUDIO_EXTENSIONS = frozenset({
|
||||
".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm",
|
||||
})
|
||||
AUDIO_EXTENSIONS = frozenset(
|
||||
{
|
||||
".mp3",
|
||||
".mp4",
|
||||
".mpeg",
|
||||
".mpga",
|
||||
".m4a",
|
||||
".wav",
|
||||
".webm",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_plaintext_file(filename: str) -> bool:
|
||||
|
|
@ -81,6 +161,7 @@ def _needs_etl(filename: str) -> bool:
|
|||
"""File is not plaintext and not audio — requires ETL service to parse."""
|
||||
return not _is_plaintext_file(filename) and not _is_audio_file(filename)
|
||||
|
||||
|
||||
HeartbeatCallbackType = Callable[[int], Awaitable[None]]
|
||||
|
||||
DEFAULT_EXCLUDE_PATTERNS = [
|
||||
|
|
@ -121,9 +202,7 @@ def scan_folder(
|
|||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
rel_dir = Path(dirpath).relative_to(root)
|
||||
|
||||
dirnames[:] = [
|
||||
d for d in dirnames if d not in exclude_patterns
|
||||
]
|
||||
dirnames[:] = [d for d in dirnames if d not in exclude_patterns]
|
||||
|
||||
if any(part in exclude_patterns for part in rel_dir.parts):
|
||||
continue
|
||||
|
|
@ -134,9 +213,11 @@ def scan_folder(
|
|||
|
||||
full = Path(dirpath) / fname
|
||||
|
||||
if file_extensions is not None:
|
||||
if full.suffix.lower() not in file_extensions:
|
||||
continue
|
||||
if (
|
||||
file_extensions is not None
|
||||
and full.suffix.lower() not in file_extensions
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
stat = full.stat()
|
||||
|
|
@ -209,11 +290,14 @@ def _content_hash(content: str, search_space_id: int) -> str:
|
|||
pipeline so that dedup checks are consistent.
|
||||
"""
|
||||
import hashlib
|
||||
return hashlib.sha256(f"{search_space_id}:{content}".encode("utf-8")).hexdigest()
|
||||
|
||||
return hashlib.sha256(f"{search_space_id}:{content}".encode()).hexdigest()
|
||||
|
||||
|
||||
async def _compute_file_content_hash(
|
||||
file_path: str, filename: str, search_space_id: int,
|
||||
file_path: str,
|
||||
filename: str,
|
||||
search_space_id: int,
|
||||
) -> tuple[str, str]:
|
||||
"""Read a file (via ETL if needed) and compute its content hash.
|
||||
|
||||
|
|
@ -257,9 +341,7 @@ async def _mirror_folder_structure(
|
|||
|
||||
if root_folder_id:
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(Folder).where(Folder.id == root_folder_id)
|
||||
)
|
||||
await session.execute(select(Folder).where(Folder.id == root_folder_id))
|
||||
).scalar_one_or_none()
|
||||
if existing:
|
||||
mapping[""] = existing.id
|
||||
|
|
@ -412,13 +494,17 @@ async def _cleanup_empty_folders(
|
|||
id_to_rel: dict[int, str] = {fid: rel for rel, fid in folder_mapping.items() if rel}
|
||||
|
||||
all_folders = (
|
||||
await session.execute(
|
||||
select(Folder).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
Folder.id != root_folder_id,
|
||||
(
|
||||
await session.execute(
|
||||
select(Folder).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
Folder.id != root_folder_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
candidates: list[Folder] = []
|
||||
for folder in all_folders:
|
||||
|
|
@ -520,7 +606,9 @@ async def index_local_folder(
|
|||
metadata={
|
||||
"folder_path": folder_path,
|
||||
"user_id": str(user_id),
|
||||
"target_file_paths_count": len(target_file_paths) if target_file_paths else None,
|
||||
"target_file_paths_count": len(target_file_paths)
|
||||
if target_file_paths
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -532,7 +620,12 @@ async def index_local_folder(
|
|||
"Folder not found",
|
||||
{},
|
||||
)
|
||||
return 0, 0, root_folder_id, f"Folder path missing or does not exist: {folder_path}"
|
||||
return (
|
||||
0,
|
||||
0,
|
||||
root_folder_id,
|
||||
f"Folder path missing or does not exist: {folder_path}",
|
||||
)
|
||||
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = DEFAULT_EXCLUDE_PATTERNS
|
||||
|
|
@ -639,7 +732,9 @@ async def index_local_folder(
|
|||
)
|
||||
|
||||
if existing_document:
|
||||
stored_mtime = (existing_document.document_metadata or {}).get("mtime")
|
||||
stored_mtime = (existing_document.document_metadata or {}).get(
|
||||
"mtime"
|
||||
)
|
||||
current_mtime = file_info["modified_at"].timestamp()
|
||||
|
||||
if stored_mtime and abs(current_mtime - stored_mtime) < 1.0:
|
||||
|
|
@ -709,23 +804,31 @@ async def index_local_folder(
|
|||
# ================================================================
|
||||
all_root_folder_ids = set(folder_mapping.values())
|
||||
all_db_folders = (
|
||||
await session.execute(
|
||||
select(Folder.id).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
(
|
||||
await session.execute(
|
||||
select(Folder.id).where(
|
||||
Folder.search_space_id == search_space_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
all_root_folder_ids.update(all_db_folders)
|
||||
|
||||
all_folder_docs = (
|
||||
await session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == search_space_id,
|
||||
Document.folder_id.in_(list(all_root_folder_ids)),
|
||||
(
|
||||
await session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == search_space_id,
|
||||
Document.folder_id.in_(list(all_root_folder_ids)),
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
for doc in all_folder_docs:
|
||||
if doc.unique_identifier_hash not in seen_unique_hashes:
|
||||
|
|
@ -742,9 +845,7 @@ async def index_local_folder(
|
|||
)
|
||||
|
||||
pipeline = IndexingPipelineService(session)
|
||||
doc_map = {
|
||||
compute_unique_identifier_hash(cd): cd for cd in connector_docs
|
||||
}
|
||||
doc_map = {compute_unique_identifier_hash(cd): cd for cd in connector_docs}
|
||||
documents = await pipeline.prepare_for_indexing(connector_docs)
|
||||
|
||||
# Assign folder_id immediately so docs appear in the correct
|
||||
|
|
@ -1033,7 +1134,9 @@ async def _index_single_file(
|
|||
db_doc.document_metadata = doc_meta
|
||||
await session.commit()
|
||||
|
||||
indexed = 1 if DocumentStatus.is_state(db_doc.status, DocumentStatus.READY) else 0
|
||||
indexed = (
|
||||
1 if DocumentStatus.is_state(db_doc.status, DocumentStatus.READY) else 0
|
||||
)
|
||||
failed_msg = None if indexed else "Indexing failed"
|
||||
|
||||
if indexed:
|
||||
|
|
|
|||
|
|
@ -83,9 +83,9 @@ async def create_version_snapshot(
|
|||
# Cleanup: cap at MAX_VERSIONS_PER_DOCUMENT
|
||||
count = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(DocumentVersion).where(
|
||||
DocumentVersion.document_id == document.id
|
||||
)
|
||||
select(func.count())
|
||||
.select_from(DocumentVersion)
|
||||
.where(DocumentVersion.document_id == document.id)
|
||||
)
|
||||
).scalar_one()
|
||||
|
||||
|
|
|
|||
|
|
@ -166,5 +166,3 @@ def make_connector_document(db_connector, db_user):
|
|||
return ConnectorDocument(**defaults)
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ from app.db import (
|
|||
pytestmark = pytest.mark.integration
|
||||
|
||||
UNIFIED_FIXTURES = (
|
||||
"patched_summarize", "patched_embed_texts", "patched_chunk_text",
|
||||
"patched_summarize",
|
||||
"patched_embed_texts",
|
||||
"patched_chunk_text",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -37,6 +39,7 @@ class _FakeSessionMaker:
|
|||
@asynccontextmanager
|
||||
async def _ctx():
|
||||
yield self._session
|
||||
|
||||
return _ctx()
|
||||
|
||||
|
||||
|
|
@ -59,7 +62,6 @@ def patched_batch_sessions(monkeypatch, db_session):
|
|||
|
||||
|
||||
class TestFullIndexer:
|
||||
|
||||
@pytest.mark.usefixtures(*UNIFIED_FIXTURES)
|
||||
async def test_i1_new_file_indexed(
|
||||
self,
|
||||
|
|
@ -73,7 +75,7 @@ class TestFullIndexer:
|
|||
|
||||
(tmp_path / "note.md").write_text("# Hello World\n\nContent here.")
|
||||
|
||||
count, skipped, root_folder_id, err = await index_local_folder(
|
||||
count, _skipped, _root_folder_id, err = await index_local_folder(
|
||||
session=db_session,
|
||||
search_space_id=db_search_space.id,
|
||||
user_id=str(db_user.id),
|
||||
|
|
@ -85,13 +87,17 @@ class TestFullIndexer:
|
|||
assert count == 1
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(docs) == 1
|
||||
assert docs[0].document_type == DocumentType.LOCAL_FOLDER_FILE
|
||||
assert DocumentStatus.is_state(docs[0].status, DocumentStatus.READY)
|
||||
|
|
@ -130,7 +136,9 @@ class TestFullIndexer:
|
|||
|
||||
total = (
|
||||
await db_session.execute(
|
||||
select(func.count()).select_from(Document).where(
|
||||
select(func.count())
|
||||
.select_from(Document)
|
||||
.where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
|
|
@ -174,13 +182,19 @@ class TestFullIndexer:
|
|||
assert count == 1
|
||||
|
||||
versions = (
|
||||
await db_session.execute(
|
||||
select(DocumentVersion).join(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(DocumentVersion)
|
||||
.join(Document)
|
||||
.where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(versions) >= 1
|
||||
|
||||
@pytest.mark.usefixtures(*UNIFIED_FIXTURES)
|
||||
|
|
@ -207,7 +221,9 @@ class TestFullIndexer:
|
|||
|
||||
docs_before = (
|
||||
await db_session.execute(
|
||||
select(func.count()).select_from(Document).where(
|
||||
select(func.count())
|
||||
.select_from(Document)
|
||||
.where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
|
|
@ -228,7 +244,9 @@ class TestFullIndexer:
|
|||
|
||||
docs_after = (
|
||||
await db_session.execute(
|
||||
select(func.count()).select_from(Document).where(
|
||||
select(func.count())
|
||||
.select_from(Document)
|
||||
.where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
|
|
@ -262,13 +280,17 @@ class TestFullIndexer:
|
|||
assert count == 1
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(docs) == 1
|
||||
assert docs[0].title == "b.md"
|
||||
|
||||
|
|
@ -279,7 +301,6 @@ class TestFullIndexer:
|
|||
|
||||
|
||||
class TestFolderMirroring:
|
||||
|
||||
@pytest.mark.usefixtures(*UNIFIED_FIXTURES)
|
||||
async def test_f1_root_folder_created(
|
||||
self,
|
||||
|
|
@ -335,10 +356,14 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
folders = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
folder_names = {f.name for f in folders}
|
||||
assert "notes" in folder_names
|
||||
|
|
@ -376,10 +401,14 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
folders_before = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
ids_before = {f.id for f in folders_before}
|
||||
|
||||
await index_local_folder(
|
||||
|
|
@ -392,10 +421,14 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
folders_after = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.search_space_id == db_search_space.id)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
ids_after = {f.id for f in folders_after}
|
||||
|
||||
assert ids_before == ids_after
|
||||
|
|
@ -425,21 +458,23 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
today_doc = next(d for d in docs if d.title == "today.md")
|
||||
root_doc = next(d for d in docs if d.title == "root.md")
|
||||
|
||||
daily_folder = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "daily")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "daily"))
|
||||
).scalar_one()
|
||||
|
||||
assert today_doc.folder_id == daily_folder.id
|
||||
|
|
@ -455,9 +490,10 @@ class TestFolderMirroring:
|
|||
tmp_path: Path,
|
||||
):
|
||||
"""F5: Deleted dir's empty Folder row is cleaned up on re-sync."""
|
||||
from app.tasks.connector_indexers.local_folder_indexer import index_local_folder
|
||||
import shutil
|
||||
|
||||
from app.tasks.connector_indexers.local_folder_indexer import index_local_folder
|
||||
|
||||
daily = tmp_path / "notes" / "daily"
|
||||
daily.mkdir(parents=True)
|
||||
weekly = tmp_path / "notes" / "weekly"
|
||||
|
|
@ -474,9 +510,7 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
weekly_folder = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "weekly")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "weekly"))
|
||||
).scalar_one_or_none()
|
||||
assert weekly_folder is not None
|
||||
|
||||
|
|
@ -492,16 +526,12 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
weekly_after = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "weekly")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "weekly"))
|
||||
).scalar_one_or_none()
|
||||
assert weekly_after is None
|
||||
|
||||
daily_after = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "daily")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "daily"))
|
||||
).scalar_one_or_none()
|
||||
assert daily_after is not None
|
||||
|
||||
|
|
@ -551,18 +581,14 @@ class TestFolderMirroring:
|
|||
).scalar_one()
|
||||
|
||||
daily_folder = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "daily")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "daily"))
|
||||
).scalar_one()
|
||||
|
||||
assert doc.folder_id == daily_folder.id
|
||||
assert daily_folder.parent_id is not None
|
||||
|
||||
notes_folder = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "notes")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "notes"))
|
||||
).scalar_one()
|
||||
assert daily_folder.parent_id == notes_folder.id
|
||||
assert notes_folder.parent_id == root_folder_id
|
||||
|
|
@ -592,9 +618,7 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
eph_folder = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "ephemeral")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "ephemeral"))
|
||||
).scalar_one_or_none()
|
||||
assert eph_folder is not None
|
||||
|
||||
|
|
@ -612,16 +636,12 @@ class TestFolderMirroring:
|
|||
)
|
||||
|
||||
eph_after = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "ephemeral")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "ephemeral"))
|
||||
).scalar_one_or_none()
|
||||
assert eph_after is None
|
||||
|
||||
notes_after = (
|
||||
await db_session.execute(
|
||||
select(Folder).where(Folder.name == "notes")
|
||||
)
|
||||
await db_session.execute(select(Folder).where(Folder.name == "notes"))
|
||||
).scalar_one_or_none()
|
||||
assert notes_after is None
|
||||
|
||||
|
|
@ -632,7 +652,6 @@ class TestFolderMirroring:
|
|||
|
||||
|
||||
class TestBatchMode:
|
||||
|
||||
@pytest.mark.usefixtures(*UNIFIED_FIXTURES)
|
||||
async def test_b1_batch_indexes_multiple_files(
|
||||
self,
|
||||
|
|
@ -649,7 +668,7 @@ class TestBatchMode:
|
|||
(tmp_path / "b.md").write_text("File B content")
|
||||
(tmp_path / "c.md").write_text("File C content")
|
||||
|
||||
count, failed, root_folder_id, err = await index_local_folder(
|
||||
count, failed, _root_folder_id, err = await index_local_folder(
|
||||
session=db_session,
|
||||
search_space_id=db_search_space.id,
|
||||
user_id=str(db_user.id),
|
||||
|
|
@ -667,13 +686,17 @@ class TestBatchMode:
|
|||
assert err is None
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(docs) == 3
|
||||
assert {d.title for d in docs} == {"a.md", "b.md", "c.md"}
|
||||
assert all(
|
||||
|
|
@ -714,13 +737,17 @@ class TestBatchMode:
|
|||
assert err is not None
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(docs) == 2
|
||||
assert {d.title for d in docs} == {"good1.md", "good2.md"}
|
||||
|
||||
|
|
@ -731,7 +758,6 @@ class TestBatchMode:
|
|||
|
||||
|
||||
class TestPipelineIntegration:
|
||||
|
||||
@pytest.mark.usefixtures(*UNIFIED_FIXTURES)
|
||||
async def test_p1_local_folder_file_through_pipeline(
|
||||
self,
|
||||
|
|
@ -742,7 +768,9 @@ class TestPipelineIntegration:
|
|||
):
|
||||
"""P1: LOCAL_FOLDER_FILE ConnectorDocument through prepare+index to READY."""
|
||||
from app.indexing_pipeline.connector_document import ConnectorDocument
|
||||
from app.indexing_pipeline.indexing_pipeline_service import IndexingPipelineService
|
||||
from app.indexing_pipeline.indexing_pipeline_service import (
|
||||
IndexingPipelineService,
|
||||
)
|
||||
|
||||
doc = ConnectorDocument(
|
||||
title="Test Local File",
|
||||
|
|
@ -763,12 +791,16 @@ class TestPipelineIntegration:
|
|||
assert result is not None
|
||||
|
||||
docs = (
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
(
|
||||
await db_session.execute(
|
||||
select(Document).where(
|
||||
Document.document_type == DocumentType.LOCAL_FOLDER_FILE,
|
||||
Document.search_space_id == db_search_space.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert len(docs) == 1
|
||||
assert DocumentStatus.is_state(docs[0].status, DocumentStatus.READY)
|
||||
|
|
|
|||
|
|
@ -34,14 +34,16 @@ async def db_document(
|
|||
|
||||
async def _version_count(session: AsyncSession, document_id: int) -> int:
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(DocumentVersion).where(
|
||||
DocumentVersion.document_id == document_id
|
||||
)
|
||||
select(func.count())
|
||||
.select_from(DocumentVersion)
|
||||
.where(DocumentVersion.document_id == document_id)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def _get_versions(session: AsyncSession, document_id: int) -> list[DocumentVersion]:
|
||||
async def _get_versions(
|
||||
session: AsyncSession, document_id: int
|
||||
) -> list[DocumentVersion]:
|
||||
result = await session.execute(
|
||||
select(DocumentVersion)
|
||||
.where(DocumentVersion.document_id == document_id)
|
||||
|
|
@ -74,18 +76,14 @@ class TestCreateVersionSnapshot:
|
|||
from app.utils.document_versioning import create_version_snapshot
|
||||
|
||||
t0 = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda: t0
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda: t0)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
||||
# Simulate content change and time passing
|
||||
db_document.source_markdown = "# Test\n\nUpdated content."
|
||||
db_document.content_hash = "def456"
|
||||
t1 = t0 + timedelta(minutes=31)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda: t1
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda: t1)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
||||
versions = await _get_versions(db_session, db_document.id)
|
||||
|
|
@ -101,9 +99,7 @@ class TestCreateVersionSnapshot:
|
|||
from app.utils.document_versioning import create_version_snapshot
|
||||
|
||||
t0 = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda: t0
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda: t0)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
count_after_first = await _version_count(db_session, db_document.id)
|
||||
assert count_after_first == 1
|
||||
|
|
@ -112,9 +108,7 @@ class TestCreateVersionSnapshot:
|
|||
db_document.source_markdown = "# Test\n\nQuick edit."
|
||||
db_document.content_hash = "quick123"
|
||||
t1 = t0 + timedelta(minutes=10)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda: t1
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda: t1)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
||||
count_after_second = await _version_count(db_session, db_document.id)
|
||||
|
|
@ -134,22 +128,15 @@ class TestCreateVersionSnapshot:
|
|||
|
||||
# Create 5 versions spread across time: 3 older than 90 days, 2 recent
|
||||
for i in range(5):
|
||||
db_document.source_markdown = f"Content v{i+1}"
|
||||
db_document.content_hash = f"hash_{i+1}"
|
||||
if i < 3:
|
||||
t = base + timedelta(days=i) # old
|
||||
else:
|
||||
t = base + timedelta(days=100 + i) # recent
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda _t=t: _t
|
||||
)
|
||||
db_document.source_markdown = f"Content v{i + 1}"
|
||||
db_document.content_hash = f"hash_{i + 1}"
|
||||
t = base + timedelta(days=i) if i < 3 else base + timedelta(days=100 + i)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda _t=t: _t)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
||||
# Now trigger cleanup from a "current" time that makes the first 3 versions > 90 days old
|
||||
now = base + timedelta(days=200)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda: now
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda: now)
|
||||
db_document.source_markdown = "Content v6"
|
||||
db_document.content_hash = "hash_6"
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
|
@ -160,9 +147,7 @@ class TestCreateVersionSnapshot:
|
|||
age = now - v.created_at.replace(tzinfo=UTC)
|
||||
assert age <= timedelta(days=90), f"Version {v.version_number} is too old"
|
||||
|
||||
async def test_v5_cap_at_20_versions(
|
||||
self, db_session, db_document, monkeypatch
|
||||
):
|
||||
async def test_v5_cap_at_20_versions(self, db_session, db_document, monkeypatch):
|
||||
"""V5: More than 20 versions triggers cap — oldest gets deleted."""
|
||||
from app.utils.document_versioning import create_version_snapshot
|
||||
|
||||
|
|
@ -170,12 +155,10 @@ class TestCreateVersionSnapshot:
|
|||
|
||||
# Create 21 versions (all within 90 days, each 31 min apart)
|
||||
for i in range(21):
|
||||
db_document.source_markdown = f"Content v{i+1}"
|
||||
db_document.content_hash = f"hash_{i+1}"
|
||||
db_document.source_markdown = f"Content v{i + 1}"
|
||||
db_document.content_hash = f"hash_{i + 1}"
|
||||
t = base + timedelta(minutes=31 * i)
|
||||
monkeypatch.setattr(
|
||||
"app.utils.document_versioning._now", lambda _t=t: _t
|
||||
)
|
||||
monkeypatch.setattr("app.utils.document_versioning._now", lambda _t=t: _t)
|
||||
await create_version_snapshot(db_session, db_document)
|
||||
|
||||
versions = await _get_versions(db_session, db_document.id)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,7 @@ class TestScanFolder:
|
|||
git.mkdir()
|
||||
(git / "config").write_text("gitconfig")
|
||||
|
||||
results = scan_folder(
|
||||
str(tmp_path), exclude_patterns=["node_modules", ".git"]
|
||||
)
|
||||
results = scan_folder(str(tmp_path), exclude_patterns=["node_modules", ".git"])
|
||||
names = {r["relative_path"] for r in results}
|
||||
|
||||
assert "good.md" in names
|
||||
|
|
|
|||
|
|
@ -160,11 +160,11 @@ export function LocalLoginForm() {
|
|||
placeholder="you@example.com"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isLoggingIn}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -181,11 +181,11 @@ export function LocalLoginForm() {
|
|||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isLoggingIn}
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -229,72 +229,66 @@ export default function RegisterPage() {
|
|||
</AnimatePresence>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("password")}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
{t("password")}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("confirm_password")}
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Confirm your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
{t("confirm_password")}
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Confirm your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-1 bg-background text-foreground transition-all ${
|
||||
error.title
|
||||
? "border-destructive focus:border-destructive focus:ring-destructive/40"
|
||||
: "border-border focus:border-primary focus:ring-primary/40"
|
||||
}`}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -312,12 +306,9 @@ export default function RegisterPage() {
|
|||
</form>
|
||||
|
||||
<div className="mt-4 text-center text-sm">
|
||||
<p className="text-muted-foreground">
|
||||
{t("already_have_account")}{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-medium text-primary hover:text-primary/90"
|
||||
>
|
||||
<p className="text-muted-foreground">
|
||||
{t("already_have_account")}{" "}
|
||||
<Link href="/login" className="font-medium text-primary hover:text-primary/90">
|
||||
{t("sign_in")}
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -214,17 +214,17 @@ export function DocumentsFilters({
|
|||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
data-joyride="upload-button"
|
||||
onClick={openUploadDialog}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 shrink-0 gap-1.5 bg-white text-gray-700 border-white hover:bg-gray-50 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100"
|
||||
>
|
||||
<Upload size={14} />
|
||||
<span>Upload</span>
|
||||
</Button>
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
data-joyride="upload-button"
|
||||
onClick={openUploadDialog}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 shrink-0 gap-1.5 bg-white text-gray-700 border-white hover:bg-gray-50 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100"
|
||||
>
|
||||
<Upload size={14} />
|
||||
<span>Upload</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { AlertTriangle, Globe, Lock, PenLine, Sparkles, Trash2 } from "lucide-react";
|
||||
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -24,6 +23,7 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { PromptRead } from "@/contracts/types/prompts.types";
|
||||
|
|
@ -145,9 +145,8 @@ export function PromptsContent() {
|
|||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create prompt templates triggered with{" "}
|
||||
<ShortcutKbd keys={["/"]} className="ml-0" /> in the
|
||||
chat composer.
|
||||
Create prompt templates triggered with <ShortcutKbd keys={["/"]} className="ml-0" /> in
|
||||
the chat composer.
|
||||
</p>
|
||||
{!showForm && (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -374,7 +374,10 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
<div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16">
|
||||
{/* LLM Configuration Warning */}
|
||||
{!llmConfigLoading && !hasDocumentSummaryLLM && (
|
||||
<Alert variant="destructive" className="mb-6 bg-muted/50 rounded-xl border-destructive/30">
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="mb-6 bg-muted/50 rounded-xl border-destructive/30"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>LLM Configuration Required</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
|
|
|
|||
|
|
@ -294,36 +294,36 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
)}
|
||||
|
||||
{(() => {
|
||||
const isGoogleDrive = connector.connector_type === "GOOGLE_DRIVE_CONNECTOR";
|
||||
const isComposioGoogleDrive =
|
||||
connector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
|
||||
const requiresFolderSelection = isGoogleDrive || isComposioGoogleDrive;
|
||||
const selectedFolders =
|
||||
(connector.config?.selected_folders as
|
||||
| Array<{ id: string; name: string }>
|
||||
| undefined) || [];
|
||||
const selectedFiles =
|
||||
(connector.config?.selected_files as
|
||||
| Array<{ id: string; name: string }>
|
||||
| undefined) || [];
|
||||
const hasItemsSelected = selectedFolders.length > 0 || selectedFiles.length > 0;
|
||||
const isDisabled = requiresFolderSelection && !hasItemsSelected;
|
||||
const isGoogleDrive = connector.connector_type === "GOOGLE_DRIVE_CONNECTOR";
|
||||
const isComposioGoogleDrive =
|
||||
connector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
|
||||
const requiresFolderSelection = isGoogleDrive || isComposioGoogleDrive;
|
||||
const selectedFolders =
|
||||
(connector.config?.selected_folders as
|
||||
| Array<{ id: string; name: string }>
|
||||
| undefined) || [];
|
||||
const selectedFiles =
|
||||
(connector.config?.selected_files as
|
||||
| Array<{ id: string; name: string }>
|
||||
| undefined) || [];
|
||||
const hasItemsSelected = selectedFolders.length > 0 || selectedFiles.length > 0;
|
||||
const isDisabled = requiresFolderSelection && !hasItemsSelected;
|
||||
|
||||
return (
|
||||
<PeriodicSyncConfig
|
||||
enabled={periodicEnabled}
|
||||
frequencyMinutes={frequencyMinutes}
|
||||
onEnabledChange={onPeriodicEnabledChange}
|
||||
onFrequencyChange={onFrequencyChange}
|
||||
disabled={isDisabled}
|
||||
disabledMessage={
|
||||
isDisabled
|
||||
? "Select at least one folder or file above to enable periodic sync"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<PeriodicSyncConfig
|
||||
enabled={periodicEnabled}
|
||||
frequencyMinutes={frequencyMinutes}
|
||||
onEnabledChange={onPeriodicEnabledChange}
|
||||
onFrequencyChange={onFrequencyChange}
|
||||
disabled={isDisabled}
|
||||
disabledMessage={
|
||||
isDisabled
|
||||
? "Select at least one folder or file above to enable periodic sync"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,10 @@ const DocumentUploadPopupContent: FC<{
|
|||
|
||||
<div className="px-4 sm:px-6 pb-4 sm:pb-6">
|
||||
{!isLoading && !hasDocumentSummaryLLM ? (
|
||||
<Alert variant="destructive" className="mb-4 bg-muted/50 rounded-xl border-destructive/30">
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="mb-4 bg-muted/50 rounded-xl border-destructive/30"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>LLM Configuration Required</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk =
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="ml-0.5 inline-flex h-5 min-w-5 cursor-pointer items-center justify-center rounded-md bg-muted/60 px-1.5 text-[11px] font-medium text-muted-foreground align-baseline shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none" title={`View source chunk #${chunkId}`}
|
||||
className="ml-0.5 inline-flex h-5 min-w-5 cursor-pointer items-center justify-center rounded-md bg-muted/60 px-1.5 text-[11px] font-medium text-muted-foreground align-baseline shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
|
||||
title={`View source chunk #${chunkId}`}
|
||||
>
|
||||
{chunkId}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ import { Spinner } from "@/components/ui/spinner";
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isVersionableType } from "./version-history";
|
||||
import { DND_TYPES } from "./FolderNode";
|
||||
import { isVersionableType } from "./version-history";
|
||||
|
||||
const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
|
||||
|
||||
|
|
@ -199,7 +199,10 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
|
||||
<span className="flex-1 min-w-0 truncate">{doc.title}</span>
|
||||
|
||||
{getDocumentTypeIcon(doc.document_type as DocumentTypeEnum, "h-3.5 w-3.5 text-muted-foreground") && (
|
||||
{getDocumentTypeIcon(
|
||||
doc.document_type as DocumentTypeEnum,
|
||||
"h-3.5 w-3.5 text-muted-foreground"
|
||||
) && (
|
||||
<span className="shrink-0">
|
||||
{getDocumentTypeIcon(
|
||||
doc.document_type as DocumentTypeEnum,
|
||||
|
|
@ -251,10 +254,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
</DropdownMenuSub>
|
||||
)}
|
||||
{onVersionHistory && isVersionableType(doc.document_type) && (
|
||||
<DropdownMenuItem
|
||||
disabled={isProcessing}
|
||||
onClick={() => onVersionHistory(doc)}
|
||||
>
|
||||
<DropdownMenuItem disabled={isProcessing} onClick={() => onVersionHistory(doc)}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Versions
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -300,10 +300,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
</ContextMenuSub>
|
||||
)}
|
||||
{onVersionHistory && isVersionableType(doc.document_type) && (
|
||||
<ContextMenuItem
|
||||
disabled={isProcessing}
|
||||
onClick={() => onVersionHistory(doc)}
|
||||
>
|
||||
<ContextMenuItem disabled={isProcessing} onClick={() => onVersionHistory(doc)}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Versions
|
||||
</ContextMenuItem>
|
||||
|
|
|
|||
|
|
@ -256,15 +256,15 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
isOver && !canDrop && "cursor-not-allowed"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 16 + 4}px` }}
|
||||
onClick={() => {
|
||||
onToggleExpand(folder.id);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onClick={() => {
|
||||
onToggleExpand(folder.id);
|
||||
}
|
||||
}}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onToggleExpand(folder.id);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startRename();
|
||||
|
|
@ -306,7 +306,11 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
) : (
|
||||
<Checkbox
|
||||
checked={
|
||||
selectionState === "all" ? true : selectionState === "some" ? "indeterminate" : false
|
||||
selectionState === "all"
|
||||
? true
|
||||
: selectionState === "some"
|
||||
? "indeterminate"
|
||||
: false
|
||||
}
|
||||
onCheckedChange={handleCheckChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
@ -350,107 +354,107 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRescan(folder);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStopWatching(folder);
|
||||
}}
|
||||
>
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRescan(folder);
|
||||
onCreateSubfolder(folder.id);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStopWatching(folder);
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCreateSubfolder(folder.id);
|
||||
}}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMove(folder);
|
||||
}}
|
||||
>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMove(folder);
|
||||
}}
|
||||
>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
{!isRenaming && contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<ContextMenuItem onClick={() => onRescan(folder)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
{!isRenaming && contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
{isWatched && onRescan && (
|
||||
<ContextMenuItem onClick={() => onRescan(folder)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Re-scan
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<ContextMenuItem onClick={() => onStopWatching(folder)}>
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{isWatched && onStopWatching && (
|
||||
<ContextMenuItem onClick={() => onStopWatching(folder)}>
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
Stop watching
|
||||
<ContextMenuItem onClick={() => startRename()}>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New subfolder
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => startRename()}>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onMove(folder)}>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => onDelete(folder)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
<ContextMenuItem onClick={() => onMove(folder)}>
|
||||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => onDelete(folder)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -242,10 +242,10 @@ export function FolderTreeView({
|
|||
siblingPositions={siblingPositions}
|
||||
contextMenuOpen={openContextMenuId === `folder-${f.id}`}
|
||||
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `folder-${f.id}` : null)}
|
||||
isWatched={watchedFolderIds?.has(f.id)}
|
||||
onRescan={onRescanFolder}
|
||||
onStopWatching={onStopWatchingFolder}
|
||||
/>
|
||||
isWatched={watchedFolderIds?.has(f.id)}
|
||||
onRescan={onRescanFolder}
|
||||
onStopWatching={onStopWatchingFolder}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isExpanded) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Check, ChevronRight, Clock, Copy, RotateCcw } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DocumentVersionSummary {
|
||||
version_number: number;
|
||||
|
|
@ -123,10 +118,9 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
|||
setSelectedVersion(versionNumber);
|
||||
setContentLoading(true);
|
||||
try {
|
||||
const data = (await documentsApiService.getDocumentVersion(
|
||||
documentId,
|
||||
versionNumber
|
||||
)) as { source_markdown: string };
|
||||
const data = (await documentsApiService.getDocumentVersion(documentId, versionNumber)) as {
|
||||
source_markdown: string;
|
||||
};
|
||||
setVersionContent(data.source_markdown || "");
|
||||
} catch {
|
||||
toast.error("Failed to load version content");
|
||||
|
|
@ -196,13 +190,11 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
|||
>
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{v.created_at ? formatRelativeTime(v.created_at) : `Version ${v.version_number}`}
|
||||
{v.created_at
|
||||
? formatRelativeTime(v.created_at)
|
||||
: `Version ${v.version_number}`}
|
||||
</p>
|
||||
{v.title && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{v.title}
|
||||
</p>
|
||||
)}
|
||||
{v.title && <p className="text-xs text-muted-foreground truncate">{v.title}</p>}
|
||||
</div>
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
|
|
@ -227,11 +219,7 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
|||
onClick={handleCopy}
|
||||
disabled={contentLoading || copied}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
<Button
|
||||
|
|
@ -241,11 +229,7 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
|||
disabled={restoring || contentLoading}
|
||||
onClick={() => handleRestore(selectedVersion)}
|
||||
>
|
||||
{restoring ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
)}
|
||||
{restoring ? <Spinner size="xs" /> : <RotateCcw className="h-3 w-3" />}
|
||||
Restore
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ function EditorPanelSkeleton() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export function EditorPanelContent({
|
||||
documentId,
|
||||
searchSpaceId,
|
||||
|
|
@ -194,24 +193,24 @@ export function EditorPanelContent({
|
|||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-4 py-2 shrink-0 border-b">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-sm font-semibold truncate">{displayTitle}</h2>
|
||||
{isEditableType && editedMarkdown !== null && (
|
||||
<p className="text-[10px] text-muted-foreground">Unsaved changes</p>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-sm font-semibold truncate">{displayTitle}</h2>
|
||||
{isEditableType && editedMarkdown !== null && (
|
||||
<p className="text-[10px] text-muted-foreground">Unsaved changes</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{editorDoc?.document_type && (
|
||||
<VersionHistoryButton documentId={documentId} documentType={editorDoc.document_type} />
|
||||
)}
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close editor panel</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{editorDoc?.document_type && (
|
||||
<VersionHistoryButton documentId={documentId} documentType={editorDoc.document_type} />
|
||||
)}
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close editor panel</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{isLoading ? (
|
||||
|
|
@ -233,7 +232,9 @@ export function EditorPanelContent({
|
|||
? "Document is processing"
|
||||
: "Document unavailable"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{error || "An unknown error occurred"}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{error || "An unknown error occurred"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : isLargeDocument ? (
|
||||
|
|
|
|||
|
|
@ -121,9 +121,7 @@ export function DocumentsSidebar({
|
|||
}
|
||||
const recovered = await api!.getWatchedFolders();
|
||||
const ids = new Set(
|
||||
recovered
|
||||
.filter((f) => f.rootFolderId != null)
|
||||
.map((f) => f.rootFolderId as number)
|
||||
recovered.filter((f) => f.rootFolderId != null).map((f) => f.rootFolderId as number)
|
||||
);
|
||||
setWatchedFolderIds(ids);
|
||||
return;
|
||||
|
|
@ -133,9 +131,7 @@ export function DocumentsSidebar({
|
|||
}
|
||||
|
||||
const ids = new Set(
|
||||
folders
|
||||
.filter((f) => f.rootFolderId != null)
|
||||
.map((f) => f.rootFolderId as number)
|
||||
folders.filter((f) => f.rootFolderId != null).map((f) => f.rootFolderId as number)
|
||||
);
|
||||
setWatchedFolderIds(ids);
|
||||
}
|
||||
|
|
@ -305,28 +301,25 @@ export function DocumentsSidebar({
|
|||
[searchSpaceId]
|
||||
);
|
||||
|
||||
const handleStopWatching = useCallback(
|
||||
async (folder: FolderDisplay) => {
|
||||
const api = window.electronAPI;
|
||||
if (!api) return;
|
||||
const handleStopWatching = useCallback(async (folder: FolderDisplay) => {
|
||||
const api = window.electronAPI;
|
||||
if (!api) return;
|
||||
|
||||
const watchedFolders = await api.getWatchedFolders();
|
||||
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
|
||||
if (!matched) {
|
||||
toast.error("This folder is not being watched");
|
||||
return;
|
||||
}
|
||||
const watchedFolders = await api.getWatchedFolders();
|
||||
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
|
||||
if (!matched) {
|
||||
toast.error("This folder is not being watched");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.removeWatchedFolder(matched.path);
|
||||
try {
|
||||
await foldersApiService.stopWatching(folder.id);
|
||||
} catch (err) {
|
||||
console.error("[DocumentsSidebar] Failed to clear watched metadata:", err);
|
||||
}
|
||||
toast.success(`Stopped watching: ${matched.name}`);
|
||||
},
|
||||
[]
|
||||
);
|
||||
await api.removeWatchedFolder(matched.path);
|
||||
try {
|
||||
await foldersApiService.stopWatching(folder.id);
|
||||
} catch (err) {
|
||||
console.error("[DocumentsSidebar] Failed to clear watched metadata:", err);
|
||||
}
|
||||
toast.success(`Stopped watching: ${matched.name}`);
|
||||
}, []);
|
||||
|
||||
const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => {
|
||||
try {
|
||||
|
|
@ -755,81 +748,83 @@ export function DocumentsSidebar({
|
|||
|
||||
<div className="flex-1 min-h-0 overflow-x-hidden pt-0 flex flex-col">
|
||||
<div className="px-4 pb-2">
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts}
|
||||
onSearch={setSearch}
|
||||
searchValue={search}
|
||||
onToggleType={onToggleType}
|
||||
activeTypes={activeTypes}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
/>
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts}
|
||||
onSearch={setSearch}
|
||||
searchValue={search}
|
||||
onToggleType={onToggleType}
|
||||
activeTypes={activeTypes}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1 min-h-0 overflow-auto">
|
||||
{deletableSelectedIds.length > 0 && (
|
||||
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-center px-4 py-1.5 animate-in fade-in duration-150 pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
className="pointer-events-auto flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-lg text-xs font-medium hover:bg-destructive/90 transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete {deletableSelectedIds.length}{" "}
|
||||
{deletableSelectedIds.length === 1 ? "item" : "items"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex-1 min-h-0 overflow-auto">
|
||||
{deletableSelectedIds.length > 0 && (
|
||||
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-center px-4 py-1.5 animate-in fade-in duration-150 pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
className="pointer-events-auto flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-lg text-xs font-medium hover:bg-destructive/90 transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete {deletableSelectedIds.length}{" "}
|
||||
{deletableSelectedIds.length === 1 ? "item" : "items"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FolderTreeView
|
||||
folders={treeFolders}
|
||||
documents={searchFilteredDocuments}
|
||||
expandedIds={expandedIds}
|
||||
onToggleExpand={toggleFolderExpand}
|
||||
mentionedDocIds={mentionedDocIds}
|
||||
onToggleChatMention={handleToggleChatMention}
|
||||
onToggleFolderSelect={handleToggleFolderSelect}
|
||||
onRenameFolder={handleRenameFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onMoveFolder={handleMoveFolder}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
searchQuery={debouncedSearch.trim() || undefined}
|
||||
onPreviewDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
onExportDocument={handleExportDocument}
|
||||
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
||||
activeTypes={activeTypes}
|
||||
onDropIntoFolder={handleDropIntoFolder}
|
||||
onReorderFolder={handleReorderFolder}
|
||||
watchedFolderIds={watchedFolderIds}
|
||||
onRescanFolder={handleRescanFolder}
|
||||
onStopWatchingFolder={handleStopWatching}
|
||||
/>
|
||||
<FolderTreeView
|
||||
folders={treeFolders}
|
||||
documents={searchFilteredDocuments}
|
||||
expandedIds={expandedIds}
|
||||
onToggleExpand={toggleFolderExpand}
|
||||
mentionedDocIds={mentionedDocIds}
|
||||
onToggleChatMention={handleToggleChatMention}
|
||||
onToggleFolderSelect={handleToggleFolderSelect}
|
||||
onRenameFolder={handleRenameFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onMoveFolder={handleMoveFolder}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
searchQuery={debouncedSearch.trim() || undefined}
|
||||
onPreviewDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openEditorPanel({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
onExportDocument={handleExportDocument}
|
||||
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
||||
activeTypes={activeTypes}
|
||||
onDropIntoFolder={handleDropIntoFolder}
|
||||
onReorderFolder={handleReorderFolder}
|
||||
watchedFolderIds={watchedFolderIds}
|
||||
onRescanFolder={handleRescanFolder}
|
||||
onStopWatchingFolder={handleStopWatching}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{versionDocId !== null && (
|
||||
<VersionHistoryDialog
|
||||
open
|
||||
onOpenChange={(open) => { if (!open) setVersionDocId(null); }}
|
||||
documentId={versionDocId}
|
||||
/>
|
||||
)}
|
||||
{versionDocId !== null && (
|
||||
<VersionHistoryDialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setVersionDocId(null);
|
||||
}}
|
||||
documentId={versionDocId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FolderPickerDialog
|
||||
<FolderPickerDialog
|
||||
open={folderPickerOpen}
|
||||
onOpenChange={setFolderPickerOpen}
|
||||
folders={treeFolders}
|
||||
|
|
|
|||
|
|
@ -185,9 +185,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
<p className="font-semibold text-foreground text-lg">
|
||||
{isProcessing ? "Document is processing" : "Document unavailable"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{error || "An unknown error occurred"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{error || "An unknown error occurred"}</p>
|
||||
</div>
|
||||
{!isProcessing && (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -480,9 +480,7 @@ export function SourceDetailPanel({
|
|||
<FileQuestionMark className="h-10 w-10 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground text-lg">
|
||||
Document unavailable
|
||||
</p>
|
||||
<p className="font-semibold text-foreground text-lg">Document unavailable</p>
|
||||
<p className="text-sm text-muted-foreground mt-2 max-w-md">
|
||||
{documentByChunkFetchingError.message ||
|
||||
"An unexpected error occurred. Please try again."}
|
||||
|
|
|
|||
|
|
@ -134,24 +134,27 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
preferences?.image_generation_config_id,
|
||||
]);
|
||||
|
||||
const handleRoleAssignment = useCallback(async (prefKey: string, configId: string) => {
|
||||
const value = configId === "unassigned" ? "" : parseInt(configId);
|
||||
const handleRoleAssignment = useCallback(
|
||||
async (prefKey: string, configId: string) => {
|
||||
const value = configId === "unassigned" ? "" : parseInt(configId);
|
||||
|
||||
setAssignments((prev) => ({ ...prev, [prefKey]: value }));
|
||||
setSavingRole(prefKey);
|
||||
savingRef.current = true;
|
||||
setAssignments((prev) => ({ ...prev, [prefKey]: value }));
|
||||
setSavingRole(prefKey);
|
||||
savingRef.current = true;
|
||||
|
||||
try {
|
||||
await updatePreferences({
|
||||
search_space_id: searchSpaceId,
|
||||
data: { [prefKey]: value || undefined },
|
||||
});
|
||||
toast.success("Role assignment updated");
|
||||
} finally {
|
||||
setSavingRole(null);
|
||||
savingRef.current = false;
|
||||
}
|
||||
}, [updatePreferences, searchSpaceId]);
|
||||
try {
|
||||
await updatePreferences({
|
||||
search_space_id: searchSpaceId,
|
||||
data: { [prefKey]: value || undefined },
|
||||
});
|
||||
toast.success("Role assignment updated");
|
||||
} finally {
|
||||
setSavingRole(null);
|
||||
savingRef.current = false;
|
||||
}
|
||||
},
|
||||
[updatePreferences, searchSpaceId]
|
||||
);
|
||||
|
||||
// Combine global and custom LLM configs
|
||||
const allLLMConfigs = [
|
||||
|
|
@ -199,10 +202,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
Refresh
|
||||
</Button>
|
||||
{isAssignmentComplete && !isLoading && !hasError && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs gap-1.5 text-muted-foreground"
|
||||
>
|
||||
<Badge variant="outline" className="text-xs gap-1.5 text-muted-foreground">
|
||||
<CircleCheck className="h-3 w-3" />
|
||||
All roles assigned
|
||||
</Badge>
|
||||
|
|
@ -483,7 +483,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,8 @@ const MAX_TOTAL_SIZE_BYTES = MAX_TOTAL_SIZE_MB * 1024 * 1024;
|
|||
const MAX_FILE_SIZE_MB = 500;
|
||||
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
const toggleRowClass = "flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
|
||||
const toggleRowClass =
|
||||
"flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
|
||||
|
||||
export function DocumentUploadTab({
|
||||
searchSpaceId,
|
||||
|
|
@ -326,7 +327,14 @@ export function DocumentUploadTab({
|
|||
await api.addWatchedFolder({
|
||||
path: selectedFolder.path,
|
||||
name: selectedFolder.name,
|
||||
excludePatterns: [".git", "node_modules", "__pycache__", ".DS_Store", ".obsidian", ".trash"],
|
||||
excludePatterns: [
|
||||
".git",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
".DS_Store",
|
||||
".obsidian",
|
||||
".trash",
|
||||
],
|
||||
fileExtensions: null,
|
||||
rootFolderId,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
|
|
@ -393,12 +401,20 @@ export function DocumentUploadTab({
|
|||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="sm" className={`text-xs gap-1 bg-neutral-700/50 hover:bg-neutral-600/50 ${sizeClass} ${widthClass}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`text-xs gap-1 bg-neutral-700/50 hover:bg-neutral-600/50 ${sizeClass} ${widthClass}`}
|
||||
>
|
||||
Browse
|
||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="dark:bg-neutral-800" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuContent
|
||||
align="center"
|
||||
className="dark:bg-neutral-800"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenuItem onClick={handleBrowseFiles}>
|
||||
<FileIcon className="h-4 w-4 mr-2" />
|
||||
Files
|
||||
|
|
@ -415,7 +431,11 @@ export function DocumentUploadTab({
|
|||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="secondary" size="sm" className={`text-xs gap-1 ${sizeClass} ${widthClass}`}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={`text-xs gap-1 ${sizeClass} ${widthClass}`}
|
||||
>
|
||||
Browse
|
||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
@ -457,21 +477,19 @@ export function DocumentUploadTab({
|
|||
{/* MOBILE DROP ZONE */}
|
||||
<div className="sm:hidden">
|
||||
{hasContent ? (
|
||||
!selectedFolder && !isFileCountLimitReached && (
|
||||
isElectron ? (
|
||||
<div className="w-full">
|
||||
{renderBrowseButton({ compact: true, fullWidth: true })}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-xs h-8 flex items-center justify-center gap-1.5 rounded-md border border-dashed border-muted-foreground/30 text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
Add more files
|
||||
</button>
|
||||
)
|
||||
)
|
||||
!selectedFolder &&
|
||||
!isFileCountLimitReached &&
|
||||
(isElectron ? (
|
||||
<div className="w-full">{renderBrowseButton({ compact: true, fullWidth: true })}</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-xs h-8 flex items-center justify-center gap-1.5 rounded-md border border-dashed border-muted-foreground/30 text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
Add more files
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer"
|
||||
|
|
@ -487,7 +505,9 @@ export function DocumentUploadTab({
|
|||
<p className="text-sm text-muted-foreground inline-flex items-center flex-wrap justify-center">
|
||||
<span>{t("file_size_limit")}</span>
|
||||
<Dot className="h-4 w-4 shrink-0" />
|
||||
<span>{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}</span>
|
||||
<span>
|
||||
{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full mt-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
|
@ -538,7 +558,9 @@ export function DocumentUploadTab({
|
|||
<p className="text-xs text-muted-foreground text-center inline-flex items-center flex-wrap justify-center">
|
||||
<span>{t("file_size_limit")}</span>
|
||||
<Dot className="h-4 w-4 shrink-0" />
|
||||
<span>{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}</span>
|
||||
<span>
|
||||
{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}
|
||||
</span>
|
||||
</p>
|
||||
<div className="mt-1">{renderBrowseButton()}</div>
|
||||
</div>
|
||||
|
|
@ -569,9 +591,7 @@ export function DocumentUploadTab({
|
|||
<div className="flex items-center justify-between p-3">
|
||||
<div className="space-y-0.5">
|
||||
<p className="font-medium text-sm">Watch folder</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Auto-sync when files change
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Auto-sync when files change</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="watch-folder-toggle"
|
||||
|
|
@ -612,7 +632,8 @@ export function DocumentUploadTab({
|
|||
<div className="rounded-lg border border-border p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">
|
||||
{t("selected_files", { count: files.length })} · {formatFileSize(totalFileSize)}
|
||||
{t("selected_files", { count: files.length })} ·{" "}
|
||||
{formatFileSize(totalFileSize)}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -404,7 +404,6 @@ class ConnectorsApiService {
|
|||
listDiscordChannelsResponse
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export type { SlackChannel, DiscordChannel };
|
||||
|
|
|
|||
|
|
@ -417,27 +417,47 @@ class DocumentsApiService {
|
|||
};
|
||||
|
||||
getDocumentVersion = async (documentId: number, versionNumber: number) => {
|
||||
return baseApiService.get(
|
||||
`/api/v1/documents/${documentId}/versions/${versionNumber}`
|
||||
);
|
||||
return baseApiService.get(`/api/v1/documents/${documentId}/versions/${versionNumber}`);
|
||||
};
|
||||
|
||||
restoreDocumentVersion = async (documentId: number, versionNumber: number) => {
|
||||
return baseApiService.post(
|
||||
`/api/v1/documents/${documentId}/versions/${versionNumber}/restore`
|
||||
);
|
||||
return baseApiService.post(`/api/v1/documents/${documentId}/versions/${versionNumber}/restore`);
|
||||
};
|
||||
|
||||
folderIndex = async (searchSpaceId: number, body: { folder_path: string; folder_name: string; search_space_id: number; exclude_patterns?: string[]; file_extensions?: string[]; root_folder_id?: number; enable_summary?: boolean }) => {
|
||||
folderIndex = async (
|
||||
searchSpaceId: number,
|
||||
body: {
|
||||
folder_path: string;
|
||||
folder_name: string;
|
||||
search_space_id: number;
|
||||
exclude_patterns?: string[];
|
||||
file_extensions?: string[];
|
||||
root_folder_id?: number;
|
||||
enable_summary?: boolean;
|
||||
}
|
||||
) => {
|
||||
return baseApiService.post(`/api/v1/documents/folder-index`, undefined, { body });
|
||||
};
|
||||
|
||||
folderIndexFiles = async (searchSpaceId: number, body: { folder_path: string; folder_name: string; search_space_id: number; target_file_paths: string[]; root_folder_id?: number | null; enable_summary?: boolean }) => {
|
||||
folderIndexFiles = async (
|
||||
searchSpaceId: number,
|
||||
body: {
|
||||
folder_path: string;
|
||||
folder_name: string;
|
||||
search_space_id: number;
|
||||
target_file_paths: string[];
|
||||
root_folder_id?: number | null;
|
||||
enable_summary?: boolean;
|
||||
}
|
||||
) => {
|
||||
return baseApiService.post(`/api/v1/documents/folder-index-files`, undefined, { body });
|
||||
};
|
||||
|
||||
getWatchedFolders = async (searchSpaceId: number) => {
|
||||
return baseApiService.get(`/api/v1/documents/watched-folders?search_space_id=${searchSpaceId}`, folderListResponse);
|
||||
return baseApiService.get(
|
||||
`/api/v1/documents/watched-folders?search_space_id=${searchSpaceId}`,
|
||||
folderListResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue