mirror of
https://github.com/langgenius/dify.git
synced 2026-03-14 11:47:05 +00:00
Compare commits
8 Commits
move-trigg
...
yanli/docx
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a98c467a33 | ||
|
|
cb094558e3 | ||
|
|
60c858aa48 | ||
|
|
46a3a2ae09 | ||
|
|
30af50cb47 | ||
|
|
0d6a4bac0f | ||
|
|
c693cb9789 | ||
|
|
fc91a7a38b |
@@ -114,7 +114,6 @@ class PdfExtractor(BaseExtractor):
|
||||
"""
|
||||
image_content = []
|
||||
upload_files = []
|
||||
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
|
||||
|
||||
try:
|
||||
image_objects = page.get_objects(filter=(pdfium_c.FPDF_PAGEOBJ_IMAGE,))
|
||||
@@ -164,7 +163,7 @@ class PdfExtractor(BaseExtractor):
|
||||
used_at=naive_utc_now(),
|
||||
)
|
||||
upload_files.append(upload_file)
|
||||
image_content.append(f"")
|
||||
image_content.append(f"")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to extract image from PDF: %s", e)
|
||||
continue
|
||||
|
||||
@@ -87,7 +87,6 @@ class WordExtractor(BaseExtractor):
|
||||
def _extract_images_from_docx(self, doc):
|
||||
image_count = 0
|
||||
image_map = {}
|
||||
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
|
||||
|
||||
for r_id, rel in doc.part.rels.items():
|
||||
if "image" in rel.target_ref:
|
||||
@@ -126,7 +125,7 @@ class WordExtractor(BaseExtractor):
|
||||
used_at=naive_utc_now(),
|
||||
)
|
||||
db.session.add(upload_file)
|
||||
image_map[r_id] = f""
|
||||
image_map[r_id] = f""
|
||||
else:
|
||||
image_ext = rel.target_ref.split(".")[-1]
|
||||
if image_ext is None:
|
||||
@@ -154,7 +153,7 @@ class WordExtractor(BaseExtractor):
|
||||
used_at=naive_utc_now(),
|
||||
)
|
||||
db.session.add(upload_file)
|
||||
image_map[rel.target_part] = f""
|
||||
image_map[rel.target_part] = f""
|
||||
db.session.commit()
|
||||
return image_map
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from json import JSONDecodeError
|
||||
from operator import itemgetter
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -806,41 +807,53 @@ class DocumentSegment(Base):
|
||||
def sign_content(self) -> str:
|
||||
return self.get_sign_content()
|
||||
|
||||
@staticmethod
|
||||
def _build_signed_query_params(*, sign_target: str, upload_file_id: str) -> str:
|
||||
nonce = os.urandom(16).hex()
|
||||
timestamp = str(int(time.time()))
|
||||
data_to_sign = f"{sign_target}|{upload_file_id}|{timestamp}|{nonce}"
|
||||
secret_key = dify_config.SECRET_KEY.encode() if dify_config.SECRET_KEY else b""
|
||||
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
return f"timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
|
||||
|
||||
def _get_accessible_upload_file_ids(self, upload_file_ids: set[str]) -> set[str]:
|
||||
if not upload_file_ids:
|
||||
return set()
|
||||
|
||||
matched_upload_file_ids = db.session.scalars(
|
||||
select(UploadFile.id).where(
|
||||
UploadFile.tenant_id == self.tenant_id,
|
||||
UploadFile.id.in_(list(upload_file_ids)),
|
||||
)
|
||||
).all()
|
||||
return {str(upload_file_id) for upload_file_id in matched_upload_file_ids}
|
||||
|
||||
def get_sign_content(self) -> str:
|
||||
signed_urls: list[tuple[int, int, str]] = []
|
||||
text = self.content
|
||||
|
||||
# For data before v0.10.0
|
||||
pattern = r"/files/([a-f0-9\-]+)/image-preview(?:\?.*?)?"
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
upload_file_id = match.group(1)
|
||||
nonce = os.urandom(16).hex()
|
||||
timestamp = str(int(time.time()))
|
||||
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
|
||||
secret_key = dify_config.SECRET_KEY.encode() if dify_config.SECRET_KEY else b""
|
||||
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
upload_file_preview_patterns = {
|
||||
"image-preview": r"(?:https?://[^\s\)\"\']+)?/files/([a-f0-9\-]+)/image-preview(?:\?[^\s\)\"\']*)?",
|
||||
"file-preview": r"(?:https?://[^\s\)\"\']+)?/files/([a-f0-9\-]+)/file-preview(?:\?[^\s\)\"\']*)?",
|
||||
}
|
||||
upload_file_matches: list[tuple[re.Match[str], str, str]] = []
|
||||
upload_file_ids: set[str] = set()
|
||||
|
||||
params = f"timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
|
||||
base_url = f"/files/{upload_file_id}/image-preview"
|
||||
signed_url = f"{base_url}?{params}"
|
||||
signed_urls.append((match.start(), match.end(), signed_url))
|
||||
for preview_type, pattern in upload_file_preview_patterns.items():
|
||||
for match in re.finditer(pattern, text):
|
||||
upload_file_id = match.group(1)
|
||||
upload_file_matches.append((match, preview_type, upload_file_id))
|
||||
upload_file_ids.add(upload_file_id)
|
||||
|
||||
# For data after v0.10.0
|
||||
pattern = r"/files/([a-f0-9\-]+)/file-preview(?:\?.*?)?"
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
upload_file_id = match.group(1)
|
||||
nonce = os.urandom(16).hex()
|
||||
timestamp = str(int(time.time()))
|
||||
data_to_sign = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
|
||||
secret_key = dify_config.SECRET_KEY.encode() if dify_config.SECRET_KEY else b""
|
||||
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
accessible_upload_file_ids = self._get_accessible_upload_file_ids(upload_file_ids)
|
||||
|
||||
params = f"timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
|
||||
base_url = f"/files/{upload_file_id}/file-preview"
|
||||
for match, preview_type, upload_file_id in upload_file_matches:
|
||||
if upload_file_id not in accessible_upload_file_ids:
|
||||
continue
|
||||
|
||||
params = self._build_signed_query_params(sign_target=preview_type, upload_file_id=upload_file_id)
|
||||
base_url = f"/files/{upload_file_id}/{preview_type}"
|
||||
signed_url = f"{base_url}?{params}"
|
||||
signed_urls.append((match.start(), match.end(), signed_url))
|
||||
|
||||
@@ -851,19 +864,13 @@ class DocumentSegment(Base):
|
||||
for match in matches:
|
||||
upload_file_id = match.group(1)
|
||||
file_extension = match.group(2)
|
||||
nonce = os.urandom(16).hex()
|
||||
timestamp = str(int(time.time()))
|
||||
data_to_sign = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
|
||||
secret_key = dify_config.SECRET_KEY.encode() if dify_config.SECRET_KEY else b""
|
||||
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
|
||||
params = f"timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
|
||||
params = self._build_signed_query_params(sign_target="file-preview", upload_file_id=upload_file_id)
|
||||
base_url = f"/files/tools/{upload_file_id}.{file_extension}"
|
||||
signed_url = f"{base_url}?{params}"
|
||||
signed_urls.append((match.start(), match.end(), signed_url))
|
||||
|
||||
# Reconstruct the text with signed URLs
|
||||
signed_urls.sort(key=itemgetter(0))
|
||||
offset = 0
|
||||
for start, end, signed_url in signed_urls:
|
||||
text = text[: start + offset] + signed_url + text[end + offset :]
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_extract_images_formats(mock_dependencies, monkeypatch, image_bytes, exp
|
||||
mock_raw.FPDF_PAGEOBJ_IMAGE = 1
|
||||
result = extractor._extract_images(mock_page)
|
||||
|
||||
assert f"" in result
|
||||
assert f"" in result
|
||||
assert len(saves) == 1
|
||||
assert saves[0][1] == image_bytes
|
||||
assert len(db_stub.session.added) == 1
|
||||
@@ -180,7 +180,7 @@ def test_extract_images_failures(mock_dependencies):
|
||||
result = extractor._extract_images(mock_page)
|
||||
|
||||
# Should have one success
|
||||
assert "" in result
|
||||
assert "" in result
|
||||
assert len(saves) == 1
|
||||
assert saves[0][1] == jpeg_bytes
|
||||
assert db_stub.session.committed is True
|
||||
|
||||
@@ -124,8 +124,7 @@ def test_extract_images_from_docx(monkeypatch):
|
||||
db_stub = SimpleNamespace(session=DummySession())
|
||||
monkeypatch.setattr(we, "db", db_stub)
|
||||
|
||||
# Patch config values used for URL composition and storage type
|
||||
monkeypatch.setattr(we.dify_config, "FILES_URL", "http://files.local", raising=False)
|
||||
# Patch config value used in this code path
|
||||
monkeypatch.setattr(we.dify_config, "STORAGE_TYPE", "local", raising=False)
|
||||
|
||||
# Patch UploadFile to avoid real DB models
|
||||
@@ -167,7 +166,7 @@ def test_extract_images_from_docx(monkeypatch):
|
||||
|
||||
# Returned map should contain entries for external (keyed by rId) and internal (keyed by target_part)
|
||||
assert set(image_map.keys()) == {"rId1", internal_part}
|
||||
assert all(v.startswith(" and v.endswith("/file-preview)") for v in image_map.values())
|
||||
assert all(v.startswith(" and v.endswith("/file-preview)") for v in image_map.values())
|
||||
|
||||
# Storage should receive both payloads
|
||||
payloads = {data for _, data in saves}
|
||||
@@ -179,37 +178,6 @@ def test_extract_images_from_docx(monkeypatch):
|
||||
assert db_stub.session.committed is True
|
||||
|
||||
|
||||
def test_extract_images_from_docx_uses_internal_files_url():
|
||||
"""Test that INTERNAL_FILES_URL takes precedence over FILES_URL for plugin access."""
|
||||
# Test the URL generation logic directly
|
||||
from configs import dify_config
|
||||
|
||||
# Mock the configuration values
|
||||
original_files_url = getattr(dify_config, "FILES_URL", None)
|
||||
original_internal_files_url = getattr(dify_config, "INTERNAL_FILES_URL", None)
|
||||
|
||||
try:
|
||||
# Set both URLs - INTERNAL should take precedence
|
||||
dify_config.FILES_URL = "http://external.example.com"
|
||||
dify_config.INTERNAL_FILES_URL = "http://internal.docker:5001"
|
||||
|
||||
# Test the URL generation logic (same as in word_extractor.py)
|
||||
upload_file_id = "test_file_id"
|
||||
|
||||
# This is the pattern we fixed in the word extractor
|
||||
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
|
||||
generated_url = f"{base_url}/files/{upload_file_id}/file-preview"
|
||||
|
||||
# Verify that INTERNAL_FILES_URL is used instead of FILES_URL
|
||||
assert "http://internal.docker:5001" in generated_url, f"Expected internal URL, got: {generated_url}"
|
||||
assert "http://external.example.com" not in generated_url, f"Should not use external URL, got: {generated_url}"
|
||||
|
||||
finally:
|
||||
# Restore original values
|
||||
dify_config.FILES_URL = original_files_url
|
||||
dify_config.INTERNAL_FILES_URL = original_internal_files_url
|
||||
|
||||
|
||||
def test_extract_hyperlinks(monkeypatch):
|
||||
# Mock db and storage to avoid issues during image extraction (even if no images are present)
|
||||
monkeypatch.setattr(we, "storage", SimpleNamespace(save=lambda k, d: None))
|
||||
|
||||
@@ -15,6 +15,7 @@ from datetime import UTC, datetime
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import models.dataset as dataset_module
|
||||
from models.dataset import (
|
||||
AppDatasetJoin,
|
||||
ChildChunk,
|
||||
@@ -489,6 +490,15 @@ class TestDocumentModelRelationships:
|
||||
class TestDocumentSegmentIndexing:
|
||||
"""Test suite for DocumentSegment model indexing and operations."""
|
||||
|
||||
@staticmethod
|
||||
def _mock_scalars_result(upload_file_ids: list[str]):
|
||||
class _ScalarsResult:
|
||||
@staticmethod
|
||||
def all() -> list[str]:
|
||||
return upload_file_ids
|
||||
|
||||
return _ScalarsResult()
|
||||
|
||||
def test_document_segment_creation_with_required_fields(self):
|
||||
"""Test creating a document segment with all required fields."""
|
||||
# Arrange
|
||||
@@ -547,6 +557,139 @@ class TestDocumentSegmentIndexing:
|
||||
assert segment.index_node_hash == index_node_hash
|
||||
assert segment.keywords == keywords
|
||||
|
||||
def test_document_segment_sign_content_strips_absolute_files_host(self):
|
||||
"""Test that sign_content strips scheme/host from absolute /files URLs and returns a signed relative URL."""
|
||||
# Arrange
|
||||
upload_file_id = "1602650a-4fe4-423c-85a2-af76c083e3c4"
|
||||
segment = DocumentSegment(
|
||||
tenant_id=str(uuid4()),
|
||||
dataset_id=str(uuid4()),
|
||||
document_id=str(uuid4()),
|
||||
position=1,
|
||||
content=f"",
|
||||
word_count=1,
|
||||
tokens=1,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
mock_scalars_result = self._mock_scalars_result([upload_file_id])
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(dataset_module.dify_config, "SECRET_KEY", "secret", create=True),
|
||||
patch("models.dataset.db.session.scalars", return_value=mock_scalars_result),
|
||||
patch("models.dataset.time.time", return_value=1700000000),
|
||||
patch("models.dataset.os.urandom", return_value=b"\x00" * 16),
|
||||
):
|
||||
signed = segment.get_sign_content()
|
||||
|
||||
# Assert
|
||||
assert "internal.docker:5001" not in signed
|
||||
assert f"/files/{upload_file_id}/file-preview?timestamp=" in signed
|
||||
assert "&nonce=" in signed
|
||||
assert "&sign=" in signed
|
||||
|
||||
def test_document_segment_sign_content_strips_absolute_files_host_for_image_preview(self):
|
||||
"""Test that sign_content strips scheme/host from absolute image-preview URLs."""
|
||||
# Arrange
|
||||
upload_file_id = "e2a4f7b1-1234-5678-9abc-def012345678"
|
||||
segment = DocumentSegment(
|
||||
tenant_id=str(uuid4()),
|
||||
dataset_id=str(uuid4()),
|
||||
document_id=str(uuid4()),
|
||||
position=1,
|
||||
content=f"",
|
||||
word_count=1,
|
||||
tokens=1,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
mock_scalars_result = self._mock_scalars_result([upload_file_id])
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(dataset_module.dify_config, "SECRET_KEY", "secret", create=True),
|
||||
patch("models.dataset.db.session.scalars", return_value=mock_scalars_result),
|
||||
patch("models.dataset.time.time", return_value=1700000000),
|
||||
patch("models.dataset.os.urandom", return_value=b"\x00" * 16),
|
||||
):
|
||||
signed = segment.get_sign_content()
|
||||
|
||||
# Assert
|
||||
assert "internal.docker:5001" not in signed
|
||||
assert f"/files/{upload_file_id}/image-preview?timestamp=" in signed
|
||||
assert "&nonce=" in signed
|
||||
assert "&sign=" in signed
|
||||
|
||||
def test_document_segment_sign_content_skips_upload_files_outside_tenant(self):
|
||||
"""Test that sign_content only signs upload files belonging to the segment tenant."""
|
||||
# Arrange
|
||||
allowed_upload_file_id = "1602650a-4fe4-423c-85a2-af76c083e3c4"
|
||||
denied_upload_file_id = "f8f35fca-568f-4626-adf0-4f30de96aa32"
|
||||
segment = DocumentSegment(
|
||||
tenant_id=str(uuid4()),
|
||||
dataset_id=str(uuid4()),
|
||||
document_id=str(uuid4()),
|
||||
position=1,
|
||||
content=(
|
||||
f"allowed:  "
|
||||
f"denied: "
|
||||
),
|
||||
word_count=1,
|
||||
tokens=1,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
mock_scalars_result = self._mock_scalars_result([allowed_upload_file_id])
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(dataset_module.dify_config, "SECRET_KEY", "secret", create=True),
|
||||
patch("models.dataset.db.session.scalars", return_value=mock_scalars_result),
|
||||
patch("models.dataset.time.time", return_value=1700000000),
|
||||
patch("models.dataset.os.urandom", return_value=b"\x00" * 16),
|
||||
):
|
||||
signed = segment.get_sign_content()
|
||||
|
||||
# Assert
|
||||
assert f"/files/{allowed_upload_file_id}/file-preview?timestamp=" in signed
|
||||
assert f"/files/{denied_upload_file_id}/file-preview?timestamp=" not in signed
|
||||
assert f"/files/{denied_upload_file_id}/file-preview)" in signed
|
||||
|
||||
def test_document_segment_sign_content_handles_mixed_preview_order(self):
|
||||
"""Test that sign_content preserves content when file-preview appears before image-preview."""
|
||||
# Arrange
|
||||
file_preview_id = "1602650a-4fe4-423c-85a2-af76c083e3c4"
|
||||
image_preview_id = "e2a4f7b1-1234-5678-9abc-def012345678"
|
||||
segment = DocumentSegment(
|
||||
tenant_id=str(uuid4()),
|
||||
dataset_id=str(uuid4()),
|
||||
document_id=str(uuid4()),
|
||||
position=1,
|
||||
content=(
|
||||
f"file-first:  "
|
||||
f"then-image: "
|
||||
),
|
||||
word_count=1,
|
||||
tokens=1,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
mock_scalars_result = self._mock_scalars_result([file_preview_id, image_preview_id])
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(dataset_module.dify_config, "SECRET_KEY", "secret", create=True),
|
||||
patch("models.dataset.db.session.scalars", return_value=mock_scalars_result),
|
||||
patch("models.dataset.time.time", return_value=1700000000),
|
||||
patch("models.dataset.os.urandom", return_value=b"\x00" * 16),
|
||||
):
|
||||
signed = segment.get_sign_content()
|
||||
|
||||
# Assert
|
||||
file_signed = f"/files/{file_preview_id}/file-preview?timestamp="
|
||||
image_signed = f"/files/{image_preview_id}/image-preview?timestamp="
|
||||
assert file_signed in signed
|
||||
assert image_signed in signed
|
||||
assert signed.index(file_signed) < signed.index(image_signed)
|
||||
assert signed.count("&sign=") == 2
|
||||
|
||||
def test_document_segment_with_answer_field(self):
|
||||
"""Test creating a document segment with answer field for QA model."""
|
||||
# Arrange
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('App Card Operations Flow', () => {
|
||||
|
||||
// Find and click the more button (popover trigger)
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
const moreFill = [...moreIcons].find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
@@ -364,7 +364,7 @@ describe('App Card Operations Flow', () => {
|
||||
renderAppCard({ id: 'app-edit', name: 'Editable App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
const moreFill = [...moreIcons].find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
@@ -400,7 +400,7 @@ describe('App Card Operations Flow', () => {
|
||||
renderAppCard({ id: 'app-export', mode: AppModeEnum.COMPLETION, name: 'Export App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
const moreFill = [...moreIcons].find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
@@ -439,7 +439,7 @@ describe('App Card Operations Flow', () => {
|
||||
renderAppCard({ id: 'app-switch', mode: AppModeEnum.CHAT })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
const moreFill = [...moreIcons].find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
@@ -456,7 +456,7 @@ describe('App Card Operations Flow', () => {
|
||||
renderAppCard({ id: 'app-wf', mode: AppModeEnum.WORKFLOW, name: 'WF App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
const moreFill = [...moreIcons].find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
|
||||
@@ -588,7 +588,7 @@ export default translation
|
||||
const trimmedKeyLine = keyLine.trim()
|
||||
|
||||
// If key line ends with ":" (not complete value), it's likely multiline
|
||||
if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !/:\s*['"`]/.exec(trimmedKeyLine)) {
|
||||
if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !/:\s*['"`]/.test(trimmedKeyLine)) {
|
||||
// Find the value lines that belong to this key
|
||||
let currentLine = targetLineIndex + 1
|
||||
let foundValue = false
|
||||
@@ -604,7 +604,7 @@ export default translation
|
||||
}
|
||||
|
||||
// Check if this line starts a new key (indicates end of current value)
|
||||
if (/^\w+\s*:/.exec(trimmed))
|
||||
if (/^\w+\s*:/.test(trimmed))
|
||||
break
|
||||
|
||||
// Check if this line is part of the value
|
||||
@@ -632,7 +632,7 @@ export default translation
|
||||
}
|
||||
|
||||
// Remove duplicates and sort in reverse order
|
||||
const uniqueLinesToRemove = [...new Set(linesToRemove)].sort((a, b) => b - a)
|
||||
const uniqueLinesToRemove = new Set(linesToRemove).toSorted((a, b) => b - a)
|
||||
|
||||
for (const lineIndex of uniqueLinesToRemove)
|
||||
lines.splice(lineIndex, 1)
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('Document Management Flow', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.options.history).toBe('replace')
|
||||
expect(update.searchParams.get('keyword')).toBe('test')
|
||||
expect(update.searchParams.get('page')).toBe('2')
|
||||
@@ -123,7 +123,7 @@ describe('Document Management Flow', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.options.history).toBe('replace')
|
||||
expect(update.searchParams.toString()).toBe('')
|
||||
})
|
||||
|
||||
@@ -325,12 +325,12 @@ describe('Metadata Management Flow - Cross-Module Validation Composition', () =>
|
||||
|
||||
// Validate the same name multiple times for consistency
|
||||
const name = 'consistent_field'
|
||||
const results = Array.from({ length: 5 }, () => result.current.checkName(name))
|
||||
const results = Array.from({ length: 5 }).fill(result.current.checkName(name))
|
||||
|
||||
expect(results.every(r => r.errorMsg === '')).toBe(true)
|
||||
|
||||
// Validate an invalid name multiple times
|
||||
const invalidResults = Array.from({ length: 5 }, () => result.current.checkName('Invalid'))
|
||||
const invalidResults = Array.from({ length: 5 }).fill(result.current.checkName('Invalid'))
|
||||
expect(invalidResults.every(r => r.errorMsg !== '')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -327,11 +327,11 @@ const ConfigPopup: FC<PopupProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<TracingIcon size="md" className="mr-2" />
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
|
||||
<div className="text-text-primary title-2xl-semi-bold">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
||||
<div className={cn('ml-1 text-text-tertiary system-xs-semibold-uppercase', enabled && 'text-util-colors-green-green-600')}>
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
@@ -350,7 +350,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-xs-regular mt-2 text-text-tertiary">
|
||||
<div className="mt-2 text-text-tertiary system-xs-regular">
|
||||
{t(`${I18N_PREFIX}.tracingDescription`, { ns: 'app' })}
|
||||
</div>
|
||||
<Divider className="my-3" />
|
||||
@@ -358,7 +358,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
||||
{(providerAllConfigured || providerAllNotConfigured)
|
||||
? (
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`, { ns: 'app' })}</div>
|
||||
<div className="text-text-tertiary system-xs-medium-uppercase">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
|
||||
{langfusePanel}
|
||||
{langSmithPanel}
|
||||
@@ -375,11 +375,11 @@ const ConfigPopup: FC<PopupProps> = ({
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`, { ns: 'app' })}</div>
|
||||
<div className="text-text-tertiary system-xs-medium-uppercase">{t(`${I18N_PREFIX}.configProviderTitle.configured`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{configuredProviderPanel()}
|
||||
</div>
|
||||
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}</div>
|
||||
<div className="mt-3 text-text-tertiary system-xs-medium-uppercase">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{moreProviderPanel()}
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
isShow={isShowDeleteConfirm}
|
||||
onClose={() => setIsShowDeleteConfirm(false)}
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<div className="mb-3 text-text-primary title-2xl-semi-bold">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
|
||||
@@ -209,14 +209,14 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<div className="text-text-warning body-md-medium">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content1"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
@@ -241,19 +241,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
)}
|
||||
{step === STEP.verifyOrigin && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content2"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
@@ -278,25 +278,25 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
<span onClick={sendCodeToOriginEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.newEmail && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
<div className="text-text-secondary body-md-regular">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
|
||||
@@ -305,10 +305,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
destructive={newEmailExited || unAvailableEmail}
|
||||
/>
|
||||
{newEmailExited && (
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
{unAvailableEmail && (
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
@@ -331,19 +331,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
)}
|
||||
{step === STEP.verifyNew && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content4"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email: mail }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
@@ -368,13 +368,13 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
<span onClick={sendCodeToNewEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function AccountPage() {
|
||||
imageUrl={icon_url}
|
||||
/>
|
||||
</div>
|
||||
<div className="system-sm-medium mt-[3px] text-text-secondary">{item.name}</div>
|
||||
<div className="mt-[3px] text-text-secondary system-sm-medium">{item.name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -146,12 +146,12 @@ export default function AccountPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="pb-3 pt-2">
|
||||
<h4 className="title-2xl-semi-bold text-text-primary">{t('account.myAccount', { ns: 'common' })}</h4>
|
||||
<h4 className="text-text-primary title-2xl-semi-bold">{t('account.myAccount', { ns: 'common' })}</h4>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
|
||||
<div className="ml-4">
|
||||
<p className="system-xl-semibold text-text-primary">
|
||||
<p className="text-text-primary system-xl-semibold">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
|
||||
@@ -160,16 +160,16 @@ export default function AccountPage() {
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</p>
|
||||
<p className="system-xs-regular text-text-tertiary">{userProfile.email}</p>
|
||||
<p className="text-text-tertiary system-xs-regular">{userProfile.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled system-sm-regular">
|
||||
<span className="pl-1">{userProfile.name}</span>
|
||||
</div>
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={handleEditName}>
|
||||
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text system-sm-medium" onClick={handleEditName}>
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,11 +177,11 @@ export default function AccountPage() {
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('account.email', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled system-sm-regular">
|
||||
<span className="pl-1">{userProfile.email}</span>
|
||||
</div>
|
||||
{systemFeatures.enable_change_email && (
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
|
||||
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text system-sm-medium" onClick={() => setShowUpdateEmail(true)}>
|
||||
{t('operation.change', { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
@@ -191,8 +191,8 @@ export default function AccountPage() {
|
||||
systemFeatures.enable_email_password_login && (
|
||||
<div className="mb-8 flex justify-between gap-2">
|
||||
<div>
|
||||
<div className="system-sm-semibold mb-1 text-text-secondary">{t('account.password', { ns: 'common' })}</div>
|
||||
<div className="body-xs-regular mb-2 text-text-tertiary">{t('account.passwordTip', { ns: 'common' })}</div>
|
||||
<div className="mb-1 text-text-secondary system-sm-semibold">{t('account.password', { ns: 'common' })}</div>
|
||||
<div className="mb-2 text-text-tertiary body-xs-regular">{t('account.passwordTip', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
@@ -219,7 +219,7 @@ export default function AccountPage() {
|
||||
onClose={() => setEditNameModalVisible(false)}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
@@ -249,7 +249,7 @@ export default function AccountPage() {
|
||||
}}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
@@ -272,7 +272,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
@@ -291,7 +291,7 @@ export default function AccountPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
|
||||
@@ -61,7 +61,7 @@ function setupDomMeasurements(navWidth: number, moreWidth: number, childWidths:
|
||||
if (this.id === 'more-measure')
|
||||
return moreWidth
|
||||
if (this.dataset.targetid) {
|
||||
const idx = Array.from(this.parentElement?.children ?? []).indexOf(this)
|
||||
const idx = [...this.parentElement?.children ?? []].indexOf(this)
|
||||
return childWidths[idx] ?? 50
|
||||
}
|
||||
return 0
|
||||
@@ -220,7 +220,7 @@ describe('AppOperations', () => {
|
||||
const ops = [createOperation('edit', 'Edit')]
|
||||
const { container } = render(<AppOperations gap={4} operations={ops} />)
|
||||
const containers = container.querySelectorAll('div[style]')
|
||||
const visibleContainer = Array.from(containers).find(
|
||||
const visibleContainer = [...containers].find(
|
||||
el => el.getAttribute('aria-hidden') !== 'true',
|
||||
)
|
||||
if (visibleContainer)
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('AppInfo', () => {
|
||||
|
||||
const { unmount } = render(<AppInfo expand={false} />)
|
||||
const triggers = screen.getAllByTestId('trigger')
|
||||
expect(triggers[triggers.length - 1]).toHaveAttribute('data-expand', 'false')
|
||||
expect(triggers.at(-1)).toHaveAttribute('data-expand', 'false')
|
||||
unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ const AppOperations = ({
|
||||
pre[cur.id] = false
|
||||
return pre
|
||||
}, {} as Record<string, boolean>)
|
||||
const childrens = Array.from(navElement.children).slice(0, -1)
|
||||
const childrens = [...navElement.children].slice(0, -1)
|
||||
for (let i = 0; i < childrens.length; i++) {
|
||||
const child = childrens[i] as HTMLElement
|
||||
const id = child.dataset.targetid
|
||||
|
||||
@@ -48,7 +48,7 @@ const CSVUploader: FC<Props> = ({
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
return
|
||||
@@ -94,7 +94,7 @@ const CSVUploader: FC<Props> = ({
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('system-sm-regular flex h-20 items-center rounded-xl border border-dashed border-components-dropzone-border bg-components-dropzone-bg', dragging && 'border border-components-dropzone-border-accent bg-components-dropzone-bg-accent')}>
|
||||
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-components-dropzone-border bg-components-dropzone-bg system-sm-regular', dragging && 'border border-components-dropzone-border-accent bg-components-dropzone-bg-accent')}>
|
||||
<div className="flex w-full items-center justify-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-tertiary">
|
||||
|
||||
@@ -122,7 +122,7 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
}
|
||||
const handleBlur = () => {
|
||||
const keys = getVars(value)
|
||||
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.find(item => item.variable === key)).map(key => getNewVar(key, ''))
|
||||
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.some(item => item.variable === key)).map(key => getNewVar(key, ''))
|
||||
if (newPromptVariables.length > 0) {
|
||||
setNewPromptVariables(newPromptVariables)
|
||||
showConfirmAddVar()
|
||||
|
||||
@@ -110,7 +110,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
// Filter out keys that are not properly defined (either not exist or exist but without valid name)
|
||||
const newPromptVariables = keys.filter((key) => {
|
||||
// Check if key exists in external data tools
|
||||
if (externalDataToolsConfig.find((item: ExternalDataTool) => item.variable === key))
|
||||
if (externalDataToolsConfig.some((item: ExternalDataTool) => item.variable === key))
|
||||
return false
|
||||
|
||||
// Check if key exists in prompt variables
|
||||
@@ -178,7 +178,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
{!noTitle && (
|
||||
<div className="flex h-11 items-center justify-between pl-3 pr-2.5">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="h2 system-sm-semibold-uppercase text-text-secondary">{mode !== AppModeEnum.COMPLETION ? t('chatSubTitle', { ns: 'appDebug' }) : t('completionSubTitle', { ns: 'appDebug' })}</div>
|
||||
<div className="h2 text-text-secondary system-sm-semibold-uppercase">{mode !== AppModeEnum.COMPLETION ? t('chatSubTitle', { ns: 'appDebug' }) : t('completionSubTitle', { ns: 'appDebug' })}</div>
|
||||
{!readonly && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
|
||||
@@ -35,11 +35,11 @@ const ConfigVision: FC = () => {
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (value) {
|
||||
draft.file!.allowed_file_types = Array.from(new Set([
|
||||
draft.file!.allowed_file_types = [...new Set([
|
||||
...(draft.file?.allowed_file_types || []),
|
||||
SupportUploadFileTypes.image,
|
||||
...(isAllowVideoUpload ? [SupportUploadFileTypes.video] : []),
|
||||
]))
|
||||
])]
|
||||
}
|
||||
else {
|
||||
draft.file!.allowed_file_types = draft.file!.allowed_file_types?.filter(
|
||||
@@ -69,7 +69,7 @@ const ConfigVision: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex grow items-center">
|
||||
<div className="system-sm-semibold mr-1 text-text-secondary">{t('vision.name', { ns: 'appDebug' })}</div>
|
||||
<div className="mr-1 text-text-secondary system-sm-semibold">{t('vision.name', { ns: 'appDebug' })}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
@@ -83,7 +83,7 @@ const ConfigVision: FC = () => {
|
||||
? (
|
||||
<>
|
||||
<div className="mr-2 flex items-center gap-0.5">
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t('vision.visionSettings.resolution', { ns: 'appDebug' })}</div>
|
||||
<div className="text-text-tertiary system-xs-medium-uppercase">{t('vision.visionSettings.resolution', { ns: 'appDebug' })}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
@@ -100,7 +100,7 @@ const ConfigVision: FC = () => {
|
||||
selected={file?.image?.detail === Resolution.high}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.high && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
@@ -109,7 +109,7 @@ const ConfigVision: FC = () => {
|
||||
selected={file?.image?.detail === Resolution.low}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.low && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -209,11 +209,11 @@ const AgentTools: FC = () => {
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-regular ml-1.5 flex w-0 grow items-center truncate',
|
||||
'ml-1.5 flex w-0 grow items-center truncate system-xs-regular',
|
||||
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
|
||||
)}
|
||||
>
|
||||
<span className="system-xs-medium pr-1.5 text-text-secondary">{getProviderShowName(item)}</span>
|
||||
<span className="pr-1.5 text-text-secondary system-xs-medium">{getProviderShowName(item)}</span>
|
||||
<span className="text-text-tertiary">{item.tool_label}</span>
|
||||
{!item.isDeleted && !readonly && (
|
||||
<Tooltip
|
||||
@@ -268,7 +268,7 @@ const AgentTools: FC = () => {
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
|
||||
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
|
||||
onClick={() => {
|
||||
setCurrentTool(item)
|
||||
setIsShowSettingTool(true)
|
||||
|
||||
@@ -96,7 +96,7 @@ const Editor: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(editorHeight, ' min-h-[102px] overflow-y-auto px-4 text-sm text-gray-700')}>
|
||||
<div className={cn(editorHeight, 'min-h-[102px] overflow-y-auto px-4 text-sm text-gray-700')}>
|
||||
<PromptEditor
|
||||
className={editorHeight}
|
||||
value={value}
|
||||
|
||||
@@ -29,10 +29,10 @@ const ConfigAudio: FC = () => {
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (value) {
|
||||
draft.file!.allowed_file_types = Array.from(new Set([
|
||||
draft.file!.allowed_file_types = [...new Set([
|
||||
...(draft.file?.allowed_file_types || []),
|
||||
SupportUploadFileTypes.audio,
|
||||
]))
|
||||
])]
|
||||
}
|
||||
else {
|
||||
draft.file!.allowed_file_types = draft.file!.allowed_file_types?.filter(
|
||||
@@ -56,7 +56,7 @@ const ConfigAudio: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex grow items-center">
|
||||
<div className="system-sm-semibold mr-1 text-text-secondary">{t('feature.audioUpload.title', { ns: 'appDebug' })}</div>
|
||||
<div className="mr-1 text-text-secondary system-sm-semibold">{t('feature.audioUpload.title', { ns: 'appDebug' })}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
|
||||
@@ -29,10 +29,10 @@ const ConfigDocument: FC = () => {
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (value) {
|
||||
draft.file!.allowed_file_types = Array.from(new Set([
|
||||
draft.file!.allowed_file_types = [...new Set([
|
||||
...(draft.file?.allowed_file_types || []),
|
||||
SupportUploadFileTypes.document,
|
||||
]))
|
||||
])]
|
||||
}
|
||||
else {
|
||||
draft.file!.allowed_file_types = draft.file!.allowed_file_types?.filter(
|
||||
@@ -56,7 +56,7 @@ const ConfigDocument: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex grow items-center">
|
||||
<div className="system-sm-semibold mr-1 text-text-secondary">{t('feature.documentUpload.title', { ns: 'appDebug' })}</div>
|
||||
<div className="mr-1 text-text-secondary system-sm-semibold">{t('feature.documentUpload.title', { ns: 'appDebug' })}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
|
||||
@@ -210,7 +210,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
<div className="overflow-y-auto border-b border-divider-regular p-6 pb-[68px] pt-5">
|
||||
<div className={cn(rowClass, 'items-center')}>
|
||||
<div className={labelClass}>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.name', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-text-secondary system-sm-semibold">{t('form.name', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
<Input
|
||||
value={localeCurrentDataset.name}
|
||||
@@ -221,7 +221,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.desc', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-text-secondary system-sm-semibold">{t('form.desc', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Textarea
|
||||
@@ -234,7 +234,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.permissions', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-text-secondary system-sm-semibold">{t('form.permissions', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<PermissionSelector
|
||||
@@ -250,7 +250,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
{!!(currentDataset && currentDataset.indexing_technique) && (
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.indexMethod', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-text-secondary system-sm-semibold">{t('form.indexMethod', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<IndexMethod
|
||||
@@ -267,7 +267,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
{indexMethod === IndexingType.QUALIFIED && (
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.embeddingModel', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-text-secondary system-sm-semibold">{t('form.embeddingModel', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="h-8 w-full rounded-lg bg-components-input-bg-normal opacity-60">
|
||||
|
||||
@@ -179,7 +179,7 @@ const Debug: FC<IDebug> = ({
|
||||
return false
|
||||
}
|
||||
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
if (completionFiles.some(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
return false
|
||||
}
|
||||
@@ -394,7 +394,7 @@ const Debug: FC<IDebug> = ({
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
<div className="flex items-center justify-between px-4 pb-2 pt-3">
|
||||
<div className="system-xl-semibold text-text-primary">{t('inputs.title', { ns: 'appDebug' })}</div>
|
||||
<div className="text-text-primary system-xl-semibold">{t('inputs.title', { ns: 'appDebug' })}</div>
|
||||
<div className="flex items-center">
|
||||
{
|
||||
debugWithMultipleModel
|
||||
@@ -539,7 +539,7 @@ const Debug: FC<IDebug> = ({
|
||||
{!completionRes && !isResponding && (
|
||||
<div className="flex grow flex-col items-center justify-center gap-2">
|
||||
<RiSparklingFill className="h-12 w-12 text-text-empty-state-icon" />
|
||||
<div className="system-sm-regular text-text-quaternary">{t('noResult', { ns: 'appDebug' })}</div>
|
||||
<div className="text-text-quaternary system-sm-regular">{t('noResult', { ns: 'appDebug' })}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -263,7 +263,7 @@ const Configuration: FC = () => {
|
||||
|
||||
formattingChangedDispatcher()
|
||||
let newDatasets = data
|
||||
if (data.find(item => !item.name)) { // has not loaded selected dataset
|
||||
if (data.some(item => !item.name)) { // has not loaded selected dataset
|
||||
const newSelected = produce(data, (draft) => {
|
||||
data.forEach((item, index) => {
|
||||
if (!item.name) { // not fetched database
|
||||
@@ -966,10 +966,10 @@ const Configuration: FC = () => {
|
||||
<div className="bg-default-subtle absolute left-0 top-0 h-14 w-full">
|
||||
<div className="flex h-14 items-center justify-between px-6">
|
||||
<div className="flex items-center">
|
||||
<div className="system-xl-semibold text-text-primary">{t('orchestrate', { ns: 'appDebug' })}</div>
|
||||
<div className="text-text-primary system-xl-semibold">{t('orchestrate', { ns: 'appDebug' })}</div>
|
||||
<div className="flex h-[14px] items-center space-x-1 text-xs">
|
||||
{isAdvancedMode && (
|
||||
<div className="system-xs-medium-uppercase ml-1 flex h-5 items-center rounded-md border border-components-button-secondary-border px-1.5 uppercase text-text-tertiary">{t('promptMode.advanced', { ns: 'appDebug' })}</div>
|
||||
<div className="ml-1 flex h-5 items-center rounded-md border border-components-button-secondary-border px-1.5 uppercase text-text-tertiary system-xs-medium-uppercase">{t('promptMode.advanced', { ns: 'appDebug' })}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1030,8 +1030,8 @@ const Configuration: FC = () => {
|
||||
<Config />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="relative flex h-full w-1/2 grow flex-col overflow-y-auto " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
<div className="flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg ">
|
||||
<div className="relative flex h-full w-1/2 grow flex-col overflow-y-auto" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
<div className="flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg">
|
||||
<Debug
|
||||
isAPIKeySet={isAPIKeySet}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
|
||||
@@ -217,7 +217,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
<AppIcon
|
||||
size="large"
|
||||
onClick={() => { setShowEmojiPicker(true) }}
|
||||
className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border "
|
||||
className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border"
|
||||
icon={localeData.icon}
|
||||
background={localeData.icon_background}
|
||||
/>
|
||||
|
||||
@@ -117,10 +117,10 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<div className="px-10">
|
||||
<div className="h-6 w-full 2xl:h-[139px]" />
|
||||
<div className="pb-6 pt-1">
|
||||
<span className="title-2xl-semi-bold text-text-primary">{t('newApp.startFromBlank', { ns: 'app' })}</span>
|
||||
<span className="text-text-primary title-2xl-semi-bold">{t('newApp.startFromBlank', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<div className="mb-2 leading-6">
|
||||
<span className="system-sm-semibold text-text-secondary">{t('newApp.chooseAppType', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-semibold">{t('newApp.chooseAppType', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<div className="flex w-[660px] flex-col gap-4">
|
||||
<div>
|
||||
@@ -160,7 +160,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
className="flex cursor-pointer items-center border-0 bg-transparent p-0"
|
||||
onClick={() => setIsAppTypeExpanded(!isAppTypeExpanded)}
|
||||
>
|
||||
<span className="system-2xs-medium-uppercase text-text-tertiary">{t('newApp.forBeginners', { ns: 'app' })}</span>
|
||||
<span className="text-text-tertiary system-2xs-medium-uppercase">{t('newApp.forBeginners', { ns: 'app' })}</span>
|
||||
<RiArrowRightSLine className={`ml-1 h-4 w-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex h-6 items-center">
|
||||
<label className="system-sm-semibold text-text-secondary">{t('newApp.captionName', { ns: 'app' })}</label>
|
||||
<label className="text-text-secondary system-sm-semibold">{t('newApp.captionName', { ns: 'app' })}</label>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
@@ -243,8 +243,8 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center">
|
||||
<label className="system-sm-semibold text-text-secondary">{t('newApp.captionDescription', { ns: 'app' })}</label>
|
||||
<span className="system-xs-regular ml-1 text-text-tertiary">
|
||||
<label className="text-text-secondary system-sm-semibold">{t('newApp.captionDescription', { ns: 'app' })}</label>
|
||||
<span className="ml-1 text-text-tertiary system-xs-regular">
|
||||
(
|
||||
{t('newApp.optional', { ns: 'app' })}
|
||||
)
|
||||
@@ -260,7 +260,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
</div>
|
||||
{isAppsFull && <AppsFull className="mt-4" loc="app-create" />}
|
||||
<div className="flex items-center justify-between pb-10 pt-5">
|
||||
<div className="system-xs-regular flex cursor-pointer items-center gap-1 text-text-tertiary" onClick={onCreateFromTemplate}>
|
||||
<div className="flex cursor-pointer items-center gap-1 text-text-tertiary system-xs-regular" onClick={onCreateFromTemplate}>
|
||||
<span>{t('newApp.noIdeaTip', { ns: 'app' })}</span>
|
||||
<div className="p-[1px]">
|
||||
<RiArrowRightLine className="h-3.5 w-3.5" />
|
||||
@@ -334,8 +334,8 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
<div className="system-sm-semibold mb-0.5 mt-2 text-text-secondary">{title}</div>
|
||||
<div className="system-xs-regular line-clamp-2 text-text-tertiary" title={description}>{description}</div>
|
||||
<div className="mb-0.5 mt-2 text-text-secondary system-sm-semibold">{title}</div>
|
||||
<div className="line-clamp-2 text-text-tertiary system-xs-regular" title={description}>{description}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -367,8 +367,8 @@ function AppPreview({ mode }: { mode: AppModeEnum }) {
|
||||
const previewInfo = modeToPreviewInfoMap[mode]
|
||||
return (
|
||||
<div className="px-8 py-4">
|
||||
<h4 className="system-sm-semibold-uppercase text-text-secondary">{previewInfo.title}</h4>
|
||||
<div className="system-xs-regular mt-1 min-h-8 max-w-96 text-text-tertiary">
|
||||
<h4 className="text-text-secondary system-sm-semibold-uppercase">{previewInfo.title}</h4>
|
||||
<div className="mt-1 min-h-8 max-w-96 text-text-tertiary system-xs-regular">
|
||||
<span>{previewInfo.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,7 +232,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
>
|
||||
<div className="title-2xl-semi-bold flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary">
|
||||
<div className="flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary title-2xl-semi-bold">
|
||||
{t('importFromDSL', { ns: 'app' })}
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center"
|
||||
@@ -241,7 +241,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-md-semibold flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary">
|
||||
<div className="flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary system-md-semibold">
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
@@ -275,7 +275,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className="system-md-semibold mb-1 text-text-secondary">DSL URL</div>
|
||||
<div className="mb-1 text-text-secondary system-md-semibold">DSL URL</div>
|
||||
<Input
|
||||
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
|
||||
value={dslUrlValue}
|
||||
@@ -309,8 +309,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
className="w-[480px]"
|
||||
>
|
||||
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
|
||||
<div className="system-md-regular flex grow flex-col text-text-secondary">
|
||||
<div className="text-text-primary title-2xl-semi-bold">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
|
||||
<div className="flex grow flex-col text-text-secondary system-md-regular">
|
||||
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
|
||||
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
|
||||
<br />
|
||||
|
||||
@@ -58,7 +58,7 @@ const Uploader: FC<Props> = ({
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
return
|
||||
@@ -121,7 +121,7 @@ const Uploader: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className={cn('group flex items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', ' hover:bg-components-panel-on-panel-item-bg-hover')}>
|
||||
<div className={cn('group flex items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', 'hover:bg-components-panel-on-panel-item-bg-hover')}>
|
||||
<div className="flex items-center justify-center p-3">
|
||||
<YamlIcon className="h-6 w-6 shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
|
||||
if (statusCount.paused > 0) {
|
||||
return (
|
||||
<div className="system-xs-semibold-uppercase inline-flex items-center gap-1">
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="yellow" />
|
||||
<span className="text-util-colors-warning-warning-600">Pending</span>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
}
|
||||
else if (statusCount.partial_success + statusCount.failed === 0) {
|
||||
return (
|
||||
<div className="system-xs-semibold-uppercase inline-flex items-center gap-1">
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<span className="text-util-colors-green-green-600">Success</span>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
}
|
||||
else if (statusCount.failed === 0) {
|
||||
return (
|
||||
<div className="system-xs-semibold-uppercase inline-flex items-center gap-1">
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<span className="text-util-colors-green-green-600">Partial Success</span>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="system-xs-semibold-uppercase inline-flex items-center gap-1">
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="red" />
|
||||
<span className="text-util-colors-red-red-600">
|
||||
{statusCount.failed}
|
||||
@@ -157,7 +157,7 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
log: [
|
||||
...(item.message ?? []),
|
||||
...(item.message?.[item.message.length - 1]?.role !== 'assistant'
|
||||
...(item.message.at(-1)?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
@@ -325,7 +325,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
const lastMessageId = allChatItems.length > 0 ? allChatItems[allChatItems.length - 1].id : undefined
|
||||
const lastMessageId = allChatItems.length > 0 ? allChatItems.at(-1).id : undefined
|
||||
setThreadChatItems(getThreadMessages(tree, lastMessageId))
|
||||
|
||||
// Update pagination anchor ref with the oldest answer ID
|
||||
@@ -562,9 +562,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
{/* Panel Header */}
|
||||
<div className="flex shrink-0 items-center gap-2 rounded-t-xl bg-components-panel-bg pb-2 pl-4 pr-3 pt-3">
|
||||
<div className="shrink-0">
|
||||
<div className="system-xs-semibold-uppercase mb-0.5 text-text-primary">{isChatMode ? t('detail.conversationId', { ns: 'appLog' }) : t('detail.time', { ns: 'appLog' })}</div>
|
||||
<div className="mb-0.5 text-text-primary system-xs-semibold-uppercase">{isChatMode ? t('detail.conversationId', { ns: 'appLog' }) : t('detail.time', { ns: 'appLog' })}</div>
|
||||
{isChatMode && (
|
||||
<div className="system-2xs-regular-uppercase flex items-center text-text-secondary">
|
||||
<div className="flex items-center text-text-secondary system-2xs-regular-uppercase">
|
||||
<Tooltip
|
||||
popupContent={detail.id}
|
||||
>
|
||||
@@ -574,7 +574,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
</div>
|
||||
)}
|
||||
{!isChatMode && (
|
||||
<div className="system-2xs-regular-uppercase text-text-secondary">{formatTime(detail.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
|
||||
<div className="text-text-secondary system-2xs-regular-uppercase">{formatTime(detail.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex grow flex-wrap items-center justify-end gap-y-1">
|
||||
@@ -600,7 +600,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
? (
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex h-[18px] items-center space-x-3">
|
||||
<div className="system-xs-semibold-uppercase text-text-tertiary">{t('table.header.output', { ns: 'appLog' })}</div>
|
||||
<div className="text-text-tertiary system-xs-semibold-uppercase">{t('table.header.output', { ns: 'appLog' })}</div>
|
||||
<div
|
||||
className="h-px grow"
|
||||
style={{
|
||||
@@ -692,7 +692,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
</div>
|
||||
{hasMore && (
|
||||
<div className="py-3 text-center">
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
<div className="text-text-tertiary system-xs-regular">
|
||||
{t('detail.loading', { ns: 'appLog' })}
|
||||
...
|
||||
</div>
|
||||
@@ -950,7 +950,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
)}
|
||||
popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
|
||||
>
|
||||
<div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
|
||||
<div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'overflow-hidden text-ellipsis whitespace-nowrap system-sm-regular')}>
|
||||
{value || '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -963,7 +963,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
return (
|
||||
<div className="relative mt-2 grow overflow-x-auto">
|
||||
<table className={cn('w-full min-w-[440px] border-collapse border-0')}>
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<thead className="text-text-tertiary system-xs-medium-uppercase">
|
||||
<tr>
|
||||
<td className="w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1"></td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{isChatMode ? t('table.header.summary', { ns: 'appLog' }) : t('table.header.input', { ns: 'appLog' })}</td>
|
||||
@@ -976,7 +976,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
<td className="whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3">{t('table.header.time', { ns: 'appLog' })}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="system-sm-regular text-text-secondary">
|
||||
<tbody className="text-text-secondary system-sm-regular">
|
||||
{logs.data.map((log: any) => {
|
||||
const endUser = log.from_end_user_session_id || log.from_account_name
|
||||
const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
|
||||
|
||||
@@ -266,7 +266,7 @@ function AppCard({
|
||||
</div>
|
||||
{!isMinimalState && (
|
||||
<div className="flex flex-col items-start justify-center self-stretch">
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">
|
||||
<div className="pb-1 text-text-tertiary system-xs-medium">
|
||||
{isApp
|
||||
? t('overview.appInfo.accessibleAddress', { ns: 'appOverview' })
|
||||
: t('overview.apiInfo.accessibleAddress', { ns: 'appOverview' })}
|
||||
@@ -319,9 +319,9 @@ function AppCard({
|
||||
)}
|
||||
{!isMinimalState && isApp && systemFeatures.webapp_auth.enabled && appDetail && (
|
||||
<div className="flex flex-col items-start justify-center self-stretch">
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">{t('publishApp.title', { ns: 'app' })}</div>
|
||||
<div className="pb-1 text-text-tertiary system-xs-medium">{t('publishApp.title', { ns: 'app' })}</div>
|
||||
<div
|
||||
className="flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2"
|
||||
className="flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2"
|
||||
onClick={handleClickAccessControl}
|
||||
>
|
||||
<div className="flex grow items-center gap-x-1.5 pr-1">
|
||||
@@ -329,32 +329,32 @@ function AppCard({
|
||||
&& (
|
||||
<>
|
||||
<RiBuildingLine className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
<p className="system-sm-medium text-text-secondary">{t('accessControlDialog.accessItems.organization', { ns: 'app' })}</p>
|
||||
<p className="text-text-secondary system-sm-medium">{t('accessControlDialog.accessItems.organization', { ns: 'app' })}</p>
|
||||
</>
|
||||
)}
|
||||
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
|
||||
&& (
|
||||
<>
|
||||
<RiLockLine className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
<p className="system-sm-medium text-text-secondary">{t('accessControlDialog.accessItems.specific', { ns: 'app' })}</p>
|
||||
<p className="text-text-secondary system-sm-medium">{t('accessControlDialog.accessItems.specific', { ns: 'app' })}</p>
|
||||
</>
|
||||
)}
|
||||
{appDetail?.access_mode === AccessMode.PUBLIC
|
||||
&& (
|
||||
<>
|
||||
<RiGlobalLine className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
<p className="system-sm-medium text-text-secondary">{t('accessControlDialog.accessItems.anyone', { ns: 'app' })}</p>
|
||||
<p className="text-text-secondary system-sm-medium">{t('accessControlDialog.accessItems.anyone', { ns: 'app' })}</p>
|
||||
</>
|
||||
)}
|
||||
{appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
|
||||
&& (
|
||||
<>
|
||||
<RiVerifiedBadgeLine className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
<p className="system-sm-medium text-text-secondary">{t('accessControlDialog.accessItems.external', { ns: 'app' })}</p>
|
||||
<p className="text-text-secondary system-sm-medium">{t('accessControlDialog.accessItems.external', { ns: 'app' })}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="system-xs-regular shrink-0 text-text-tertiary">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
{!isAppAccessSet && <p className="shrink-0 text-text-tertiary system-xs-regular">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<RiArrowRightSLine className="h-4 w-4 text-text-quaternary" />
|
||||
</div>
|
||||
@@ -389,7 +389,7 @@ function AppCard({
|
||||
>
|
||||
<div className="flex items-center justify-center gap-[1px]">
|
||||
<op.opIcon className="h-3.5 w-3.5" />
|
||||
<div className={`${(runningStatus || !disabled) ? 'text-text-tertiary' : 'text-components-button-ghost-text-disabled'} system-xs-medium px-[3px]`}>{op.opName}</div>
|
||||
<div className={`${(runningStatus || !disabled) ? 'text-text-tertiary' : 'text-components-button-ghost-text-disabled'} px-[3px] system-xs-medium`}>{op.opName}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
|
||||
@@ -231,12 +231,12 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{/* header */}
|
||||
<div className="pb-3 pl-6 pr-5 pt-5">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="title-2xl-semi-bold grow text-text-primary">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</div>
|
||||
<div className="grow text-text-primary title-2xl-semi-bold">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</div>
|
||||
<ActionButton className="shrink-0" onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-0.5 text-text-tertiary">
|
||||
<div className="mt-0.5 text-text-tertiary system-xs-regular">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +245,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn('system-sm-semibold mb-1 py-1 text-text-secondary')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('mb-1 py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={inputInfo.title}
|
||||
@@ -265,32 +265,32 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</div>
|
||||
{/* description */}
|
||||
<div className="relative">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('body-xs-regular pb-0.5 text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<Switch
|
||||
value={inputInfo.use_icon_as_answer_icon}
|
||||
onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="body-xs-regular pb-0.5 text-text-tertiary">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
<p className="pb-0.5 text-text-tertiary body-xs-regular">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
</div>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('system-sm-semibold grow py-1 text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('grow py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<SimpleSelect
|
||||
wrapperClassName="w-[200px]"
|
||||
items={languages.filter(item => item.supported)}
|
||||
@@ -303,8 +303,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="body-xs-regular pb-0.5 text-text-tertiary">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="pb-0.5 text-text-tertiary body-xs-regular">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Input
|
||||
@@ -314,7 +314,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<p className={cn('text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<Switch value={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,22 +323,22 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{/* workflow detail */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<Switch
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
value={inputInfo.show_workflow_steps}
|
||||
onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="body-xs-regular pb-0.5 text-text-tertiary">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
<p className="pb-0.5 text-text-tertiary body-xs-regular">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('body-xs-regular pb-0.5 text-text-tertiary')}>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>
|
||||
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
&
|
||||
@@ -356,7 +356,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('system-sm-semibold mr-1 py-1 text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
<div className={cn('mr-1 py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{enableBilling && isFreePlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
@@ -385,7 +385,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="body-xs-regular pb-0.5 text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
<p className="pb-0.5 text-text-tertiary body-xs-regular">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
@@ -397,8 +397,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('body-xs-regular pb-0.5 text-text-tertiary')}>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
@@ -414,8 +414,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('body-xs-regular pb-0.5 text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
|
||||
@@ -161,7 +161,7 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
|
||||
<TriggerAll className="h-4 w-4 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
|
||||
<div className="min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary system-md-semibold group-hover:text-text-primary">
|
||||
{triggerCount > 0
|
||||
? t('overview.triggerInfo.triggersAdded', { ns: 'appOverview', count: triggerCount })
|
||||
: t('overview.triggerInfo.noTriggerAdded', { ns: 'appOverview' })}
|
||||
@@ -179,12 +179,12 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
|
||||
<div className="shrink-0">
|
||||
{getTriggerIcon(trigger, triggerPlugins || [])}
|
||||
</div>
|
||||
<div className="system-sm-medium min-w-0 flex-1 truncate text-text-secondary">
|
||||
<div className="min-w-0 flex-1 truncate text-text-secondary system-sm-medium">
|
||||
{trigger.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className={`${trigger.status === 'enabled' ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase whitespace-nowrap`}>
|
||||
<div className={`${trigger.status === 'enabled' ? 'text-text-success' : 'text-text-warning'} whitespace-nowrap system-xs-semibold-uppercase`}>
|
||||
{trigger.status === 'enabled'
|
||||
? t('overview.status.running', { ns: 'appOverview' })
|
||||
: t('overview.status.disable', { ns: 'appOverview' })}
|
||||
@@ -204,7 +204,7 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
|
||||
|
||||
{triggerCount === 0 && (
|
||||
<div className="p-3">
|
||||
<div className="system-xs-regular leading-4 text-text-tertiary">
|
||||
<div className="leading-4 text-text-tertiary system-xs-regular">
|
||||
{t('overview.triggerInfo.triggerStatusDescription', { ns: 'appOverview' })}
|
||||
{' '}
|
||||
<Link
|
||||
|
||||
@@ -277,7 +277,7 @@ describe('List', () => {
|
||||
fireEvent.click(screen.getByText('app.types.workflow'))
|
||||
|
||||
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const lastCall = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(lastCall.searchParams.get('category')).toBe(AppModeEnum.WORKFLOW)
|
||||
})
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('List', () => {
|
||||
fireEvent.click(screen.getByText('app.types.all'))
|
||||
|
||||
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const lastCall = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
// nuqs removes the default value ('all') from URL params
|
||||
expect(lastCall.searchParams.has('category')).toBe(false)
|
||||
})
|
||||
@@ -449,7 +449,7 @@ describe('List', () => {
|
||||
onUrlUpdate.mockClear()
|
||||
fireEvent.click(screen.getByText(text))
|
||||
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const lastCall = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(lastCall.searchParams.get('category')).toBe(mode)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -114,7 +114,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('keywords')).toBe('search')
|
||||
expect(update.options.history).toBe('push')
|
||||
})
|
||||
@@ -127,7 +127,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('tagIDs')).toBe('tag1;tag2')
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('isCreatedByMe')).toBe('true')
|
||||
})
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('keywords')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('tagIDs')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('useAppsQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('isCreatedByMe')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('AudioBtn', () => {
|
||||
}
|
||||
|
||||
const getLatestAudioCallback = () => {
|
||||
const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
|
||||
const lastCall = mockGetAudioPlayer.mock.calls.at(-1)
|
||||
const callback = lastCall?.[5]
|
||||
|
||||
if (typeof callback !== 'function')
|
||||
|
||||
@@ -65,7 +65,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
if (primarySrc) {
|
||||
// Delayed generation of waveform data
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const timer = setTimeout(() => generateWaveformData(primarySrc), 1000)
|
||||
const timer = setTimeout(generateWaveformData, 1000, primarySrc)
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', setAudioData)
|
||||
audio.removeEventListener('timeupdate', setAudioTime)
|
||||
|
||||
@@ -267,7 +267,7 @@ describe('AudioPlayer — waveform generation', () => {
|
||||
render(<AudioPlayer src="https://example.com/audio.mp3" />)
|
||||
await advanceWaveformTimer()
|
||||
|
||||
const toastFound = Array.from(document.body.querySelectorAll('div')).some(
|
||||
const toastFound = [...document.body.querySelectorAll('div')].some(
|
||||
d => d.textContent?.includes('Web Audio API is not supported in this browser'),
|
||||
)
|
||||
expect(toastFound).toBe(true)
|
||||
|
||||
@@ -40,7 +40,7 @@ const createMockEmblaApi = (): MockEmblaApi => ({
|
||||
canScrollPrev: vi.fn(() => mockCanScrollPrev),
|
||||
canScrollNext: vi.fn(() => mockCanScrollNext),
|
||||
slideNodes: vi.fn(() =>
|
||||
Array.from({ length: mockSlideCount }, () => document.createElement('div')),
|
||||
Array.from({ length: mockSlideCount }).fill(document.createElement('div')),
|
||||
),
|
||||
on: vi.fn((event: EmblaEventName, callback: EmblaListener) => {
|
||||
listeners[event].push(callback)
|
||||
|
||||
@@ -319,7 +319,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const disabledContainer = chatInput.closest('.pointer-events-none')
|
||||
expect(disabledContainer).toBeInTheDocument()
|
||||
expect(disabledContainer).toHaveClass('opacity-50')
|
||||
@@ -336,7 +336,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -360,7 +360,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
@@ -410,7 +410,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
@@ -1101,7 +1101,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
@@ -1242,7 +1242,7 @@ describe('ChatWrapper', () => {
|
||||
render(<ChatWrapper />)
|
||||
// This tests line 106 - early return when hasEmptyInput is set
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
@@ -1270,7 +1270,7 @@ describe('ChatWrapper', () => {
|
||||
render(<ChatWrapper />)
|
||||
// This tests line 109 - early return when fileIsUploading is set
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
@@ -1809,7 +1809,7 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
const textboxes = screen.getAllByRole('textbox')
|
||||
const chatInput = textboxes[textboxes.length - 1]
|
||||
const chatInput = textboxes.at(-1)
|
||||
const container = chatInput.closest('.pointer-events-none')
|
||||
// Should not be disabled because it's not required
|
||||
expect(container).not.toBeInTheDocument()
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Header Component', () => {
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
// Sidebar, NewChat, ResetChat (3)
|
||||
const resetChatBtn = buttons[buttons.length - 1]
|
||||
const resetChatBtn = buttons.at(-1)
|
||||
await userEvent.click(resetChatBtn)
|
||||
|
||||
expect(handleNewConversation).toHaveBeenCalled()
|
||||
|
||||
@@ -483,7 +483,7 @@ describe('useChat', () => {
|
||||
callbacks.onNodeFinished({ data: { id: 'n-1', iteration_id: 'iter-1' } })
|
||||
})
|
||||
|
||||
const traceLen1 = result.current.chatList[result.current.chatList.length - 1].workflowProcess?.tracing?.length
|
||||
const traceLen1 = result.current.chatList.at(-1).workflowProcess?.tracing?.length
|
||||
expect(traceLen1).toBe(0) // None added due to iteration early hits
|
||||
})
|
||||
|
||||
@@ -567,7 +567,7 @@ describe('useChat', () => {
|
||||
|
||||
expect(result.current.chatList.some(item => item.id === 'question-m-child')).toBe(true)
|
||||
expect(result.current.chatList.some(item => item.id === 'm-child')).toBe(true)
|
||||
expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('child answer')
|
||||
expect(result.current.chatList.at(-1).content).toBe('child answer')
|
||||
})
|
||||
|
||||
it('should strip local file urls before sending payload', () => {
|
||||
@@ -665,7 +665,7 @@ describe('useChat', () => {
|
||||
})
|
||||
|
||||
expect(onGetConversationMessages).toHaveBeenCalled()
|
||||
expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('streamed content')
|
||||
expect(result.current.chatList.at(-1).content).toBe('streamed content')
|
||||
})
|
||||
|
||||
it('should clear suggested questions when suggestion fetch fails after completion', async () => {
|
||||
@@ -711,7 +711,7 @@ describe('useChat', () => {
|
||||
callbacks.onNodeFinished({ data: { node_id: 'n-loop', id: 'n-loop' } })
|
||||
})
|
||||
|
||||
const latestResponse = result.current.chatList[result.current.chatList.length - 1]
|
||||
const latestResponse = result.current.chatList.at(-1)
|
||||
expect(latestResponse.workflowProcess?.tracing).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -738,7 +738,7 @@ describe('useChat', () => {
|
||||
callbacks.onTTSChunk('m-th-bind', '')
|
||||
})
|
||||
|
||||
const latestResponse = result.current.chatList[result.current.chatList.length - 1]
|
||||
const latestResponse = result.current.chatList.at(-1)
|
||||
expect(latestResponse.id).toBe('m-th-bind')
|
||||
expect(latestResponse.conversationId).toBe('c-th-bind')
|
||||
expect(latestResponse.workflowProcess?.status).toBe('succeeded')
|
||||
@@ -831,7 +831,7 @@ describe('useChat', () => {
|
||||
callbacks.onCompleted()
|
||||
})
|
||||
|
||||
const lastResponse = result.current.chatList[result.current.chatList.length - 1]
|
||||
const lastResponse = result.current.chatList.at(-1)
|
||||
expect(lastResponse.agent_thoughts![0].thought).toContain('resumed')
|
||||
|
||||
expect(lastResponse.workflowProcess?.tracing?.length).toBeGreaterThan(0)
|
||||
|
||||
@@ -458,7 +458,7 @@ describe('Operation', () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line')
|
||||
const adminThumb = thumbs[thumbs.length - 1].closest('button')!
|
||||
const adminThumb = thumbs.at(-1).closest('button')!
|
||||
await user.click(adminThumb)
|
||||
expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: 'like', content: undefined })
|
||||
})
|
||||
@@ -467,7 +467,7 @@ describe('Operation', () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line')
|
||||
const adminThumb = thumbs[thumbs.length - 1].closest('button')!
|
||||
const adminThumb = thumbs.at(-1).closest('button')!
|
||||
await user.click(adminThumb)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
@@ -508,12 +508,12 @@ describe('Operation', () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line')
|
||||
const adminThumb = thumbs[thumbs.length - 1].closest('button')!
|
||||
const adminThumb = thumbs.at(-1).closest('button')!
|
||||
await user.click(adminThumb)
|
||||
expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: 'like', content: undefined })
|
||||
|
||||
const thumbsUndo = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line')
|
||||
const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1].closest('button')!
|
||||
const adminThumbUndo = thumbsUndo.at(-1).closest('button')!
|
||||
await user.click(adminThumbUndo)
|
||||
expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: null, content: undefined })
|
||||
})
|
||||
@@ -522,13 +522,13 @@ describe('Operation', () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line')
|
||||
const adminThumb = thumbs[thumbs.length - 1].closest('button')!
|
||||
const adminThumb = thumbs.at(-1).closest('button')!
|
||||
await user.click(adminThumb)
|
||||
const submitBtn = screen.getByText(/submit/i)
|
||||
await user.click(submitBtn)
|
||||
|
||||
const thumbsUndo = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line')
|
||||
const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1].closest('button')!
|
||||
const adminThumbUndo = thumbsUndo.at(-1).closest('button')!
|
||||
await user.click(adminThumbUndo)
|
||||
expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: null, content: undefined })
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ const WorkflowProcessItem = ({
|
||||
const succeeded = data.status === WorkflowRunningStatus.Succeeded
|
||||
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
|
||||
const paused = data.status === WorkflowRunningStatus.Paused
|
||||
const latestNode = data.tracing[data.tracing.length - 1]
|
||||
const latestNode = data.tracing.at(-1)
|
||||
|
||||
useEffect(() => {
|
||||
setCollapse(!expand)
|
||||
|
||||
@@ -168,7 +168,7 @@ describe('Chip', () => {
|
||||
// Click on an item in the dropdown panel
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
// The second one should be in the dropdown
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
fireEvent.click(activeItems.at(-1))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
})
|
||||
@@ -181,7 +181,7 @@ describe('Chip', () => {
|
||||
openPanel(container)
|
||||
// Get all "Active" texts and click the one in the dropdown (should be the last one)
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
fireEvent.click(activeItems.at(-1))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect).toHaveBeenCalledWith(items[1])
|
||||
@@ -194,7 +194,7 @@ describe('Chip', () => {
|
||||
const trigger = getTrigger(container)
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
// The close icon should be the last SVG element
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const closeIcon = svgs.at(-1)
|
||||
const clearButton = closeIcon?.parentElement
|
||||
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
@@ -212,7 +212,7 @@ describe('Chip', () => {
|
||||
|
||||
// Find the close icon (last SVG) and click its parent
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const closeIcon = svgs.at(-1)
|
||||
const clearButton = closeIcon?.parentElement
|
||||
|
||||
if (clearButton)
|
||||
@@ -299,7 +299,7 @@ describe('Chip', () => {
|
||||
// Find the dropdown panel items
|
||||
const allActiveTexts = screen.getAllByText('Active')
|
||||
// The dropdown item should be the last one
|
||||
const dropdownItem = allActiveTexts[allActiveTexts.length - 1]
|
||||
const dropdownItem = allActiveTexts.at(-1)
|
||||
const parentContainer = dropdownItem.parentElement
|
||||
|
||||
// The check icon should be a sibling within the parent
|
||||
@@ -346,7 +346,7 @@ describe('Chip', () => {
|
||||
|
||||
// Click on the already selected item in the dropdown
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
fireEvent.click(activeItems.at(-1))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect).toHaveBeenCalledWith(items[1])
|
||||
@@ -367,7 +367,7 @@ describe('Chip', () => {
|
||||
openPanel(container)
|
||||
|
||||
const thirdItems = screen.getAllByText('Third')
|
||||
fireEvent.click(thirdItems[thirdItems.length - 1])
|
||||
fireEvent.click(thirdItems.at(-1))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(numericItems[2])
|
||||
})
|
||||
@@ -386,7 +386,7 @@ describe('Chip', () => {
|
||||
openPanel(container)
|
||||
|
||||
const itemBs = screen.getAllByText('Item B')
|
||||
fireEvent.click(itemBs[itemBs.length - 1])
|
||||
fireEvent.click(itemBs.at(-1))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(itemsWithExtra[1])
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ function Confirm({
|
||||
setIsVisible(true)
|
||||
}
|
||||
else {
|
||||
const timer = setTimeout(() => setIsVisible(false), 200)
|
||||
const timer = setTimeout(setIsVisible, 200, false)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isShow])
|
||||
@@ -119,11 +119,11 @@ function Confirm({
|
||||
asChild={false}
|
||||
triggerClassName="w-full"
|
||||
>
|
||||
<div ref={titleRef} className="title-2xl-semi-bold w-full truncate text-text-primary">
|
||||
<div ref={titleRef} className="w-full truncate text-text-primary title-2xl-semi-bold">
|
||||
{title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="system-md-regular w-full whitespace-pre-wrap break-words text-text-tertiary">{content}</div>
|
||||
<div className="w-full whitespace-pre-wrap break-words text-text-tertiary system-md-regular">{content}</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-2 self-stretch p-6">
|
||||
{showCancel && <Button onClick={onCancel}>{cancelTxt}</Button>}
|
||||
|
||||
@@ -435,7 +435,7 @@ describe('DatePicker', () => {
|
||||
|
||||
// Confirm - click the last OK button (year/month footer)
|
||||
const okButtons = screen.getAllByText(/operation\.ok/)
|
||||
fireEvent.click(okButtons[okButtons.length - 1])
|
||||
fireEvent.click(okButtons.at(-1))
|
||||
|
||||
// Should return to date view
|
||||
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
|
||||
@@ -472,7 +472,7 @@ describe('DatePicker', () => {
|
||||
|
||||
// Confirm the selection - click the last OK button (year/month footer)
|
||||
const okButtons = screen.getAllByText(/operation\.ok/)
|
||||
fireEvent.click(okButtons[okButtons.length - 1])
|
||||
fireEvent.click(okButtons.at(-1))
|
||||
|
||||
// Should return to date view
|
||||
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
|
||||
|
||||
@@ -92,20 +92,20 @@ const AnnotationReply = ({
|
||||
>
|
||||
<>
|
||||
{!annotationReply?.enabled && (
|
||||
<div className="system-xs-regular line-clamp-2 min-h-8 text-text-tertiary">{t('feature.annotation.description', { ns: 'appDebug' })}</div>
|
||||
<div className="line-clamp-2 min-h-8 text-text-tertiary system-xs-regular">{t('feature.annotation.description', { ns: 'appDebug' })}</div>
|
||||
)}
|
||||
{!!annotationReply?.enabled && (
|
||||
<>
|
||||
{!isHovering && (
|
||||
<div className="flex items-center gap-4 pt-0.5">
|
||||
<div className="">
|
||||
<div className="system-2xs-medium-uppercase mb-0.5 text-text-tertiary">{t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}</div>
|
||||
<div className="system-xs-regular text-text-secondary">{annotationReply.score_threshold || '-'}</div>
|
||||
<div className="mb-0.5 text-text-tertiary system-2xs-medium-uppercase">{t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}</div>
|
||||
<div className="text-text-secondary system-xs-regular">{annotationReply.score_threshold || '-'}</div>
|
||||
</div>
|
||||
<div className="h-[27px] w-px rotate-12 bg-divider-subtle"></div>
|
||||
<div className="">
|
||||
<div className="system-2xs-medium-uppercase mb-0.5 text-text-tertiary">{t('modelProvider.embeddingModel.key', { ns: 'common' })}</div>
|
||||
<div className="system-xs-regular text-text-secondary">{annotationReply.embedding_model?.embedding_model_name}</div>
|
||||
<div className="mb-0.5 text-text-tertiary system-2xs-medium-uppercase">{t('modelProvider.embeddingModel.key', { ns: 'common' })}</div>
|
||||
<div className="text-text-secondary system-xs-regular">{annotationReply.embedding_model?.embedding_model_name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ const FeatureCard = ({
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
{icon}
|
||||
<div className="system-sm-semibold flex grow items-center text-text-secondary">
|
||||
<div className="flex grow items-center text-text-secondary system-sm-semibold">
|
||||
{title}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
@@ -51,7 +51,7 @@ const FeatureCard = ({
|
||||
<Switch disabled={disabled} className="shrink-0" onChange={state => onChange?.(state)} value={value} />
|
||||
</div>
|
||||
{description && (
|
||||
<div className="system-xs-regular line-clamp-2 min-h-8 text-text-tertiary">{description}</div>
|
||||
<div className="line-clamp-2 min-h-8 text-text-tertiary system-xs-regular">{description}</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
const arr = value.split('\n').reduce((prev: string[], next: string) => {
|
||||
if (next !== '')
|
||||
prev.push(next.slice(0, 100))
|
||||
if (next === '' && prev[prev.length - 1] !== '')
|
||||
if (next === '' && prev.at(-1) !== '')
|
||||
prev.push(next)
|
||||
|
||||
return prev
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('FileImageItem', () => {
|
||||
)
|
||||
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
const progressSvg = Array.from(svgs).find(svg => svg.querySelector('circle'))
|
||||
const progressSvg = [...svgs].find(svg => svg.querySelector('circle'))
|
||||
expect(progressSvg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('FileImageItem', () => {
|
||||
|
||||
// Find the RiDownloadLine SVG (it doesn't have data-icon attribute, unlike ReplayLine)
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
const downloadSvg = Array.from(svgs).find(
|
||||
const downloadSvg = [...svgs].find(
|
||||
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
|
||||
)
|
||||
fireEvent.click(downloadSvg!.parentElement!)
|
||||
@@ -174,7 +174,7 @@ describe('FileImageItem', () => {
|
||||
// The download SVG should be rendered
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs.length).toBeGreaterThanOrEqual(1)
|
||||
const downloadSvg = Array.from(svgs).find(
|
||||
const downloadSvg = [...svgs].find(
|
||||
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
|
||||
)
|
||||
fireEvent.click(downloadSvg!.parentElement!)
|
||||
@@ -189,7 +189,7 @@ describe('FileImageItem', () => {
|
||||
const { container } = render(<FileImageItem file={file} showDownloadAction />)
|
||||
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
const downloadSvg = Array.from(svgs).find(
|
||||
const downloadSvg = [...svgs].find(
|
||||
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
|
||||
)
|
||||
fireEvent.click(downloadSvg!.parentElement!)
|
||||
@@ -233,7 +233,7 @@ describe('FileImageItem', () => {
|
||||
const { container } = render(<FileImageItem file={file} showDownloadAction />)
|
||||
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
const downloadSvg = Array.from(svgs).find(
|
||||
const downloadSvg = [...svgs].find(
|
||||
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
|
||||
)
|
||||
fireEvent.click(downloadSvg!.parentElement!)
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('ImagePreview', () => {
|
||||
// In some test environments `write` lives on the prototype rather than
|
||||
// the clipboard instance itself; locate the actual owner so vi.spyOn
|
||||
// patches the right object.
|
||||
const writeOwner = Object.prototype.hasOwnProperty.call(clipboardTarget, 'write')
|
||||
const writeOwner = Object.hasOwn(clipboardTarget, 'write')
|
||||
? clipboardTarget
|
||||
: (Object.getPrototypeOf(clipboardTarget) as { write: (items: ClipboardItem[]) => Promise<void> })
|
||||
vi.spyOn(writeOwner, 'write').mockImplementation((items: ClipboardItem[]) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ type CapturedProps = {
|
||||
|
||||
const getLastWrapperProps = (): CapturedProps => {
|
||||
const calls = mockReactMarkdownWrapper.mock.calls
|
||||
const lastCall = calls[calls.length - 1]
|
||||
const lastCall = calls.at(-1)
|
||||
return lastCall[0] as CapturedProps
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('AudioBtn', () => {
|
||||
}
|
||||
|
||||
const getAudioCallback = () => {
|
||||
const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
|
||||
const lastCall = mockGetAudioPlayer.mock.calls.at(-1)
|
||||
const callback = lastCall?.find((arg: unknown) => typeof arg === 'function') as ((event: string) => void) | undefined
|
||||
if (!callback)
|
||||
throw new Error('Audio callback not found - ensure mockGetAudioPlayer was called with a callback argument')
|
||||
|
||||
@@ -92,7 +92,7 @@ const NotionPageSelector = ({
|
||||
}, [notionsPages?.notion_info])
|
||||
|
||||
const defaultSelectedPagesId = useMemo(() => {
|
||||
return [...Array.from(pagesMapAndSelectedPagesId[1]), ...(value || [])]
|
||||
return [...[...pagesMapAndSelectedPagesId[1]], ...(value || [])]
|
||||
}, [pagesMapAndSelectedPagesId, value])
|
||||
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(() => new Set(defaultSelectedPagesId))
|
||||
|
||||
@@ -113,9 +113,9 @@ const NotionPageSelector = ({
|
||||
}, [datasetId, invalidPreImportNotionPages, notionCredentials, onSelect, onSelectCredential])
|
||||
|
||||
const handleSelectPages = useCallback((newSelectedPagesId: Set<string>) => {
|
||||
const selectedPages = Array.from(newSelectedPagesId).map(pageId => pagesMapAndSelectedPagesId[0][pageId])
|
||||
const selectedPages = Array.from(newSelectedPagesId, pageId => pagesMapAndSelectedPagesId[0][pageId])
|
||||
|
||||
setSelectedPagesId(new Set(Array.from(newSelectedPagesId)))
|
||||
setSelectedPagesId(new Set([...newSelectedPagesId]))
|
||||
onSelect(selectedPages)
|
||||
}, [pagesMapAndSelectedPagesId, onSelect])
|
||||
|
||||
|
||||
@@ -236,8 +236,8 @@ const PageSelector = ({
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
const descendantsIds = [...currentWithChildrenAndDescendants.descendants]
|
||||
const childrenIds = [...currentWithChildrenAndDescendants.children]
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('CustomizedPagination', () => {
|
||||
render(<CustomizedPagination {...defaultProps} current={9} total={100} limit={10} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
// Last button is next
|
||||
expect(buttons[buttons.length - 1]).toBeDisabled()
|
||||
expect(buttons.at(-1)).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not render limit selector when onLimitChange is not provided', () => {
|
||||
@@ -98,7 +98,7 @@ describe('CustomizedPagination', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<CustomizedPagination {...defaultProps} current={0} onChange={onChange} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const nextButton = buttons[buttons.length - 1]
|
||||
const nextButton = buttons.at(-1)
|
||||
fireEvent.click(nextButton)
|
||||
expect(onChange).toHaveBeenCalledWith(1)
|
||||
})
|
||||
@@ -226,7 +226,7 @@ describe('CustomizedPagination', () => {
|
||||
// totalPages = 1, both buttons should be disabled
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons[0]).toBeDisabled()
|
||||
expect(buttons[buttons.length - 1]).toBeDisabled()
|
||||
expect(buttons.at(-1)).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should restore input value when blurred with empty value', () => {
|
||||
|
||||
@@ -36,7 +36,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="system-sm-semibold mr-1 text-text-secondary">{name}</span>
|
||||
<span className="mr-1 text-text-secondary system-sm-semibold">{name}</span>
|
||||
{!noTooltip && (
|
||||
<Tooltip
|
||||
triggerClassName="w-4 h-4 shrink-0"
|
||||
|
||||
@@ -553,7 +553,7 @@ describe('useVariableOptions', () => {
|
||||
() => useVariableOptions(makeVariableBlock([])),
|
||||
{ wrapper },
|
||||
)
|
||||
const lastOption = result.current[result.current.length - 1]
|
||||
const lastOption = result.current.at(-1)
|
||||
const el = lastOption.renderMenuOption(renderProps)
|
||||
expect(el).toBeTruthy()
|
||||
})
|
||||
@@ -569,7 +569,7 @@ describe('useVariableOptions', () => {
|
||||
{ wrapper },
|
||||
)
|
||||
const spy = vi.spyOn(capturedEditor!, 'update')
|
||||
const lastOption = result.current[result.current.length - 1]
|
||||
const lastOption = result.current.at(-1)
|
||||
lastOption.onSelectMenuOption()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -717,7 +717,7 @@ describe('useExternalToolOptions', () => {
|
||||
() => useExternalToolOptions(makeExternalToolBlock({}, [])),
|
||||
{ wrapper },
|
||||
)
|
||||
const lastOption = result.current[result.current.length - 1]
|
||||
const lastOption = result.current.at(-1)
|
||||
const el = lastOption.renderMenuOption(renderProps)
|
||||
expect(el).toBeTruthy()
|
||||
})
|
||||
@@ -728,7 +728,7 @@ describe('useExternalToolOptions', () => {
|
||||
() => useExternalToolOptions(makeExternalToolBlock({ onAddExternalTool }, [])),
|
||||
{ wrapper },
|
||||
)
|
||||
const lastOption = result.current[result.current.length - 1]
|
||||
const lastOption = result.current.at(-1)
|
||||
lastOption.onSelectMenuOption()
|
||||
expect(onAddExternalTool).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -741,7 +741,7 @@ describe('useExternalToolOptions', () => {
|
||||
() => useExternalToolOptions(block),
|
||||
{ wrapper },
|
||||
)
|
||||
const lastOption = result.current[result.current.length - 1]
|
||||
const lastOption = result.current.at(-1)
|
||||
expect(() => lastOption.onSelectMenuOption()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -346,7 +346,7 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
|
||||
// There is no accessible "option" role here (menu items are plain divs).
|
||||
// We locate menu items by `tabindex="-1"` inside the listbox.
|
||||
const listbox = await screen.findByRole('listbox', { name: /typeahead menu/i })
|
||||
const menuItems = Array.from(listbox.querySelectorAll('[tabindex="-1"]'))
|
||||
const menuItems = [...listbox.querySelectorAll('[tabindex="-1"]')]
|
||||
|
||||
// Expect at least: (1) our empty variable option, (2) the "add variable" option.
|
||||
expect(menuItems.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
@@ -50,7 +50,7 @@ const TagInput: FC<TagInputProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if ((items.find(item => item === valueTrimmed))) {
|
||||
if ((items.includes(valueTrimmed))) {
|
||||
notify({ type: 'error', message: t('segment.keywordDuplicate', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ vi.mock('@/service/base', () => ({
|
||||
}))
|
||||
|
||||
const getLatestStreamOptions = (): IOtherOptions => {
|
||||
const latestCall = mockSsePost.mock.calls[mockSsePost.mock.calls.length - 1]
|
||||
const latestCall = mockSsePost.mock.calls.at(-1)
|
||||
if (!latestCall)
|
||||
throw new Error('Expected ssePost to be called at least once')
|
||||
return latestCall[2]
|
||||
|
||||
@@ -94,7 +94,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src, srcs }) => {
|
||||
if (controlsTimeoutRef.current)
|
||||
clearTimeout(controlsTimeoutRef.current)
|
||||
|
||||
controlsTimeoutRef.current = setTimeout(() => setIsControlsVisible(false), 3000)
|
||||
controlsTimeoutRef.current = setTimeout(setIsControlsVisible, 3000, false)
|
||||
}, [])
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('PlanComp', () => {
|
||||
await waitFor(() => expect(screen.getByTestId('verify-modal').getAttribute('data-is-show')).toBe('true'))
|
||||
|
||||
// Get the props passed to the modal and call onConfirm/onCancel
|
||||
const lastCall = verifyStateModalMock.mock.calls[verifyStateModalMock.mock.calls.length - 1][0]
|
||||
const lastCall = verifyStateModalMock.mock.calls.at(-1)[0]
|
||||
expect(lastCall.onConfirm).toBeDefined()
|
||||
expect(lastCall.onCancel).toBeDefined()
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('Enterprise Icon Component', () => {
|
||||
it('should use CSS custom properties for colors', () => {
|
||||
const { container } = render(<Enterprise />)
|
||||
const allFillElements = container.querySelectorAll('[fill]')
|
||||
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
|
||||
const elementsWithCSSVars = [...allFillElements].filter(el =>
|
||||
el.getAttribute('fill')?.startsWith('var('),
|
||||
)
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('Professional Icon Component', () => {
|
||||
it('should use CSS custom properties for colors', () => {
|
||||
const { container } = render(<Professional />)
|
||||
const allFillElements = container.querySelectorAll('[fill]')
|
||||
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
|
||||
const elementsWithCSSVars = [...allFillElements].filter(el =>
|
||||
el.getAttribute('fill')?.startsWith('var('),
|
||||
)
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('Sandbox Icon Component', () => {
|
||||
it('should use CSS custom properties for colors', () => {
|
||||
const { container } = render(<Sandbox />)
|
||||
const allFillElements = container.querySelectorAll('[fill]')
|
||||
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
|
||||
const elementsWithCSSVars = [...allFillElements].filter(el =>
|
||||
el.getAttribute('fill')?.startsWith('var('),
|
||||
)
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Team Icon Component', () => {
|
||||
it('should use CSS custom properties for colors', () => {
|
||||
const { container } = render(<Team />)
|
||||
const allFillElements = container.querySelectorAll('[fill]')
|
||||
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
|
||||
const elementsWithCSSVars = [...allFillElements].filter(el =>
|
||||
el.getAttribute('fill')?.startsWith('var('),
|
||||
)
|
||||
|
||||
|
||||
@@ -44,28 +44,28 @@ describe('Pricing Assets', () => {
|
||||
it('should render active state for Cloud', () => {
|
||||
const { container } = render(<Cloud isActive />)
|
||||
|
||||
const rects = Array.from(container.querySelectorAll('rect'))
|
||||
const rects = [...container.querySelectorAll('rect')]
|
||||
expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-saas-dify-blue-accessible)')).toBe(true)
|
||||
})
|
||||
|
||||
it('should render inactive state for Cloud', () => {
|
||||
const { container } = render(<Cloud isActive={false} />)
|
||||
|
||||
const rects = Array.from(container.querySelectorAll('rect'))
|
||||
const rects = [...container.querySelectorAll('rect')]
|
||||
expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-text-primary)')).toBe(true)
|
||||
})
|
||||
|
||||
it('should render active state for SelfHosted', () => {
|
||||
const { container } = render(<SelfHosted isActive />)
|
||||
|
||||
const rects = Array.from(container.querySelectorAll('rect'))
|
||||
const rects = [...container.querySelectorAll('rect')]
|
||||
expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-saas-dify-blue-accessible)')).toBe(true)
|
||||
})
|
||||
|
||||
it('should render inactive state for SelfHosted', () => {
|
||||
const { container } = render(<SelfHosted isActive={false} />)
|
||||
|
||||
const rects = Array.from(container.querySelectorAll('rect'))
|
||||
const rects = [...container.querySelectorAll('rect')]
|
||||
expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-text-primary)')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('ImagePreviewer', () => {
|
||||
|
||||
// Find and click next button (right arrow)
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const nextButton = Array.from(buttons).find(btn =>
|
||||
const nextButton = [...buttons].find(btn =>
|
||||
btn.className.includes('right-8'),
|
||||
)
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('ImagePreviewer', () => {
|
||||
|
||||
// Find and click prev button (left arrow)
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const prevButton = Array.from(buttons).find(btn =>
|
||||
const prevButton = [...buttons].find(btn =>
|
||||
btn.className.includes('left-8'),
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('ImagePreviewer', () => {
|
||||
})
|
||||
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const prevButton = Array.from(buttons).find(btn =>
|
||||
const prevButton = [...buttons].find(btn =>
|
||||
btn.className.includes('left-8'),
|
||||
)
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('ImagePreviewer', () => {
|
||||
})
|
||||
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const nextButton = Array.from(buttons).find(btn =>
|
||||
const nextButton = [...buttons].find(btn =>
|
||||
btn.className.includes('right-8'),
|
||||
)
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('ImagePreviewer', () => {
|
||||
})
|
||||
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const prevButton = Array.from(buttons).find(btn =>
|
||||
const prevButton = [...buttons].find(btn =>
|
||||
btn.className.includes('left-8'),
|
||||
)
|
||||
|
||||
@@ -324,7 +324,7 @@ describe('ImagePreviewer', () => {
|
||||
})
|
||||
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const nextButton = Array.from(buttons).find(btn =>
|
||||
const nextButton = [...buttons].find(btn =>
|
||||
btn.className.includes('right-8'),
|
||||
)
|
||||
|
||||
@@ -398,7 +398,7 @@ describe('ImagePreviewer', () => {
|
||||
|
||||
// Find and click the retry button (not the nav buttons)
|
||||
const allButtons = document.querySelectorAll('button')
|
||||
const retryButton = Array.from(allButtons).find(btn =>
|
||||
const retryButton = [...allButtons].find(btn =>
|
||||
btn.className.includes('rounded-full') && !btn.className.includes('left-8') && !btn.className.includes('right-8'),
|
||||
)
|
||||
|
||||
@@ -459,10 +459,10 @@ describe('ImagePreviewer', () => {
|
||||
|
||||
// Both navigation buttons should be disabled
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const prevButton = Array.from(buttons).find(btn =>
|
||||
const prevButton = [...buttons].find(btn =>
|
||||
btn.className.includes('left-8'),
|
||||
)
|
||||
const nextButton = Array.from(buttons).find(btn =>
|
||||
const nextButton = [...buttons].find(btn =>
|
||||
btn.className.includes('right-8'),
|
||||
)
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<span className="system-sm-semibold mr-0.5 text-text-secondary">{t('modelProvider.rerankModel.key', { ns: 'common' })}</span>
|
||||
<span className="mr-0.5 text-text-secondary system-sm-semibold">{t('modelProvider.rerankModel.key', { ns: 'common' })}</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="w-[200px]">{t('modelProvider.rerankModel.tip', { ns: 'common' })}</div>
|
||||
@@ -157,7 +157,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
<div className="p-1">
|
||||
<AlertTriangle className="size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('form.retrievalSetting.multiModalTip', { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -281,7 +281,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
<div className="p-1">
|
||||
<AlertTriangle className="size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('form.retrievalSetting.multiModalTip', { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('CreateFromPipeline', () => {
|
||||
describe('Component Order', () => {
|
||||
it('should render components in correct order', () => {
|
||||
const { container } = render(<CreateFromPipeline />)
|
||||
const children = Array.from(container.firstChild?.childNodes || [])
|
||||
const children = [...container.firstChild?.childNodes || []]
|
||||
|
||||
// Effect, Header, List, Footer
|
||||
expect(children.length).toBe(4)
|
||||
|
||||
@@ -1131,7 +1131,7 @@ describe('CreateFromDSLModal', () => {
|
||||
// There are two Cancel buttons now (one in main modal footer, one in error modal)
|
||||
// Find the Cancel button in the error modal context
|
||||
const cancelButtons = screen.getAllByText('app.newApp.Cancel')
|
||||
fireEvent.click(cancelButtons[cancelButtons.length - 1])
|
||||
fireEvent.click(cancelButtons.at(-1))
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ const Uploader: FC<Props> = ({
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
return
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('List', () => {
|
||||
describe('Component Order', () => {
|
||||
it('should render BuiltInPipelineList before CustomizedList', () => {
|
||||
const { container } = render(<List />)
|
||||
const children = Array.from(container.firstChild?.childNodes || [])
|
||||
const children = [...container.firstChild?.childNodes || []]
|
||||
|
||||
expect(children.length).toBe(2)
|
||||
expect((children[0] as HTMLElement).getAttribute('data-testid')).toBe('built-in-list')
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('FilePreview', () => {
|
||||
it('should show loading indicator initially', async () => {
|
||||
// Arrange - Delay API response to keep loading state
|
||||
mockFetchFilePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
|
||||
() => new Promise(resolve => setTimeout(resolve, 100, { content: 'test' })),
|
||||
)
|
||||
|
||||
const { container } = renderFilePreview()
|
||||
@@ -607,7 +607,7 @@ describe('FilePreview', () => {
|
||||
|
||||
it('should handle unmount during loading', async () => {
|
||||
mockFetchFilePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
|
||||
() => new Promise(resolve => setTimeout(resolve, 1000, { content: 'delayed' })),
|
||||
)
|
||||
|
||||
const { unmount } = renderFilePreview()
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('FileUploader', () => {
|
||||
// Find the delete button (the span with cursor-pointer containing the icon)
|
||||
const deleteButtons = container.querySelectorAll('[class*="cursor-pointer"]')
|
||||
// Get the last one which should be the delete button (not the browse label)
|
||||
const deleteButton = deleteButtons[deleteButtons.length - 1]
|
||||
const deleteButton = deleteButtons.at(-1)
|
||||
if (deleteButton)
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
|
||||
@@ -98,8 +98,7 @@ export const useFileUpload = ({
|
||||
docx: 'docx',
|
||||
}
|
||||
|
||||
return [...supportTypes]
|
||||
.map(item => extensionMap[item] || item)
|
||||
return Array.from(supportTypes, item => extensionMap[item] || item)
|
||||
.map(item => item.toLowerCase())
|
||||
.filter((item, index, self) => self.indexOf(item) === index)
|
||||
.map(item => item.toUpperCase())
|
||||
@@ -271,7 +270,7 @@ export const useFileUpload = ({
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const nested = await Promise.all(
|
||||
Array.from(e.dataTransfer.items).map((it) => {
|
||||
Array.from(e.dataTransfer.items, (it) => {
|
||||
const entry = (it as DataTransferItem & { webkitGetAsEntry?: () => FileSystemEntry | null }).webkitGetAsEntry?.()
|
||||
if (entry)
|
||||
return traverseFileEntry(entry)
|
||||
@@ -303,7 +302,7 @@ export const useFileUpload = ({
|
||||
}, [onFileListUpdate])
|
||||
|
||||
const fileChangeHandle = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let files = Array.from(e.target.files ?? []) as File[]
|
||||
let files = [...e.target.files ?? []] as File[]
|
||||
files = files.slice(0, fileUploadConfig.batch_count_limit)
|
||||
initialUpload(files.filter(isValid))
|
||||
}, [isValid, initialUpload, fileUploadConfig])
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('NotionPagePreview', () => {
|
||||
it('should show loading indicator initially', async () => {
|
||||
// Arrange - Delay API response to keep loading state
|
||||
mockFetchNotionPagePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
|
||||
() => new Promise(resolve => setTimeout(resolve, 100, { content: 'test' })),
|
||||
)
|
||||
|
||||
// Act - Don't wait for content to load
|
||||
@@ -701,7 +701,7 @@ describe('NotionPagePreview', () => {
|
||||
|
||||
it('should handle unmount during loading', async () => {
|
||||
mockFetchNotionPagePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
|
||||
() => new Promise(resolve => setTimeout(resolve, 1000, { content: 'delayed' })),
|
||||
)
|
||||
|
||||
// Act - Don't wait for content
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) =>
|
||||
return (
|
||||
<FormField label={(
|
||||
<div className="mb-1 flex items-center">
|
||||
<span className="system-sm-semibold mr-0.5">{t('stepTwo.separator', { ns: 'datasetCreation' })}</span>
|
||||
<span className="mr-0.5 system-sm-semibold">{t('stepTwo.separator', { ns: 'datasetCreation' })}</span>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="max-w-[200px]">
|
||||
@@ -52,7 +52,7 @@ export const MaxLengthInput: FC<InputNumberProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<FormField label={(
|
||||
<div className="system-sm-semibold mb-1">
|
||||
<div className="mb-1 system-sm-semibold">
|
||||
{t('stepTwo.maxLength', { ns: 'datasetCreation' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -251,7 +251,7 @@ describe('PreviewItem', () => {
|
||||
|
||||
// Assert - Check content is in pre-line div
|
||||
const preLineDivs = container.querySelectorAll('[style*="white-space: pre-line"]')
|
||||
const questionDiv = Array.from(preLineDivs).find(div => div.textContent?.includes('Question line 1'))
|
||||
const questionDiv = [...preLineDivs].find(div => div.textContent?.includes('Question line 1'))
|
||||
expect(questionDiv).toBeTruthy()
|
||||
expect(questionDiv?.textContent).toContain('Question line 2')
|
||||
})
|
||||
@@ -268,7 +268,7 @@ describe('PreviewItem', () => {
|
||||
|
||||
// Assert - Check content is in pre-line div
|
||||
const preLineDivs = container.querySelectorAll('[style*="white-space: pre-line"]')
|
||||
const answerDiv = Array.from(preLineDivs).find(div => div.textContent?.includes('Answer line 1'))
|
||||
const answerDiv = [...preLineDivs].find(div => div.textContent?.includes('Answer line 1'))
|
||||
expect(answerDiv).toBeTruthy()
|
||||
expect(answerDiv?.textContent).toContain('Answer line 2')
|
||||
})
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
// Act - Find the close span (it should be the span with onClick handler)
|
||||
const spans = container.querySelectorAll('span')
|
||||
const closeSpan = Array.from(spans).find(span =>
|
||||
const closeSpan = [...spans].find(span =>
|
||||
span.className && span.getAttribute('class')?.includes('close'),
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ describe('StopEmbeddingModal', () => {
|
||||
const { container } = renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
const spans = container.querySelectorAll('span')
|
||||
const closeSpan = Array.from(spans).find(span =>
|
||||
const closeSpan = [...spans].find(span =>
|
||||
span.className && span.getAttribute('class')?.includes('close'),
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
||||
vi.mock('@/utils/format', () => ({
|
||||
getFileExtension: (filename: string) => {
|
||||
const parts = filename.split('.')
|
||||
return parts[parts.length - 1] || ''
|
||||
return parts.at(-1) || ''
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -896,7 +896,7 @@ describe('useLocalFileUpload', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const calls = mockSetLocalFileList.mock.calls
|
||||
const lastCall = calls[calls.length - 1][0]
|
||||
const lastCall = calls.at(-1)[0]
|
||||
expect(lastCall.some((f: FileItem) => f.progress === PROGRESS_ERROR)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('FilePreview', () => {
|
||||
it('should call hidePreview when close button clicked', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const closeBtn = buttons[buttons.length - 1]
|
||||
const closeBtn = buttons.at(-1)
|
||||
fireEvent.click(closeBtn)
|
||||
expect(defaultProps.hidePreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('WebPreview', () => {
|
||||
it('should call hidePreview when close button clicked', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const closeBtn = buttons[buttons.length - 1]
|
||||
const closeBtn = buttons.at(-1)
|
||||
fireEvent.click(closeBtn)
|
||||
expect(defaultProps.hidePreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -62,10 +62,10 @@ const CSVUploader: FC<Props> = ({
|
||||
onprogress: onProgress,
|
||||
}, false, undefined, '?source=datasets')
|
||||
.then((res: UploadResult) => {
|
||||
const updatedFile = Object.assign({}, fileItem.file, {
|
||||
const updatedFile = { ...fileItem.file, ...{
|
||||
id: res.id,
|
||||
...(res as Partial<File>),
|
||||
}) as File
|
||||
} } as File
|
||||
const completeFile: FileItem = {
|
||||
fileID: fileItem.fileID,
|
||||
file: updatedFile,
|
||||
@@ -126,7 +126,7 @@ const CSVUploader: FC<Props> = ({
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
return
|
||||
@@ -148,7 +148,7 @@ const CSVUploader: FC<Props> = ({
|
||||
return ''
|
||||
|
||||
const arr = currentFile.name.split('.')
|
||||
return arr[arr.length - 1]
|
||||
return arr.at(-1)
|
||||
}
|
||||
|
||||
const isValid = useCallback((file?: File) => {
|
||||
@@ -204,7 +204,7 @@ const CSVUploader: FC<Props> = ({
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-components-panel-border bg-components-panel-bg-blur text-sm font-normal', dragging && 'border border-divider-subtle bg-components-panel-on-panel-item-bg-hover')}>
|
||||
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-components-panel-border bg-components-panel-bg-blur text-sm font-normal', dragging && 'border border-divider-subtle bg-components-panel-on-panel-item-bg-hover')}>
|
||||
<div className="flex w-full items-center justify-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-secondary">
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('ActionButtons', () => {
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const saveButton = buttons[buttons.length - 1] // Save button is last
|
||||
const saveButton = buttons.at(-1) // Save button is last
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(mockHandleSave).toHaveBeenCalledTimes(1)
|
||||
@@ -166,7 +166,7 @@ describe('ActionButtons', () => {
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const saveButton = buttons[buttons.length - 1]
|
||||
const saveButton = buttons.at(-1)
|
||||
expect(saveButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
|
||||
<Divider type="vertical" className="mx-1 h-3 bg-divider-regular" />
|
||||
<button
|
||||
type="button"
|
||||
className="system-xs-semibold text-text-accent"
|
||||
className="text-text-accent system-xs-semibold"
|
||||
onClick={() => {
|
||||
clearTimeout(refreshTimer.current)
|
||||
viewNewlyAddedChildChunk?.()
|
||||
@@ -120,11 +120,11 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
|
||||
<div className="flex h-full flex-col">
|
||||
<div className={cn('flex items-center justify-between', fullScreen ? 'border border-divider-subtle py-3 pl-6 pr-4' : 'pl-4 pr-3 pt-3')}>
|
||||
<div className="flex flex-col">
|
||||
<div className="system-xl-semibold text-text-primary">{t('segment.addChildChunk', { ns: 'datasetDocuments' })}</div>
|
||||
<div className="text-text-primary system-xl-semibold">{t('segment.addChildChunk', { ns: 'datasetDocuments' })}</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SegmentIndexTag label={t('segment.newChildChunk', { ns: 'datasetDocuments' }) as string} />
|
||||
<Dot />
|
||||
<span className="system-xs-medium text-text-tertiary">{wordCountText}</span>
|
||||
<span className="text-text-tertiary system-xs-medium">{wordCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -155,9 +155,9 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
labelPrefix={labelPrefix}
|
||||
/>
|
||||
<Dot />
|
||||
<div className={cn('system-xs-medium text-text-tertiary', contentOpacity)}>{wordCountText}</div>
|
||||
<div className={cn('text-text-tertiary system-xs-medium', contentOpacity)}>{wordCountText}</div>
|
||||
<Dot />
|
||||
<div className={cn('system-xs-medium text-text-tertiary', contentOpacity)}>{`${formatNumber(hit_count)} ${t('segment.hitCount', { ns: 'datasetDocuments' })}`}</div>
|
||||
<div className={cn('text-text-tertiary system-xs-medium', contentOpacity)}>{`${formatNumber(hit_count)} ${t('segment.hitCount', { ns: 'datasetDocuments' })}`}</div>
|
||||
{chunkEdited && (
|
||||
<>
|
||||
<Dot />
|
||||
@@ -254,7 +254,7 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="system-xs-semibold-uppercase mb-2 mt-0.5 text-text-accent"
|
||||
className="mb-2 mt-0.5 text-text-accent system-xs-semibold-uppercase"
|
||||
onClick={() => onClick?.()}
|
||||
>
|
||||
{t('operation.viewMore', { ns: 'common' })}
|
||||
|
||||
@@ -57,7 +57,7 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
|
||||
onSuccess: (res) => {
|
||||
setImportStatus(res.job_status)
|
||||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
setTimeout(checkProcess, 2500, res.job_id)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
Toast.notify({ type: 'error', message: `${t('list.batchModal.runError', { ns: 'datasetDocuments' })}` })
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
<Divider type="vertical" className="mx-1 h-3 bg-divider-regular" />
|
||||
<button
|
||||
type="button"
|
||||
className="system-xs-semibold text-text-accent"
|
||||
className="text-text-accent system-xs-semibold"
|
||||
onClick={() => {
|
||||
clearTimeout(refreshTimer.current)
|
||||
viewNewlyAddedChunk()
|
||||
@@ -158,13 +158,13 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
className={cn('flex items-center justify-between', fullScreen ? 'border border-divider-subtle py-3 pl-6 pr-4' : 'pl-4 pr-3 pt-3')}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="system-xl-semibold text-text-primary">
|
||||
<div className="text-text-primary system-xl-semibold">
|
||||
{t('segment.addChunk', { ns: 'datasetDocuments' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SegmentIndexTag label={t('segment.newChunk', { ns: 'datasetDocuments' })!} />
|
||||
<Dot />
|
||||
<span className="system-xs-medium text-text-tertiary">{wordCountText}</span>
|
||||
<span className="text-text-tertiary system-xs-medium">{wordCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('page')).toBe('2')
|
||||
expect(update.options.history).toBe('push')
|
||||
})
|
||||
@@ -177,7 +177,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('status')).toBe('error')
|
||||
})
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('status')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('sort')).toBe('hit_count')
|
||||
})
|
||||
|
||||
@@ -213,7 +213,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('sort')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('keyword')).toBe('test query')
|
||||
expect(update.options.history).toBe('replace')
|
||||
})
|
||||
@@ -238,7 +238,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('keyword')).toBe('hello')
|
||||
expect(update.searchParams.has('page')).toBe(false)
|
||||
expect(update.options.history).toBe('replace')
|
||||
@@ -252,7 +252,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('keyword')).toBe(false)
|
||||
expect(update.options.history).toBe('replace')
|
||||
})
|
||||
@@ -265,7 +265,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('keyword')).toBe(false)
|
||||
expect(result.current.query.keyword).toBe('')
|
||||
})
|
||||
@@ -278,7 +278,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('keyword')).toBe('%2F')
|
||||
expect(result.current.query.keyword).toBe('%2F')
|
||||
})
|
||||
@@ -302,7 +302,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('status')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('sort')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -326,7 +326,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('page')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('page')).toBe('2')
|
||||
})
|
||||
|
||||
@@ -350,7 +350,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.get('limit')).toBe('25')
|
||||
})
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('page')).toBe(false)
|
||||
expect(result.current.query.page).toBe(1)
|
||||
})
|
||||
@@ -375,7 +375,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('limit')).toBe(false)
|
||||
expect(result.current.query.limit).toBe(10)
|
||||
})
|
||||
@@ -406,7 +406,7 @@ describe('useDocumentListQueryState', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
const update = onUrlUpdate.mock.calls.at(-1)[0]
|
||||
expect(update.searchParams.has('page')).toBe(false)
|
||||
expect(update.searchParams.has('status')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('AddExternalAPIModal', () => {
|
||||
|
||||
// There are multiple cancel buttons, find the one in the confirm dialog
|
||||
const cancelButtons = screen.getAllByRole('button', { name: /cancel/i })
|
||||
const confirmDialogCancelButton = cancelButtons[cancelButtons.length - 1]
|
||||
const confirmDialogCancelButton = cancelButtons.at(-1)
|
||||
fireEvent.click(confirmDialogCancelButton)
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -125,13 +125,13 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCan
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black/[.25]">
|
||||
<div className="shadows-shadow-xl relative flex w-[480px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg">
|
||||
<div className="flex flex-col items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
|
||||
<div className="title-2xl-semi-bold grow self-stretch text-text-primary">
|
||||
<div className="grow self-stretch text-text-primary title-2xl-semi-bold">
|
||||
{
|
||||
isEditMode ? t('editExternalAPIFormTitle', { ns: 'dataset' }) : t('createExternalAPI', { ns: 'dataset' })
|
||||
}
|
||||
</div>
|
||||
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
|
||||
<div className="system-xs-regular flex items-center text-text-tertiary">
|
||||
<div className="flex items-center text-text-tertiary system-xs-regular">
|
||||
{t('editExternalAPIFormWarning.front', { ns: 'dataset' })}
|
||||
<span className="flex cursor-pointer items-center text-text-accent">
|
||||
|
||||
@@ -144,12 +144,12 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCan
|
||||
popupContent={(
|
||||
<div className="p-1">
|
||||
<div className="flex items-start self-stretch pb-0.5 pl-2 pr-3 pt-1">
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{`${datasetBindings?.length} ${t('editExternalAPITooltipTitle', { ns: 'dataset' })}`}</div>
|
||||
<div className="text-text-tertiary system-xs-medium-uppercase">{`${datasetBindings?.length} ${t('editExternalAPITooltipTitle', { ns: 'dataset' })}`}</div>
|
||||
</div>
|
||||
{datasetBindings?.map(binding => (
|
||||
<div key={binding.id} className="flex items-center gap-1 self-stretch px-2 py-1">
|
||||
<RiBook2Line className="h-4 w-4 text-text-secondary" />
|
||||
<div className="system-sm-medium text-text-secondary">{binding.name}</div>
|
||||
<div className="text-text-secondary system-sm-medium">{binding.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -193,8 +193,8 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCan
|
||||
{t('externalAPIForm.save', { ns: 'dataset' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="system-xs-regular flex items-center justify-center gap-1 self-stretch rounded-b-2xl border-t-[0.5px]
|
||||
border-divider-subtle bg-background-soft px-2 py-3 text-text-tertiary"
|
||||
<div className="flex items-center justify-center gap-1 self-stretch rounded-b-2xl border-t-[0.5px] border-divider-subtle
|
||||
bg-background-soft px-2 py-3 text-text-tertiary system-xs-regular"
|
||||
>
|
||||
<RiLock2Fill className="h-3 w-3 text-text-quaternary" />
|
||||
{t('externalAPIForm.encrypted.front', { ns: 'dataset' })}
|
||||
|
||||
@@ -65,7 +65,7 @@ const Card = ({
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
/>
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
<div className="text-text-tertiary system-xs-regular">
|
||||
{t('appMenus.apiAccessTip', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ const Card = ({
|
||||
className="flex h-8 items-center space-x-[7px] rounded-lg px-2 text-text-tertiary hover:bg-state-base-hover"
|
||||
>
|
||||
<RiBookOpenLine className="size-3.5 shrink-0" />
|
||||
<div className="system-sm-regular grow truncate">
|
||||
<div className="grow truncate system-sm-regular">
|
||||
{t('overview.apiInfo.doc', { ns: 'appOverview' })}
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-3.5 shrink-0" />
|
||||
|
||||
@@ -555,7 +555,7 @@ describe('Card (service-api)', () => {
|
||||
})
|
||||
|
||||
it('should handle very long apiBaseUrl', () => {
|
||||
const longUrl = 'https://'.concat('a'.repeat(500), '.com')
|
||||
const longUrl = [...'https://', ...'a'.repeat(500), ...'.com']
|
||||
render(<Card apiBaseUrl={longUrl} />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ const { EditSlice } = await import('../edit-slice')
|
||||
|
||||
// Helper to find divider span (zero-width space)
|
||||
const findDividerSpan = (container: HTMLElement) =>
|
||||
Array.from(container.querySelectorAll('span')).find(s => s.textContent?.includes('\u200B'))
|
||||
[...container.querySelectorAll('span')].find(s => s.textContent?.includes('\u200B'))
|
||||
|
||||
describe('EditSlice', () => {
|
||||
const defaultProps = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user