mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-01 13:02:42 +00:00
Compare commits
1 Commits
main
...
bo/hook_ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a3ee1a0a |
@@ -1,54 +0,0 @@
|
||||
"""csv to tabular chat file type
|
||||
|
||||
Revision ID: 8188861f4e92
|
||||
Revises: d8cdfee5df80
|
||||
Create Date: 2026-03-31 19:23:05.753184
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "8188861f4e92"
|
||||
down_revision = "d8cdfee5df80"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE chat_message
|
||||
SET files = (
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN elem->>'type' = 'csv'
|
||||
THEN jsonb_set(elem, '{type}', '"tabular"')
|
||||
ELSE elem
|
||||
END
|
||||
)
|
||||
FROM jsonb_array_elements(files) AS elem
|
||||
)
|
||||
WHERE files::text LIKE '%"type": "csv"%'
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE chat_message
|
||||
SET files = (
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN elem->>'type' = 'tabular'
|
||||
THEN jsonb_set(elem, '{type}', '"csv"')
|
||||
ELSE elem
|
||||
END
|
||||
)
|
||||
FROM jsonb_array_elements(files) AS elem
|
||||
)
|
||||
WHERE files::text LIKE '%"type": "tabular"%'
|
||||
"""
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
"""add skipped to userfilestatus
|
||||
|
||||
Revision ID: d8cdfee5df80
|
||||
Revises: 1d78c0ca7853
|
||||
Create Date: 2026-04-01 10:47:12.593950
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d8cdfee5df80"
|
||||
down_revision = "1d78c0ca7853"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
TABLE = "user_file"
|
||||
COLUMN = "status"
|
||||
CONSTRAINT_NAME = "ck_user_file_status"
|
||||
|
||||
OLD_VALUES = ("PROCESSING", "INDEXING", "COMPLETED", "FAILED", "CANCELED", "DELETING")
|
||||
NEW_VALUES = (
|
||||
"PROCESSING",
|
||||
"INDEXING",
|
||||
"COMPLETED",
|
||||
"SKIPPED",
|
||||
"FAILED",
|
||||
"CANCELED",
|
||||
"DELETING",
|
||||
)
|
||||
|
||||
|
||||
def _drop_status_check_constraint() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
for constraint in inspector.get_check_constraints(TABLE):
|
||||
if COLUMN in constraint.get("sqltext", ""):
|
||||
constraint_name = constraint["name"]
|
||||
if constraint_name is not None:
|
||||
op.drop_constraint(constraint_name, TABLE, type_="check")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_drop_status_check_constraint()
|
||||
in_clause = ", ".join(f"'{v}'" for v in NEW_VALUES)
|
||||
op.create_check_constraint(CONSTRAINT_NAME, TABLE, f"{COLUMN} IN ({in_clause})")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(f"UPDATE {TABLE} SET {COLUMN} = 'COMPLETED' WHERE {COLUMN} = 'SKIPPED'")
|
||||
_drop_status_check_constraint()
|
||||
in_clause = ", ".join(f"'{v}'" for v in OLD_VALUES)
|
||||
op.create_check_constraint(CONSTRAINT_NAME, TABLE, f"{COLUMN} IN ({in_clause})")
|
||||
@@ -215,7 +215,6 @@ class UserFileStatus(str, PyEnum):
|
||||
PROCESSING = "PROCESSING"
|
||||
INDEXING = "INDEXING"
|
||||
COMPLETED = "COMPLETED"
|
||||
SKIPPED = "SKIPPED"
|
||||
FAILED = "FAILED"
|
||||
CANCELED = "CANCELED"
|
||||
DELETING = "DELETING"
|
||||
|
||||
@@ -7,7 +7,6 @@ from fastapi import HTTPException
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.background import BackgroundTasks
|
||||
@@ -18,7 +17,6 @@ from onyx.configs.constants import FileOrigin
|
||||
from onyx.configs.constants import OnyxCeleryPriority
|
||||
from onyx.configs.constants import OnyxCeleryQueues
|
||||
from onyx.configs.constants import OnyxCeleryTask
|
||||
from onyx.db.enums import UserFileStatus
|
||||
from onyx.db.models import Project__UserFile
|
||||
from onyx.db.models import User
|
||||
from onyx.db.models import UserFile
|
||||
@@ -36,19 +34,9 @@ class CategorizedFilesResult(BaseModel):
|
||||
user_files: list[UserFile]
|
||||
rejected_files: list[RejectedFile]
|
||||
id_to_temp_id: dict[str, str]
|
||||
# Filenames that should be stored but not indexed.
|
||||
skip_indexing_filenames: set[str] = Field(default_factory=set)
|
||||
# Allow SQLAlchemy ORM models inside this result container
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@property
|
||||
def indexable_files(self) -> list[UserFile]:
|
||||
return [
|
||||
uf
|
||||
for uf in self.user_files
|
||||
if (uf.name or "") not in self.skip_indexing_filenames
|
||||
]
|
||||
|
||||
|
||||
def build_hashed_file_key(file: UploadFile) -> str:
|
||||
name_prefix = (file.filename or "")[:50]
|
||||
@@ -82,7 +70,6 @@ def create_user_files(
|
||||
)
|
||||
if new_temp_id is not None:
|
||||
id_to_temp_id[str(new_id)] = new_temp_id
|
||||
should_skip = (file.filename or "") in categorized_files.skip_indexing
|
||||
new_file = UserFile(
|
||||
id=new_id,
|
||||
user_id=user.id,
|
||||
@@ -94,7 +81,6 @@ def create_user_files(
|
||||
link_url=link_url,
|
||||
content_type=file.content_type,
|
||||
file_type=file.content_type,
|
||||
status=UserFileStatus.SKIPPED if should_skip else UserFileStatus.PROCESSING,
|
||||
last_accessed_at=datetime.datetime.now(datetime.timezone.utc),
|
||||
)
|
||||
# Persist the UserFile first to satisfy FK constraints for association table
|
||||
@@ -112,7 +98,6 @@ def create_user_files(
|
||||
user_files=user_files,
|
||||
rejected_files=rejected_files,
|
||||
id_to_temp_id=id_to_temp_id,
|
||||
skip_indexing_filenames=categorized_files.skip_indexing,
|
||||
)
|
||||
|
||||
|
||||
@@ -138,7 +123,6 @@ def upload_files_to_user_files_with_indexing(
|
||||
user_files = categorized_files_result.user_files
|
||||
rejected_files = categorized_files_result.rejected_files
|
||||
id_to_temp_id = categorized_files_result.id_to_temp_id
|
||||
indexable_files = categorized_files_result.indexable_files
|
||||
# Trigger per-file processing immediately for the current tenant
|
||||
tenant_id = get_current_tenant_id()
|
||||
for rejected_file in rejected_files:
|
||||
@@ -150,12 +134,12 @@ def upload_files_to_user_files_with_indexing(
|
||||
from onyx.background.task_utils import drain_processing_loop
|
||||
|
||||
background_tasks.add_task(drain_processing_loop, tenant_id)
|
||||
for user_file in indexable_files:
|
||||
for user_file in user_files:
|
||||
logger.info(f"Queued in-process processing for user_file_id={user_file.id}")
|
||||
else:
|
||||
from onyx.background.celery.versioned_apps.client import app as client_app
|
||||
|
||||
for user_file in indexable_files:
|
||||
for user_file in user_files:
|
||||
task = client_app.send_task(
|
||||
OnyxCeleryTask.PROCESS_SINGLE_USER_FILE,
|
||||
kwargs={"user_file_id": user_file.id, "tenant_id": tenant_id},
|
||||
@@ -171,7 +155,6 @@ def upload_files_to_user_files_with_indexing(
|
||||
user_files=user_files,
|
||||
rejected_files=rejected_files,
|
||||
id_to_temp_id=id_to_temp_id,
|
||||
skip_indexing_filenames=categorized_files_result.skip_indexing_filenames,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ PLAIN_TEXT_MIME_TYPE = "text/plain"
|
||||
class OnyxMimeTypes:
|
||||
IMAGE_MIME_TYPES = {"image/jpg", "image/jpeg", "image/png", "image/webp"}
|
||||
CSV_MIME_TYPES = {"text/csv"}
|
||||
TABULAR_MIME_TYPES = CSV_MIME_TYPES | {SPREADSHEET_MIME_TYPE}
|
||||
TEXT_MIME_TYPES = {
|
||||
PLAIN_TEXT_MIME_TYPE,
|
||||
"text/markdown",
|
||||
@@ -35,12 +34,13 @@ class OnyxMimeTypes:
|
||||
PDF_MIME_TYPE,
|
||||
WORD_PROCESSING_MIME_TYPE,
|
||||
PRESENTATION_MIME_TYPE,
|
||||
SPREADSHEET_MIME_TYPE,
|
||||
"message/rfc822",
|
||||
"application/epub+zip",
|
||||
}
|
||||
|
||||
ALLOWED_MIME_TYPES = IMAGE_MIME_TYPES.union(
|
||||
TEXT_MIME_TYPES, DOCUMENT_MIME_TYPES, TABULAR_MIME_TYPES
|
||||
TEXT_MIME_TYPES, DOCUMENT_MIME_TYPES, CSV_MIME_TYPES
|
||||
)
|
||||
|
||||
EXCLUDED_IMAGE_TYPES = {
|
||||
@@ -53,11 +53,6 @@ class OnyxMimeTypes:
|
||||
|
||||
|
||||
class OnyxFileExtensions:
|
||||
TABULAR_EXTENSIONS = {
|
||||
".csv",
|
||||
".tsv",
|
||||
".xlsx",
|
||||
}
|
||||
PLAIN_TEXT_EXTENSIONS = {
|
||||
".txt",
|
||||
".md",
|
||||
|
||||
@@ -13,14 +13,13 @@ class ChatFileType(str, Enum):
|
||||
DOC = "document"
|
||||
# Plain text only contain the text
|
||||
PLAIN_TEXT = "plain_text"
|
||||
# Tabular data files (CSV, XLSX)
|
||||
TABULAR = "tabular"
|
||||
CSV = "csv"
|
||||
|
||||
def is_text_file(self) -> bool:
|
||||
return self in (
|
||||
ChatFileType.PLAIN_TEXT,
|
||||
ChatFileType.DOC,
|
||||
ChatFileType.TABULAR,
|
||||
ChatFileType.CSV,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,18 +76,11 @@ class CategorizedFiles(BaseModel):
|
||||
acceptable: list[UploadFile] = Field(default_factory=list)
|
||||
rejected: list[RejectedFile] = Field(default_factory=list)
|
||||
acceptable_file_to_token_count: dict[str, int] = Field(default_factory=dict)
|
||||
# Filenames within `acceptable` that should be stored but not indexed.
|
||||
skip_indexing: set[str] = Field(default_factory=set)
|
||||
|
||||
# Allow FastAPI UploadFile instances
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
def _skip_token_threshold(extension: str) -> bool:
|
||||
"""Return True if this file extension should bypass the token limit."""
|
||||
return extension.lower() in OnyxFileExtensions.TABULAR_EXTENSIONS
|
||||
|
||||
|
||||
def _apply_long_side_cap(width: int, height: int, cap: int) -> tuple[int, int]:
|
||||
if max(width, height) <= cap:
|
||||
return width, height
|
||||
@@ -271,17 +264,7 @@ def categorize_uploaded_files(
|
||||
token_count = count_tokens(
|
||||
text_content, tokenizer, token_limit=token_threshold
|
||||
)
|
||||
exceeds_threshold = (
|
||||
token_threshold is not None and token_count > token_threshold
|
||||
)
|
||||
if exceeds_threshold and _skip_token_threshold(extension):
|
||||
# Exempt extensions (e.g. spreadsheets) are accepted
|
||||
# but flagged to skip indexing — only metadata is
|
||||
# injected into the LLM context.
|
||||
results.acceptable.append(upload)
|
||||
results.acceptable_file_to_token_count[filename] = token_count
|
||||
results.skip_indexing.add(filename)
|
||||
elif exceeds_threshold:
|
||||
if token_threshold is not None and token_count > token_threshold:
|
||||
results.rejected.append(
|
||||
RejectedFile(
|
||||
filename=filename,
|
||||
|
||||
@@ -9,8 +9,8 @@ def mime_type_to_chat_file_type(mime_type: str | None) -> ChatFileType:
|
||||
if mime_type in OnyxMimeTypes.IMAGE_MIME_TYPES:
|
||||
return ChatFileType.IMAGE
|
||||
|
||||
if mime_type in OnyxMimeTypes.TABULAR_MIME_TYPES:
|
||||
return ChatFileType.TABULAR
|
||||
if mime_type in OnyxMimeTypes.CSV_MIME_TYPES:
|
||||
return ChatFileType.CSV
|
||||
|
||||
if mime_type in OnyxMimeTypes.DOCUMENT_MIME_TYPES:
|
||||
return ChatFileType.DOC
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import io
|
||||
import json
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
@@ -10,7 +9,6 @@ from typing_extensions import override
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.db.engine.sql_engine import get_session_with_current_tenant
|
||||
from onyx.file_processing.extract_file_text import extract_file_text
|
||||
from onyx.file_store.models import ChatFileType
|
||||
from onyx.file_store.models import InMemoryChatFile
|
||||
from onyx.file_store.utils import load_chat_file_by_id
|
||||
@@ -171,13 +169,10 @@ class FileReaderTool(Tool[FileReaderToolOverrideKwargs]):
|
||||
|
||||
chat_file = self._load_file(file_id)
|
||||
|
||||
# Only PLAIN_TEXT and TABULAR are guaranteed to contain actual text bytes.
|
||||
# Only PLAIN_TEXT and CSV are guaranteed to contain actual text bytes.
|
||||
# DOC type in a loaded file means plaintext extraction failed and the
|
||||
# content is the original binary (e.g. raw PDF/DOCX bytes).
|
||||
if chat_file.file_type not in (
|
||||
ChatFileType.PLAIN_TEXT,
|
||||
ChatFileType.TABULAR,
|
||||
):
|
||||
if chat_file.file_type not in (ChatFileType.PLAIN_TEXT, ChatFileType.CSV):
|
||||
raise ToolCallException(
|
||||
message=f"File {file_id} is not a text file (type={chat_file.file_type})",
|
||||
llm_facing_message=(
|
||||
@@ -186,19 +181,7 @@ class FileReaderTool(Tool[FileReaderToolOverrideKwargs]):
|
||||
)
|
||||
|
||||
try:
|
||||
if chat_file.file_type == ChatFileType.PLAIN_TEXT:
|
||||
full_text = chat_file.content.decode("utf-8", errors="replace")
|
||||
else:
|
||||
full_text = (
|
||||
extract_file_text(
|
||||
file=io.BytesIO(chat_file.content),
|
||||
file_name=chat_file.filename or "",
|
||||
break_on_unprocessable=False,
|
||||
)
|
||||
or ""
|
||||
)
|
||||
except ToolCallException:
|
||||
raise
|
||||
full_text = chat_file.content.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
raise ToolCallException(
|
||||
message=f"Failed to decode file {file_id}",
|
||||
|
||||
@@ -1175,7 +1175,7 @@ def test_code_interpreter_receives_chat_files(
|
||||
|
||||
file_descriptor: FileDescriptor = {
|
||||
"id": user_file.file_id,
|
||||
"type": ChatFileType.TABULAR,
|
||||
"type": ChatFileType.CSV,
|
||||
"name": "data.csv",
|
||||
"user_file_id": str(user_file.id),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import mimetypes
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from tests.integration.common_utils.constants import API_SERVER_URL
|
||||
from tests.integration.common_utils.managers.chat import ChatSessionManager
|
||||
from tests.integration.common_utils.managers.file import FileManager
|
||||
from tests.integration.common_utils.managers.llm_provider import LLMProviderManager
|
||||
@@ -85,90 +79,3 @@ def test_send_message_with_text_file_attachment(admin_user: DATestUser) -> None:
|
||||
assert (
|
||||
"third line" in response.full_message.lower()
|
||||
), "Chat response should contain the contents of the file"
|
||||
|
||||
|
||||
def _set_token_threshold(admin_user: DATestUser, threshold_k: int) -> None:
|
||||
"""Set the file token count threshold via admin settings API."""
|
||||
response = requests.put(
|
||||
f"{API_SERVER_URL}/admin/settings",
|
||||
json={"file_token_count_threshold_k": threshold_k},
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
def _upload_raw(
|
||||
filename: str,
|
||||
content: bytes,
|
||||
user: DATestUser,
|
||||
) -> dict[str, Any]:
|
||||
"""Upload a file and return the full JSON response (user_files + rejected_files)."""
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
headers = user.headers.copy()
|
||||
headers.pop("Content-Type", None)
|
||||
|
||||
response = requests.post(
|
||||
f"{API_SERVER_URL}/user/projects/file/upload",
|
||||
files=[("files", (filename, content, mime_type or "application/octet-stream"))],
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_csv_over_token_threshold_uploaded_not_indexed(
|
||||
admin_user: DATestUser,
|
||||
) -> None:
|
||||
"""CSV exceeding token threshold is uploaded (accepted) but skips indexing."""
|
||||
_set_token_threshold(admin_user, threshold_k=1)
|
||||
try:
|
||||
# ~2000 tokens with default tokenizer, well over 1K threshold
|
||||
content = ("x " * 100 + "\n") * 20
|
||||
result = _upload_raw("large.csv", content.encode(), admin_user)
|
||||
|
||||
assert len(result["user_files"]) == 1, "CSV should be accepted"
|
||||
assert len(result["rejected_files"]) == 0, "CSV should not be rejected"
|
||||
assert (
|
||||
result["user_files"][0]["status"] == "SKIPPED"
|
||||
), "CSV over threshold should be SKIPPED (uploaded but not indexed)"
|
||||
assert (
|
||||
result["user_files"][0]["chunk_count"] is None
|
||||
), "Skipped file should have no chunks"
|
||||
finally:
|
||||
_set_token_threshold(admin_user, threshold_k=200)
|
||||
|
||||
|
||||
def test_csv_under_token_threshold_uploaded_and_indexed(
|
||||
admin_user: DATestUser,
|
||||
) -> None:
|
||||
"""CSV under token threshold is uploaded and queued for indexing."""
|
||||
_set_token_threshold(admin_user, threshold_k=200)
|
||||
try:
|
||||
content = "col1,col2\na,b\n"
|
||||
result = _upload_raw("small.csv", content.encode(), admin_user)
|
||||
|
||||
assert len(result["user_files"]) == 1, "CSV should be accepted"
|
||||
assert len(result["rejected_files"]) == 0, "CSV should not be rejected"
|
||||
assert (
|
||||
result["user_files"][0]["status"] == "PROCESSING"
|
||||
), "CSV under threshold should be PROCESSING (queued for indexing)"
|
||||
finally:
|
||||
_set_token_threshold(admin_user, threshold_k=200)
|
||||
|
||||
|
||||
def test_txt_over_token_threshold_rejected(
|
||||
admin_user: DATestUser,
|
||||
) -> None:
|
||||
"""Non-exempt file exceeding token threshold is rejected entirely."""
|
||||
_set_token_threshold(admin_user, threshold_k=1)
|
||||
try:
|
||||
# ~2000 tokens, well over 1K threshold. Unlike CSV, .txt is not
|
||||
# exempt from the threshold so the file should be rejected.
|
||||
content = ("x " * 100 + "\n") * 20
|
||||
result = _upload_raw("big.txt", content.encode(), admin_user)
|
||||
|
||||
assert len(result["user_files"]) == 0, "File should not be accepted"
|
||||
assert len(result["rejected_files"]) == 1, "File should be rejected"
|
||||
assert "token limit" in result["rejected_files"][0]["reason"].lower()
|
||||
finally:
|
||||
_set_token_threshold(admin_user, threshold_k=200)
|
||||
|
||||
@@ -139,7 +139,7 @@ def test_csv_file_type() -> None:
|
||||
result = _extract_referenced_file_descriptors([tool_call], message)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == ChatFileType.TABULAR
|
||||
assert result[0]["type"] == ChatFileType.CSV
|
||||
|
||||
|
||||
def test_unknown_extension_defaults_to_plain_text() -> None:
|
||||
|
||||
@@ -40,8 +40,6 @@ def test_send_task_includes_expires(
|
||||
user_files=user_files,
|
||||
rejected_files=[],
|
||||
id_to_temp_id={},
|
||||
skip_indexing_filenames=set(),
|
||||
indexable_files=user_files,
|
||||
)
|
||||
|
||||
mock_user = MagicMock()
|
||||
|
||||
@@ -417,57 +417,3 @@ def test_categorize_text_under_token_limit_accepted(
|
||||
|
||||
assert len(result.acceptable) == 1
|
||||
assert result.acceptable_file_to_token_count["ok.txt"] == 500
|
||||
|
||||
|
||||
# --- skip-indexing vs rejection by file type ---
|
||||
|
||||
|
||||
def test_csv_over_token_threshold_accepted_skip_indexing(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""CSV exceeding token threshold is uploaded but flagged to skip indexing."""
|
||||
_patch_common_dependencies(monkeypatch, upload_size_mb=1000, token_threshold_k=1)
|
||||
text = "x" * 2000 # 2000 tokens > 1000 threshold
|
||||
monkeypatch.setattr(utils, "extract_file_text", lambda **_kwargs: text)
|
||||
|
||||
upload = _make_upload("large.csv", size=2000, content=text.encode())
|
||||
result = utils.categorize_uploaded_files([upload], MagicMock())
|
||||
|
||||
assert len(result.acceptable) == 1
|
||||
assert result.acceptable[0].filename == "large.csv"
|
||||
assert "large.csv" in result.skip_indexing
|
||||
assert len(result.rejected) == 0
|
||||
|
||||
|
||||
def test_csv_under_token_threshold_accepted_and_indexed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""CSV under token threshold is uploaded and indexed normally."""
|
||||
_patch_common_dependencies(monkeypatch, upload_size_mb=1000, token_threshold_k=1)
|
||||
text = "x" * 500 # 500 tokens < 1000 threshold
|
||||
monkeypatch.setattr(utils, "extract_file_text", lambda **_kwargs: text)
|
||||
|
||||
upload = _make_upload("small.csv", size=500, content=text.encode())
|
||||
result = utils.categorize_uploaded_files([upload], MagicMock())
|
||||
|
||||
assert len(result.acceptable) == 1
|
||||
assert result.acceptable[0].filename == "small.csv"
|
||||
assert "small.csv" not in result.skip_indexing
|
||||
assert len(result.rejected) == 0
|
||||
|
||||
|
||||
def test_pdf_over_token_threshold_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""PDF exceeding token threshold is rejected entirely (not uploaded)."""
|
||||
_patch_common_dependencies(monkeypatch, upload_size_mb=1000, token_threshold_k=1)
|
||||
text = "x" * 2000 # 2000 tokens > 1000 threshold
|
||||
monkeypatch.setattr(utils, "extract_file_text", lambda **_kwargs: text)
|
||||
|
||||
upload = _make_upload("big.pdf", size=2000, content=text.encode())
|
||||
result = utils.categorize_uploaded_files([upload], MagicMock())
|
||||
|
||||
assert len(result.rejected) == 1
|
||||
assert result.rejected[0].filename == "big.pdf"
|
||||
assert "1K token limit" in result.rejected[0].reason
|
||||
assert len(result.acceptable) == 0
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestChatFileConversion:
|
||||
ChatLoadedFile(
|
||||
file_id="file-2",
|
||||
content=b"csv,data\n1,2",
|
||||
file_type=ChatFileType.TABULAR,
|
||||
file_type=ChatFileType.CSV,
|
||||
filename="data.csv",
|
||||
content_text="csv,data\n1,2",
|
||||
token_count=5,
|
||||
|
||||
@@ -100,7 +100,9 @@ function Button({
|
||||
border={interactiveProps.prominence === "secondary"}
|
||||
heightVariant={size}
|
||||
widthVariant={width}
|
||||
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
roundingVariant={
|
||||
isLarge ? "default" : size === "2xs" ? "mini" : "compact"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{iconWrapper(Icon, size, !!children)}
|
||||
|
||||
@@ -35,7 +35,7 @@ Interactive.Stateful <- selectVariant, state, interaction, onClick, href
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `roundingVariant` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
|
||||
| `roundingVariant` | `InteractiveContainerRoundingVariant` | `"default"` | Corner rounding preset (height is content-driven) |
|
||||
| `width` | `WidthVariant` | `"full"` | Container width |
|
||||
| `type` | `"submit" \| "button" \| "reset"` | `"button"` | HTML button type |
|
||||
| `tooltip` | `string` | — | Tooltip text shown on hover |
|
||||
@@ -63,7 +63,7 @@ import { LineItemButton } from "@opal/components";
|
||||
<LineItemButton
|
||||
selectVariant="select-heavy"
|
||||
state={isSelected ? "selected" : "empty"}
|
||||
roundingVariant="sm"
|
||||
roundingVariant="compact"
|
||||
onClick={handleClick}
|
||||
title="gpt-4o"
|
||||
sizePreset="main-ui"
|
||||
|
||||
@@ -33,7 +33,7 @@ type LineItemButtonOwnProps = Pick<
|
||||
/** Interactive select variant. @default "select-light" */
|
||||
selectVariant?: "select-light" | "select-heavy";
|
||||
|
||||
/** Corner rounding preset (height is always content-driven). @default "md" */
|
||||
/** Corner rounding preset (height is always content-driven). @default "default" */
|
||||
roundingVariant?: InteractiveContainerRoundingVariant;
|
||||
|
||||
/** Container width. @default "full" */
|
||||
@@ -65,7 +65,7 @@ function LineItemButton({
|
||||
type = "button",
|
||||
|
||||
// Sizing
|
||||
roundingVariant = "md",
|
||||
roundingVariant = "default",
|
||||
width = "full",
|
||||
tooltip,
|
||||
tooltipSide = "top",
|
||||
|
||||
@@ -127,7 +127,7 @@ function OpenButton({
|
||||
widthVariant={width}
|
||||
roundingVariant={
|
||||
roundingVariantOverride ??
|
||||
(isLarge ? "md" : size === "2xs" ? "xs" : "sm")
|
||||
(isLarge ? "default" : size === "2xs" ? "mini" : "compact")
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -101,7 +101,9 @@ function SelectButton({
|
||||
type={type}
|
||||
heightVariant={size}
|
||||
widthVariant={width}
|
||||
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
roundingVariant={
|
||||
isLarge ? "default" : size === "2xs" ? "mini" : "compact"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Card } from "@opal/components";
|
||||
|
||||
const BACKGROUND_VARIANTS = ["none", "light", "heavy"] as const;
|
||||
const BORDER_VARIANTS = ["none", "dashed", "solid"] as const;
|
||||
const PADDING_VARIANTS = ["fit", "2xs", "xs", "sm", "md", "lg"] as const;
|
||||
const ROUNDING_VARIANTS = ["xs", "sm", "md", "lg"] as const;
|
||||
const SIZE_VARIANTS = ["lg", "md", "sm", "xs", "2xs", "fit"] as const;
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: "opal/components/Card",
|
||||
@@ -18,9 +17,7 @@ type Story = StoryObj<typeof Card>;
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Card>
|
||||
<p>
|
||||
Default card with light background, no border, sm padding, md rounding.
|
||||
</p>
|
||||
<p>Default card with light background, no border, lg size.</p>
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
@@ -49,24 +46,12 @@ export const BorderVariants: Story = {
|
||||
),
|
||||
};
|
||||
|
||||
export const PaddingVariants: Story = {
|
||||
export const SizeVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4 w-96">
|
||||
{PADDING_VARIANTS.map((padding) => (
|
||||
<Card key={padding} paddingVariant={padding} borderVariant="solid">
|
||||
<p>paddingVariant: {padding}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const RoundingVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4 w-96">
|
||||
{ROUNDING_VARIANTS.map((rounding) => (
|
||||
<Card key={rounding} roundingVariant={rounding} borderVariant="solid">
|
||||
<p>roundingVariant: {rounding}</p>
|
||||
{SIZE_VARIANTS.map((size) => (
|
||||
<Card key={size} sizeVariant={size} borderVariant="solid">
|
||||
<p>sizeVariant: {size}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@@ -76,15 +61,15 @@ export const RoundingVariants: Story = {
|
||||
export const AllCombinations: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-8">
|
||||
{PADDING_VARIANTS.map((padding) => (
|
||||
<div key={padding}>
|
||||
<p className="font-bold pb-2">paddingVariant: {padding}</p>
|
||||
{SIZE_VARIANTS.map((size) => (
|
||||
<div key={size}>
|
||||
<p className="font-bold pb-2">sizeVariant: {size}</p>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{BACKGROUND_VARIANTS.map((bg) =>
|
||||
BORDER_VARIANTS.map((border) => (
|
||||
<Card
|
||||
key={`${padding}-${bg}-${border}`}
|
||||
paddingVariant={padding}
|
||||
key={`${size}-${bg}-${border}`}
|
||||
sizeVariant={size}
|
||||
backgroundVariant={bg}
|
||||
borderVariant={border}
|
||||
>
|
||||
|
||||
@@ -6,53 +6,52 @@ A plain container component with configurable background, border, padding, and r
|
||||
|
||||
## Architecture
|
||||
|
||||
Padding and rounding are controlled independently:
|
||||
The `sizeVariant` controls both padding and border-radius, mirroring the same mapping used by `Button` and `Interactive.Container`:
|
||||
|
||||
| `paddingVariant` | Class |
|
||||
|------------------|---------|
|
||||
| `"lg"` | `p-6` |
|
||||
| `"md"` | `p-4` |
|
||||
| `"sm"` | `p-2` |
|
||||
| `"xs"` | `p-1` |
|
||||
| `"2xs"` | `p-0.5` |
|
||||
| `"fit"` | `p-0` |
|
||||
|
||||
| `roundingVariant` | Class |
|
||||
|-------------------|--------------|
|
||||
| `"xs"` | `rounded-04` |
|
||||
| `"sm"` | `rounded-08` |
|
||||
| `"md"` | `rounded-12` |
|
||||
| `"lg"` | `rounded-16` |
|
||||
| Size | Padding | Rounding |
|
||||
|-----------|---------|----------------|
|
||||
| `lg` | `p-2` | `rounded-12` |
|
||||
| `md` | `p-1` | `rounded-08` |
|
||||
| `sm` | `p-1` | `rounded-08` |
|
||||
| `xs` | `p-0.5` | `rounded-04` |
|
||||
| `2xs` | `p-0.5` | `rounded-04` |
|
||||
| `fit` | `p-0` | `rounded-12` |
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `paddingVariant` | `PaddingVariants` | `"sm"` | Padding preset |
|
||||
| `roundingVariant` | `RoundingVariants` | `"md"` | Border-radius preset |
|
||||
| `sizeVariant` | `SizeVariant` | `"lg"` | Controls padding and border-radius |
|
||||
| `backgroundVariant` | `"none" \| "light" \| "heavy"` | `"light"` | Background fill intensity |
|
||||
| `borderVariant` | `"none" \| "dashed" \| "solid"` | `"none"` | Border style |
|
||||
| `ref` | `React.Ref<HTMLDivElement>` | — | Ref forwarded to the root div |
|
||||
| `children` | `React.ReactNode` | — | Card content |
|
||||
|
||||
## Background Variants
|
||||
|
||||
- **`none`** — Transparent background. Use for seamless inline content.
|
||||
- **`light`** — Subtle tinted background (`bg-background-tint-00`). The default, suitable for most cards.
|
||||
- **`heavy`** — Stronger tinted background (`bg-background-tint-01`). Use for emphasis or nested cards that need visual separation.
|
||||
|
||||
## Border Variants
|
||||
|
||||
- **`none`** — No border. Use when cards are visually grouped or in tight layouts.
|
||||
- **`dashed`** — Dashed border. Use for placeholder or empty states.
|
||||
- **`solid`** — Solid border. Use for prominent, standalone cards.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { Card } from "@opal/components";
|
||||
|
||||
// Default card (light background, no border, sm padding, md rounding)
|
||||
// Default card (light background, no border, lg padding + rounding)
|
||||
<Card>
|
||||
<h2>Card Title</h2>
|
||||
<p>Card content</p>
|
||||
</Card>
|
||||
|
||||
// Large padding + rounding with solid border
|
||||
<Card paddingVariant="lg" roundingVariant="lg" borderVariant="solid">
|
||||
<p>Spacious card</p>
|
||||
</Card>
|
||||
|
||||
// Compact card with solid border
|
||||
<Card paddingVariant="xs" roundingVariant="sm" borderVariant="solid">
|
||||
<Card borderVariant="solid" sizeVariant="sm">
|
||||
<p>Compact card</p>
|
||||
</Card>
|
||||
|
||||
@@ -60,4 +59,9 @@ import { Card } from "@opal/components";
|
||||
<Card backgroundVariant="none" borderVariant="dashed">
|
||||
<p>No items yet</p>
|
||||
</Card>
|
||||
|
||||
// Heavy background, tight padding
|
||||
<Card backgroundVariant="heavy" sizeVariant="xs">
|
||||
<p>Highlighted content</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@opal/components/cards/card/styles.css";
|
||||
import type { PaddingVariants, RoundingVariants } from "@opal/types";
|
||||
import type { ContainerSizeVariants } from "@opal/types";
|
||||
import { containerSizeVariants } from "@opal/shared";
|
||||
import { cn } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -11,34 +12,21 @@ type BorderVariant = "none" | "dashed" | "solid";
|
||||
|
||||
type CardProps = {
|
||||
/**
|
||||
* Padding preset.
|
||||
* Size preset — controls padding and border-radius.
|
||||
*
|
||||
* | Value | Class |
|
||||
* |---------|---------|
|
||||
* | `"lg"` | `p-6` |
|
||||
* | `"md"` | `p-4` |
|
||||
* | `"sm"` | `p-2` |
|
||||
* | `"xs"` | `p-1` |
|
||||
* | `"2xs"` | `p-0.5` |
|
||||
* | `"fit"` | `p-0` |
|
||||
* Padding comes from the shared size scale. Rounding follows the same
|
||||
* mapping as `Button` / `Interactive.Container`:
|
||||
*
|
||||
* @default "sm"
|
||||
* | Size | Rounding |
|
||||
* |--------|------------|
|
||||
* | `lg` | `default` |
|
||||
* | `md`–`sm` | `compact` |
|
||||
* | `xs`–`2xs` | `mini` |
|
||||
* | `fit` | `default` |
|
||||
*
|
||||
* @default "lg"
|
||||
*/
|
||||
paddingVariant?: PaddingVariants;
|
||||
|
||||
/**
|
||||
* Border-radius preset.
|
||||
*
|
||||
* | Value | Class |
|
||||
* |--------|--------------|
|
||||
* | `"xs"` | `rounded-04` |
|
||||
* | `"sm"` | `rounded-08` |
|
||||
* | `"md"` | `rounded-12` |
|
||||
* | `"lg"` | `rounded-16` |
|
||||
*
|
||||
* @default "md"
|
||||
*/
|
||||
roundingVariant?: RoundingVariants;
|
||||
sizeVariant?: ContainerSizeVariants;
|
||||
|
||||
/**
|
||||
* Background fill intensity.
|
||||
@@ -67,23 +55,17 @@ type CardProps = {
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mappings
|
||||
// Rounding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const paddingForVariant: Record<PaddingVariants, string> = {
|
||||
lg: "p-6",
|
||||
md: "p-4",
|
||||
sm: "p-2",
|
||||
xs: "p-1",
|
||||
"2xs": "p-0.5",
|
||||
fit: "p-0",
|
||||
};
|
||||
|
||||
const roundingForVariant: Record<RoundingVariants, string> = {
|
||||
lg: "rounded-16",
|
||||
md: "rounded-12",
|
||||
/** Maps a size variant to a rounding class, mirroring the Button pattern. */
|
||||
const roundingForSize: Record<ContainerSizeVariants, string> = {
|
||||
lg: "rounded-12",
|
||||
md: "rounded-08",
|
||||
sm: "rounded-08",
|
||||
xs: "rounded-04",
|
||||
"2xs": "rounded-04",
|
||||
fit: "rounded-12",
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -91,15 +73,14 @@ const roundingForVariant: Record<RoundingVariants, string> = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function Card({
|
||||
paddingVariant = "sm",
|
||||
roundingVariant = "md",
|
||||
sizeVariant = "lg",
|
||||
backgroundVariant = "light",
|
||||
borderVariant = "none",
|
||||
ref,
|
||||
children,
|
||||
}: CardProps) {
|
||||
const padding = paddingForVariant[paddingVariant];
|
||||
const rounding = roundingForVariant[roundingVariant];
|
||||
const { padding } = containerSizeVariants[sizeVariant];
|
||||
const rounding = roundingForSize[sizeVariant];
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -6,12 +6,12 @@ A pre-configured Card for empty states. Renders a transparent card with a dashed
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------------- | --------------------------- | ---------- | ------------------------------------------------ |
|
||||
| `icon` | `IconFunctionComponent` | `SvgEmpty` | Icon displayed alongside the title |
|
||||
| `title` | `string` | — | Primary message text (required) |
|
||||
| `paddingVariant` | `PaddingVariants` | `"sm"` | Padding preset for the card |
|
||||
| `ref` | `React.Ref<HTMLDivElement>` | — | Ref forwarded to the root div |
|
||||
| Prop | Type | Default | Description |
|
||||
| ------------- | -------------------------- | ---------- | ------------------------------------------------ |
|
||||
| `icon` | `IconFunctionComponent` | `SvgEmpty` | Icon displayed alongside the title |
|
||||
| `title` | `string` | — | Primary message text (required) |
|
||||
| `sizeVariant` | `SizeVariant` | `"lg"` | Size preset controlling padding and rounding |
|
||||
| `ref` | `React.Ref<HTMLDivElement>` | — | Ref forwarded to the root div |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -25,6 +25,6 @@ import { SvgSparkle, SvgFileText } from "@opal/icons";
|
||||
// With custom icon
|
||||
<EmptyMessageCard icon={SvgSparkle} title="No agents selected." />
|
||||
|
||||
// With custom padding
|
||||
<EmptyMessageCard paddingVariant="xs" icon={SvgFileText} title="No documents available." />
|
||||
// With custom size
|
||||
<EmptyMessageCard sizeVariant="sm" icon={SvgFileText} title="No documents available." />
|
||||
```
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Card } from "@opal/components/cards/card/components";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { SvgEmpty } from "@opal/icons";
|
||||
import type { IconFunctionComponent, PaddingVariants } from "@opal/types";
|
||||
import type { ContainerSizeVariants } from "@opal/types";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -14,8 +15,8 @@ type EmptyMessageCardProps = {
|
||||
/** Primary message text. */
|
||||
title: string;
|
||||
|
||||
/** Padding preset for the card. */
|
||||
paddingVariant?: PaddingVariants;
|
||||
/** Size preset controlling padding and rounding of the card. */
|
||||
sizeVariant?: ContainerSizeVariants;
|
||||
|
||||
/** Ref forwarded to the root Card div. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
@@ -28,7 +29,7 @@ type EmptyMessageCardProps = {
|
||||
function EmptyMessageCard({
|
||||
icon = SvgEmpty,
|
||||
title,
|
||||
paddingVariant = "sm",
|
||||
sizeVariant = "lg",
|
||||
ref,
|
||||
}: EmptyMessageCardProps) {
|
||||
return (
|
||||
@@ -36,7 +37,7 @@ function EmptyMessageCard({
|
||||
ref={ref}
|
||||
backgroundVariant="none"
|
||||
borderVariant="dashed"
|
||||
paddingVariant={paddingVariant}
|
||||
sizeVariant={sizeVariant}
|
||||
>
|
||||
<Content
|
||||
icon={icon}
|
||||
|
||||
@@ -9,7 +9,7 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heightVariant` | `SizeVariant` | `"lg"` | Height preset (`2xs`–`lg`, `fit`) |
|
||||
| `roundingVariant` | `"md" \| "sm" \| "xs"` | `"md"` | Border-radius preset |
|
||||
| `roundingVariant` | `"default" \| "compact" \| "mini"` | `"default"` | Border-radius preset |
|
||||
| `widthVariant` | `WidthVariant` | — | Width preset (`"auto"`, `"fit"`, `"full"`) |
|
||||
| `border` | `boolean` | `false` | Renders a 1px border |
|
||||
| `type` | `"submit" \| "button" \| "reset"` | — | When set, renders a `<button>` element |
|
||||
@@ -18,7 +18,7 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
|
||||
|
||||
```tsx
|
||||
<Interactive.Stateless variant="default" prominence="primary">
|
||||
<Interactive.Container heightVariant="sm" roundingVariant="sm" border>
|
||||
<Interactive.Container heightVariant="sm" roundingVariant="compact" border>
|
||||
<span>Content</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Route } from "next";
|
||||
import "@opal/core/interactive/shared.css";
|
||||
import React from "react";
|
||||
import { cn } from "@opal/utils";
|
||||
import type { ButtonType, RoundingVariants, WithoutStyles } from "@opal/types";
|
||||
import type { ButtonType, WithoutStyles } from "@opal/types";
|
||||
import {
|
||||
containerSizeVariants,
|
||||
type ContainerSizeVariants,
|
||||
@@ -16,17 +16,19 @@ import { useDisabled } from "@opal/core/disabled/components";
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type InteractiveContainerRoundingVariant = Extract<
|
||||
RoundingVariants,
|
||||
"md" | "sm" | "xs"
|
||||
>;
|
||||
const interactiveContainerRoundingVariants: Record<
|
||||
InteractiveContainerRoundingVariant,
|
||||
string
|
||||
> = {
|
||||
md: "rounded-12",
|
||||
sm: "rounded-08",
|
||||
xs: "rounded-04",
|
||||
/**
|
||||
* Border-radius presets for `Interactive.Container`.
|
||||
*
|
||||
* - `"default"` — Default radius of 0.75rem (12px), matching card rounding
|
||||
* - `"compact"` — Smaller radius of 0.5rem (8px), for tighter/inline elements
|
||||
* - `"mini"` — Smallest radius of 0.25rem (4px)
|
||||
*/
|
||||
type InteractiveContainerRoundingVariant =
|
||||
keyof typeof interactiveContainerRoundingVariants;
|
||||
const interactiveContainerRoundingVariants = {
|
||||
default: "rounded-12",
|
||||
compact: "rounded-08",
|
||||
mini: "rounded-04",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -97,7 +99,7 @@ function InteractiveContainer({
|
||||
ref,
|
||||
type,
|
||||
border,
|
||||
roundingVariant = "md",
|
||||
roundingVariant = "default",
|
||||
heightVariant = "lg",
|
||||
widthVariant = "fit",
|
||||
...props
|
||||
|
||||
@@ -37,35 +37,6 @@ export type SizeVariants = "fit" | "full" | "lg" | "md" | "sm" | "xs" | "2xs";
|
||||
*/
|
||||
export type ContainerSizeVariants = Exclude<SizeVariants, "full">;
|
||||
|
||||
/**
|
||||
* Padding size variants.
|
||||
*
|
||||
* | Variant | Class |
|
||||
* |---------|---------|
|
||||
* | `lg` | `p-6` |
|
||||
* | `md` | `p-4` |
|
||||
* | `sm` | `p-2` |
|
||||
* | `xs` | `p-1` |
|
||||
* | `2xs` | `p-0.5` |
|
||||
* | `fit` | `p-0` |
|
||||
*/
|
||||
export type PaddingVariants = Extract<
|
||||
SizeVariants,
|
||||
"fit" | "lg" | "md" | "sm" | "xs" | "2xs"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Rounding size variants.
|
||||
*
|
||||
* | Variant | Class |
|
||||
* |---------|--------------|
|
||||
* | `lg` | `rounded-16` |
|
||||
* | `md` | `rounded-12` |
|
||||
* | `sm` | `rounded-08` |
|
||||
* | `xs` | `rounded-04` |
|
||||
*/
|
||||
export type RoundingVariants = Extract<SizeVariants, "lg" | "md" | "sm" | "xs">;
|
||||
|
||||
/**
|
||||
* Extreme size variants ("fit" and "full" only).
|
||||
*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@/refresh-pages/admin/HooksPage";
|
||||
@@ -75,14 +75,14 @@ export enum ChatFileType {
|
||||
IMAGE = "image",
|
||||
DOCUMENT = "document",
|
||||
PLAIN_TEXT = "plain_text",
|
||||
TABULAR = "tabular",
|
||||
CSV = "csv",
|
||||
USER_KNOWLEDGE = "user_knowledge",
|
||||
}
|
||||
|
||||
export const isTextFile = (fileType: ChatFileType) =>
|
||||
[
|
||||
ChatFileType.PLAIN_TEXT,
|
||||
ChatFileType.TABULAR,
|
||||
ChatFileType.CSV,
|
||||
ChatFileType.USER_KNOWLEDGE,
|
||||
ChatFileType.DOCUMENT,
|
||||
].includes(fileType);
|
||||
|
||||
@@ -42,12 +42,7 @@ export default function FileDisplay({ files }: FileDisplayProps) {
|
||||
file.type === ChatFileType.DOCUMENT
|
||||
);
|
||||
const imageFiles = files.filter((file) => file.type === ChatFileType.IMAGE);
|
||||
// TODO(danelegend): XLSX files are binary (OOXML) and will fail to parse in CsvContent.
|
||||
// The backend should convert XLSX to CSV text before serving via /api/chat/file,
|
||||
// or XLSX should be split into a separate ChatFileType and rendered as an Attachment.
|
||||
const tabularFiles = files.filter(
|
||||
(file) => file.type === ChatFileType.TABULAR
|
||||
);
|
||||
const csvFiles = files.filter((file) => file.type === ChatFileType.CSV);
|
||||
|
||||
const presentingDocument: MinimalOnyxDocument = {
|
||||
document_id: previewingFile?.id ?? "",
|
||||
@@ -83,9 +78,9 @@ export default function FileDisplay({ files }: FileDisplayProps) {
|
||||
</FileContainer>
|
||||
)}
|
||||
|
||||
{tabularFiles.length > 0 && (
|
||||
{csvFiles.length > 0 && (
|
||||
<FileContainer className="overflow-auto">
|
||||
{tabularFiles.map((file) =>
|
||||
{csvFiles.map((file) =>
|
||||
close ? (
|
||||
<ExpandableContentWrapper
|
||||
key={file.id}
|
||||
|
||||
@@ -51,7 +51,6 @@ export enum UserFileStatus {
|
||||
UPLOADING = "UPLOADING", //UI only
|
||||
PROCESSING = "PROCESSING",
|
||||
COMPLETED = "COMPLETED",
|
||||
SKIPPED = "SKIPPED",
|
||||
FAILED = "FAILED",
|
||||
CANCELED = "CANCELED",
|
||||
DELETING = "DELETING",
|
||||
|
||||
1
web/src/app/ee/admin/hooks/page.tsx
Normal file
1
web/src/app/ee/admin/hooks/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "@/ee/refresh-pages/admin/HooksPage";
|
||||
@@ -1,6 +1,6 @@
|
||||
import useSWR from "swr";
|
||||
import { fetchExecutionLogs } from "@/refresh-pages/admin/HooksPage/svc";
|
||||
import type { HookExecutionRecord } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { fetchExecutionLogs } from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
import type { HookExecutionRecord } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
const ONE_HOUR_MS = 60 * 60 * 1000;
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { HookPointMeta } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { HookPointMeta } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
export function useHookSpecs() {
|
||||
const { data, isLoading, error } = useSWR<HookPointMeta[]>(
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { HookResponse } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { HookResponse } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
export function useHooks() {
|
||||
const { data, isLoading, error, mutate } = useSWR<HookResponse[]>(
|
||||
@@ -22,15 +22,15 @@ import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
|
||||
import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
import {
|
||||
activateHook,
|
||||
deactivateHook,
|
||||
deleteHook,
|
||||
validateHook,
|
||||
} from "@/refresh-pages/admin/HooksPage/svc";
|
||||
import { getHookPointIcon } from "@/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import HookStatusPopover from "@/refresh-pages/admin/HooksPage/HookStatusPopover";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
import { getHookPointIcon } from "@/ee/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import HookStatusPopover from "@/ee/refresh-pages/admin/HooksPage/HookStatusPopover";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-component: disconnect confirmation modal
|
||||
@@ -23,14 +23,14 @@ import {
|
||||
HookAuthError,
|
||||
HookTimeoutError,
|
||||
HookConnectError,
|
||||
} from "@/refresh-pages/admin/HooksPage/svc";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
import type {
|
||||
HookFailStrategy,
|
||||
HookFormState,
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
HookUpdateRequest,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -5,7 +5,7 @@ import { SvgDownload, SvgTextLines } from "@opal/icons";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import { useHookExecutionLogs } from "@/hooks/useHookExecutionLogs";
|
||||
import { useHookExecutionLogs } from "@/ee/hooks/useHookExecutionLogs";
|
||||
import { formatDateTimeLog } from "@/lib/dateUtils";
|
||||
import { downloadFile } from "@/lib/download";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
HookExecutionRecord,
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
interface HookLogsModalProps {
|
||||
open: boolean;
|
||||
@@ -16,12 +16,12 @@ import {
|
||||
SvgXOctagon,
|
||||
} from "@opal/icons";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import { useHookExecutionLogs } from "@/hooks/useHookExecutionLogs";
|
||||
import HookLogsModal from "@/refresh-pages/admin/HooksPage/HookLogsModal";
|
||||
import { useHookExecutionLogs } from "@/ee/hooks/useHookExecutionLogs";
|
||||
import HookLogsModal from "@/ee/refresh-pages/admin/HooksPage/HookLogsModal";
|
||||
import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
interface HookStatusPopoverProps {
|
||||
hook: HookResponse;
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useHookSpecs } from "@/hooks/useHookSpecs";
|
||||
import { useHooks } from "@/hooks/useHooks";
|
||||
import { useHookSpecs } from "@/ee/hooks/useHookSpecs";
|
||||
import { useHooks } from "@/ee/hooks/useHooks";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import { Button } from "@opal/components";
|
||||
import { Content } from "@opal/layouts";
|
||||
@@ -10,13 +10,13 @@ import InputSearch from "@/refresh-components/inputs/InputSearch";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgArrowExchange, SvgExternalLink } from "@opal/icons";
|
||||
import HookFormModal from "@/refresh-pages/admin/HooksPage/HookFormModal";
|
||||
import ConnectedHookCard from "@/refresh-pages/admin/HooksPage/ConnectedHookCard";
|
||||
import { getHookPointIcon } from "@/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import HookFormModal from "@/ee/refresh-pages/admin/HooksPage/HookFormModal";
|
||||
import ConnectedHookCard from "@/ee/refresh-pages/admin/HooksPage/ConnectedHookCard";
|
||||
import { getHookPointIcon } from "@/ee/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
HookResponse,
|
||||
HookUpdateRequest,
|
||||
HookValidateResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
export class HookAuthError extends Error {}
|
||||
export class HookTimeoutError extends Error {}
|
||||
@@ -229,7 +229,7 @@ export const ADMIN_ROUTES = {
|
||||
sidebarLabel: "Document Index Migration",
|
||||
},
|
||||
HOOKS: {
|
||||
path: "/admin/hooks",
|
||||
path: "/ee/admin/hooks",
|
||||
icon: SvgHookNodes,
|
||||
title: "Hook Extensions",
|
||||
sidebarLabel: "Hook Extensions",
|
||||
|
||||
@@ -664,12 +664,12 @@ export function ProjectsProvider({ children }: ProjectsProviderProps) {
|
||||
return changed ? Array.from(map.values()) : prev;
|
||||
});
|
||||
|
||||
// Remove completed/skipped/failed from tracking
|
||||
// Remove completed/failed from tracking
|
||||
const remaining = new Set(trackedUploadIds);
|
||||
const newlyFailed: ProjectFile[] = [];
|
||||
for (const f of statuses) {
|
||||
const s = String(f.status).toLowerCase();
|
||||
if (s === "completed" || s === "skipped") {
|
||||
if (s === "completed") {
|
||||
remaining.delete(f.id);
|
||||
} else if (s === "failed") {
|
||||
remaining.delete(f.id);
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function SidebarTab({
|
||||
group="group/SidebarTab"
|
||||
>
|
||||
<Interactive.Container
|
||||
roundingVariant="sm"
|
||||
roundingVariant="compact"
|
||||
heightVariant="lg"
|
||||
widthVariant="full"
|
||||
type={type}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
|
||||
variant="select-tinted"
|
||||
width="full"
|
||||
justifyContent="between"
|
||||
roundingVariant="sm"
|
||||
roundingVariant="compact"
|
||||
>
|
||||
{USER_ROLE_LABELS[user.role]}
|
||||
</OpenButton>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Suggestions({ onSubmit }: SuggestionsProps) {
|
||||
>
|
||||
<Interactive.Container
|
||||
widthVariant="full"
|
||||
roundingVariant="sm"
|
||||
roundingVariant="compact"
|
||||
heightVariant="lg"
|
||||
>
|
||||
<Content
|
||||
|
||||
@@ -225,11 +225,7 @@ function BedrockModalInternals({
|
||||
</FieldWrapper>
|
||||
|
||||
{authMethod === AUTH_METHOD_ACCESS_KEY && (
|
||||
<Card
|
||||
backgroundVariant="light"
|
||||
borderVariant="none"
|
||||
paddingVariant="sm"
|
||||
>
|
||||
<Card backgroundVariant="light" borderVariant="none" sizeVariant="lg">
|
||||
<Section gap={1}>
|
||||
<InputLayouts.Vertical
|
||||
name={FIELD_AWS_ACCESS_KEY_ID}
|
||||
@@ -267,11 +263,7 @@ function BedrockModalInternals({
|
||||
)}
|
||||
|
||||
{authMethod === AUTH_METHOD_LONG_TERM_API_KEY && (
|
||||
<Card
|
||||
backgroundVariant="light"
|
||||
borderVariant="none"
|
||||
paddingVariant="sm"
|
||||
>
|
||||
<Card backgroundVariant="light" borderVariant="none" sizeVariant="lg">
|
||||
<Section gap={0.5}>
|
||||
<InputLayouts.Vertical
|
||||
name={FIELD_AWS_BEARER_TOKEN_BEDROCK}
|
||||
|
||||
@@ -140,7 +140,7 @@ function OllamaModalInternals({
|
||||
isTesting={isTesting}
|
||||
isSubmitting={formikProps.isSubmitting}
|
||||
>
|
||||
<Card backgroundVariant="light" borderVariant="none" paddingVariant="sm">
|
||||
<Card backgroundVariant="light" borderVariant="none" sizeVariant="lg">
|
||||
<Tabs defaultValue={defaultTab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value={TAB_SELF_HOSTED}>
|
||||
|
||||
@@ -224,6 +224,9 @@ export function ModelsAccessField<T extends BaseLLMFormValues>({
|
||||
);
|
||||
}
|
||||
|
||||
const hasSelections =
|
||||
selectedGroupIds.length > 0 || selectedAgentIds.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
<FieldWrapper>
|
||||
@@ -250,11 +253,7 @@ export function ModelsAccessField<T extends BaseLLMFormValues>({
|
||||
</FieldWrapper>
|
||||
|
||||
{!isPublic && (
|
||||
<Card
|
||||
backgroundVariant="light"
|
||||
borderVariant="none"
|
||||
paddingVariant="sm"
|
||||
>
|
||||
<Card backgroundVariant="light" borderVariant="none" sizeVariant="lg">
|
||||
<Section gap={0.5}>
|
||||
<InputComboBox
|
||||
placeholder="Add groups and agents"
|
||||
@@ -452,7 +451,7 @@ export function ModelsField<T extends BaseLLMFormValues>({
|
||||
const visibleModels = modelConfigurations.filter((m) => m.is_visible);
|
||||
|
||||
return (
|
||||
<Card backgroundVariant="light" borderVariant="none" paddingVariant="sm">
|
||||
<Card backgroundVariant="light" borderVariant="none" sizeVariant="lg">
|
||||
<Section gap={0.5}>
|
||||
<InputLayouts.Horizontal
|
||||
title="Models"
|
||||
|
||||
Reference in New Issue
Block a user