mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into fix/secret_variable
This commit is contained in:
commit
679fd9d188
|
|
@ -14,7 +14,7 @@ The codebase is split into:
|
|||
|
||||
- Run backend CLI commands through `uv run --project api <command>`.
|
||||
|
||||
- Backend QA gate requires passing `make lint`, `make type-check`, and `uv run --project api --dev dev/pytest/pytest_unit_tests.sh` before review.
|
||||
- Before submission, all backend modifications must pass local checks: `make lint`, `make type-check`, and `uv run --project api --dev dev/pytest/pytest_unit_tests.sh`.
|
||||
|
||||
- Use Makefile targets for linting and formatting; `make lint` and `make type-check` cover the required checks.
|
||||
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ class UpdateConfig(BaseSettings):
|
|||
|
||||
class WorkflowVariableTruncationConfig(BaseSettings):
|
||||
WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE: PositiveInt = Field(
|
||||
# 100KB
|
||||
# 1000 KiB
|
||||
1024_000,
|
||||
description="Maximum size for variable to trigger final truncation.",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,62 +49,80 @@ class IndexingRunner:
|
|||
self.storage = storage
|
||||
self.model_manager = ModelManager()
|
||||
|
||||
def _handle_indexing_error(self, document_id: str, error: Exception) -> None:
|
||||
"""Handle indexing errors by updating document status."""
|
||||
logger.exception("consume document failed")
|
||||
document = db.session.get(DatasetDocument, document_id)
|
||||
if document:
|
||||
document.indexing_status = "error"
|
||||
error_message = getattr(error, "description", str(error))
|
||||
document.error = str(error_message)
|
||||
document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
|
||||
def run(self, dataset_documents: list[DatasetDocument]):
|
||||
"""Run the indexing process."""
|
||||
for dataset_document in dataset_documents:
|
||||
document_id = dataset_document.id
|
||||
try:
|
||||
# Re-query the document to ensure it's bound to the current session
|
||||
requeried_document = db.session.get(DatasetDocument, document_id)
|
||||
if not requeried_document:
|
||||
logger.warning("Document not found, skipping document id: %s", document_id)
|
||||
continue
|
||||
|
||||
# get dataset
|
||||
dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
|
||||
dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first()
|
||||
|
||||
if not dataset:
|
||||
raise ValueError("no dataset found")
|
||||
# get the process rule
|
||||
stmt = select(DatasetProcessRule).where(
|
||||
DatasetProcessRule.id == dataset_document.dataset_process_rule_id
|
||||
DatasetProcessRule.id == requeried_document.dataset_process_rule_id
|
||||
)
|
||||
processing_rule = db.session.scalar(stmt)
|
||||
if not processing_rule:
|
||||
raise ValueError("no process rule found")
|
||||
index_type = dataset_document.doc_form
|
||||
index_type = requeried_document.doc_form
|
||||
index_processor = IndexProcessorFactory(index_type).init_index_processor()
|
||||
# extract
|
||||
text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict())
|
||||
text_docs = self._extract(index_processor, requeried_document, processing_rule.to_dict())
|
||||
|
||||
# transform
|
||||
documents = self._transform(
|
||||
index_processor, dataset, text_docs, dataset_document.doc_language, processing_rule.to_dict()
|
||||
index_processor, dataset, text_docs, requeried_document.doc_language, processing_rule.to_dict()
|
||||
)
|
||||
# save segment
|
||||
self._load_segments(dataset, dataset_document, documents)
|
||||
self._load_segments(dataset, requeried_document, documents)
|
||||
|
||||
# load
|
||||
self._load(
|
||||
index_processor=index_processor,
|
||||
dataset=dataset,
|
||||
dataset_document=dataset_document,
|
||||
dataset_document=requeried_document,
|
||||
documents=documents,
|
||||
)
|
||||
except DocumentIsPausedError:
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {dataset_document.id}")
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {document_id}")
|
||||
except ProviderTokenNotInitError as e:
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e.description)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
except ObjectDeletedError:
|
||||
logger.warning("Document deleted, document id: %s", dataset_document.id)
|
||||
logger.warning("Document deleted, document id: %s", document_id)
|
||||
except Exception as e:
|
||||
logger.exception("consume document failed")
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
|
||||
def run_in_splitting_status(self, dataset_document: DatasetDocument):
|
||||
"""Run the indexing process when the index_status is splitting."""
|
||||
document_id = dataset_document.id
|
||||
try:
|
||||
# Re-query the document to ensure it's bound to the current session
|
||||
requeried_document = db.session.get(DatasetDocument, document_id)
|
||||
if not requeried_document:
|
||||
logger.warning("Document not found: %s", document_id)
|
||||
return
|
||||
|
||||
# get dataset
|
||||
dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
|
||||
dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first()
|
||||
|
||||
if not dataset:
|
||||
raise ValueError("no dataset found")
|
||||
|
|
@ -112,57 +130,60 @@ class IndexingRunner:
|
|||
# get exist document_segment list and delete
|
||||
document_segments = (
|
||||
db.session.query(DocumentSegment)
|
||||
.filter_by(dataset_id=dataset.id, document_id=dataset_document.id)
|
||||
.filter_by(dataset_id=dataset.id, document_id=requeried_document.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
for document_segment in document_segments:
|
||||
db.session.delete(document_segment)
|
||||
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
|
||||
if requeried_document.doc_form == IndexType.PARENT_CHILD_INDEX:
|
||||
# delete child chunks
|
||||
db.session.query(ChildChunk).where(ChildChunk.segment_id == document_segment.id).delete()
|
||||
db.session.commit()
|
||||
# get the process rule
|
||||
stmt = select(DatasetProcessRule).where(DatasetProcessRule.id == dataset_document.dataset_process_rule_id)
|
||||
stmt = select(DatasetProcessRule).where(DatasetProcessRule.id == requeried_document.dataset_process_rule_id)
|
||||
processing_rule = db.session.scalar(stmt)
|
||||
if not processing_rule:
|
||||
raise ValueError("no process rule found")
|
||||
|
||||
index_type = dataset_document.doc_form
|
||||
index_type = requeried_document.doc_form
|
||||
index_processor = IndexProcessorFactory(index_type).init_index_processor()
|
||||
# extract
|
||||
text_docs = self._extract(index_processor, dataset_document, processing_rule.to_dict())
|
||||
text_docs = self._extract(index_processor, requeried_document, processing_rule.to_dict())
|
||||
|
||||
# transform
|
||||
documents = self._transform(
|
||||
index_processor, dataset, text_docs, dataset_document.doc_language, processing_rule.to_dict()
|
||||
index_processor, dataset, text_docs, requeried_document.doc_language, processing_rule.to_dict()
|
||||
)
|
||||
# save segment
|
||||
self._load_segments(dataset, dataset_document, documents)
|
||||
self._load_segments(dataset, requeried_document, documents)
|
||||
|
||||
# load
|
||||
self._load(
|
||||
index_processor=index_processor, dataset=dataset, dataset_document=dataset_document, documents=documents
|
||||
index_processor=index_processor,
|
||||
dataset=dataset,
|
||||
dataset_document=requeried_document,
|
||||
documents=documents,
|
||||
)
|
||||
except DocumentIsPausedError:
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {dataset_document.id}")
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {document_id}")
|
||||
except ProviderTokenNotInitError as e:
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e.description)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
except Exception as e:
|
||||
logger.exception("consume document failed")
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
|
||||
def run_in_indexing_status(self, dataset_document: DatasetDocument):
|
||||
"""Run the indexing process when the index_status is indexing."""
|
||||
document_id = dataset_document.id
|
||||
try:
|
||||
# Re-query the document to ensure it's bound to the current session
|
||||
requeried_document = db.session.get(DatasetDocument, document_id)
|
||||
if not requeried_document:
|
||||
logger.warning("Document not found: %s", document_id)
|
||||
return
|
||||
|
||||
# get dataset
|
||||
dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
|
||||
dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first()
|
||||
|
||||
if not dataset:
|
||||
raise ValueError("no dataset found")
|
||||
|
|
@ -170,7 +191,7 @@ class IndexingRunner:
|
|||
# get exist document_segment list and delete
|
||||
document_segments = (
|
||||
db.session.query(DocumentSegment)
|
||||
.filter_by(dataset_id=dataset.id, document_id=dataset_document.id)
|
||||
.filter_by(dataset_id=dataset.id, document_id=requeried_document.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
|
@ -188,7 +209,7 @@ class IndexingRunner:
|
|||
"dataset_id": document_segment.dataset_id,
|
||||
},
|
||||
)
|
||||
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
|
||||
if requeried_document.doc_form == IndexType.PARENT_CHILD_INDEX:
|
||||
child_chunks = document_segment.get_child_chunks()
|
||||
if child_chunks:
|
||||
child_documents = []
|
||||
|
|
@ -206,24 +227,20 @@ class IndexingRunner:
|
|||
document.children = child_documents
|
||||
documents.append(document)
|
||||
# build index
|
||||
index_type = dataset_document.doc_form
|
||||
index_type = requeried_document.doc_form
|
||||
index_processor = IndexProcessorFactory(index_type).init_index_processor()
|
||||
self._load(
|
||||
index_processor=index_processor, dataset=dataset, dataset_document=dataset_document, documents=documents
|
||||
index_processor=index_processor,
|
||||
dataset=dataset,
|
||||
dataset_document=requeried_document,
|
||||
documents=documents,
|
||||
)
|
||||
except DocumentIsPausedError:
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {dataset_document.id}")
|
||||
raise DocumentIsPausedError(f"Document paused, document id: {document_id}")
|
||||
except ProviderTokenNotInitError as e:
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e.description)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
except Exception as e:
|
||||
logger.exception("consume document failed")
|
||||
dataset_document.indexing_status = "error"
|
||||
dataset_document.error = str(e)
|
||||
dataset_document.stopped_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
self._handle_indexing_error(document_id, e)
|
||||
|
||||
def indexing_estimate(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class LLMGenerator:
|
|||
return name
|
||||
|
||||
@classmethod
|
||||
def generate_suggested_questions_after_answer(cls, tenant_id: str, histories: str):
|
||||
def generate_suggested_questions_after_answer(cls, tenant_id: str, histories: str) -> Sequence[str]:
|
||||
output_parser = SuggestedQuestionsAfterAnswerOutputParser()
|
||||
format_instructions = output_parser.get_format_instructions()
|
||||
|
||||
|
|
@ -119,6 +119,8 @@ class LLMGenerator:
|
|||
|
||||
prompt_messages = [UserPromptMessage(content=prompt)]
|
||||
|
||||
questions: Sequence[str] = []
|
||||
|
||||
try:
|
||||
response: LLMResult = model_instance.invoke_llm(
|
||||
prompt_messages=list(prompt_messages),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
|
||||
from core.llm_generator.prompts import SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SuggestedQuestionsAfterAnswerOutputParser:
|
||||
def get_format_instructions(self) -> str:
|
||||
return SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT
|
||||
|
||||
def parse(self, text: str):
|
||||
def parse(self, text: str) -> Sequence[str]:
|
||||
action_match = re.search(r"\[.*?\]", text.strip(), re.DOTALL)
|
||||
questions: list[str] = []
|
||||
if action_match is not None:
|
||||
json_obj = json.loads(action_match.group(0).strip())
|
||||
else:
|
||||
json_obj = []
|
||||
return json_obj
|
||||
try:
|
||||
json_obj = json.loads(action_match.group(0).strip())
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning("Failed to decode suggested questions payload: %s", exc)
|
||||
else:
|
||||
if isinstance(json_obj, list):
|
||||
questions = [question for question in json_obj if isinstance(question, str)]
|
||||
return questions
|
||||
|
|
|
|||
|
|
@ -441,10 +441,14 @@ class LLMNode(Node):
|
|||
usage = LLMUsage.empty_usage()
|
||||
finish_reason = None
|
||||
full_text_buffer = io.StringIO()
|
||||
collected_structured_output = None # Collect structured_output from streaming chunks
|
||||
# Consume the invoke result and handle generator exception
|
||||
try:
|
||||
for result in invoke_result:
|
||||
if isinstance(result, LLMResultChunkWithStructuredOutput):
|
||||
# Collect structured_output from the chunk
|
||||
if result.structured_output is not None:
|
||||
collected_structured_output = dict(result.structured_output)
|
||||
yield result
|
||||
if isinstance(result, LLMResultChunk):
|
||||
contents = result.delta.message.content
|
||||
|
|
@ -492,6 +496,8 @@ class LLMNode(Node):
|
|||
finish_reason=finish_reason,
|
||||
# Reasoning content for workflow variables and downstream nodes
|
||||
reasoning_content=reasoning_content,
|
||||
# Pass structured output if collected from streaming chunks
|
||||
structured_output=collected_structured_output,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from tasks.clean_dataset_task import clean_dataset_task
|
|||
@dataset_was_deleted.connect
|
||||
def handle(sender: Dataset, **kwargs):
|
||||
dataset = sender
|
||||
assert dataset.doc_form
|
||||
assert dataset.indexing_technique
|
||||
if not dataset.doc_form or not dataset.indexing_technique:
|
||||
return
|
||||
clean_dataset_task.delay(
|
||||
dataset.id,
|
||||
dataset.tenant_id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ def handle(sender, **kwargs):
|
|||
dataset_id = kwargs.get("dataset_id")
|
||||
doc_form = kwargs.get("doc_form")
|
||||
file_id = kwargs.get("file_id")
|
||||
assert dataset_id is not None
|
||||
assert doc_form is not None
|
||||
if not dataset_id or not doc_form:
|
||||
return
|
||||
clean_document_task.delay(document_id, dataset_id, doc_form, file_id)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
from configs import dify_config
|
||||
from constants import HEADER_NAME_APP_CODE, HEADER_NAME_CSRF_TOKEN
|
||||
from constants import HEADER_NAME_APP_CODE, HEADER_NAME_CSRF_TOKEN, HEADER_NAME_PASSPORT
|
||||
from dify_app import DifyApp
|
||||
|
||||
BASE_CORS_HEADERS: tuple[str, ...] = ("Content-Type", HEADER_NAME_APP_CODE, HEADER_NAME_PASSPORT)
|
||||
SERVICE_API_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, "Authorization")
|
||||
AUTHENTICATED_HEADERS: tuple[str, ...] = (*SERVICE_API_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
||||
FILES_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
||||
|
||||
|
||||
def init_app(app: DifyApp):
|
||||
# register blueprint routers
|
||||
|
|
@ -17,7 +22,7 @@ def init_app(app: DifyApp):
|
|||
|
||||
CORS(
|
||||
service_api_bp,
|
||||
allow_headers=["Content-Type", "Authorization", HEADER_NAME_APP_CODE],
|
||||
allow_headers=list(SERVICE_API_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
)
|
||||
app.register_blueprint(service_api_bp)
|
||||
|
|
@ -26,7 +31,7 @@ def init_app(app: DifyApp):
|
|||
web_bp,
|
||||
resources={r"/*": {"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS}},
|
||||
supports_credentials=True,
|
||||
allow_headers=["Content-Type", "Authorization", HEADER_NAME_APP_CODE, HEADER_NAME_CSRF_TOKEN],
|
||||
allow_headers=list(AUTHENTICATED_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=["X-Version", "X-Env"],
|
||||
)
|
||||
|
|
@ -36,7 +41,7 @@ def init_app(app: DifyApp):
|
|||
console_app_bp,
|
||||
resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}},
|
||||
supports_credentials=True,
|
||||
allow_headers=["Content-Type", "Authorization", HEADER_NAME_CSRF_TOKEN],
|
||||
allow_headers=list(AUTHENTICATED_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=["X-Version", "X-Env"],
|
||||
)
|
||||
|
|
@ -44,7 +49,7 @@ def init_app(app: DifyApp):
|
|||
|
||||
CORS(
|
||||
files_bp,
|
||||
allow_headers=["Content-Type", HEADER_NAME_CSRF_TOKEN],
|
||||
allow_headers=list(FILES_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
)
|
||||
app.register_blueprint(files_bp)
|
||||
|
|
|
|||
|
|
@ -22,55 +22,6 @@ def upgrade():
|
|||
batch_op.add_column(sa.Column('app_mode', sa.String(length=255), nullable=True))
|
||||
batch_op.create_index('message_app_mode_idx', ['app_mode'], unique=False)
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
# Strategy: Update in batches to minimize lock time
|
||||
# For large tables (millions of rows), this prevents long-running transactions
|
||||
batch_size = 10000
|
||||
|
||||
print("Starting backfill of app_mode from conversations...")
|
||||
|
||||
# Use a more efficient UPDATE with JOIN
|
||||
# This query updates messages.app_mode from conversations.mode
|
||||
# Using string formatting for LIMIT since it's a constant
|
||||
update_query = f"""
|
||||
UPDATE messages m
|
||||
SET app_mode = c.mode
|
||||
FROM conversations c
|
||||
WHERE m.conversation_id = c.id
|
||||
AND m.app_mode IS NULL
|
||||
AND m.id IN (
|
||||
SELECT id FROM messages
|
||||
WHERE app_mode IS NULL
|
||||
LIMIT {batch_size}
|
||||
)
|
||||
"""
|
||||
|
||||
# Execute batched updates
|
||||
total_updated = 0
|
||||
iteration = 0
|
||||
while True:
|
||||
iteration += 1
|
||||
result = conn.execute(sa.text(update_query))
|
||||
|
||||
# Check if result is None or has no rowcount
|
||||
if result is None:
|
||||
print("Warning: Query returned None, stopping backfill")
|
||||
break
|
||||
|
||||
rows_updated = result.rowcount if hasattr(result, 'rowcount') else 0
|
||||
total_updated += rows_updated
|
||||
|
||||
if rows_updated == 0:
|
||||
break
|
||||
|
||||
print(f"Iteration {iteration}: Updated {rows_updated} messages (total: {total_updated})")
|
||||
|
||||
# For very large tables, add a small delay to reduce load
|
||||
# Uncomment if needed: import time; time.sleep(0.1)
|
||||
|
||||
print(f"Backfill completed. Total messages updated: {total_updated}")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ dependencies = [
|
|||
"pycryptodome==3.19.1",
|
||||
"pydantic~=2.11.4",
|
||||
"pydantic-extra-types~=2.10.3",
|
||||
"pydantic-settings~=2.9.1",
|
||||
"pydantic-settings~=2.11.0",
|
||||
"pyjwt~=2.10.1",
|
||||
"pypdfium2==4.30.0",
|
||||
"python-docx~=1.1.0",
|
||||
|
|
|
|||
|
|
@ -288,9 +288,10 @@ class MessageService:
|
|||
)
|
||||
|
||||
with measure_time() as timer:
|
||||
questions: list[str] = LLMGenerator.generate_suggested_questions_after_answer(
|
||||
questions_sequence = LLMGenerator.generate_suggested_questions_after_answer(
|
||||
tenant_id=app_model.tenant_id, histories=histories
|
||||
)
|
||||
questions: list[str] = list(questions_sequence)
|
||||
|
||||
# get tracing instance
|
||||
trace_manager = TraceQueueManager(app_id=app_model.id)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class VariableTruncator:
|
|||
self,
|
||||
string_length_limit=5000,
|
||||
array_element_limit: int = 20,
|
||||
max_size_bytes: int = 1024_000, # 100KB
|
||||
max_size_bytes: int = 1024_000, # 1000 KiB
|
||||
):
|
||||
if string_length_limit <= 3:
|
||||
raise ValueError("string_length_limit should be greater than 3.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from models.account import Account, TenantAccountRole
|
||||
from models.dataset import Dataset
|
||||
from services.dataset_service import DatasetService
|
||||
|
||||
|
||||
class DatasetDeleteTestDataFactory:
|
||||
"""Factory class for creating test data and mock objects for dataset delete tests."""
|
||||
|
||||
@staticmethod
|
||||
def create_dataset_mock(
|
||||
dataset_id: str = "dataset-123",
|
||||
tenant_id: str = "test-tenant-123",
|
||||
created_by: str = "creator-456",
|
||||
doc_form: str | None = None,
|
||||
indexing_technique: str | None = "high_quality",
|
||||
**kwargs,
|
||||
) -> Mock:
|
||||
"""Create a mock dataset with specified attributes."""
|
||||
dataset = Mock(spec=Dataset)
|
||||
dataset.id = dataset_id
|
||||
dataset.tenant_id = tenant_id
|
||||
dataset.created_by = created_by
|
||||
dataset.doc_form = doc_form
|
||||
dataset.indexing_technique = indexing_technique
|
||||
for key, value in kwargs.items():
|
||||
setattr(dataset, key, value)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def create_user_mock(
|
||||
user_id: str = "user-789",
|
||||
tenant_id: str = "test-tenant-123",
|
||||
role: TenantAccountRole = TenantAccountRole.ADMIN,
|
||||
**kwargs,
|
||||
) -> Mock:
|
||||
"""Create a mock user with specified attributes."""
|
||||
user = Mock(spec=Account)
|
||||
user.id = user_id
|
||||
user.current_tenant_id = tenant_id
|
||||
user.current_role = role
|
||||
for key, value in kwargs.items():
|
||||
setattr(user, key, value)
|
||||
return user
|
||||
|
||||
|
||||
class TestDatasetServiceDeleteDataset:
|
||||
"""
|
||||
Comprehensive unit tests for DatasetService.delete_dataset method.
|
||||
|
||||
This test suite covers all deletion scenarios including:
|
||||
- Normal dataset deletion with documents
|
||||
- Empty dataset deletion (no documents, doc_form is None)
|
||||
- Dataset deletion with missing indexing_technique
|
||||
- Permission checks
|
||||
- Event handling
|
||||
|
||||
This test suite provides regression protection for issue #27073.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dataset_service_dependencies(self):
|
||||
"""Common mock setup for dataset service dependencies."""
|
||||
with (
|
||||
patch("services.dataset_service.DatasetService.get_dataset") as mock_get_dataset,
|
||||
patch("services.dataset_service.DatasetService.check_dataset_permission") as mock_check_perm,
|
||||
patch("extensions.ext_database.db.session") as mock_db,
|
||||
patch("services.dataset_service.dataset_was_deleted") as mock_dataset_was_deleted,
|
||||
):
|
||||
yield {
|
||||
"get_dataset": mock_get_dataset,
|
||||
"check_permission": mock_check_perm,
|
||||
"db_session": mock_db,
|
||||
"dataset_was_deleted": mock_dataset_was_deleted,
|
||||
}
|
||||
|
||||
def test_delete_dataset_with_documents_success(self, mock_dataset_service_dependencies):
|
||||
"""
|
||||
Test successful deletion of a dataset with documents.
|
||||
|
||||
This test verifies:
|
||||
- Dataset is retrieved correctly
|
||||
- Permission check is performed
|
||||
- dataset_was_deleted event is sent
|
||||
- Dataset is deleted from database
|
||||
- Method returns True
|
||||
"""
|
||||
# Arrange
|
||||
dataset = DatasetDeleteTestDataFactory.create_dataset_mock(
|
||||
doc_form="text_model", indexing_technique="high_quality"
|
||||
)
|
||||
user = DatasetDeleteTestDataFactory.create_user_mock()
|
||||
|
||||
mock_dataset_service_dependencies["get_dataset"].return_value = dataset
|
||||
|
||||
# Act
|
||||
result = DatasetService.delete_dataset(dataset.id, user)
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
mock_dataset_service_dependencies["get_dataset"].assert_called_once_with(dataset.id)
|
||||
mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user)
|
||||
mock_dataset_service_dependencies["dataset_was_deleted"].send.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].delete.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].commit.assert_called_once()
|
||||
|
||||
def test_delete_empty_dataset_success(self, mock_dataset_service_dependencies):
|
||||
"""
|
||||
Test successful deletion of an empty dataset (no documents, doc_form is None).
|
||||
|
||||
This test verifies that:
|
||||
- Empty datasets can be deleted without errors
|
||||
- dataset_was_deleted event is sent (event handler will skip cleanup if doc_form is None)
|
||||
- Dataset is deleted from database
|
||||
- Method returns True
|
||||
|
||||
This is the primary test for issue #27073 where deleting an empty dataset
|
||||
caused internal server error due to assertion failure in event handlers.
|
||||
"""
|
||||
# Arrange
|
||||
dataset = DatasetDeleteTestDataFactory.create_dataset_mock(doc_form=None, indexing_technique=None)
|
||||
user = DatasetDeleteTestDataFactory.create_user_mock()
|
||||
|
||||
mock_dataset_service_dependencies["get_dataset"].return_value = dataset
|
||||
|
||||
# Act
|
||||
result = DatasetService.delete_dataset(dataset.id, user)
|
||||
|
||||
# Assert - Verify complete deletion flow
|
||||
assert result is True
|
||||
mock_dataset_service_dependencies["get_dataset"].assert_called_once_with(dataset.id)
|
||||
mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user)
|
||||
mock_dataset_service_dependencies["dataset_was_deleted"].send.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].delete.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].commit.assert_called_once()
|
||||
|
||||
def test_delete_dataset_with_partial_none_values(self, mock_dataset_service_dependencies):
|
||||
"""
|
||||
Test deletion of dataset with partial None values.
|
||||
|
||||
This test verifies that datasets with partial None values (e.g., doc_form exists
|
||||
but indexing_technique is None) can be deleted successfully. The event handler
|
||||
will skip cleanup if any required field is None.
|
||||
|
||||
Improvement based on Gemini Code Assist suggestion: Added comprehensive assertions
|
||||
to verify all core deletion operations are performed, not just event sending.
|
||||
"""
|
||||
# Arrange
|
||||
dataset = DatasetDeleteTestDataFactory.create_dataset_mock(doc_form="text_model", indexing_technique=None)
|
||||
user = DatasetDeleteTestDataFactory.create_user_mock()
|
||||
|
||||
mock_dataset_service_dependencies["get_dataset"].return_value = dataset
|
||||
|
||||
# Act
|
||||
result = DatasetService.delete_dataset(dataset.id, user)
|
||||
|
||||
# Assert - Verify complete deletion flow (Gemini suggestion implemented)
|
||||
assert result is True
|
||||
mock_dataset_service_dependencies["get_dataset"].assert_called_once_with(dataset.id)
|
||||
mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user)
|
||||
mock_dataset_service_dependencies["dataset_was_deleted"].send.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].delete.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].commit.assert_called_once()
|
||||
|
||||
def test_delete_dataset_with_doc_form_none_indexing_technique_exists(self, mock_dataset_service_dependencies):
|
||||
"""
|
||||
Test deletion of dataset where doc_form is None but indexing_technique exists.
|
||||
|
||||
This edge case can occur in certain dataset configurations and should be handled
|
||||
gracefully by the event handler's conditional check.
|
||||
"""
|
||||
# Arrange
|
||||
dataset = DatasetDeleteTestDataFactory.create_dataset_mock(doc_form=None, indexing_technique="high_quality")
|
||||
user = DatasetDeleteTestDataFactory.create_user_mock()
|
||||
|
||||
mock_dataset_service_dependencies["get_dataset"].return_value = dataset
|
||||
|
||||
# Act
|
||||
result = DatasetService.delete_dataset(dataset.id, user)
|
||||
|
||||
# Assert - Verify complete deletion flow
|
||||
assert result is True
|
||||
mock_dataset_service_dependencies["get_dataset"].assert_called_once_with(dataset.id)
|
||||
mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user)
|
||||
mock_dataset_service_dependencies["dataset_was_deleted"].send.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].delete.assert_called_once_with(dataset)
|
||||
mock_dataset_service_dependencies["db_session"].commit.assert_called_once()
|
||||
|
||||
def test_delete_dataset_not_found(self, mock_dataset_service_dependencies):
|
||||
"""
|
||||
Test deletion attempt when dataset doesn't exist.
|
||||
|
||||
This test verifies that:
|
||||
- Method returns False when dataset is not found
|
||||
- No deletion operations are performed
|
||||
- No events are sent
|
||||
"""
|
||||
# Arrange
|
||||
dataset_id = "non-existent-dataset"
|
||||
user = DatasetDeleteTestDataFactory.create_user_mock()
|
||||
|
||||
mock_dataset_service_dependencies["get_dataset"].return_value = None
|
||||
|
||||
# Act
|
||||
result = DatasetService.delete_dataset(dataset_id, user)
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
mock_dataset_service_dependencies["get_dataset"].assert_called_once_with(dataset_id)
|
||||
mock_dataset_service_dependencies["check_permission"].assert_not_called()
|
||||
mock_dataset_service_dependencies["dataset_was_deleted"].send.assert_not_called()
|
||||
mock_dataset_service_dependencies["db_session"].delete.assert_not_called()
|
||||
mock_dataset_service_dependencies["db_session"].commit.assert_not_called()
|
||||
|
|
@ -1545,7 +1545,7 @@ requires-dist = [
|
|||
{ name = "pycryptodome", specifier = "==3.19.1" },
|
||||
{ name = "pydantic", specifier = "~=2.11.4" },
|
||||
{ name = "pydantic-extra-types", specifier = "~=2.10.3" },
|
||||
{ name = "pydantic-settings", specifier = "~=2.9.1" },
|
||||
{ name = "pydantic-settings", specifier = "~=2.11.0" },
|
||||
{ name = "pyjwt", specifier = "~=2.10.1" },
|
||||
{ name = "pypdfium2", specifier = "==4.30.0" },
|
||||
{ name = "python-docx", specifier = "~=1.1.0" },
|
||||
|
|
@ -4754,16 +4754,16 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
version = "2.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
import type { StorybookConfig } from '@storybook/nextjs'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
// stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-docs',
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/nextjs',
|
||||
options: {},
|
||||
options: {
|
||||
builder: {
|
||||
useSWC: true,
|
||||
lazyCompilation: false,
|
||||
},
|
||||
nextConfigPath: undefined,
|
||||
},
|
||||
},
|
||||
staticDirs: ['../public'],
|
||||
core: {
|
||||
disableWhatsNewNotifications: true,
|
||||
},
|
||||
docs: {
|
||||
defaultName: 'Documentation',
|
||||
},
|
||||
}
|
||||
export default config
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
import React from 'react'
|
||||
import type { Preview } from '@storybook/react'
|
||||
import { withThemeByDataAttribute } from '@storybook/addon-themes'
|
||||
import I18nServer from '../app/components/i18n-server'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import I18N from '../app/components/i18n'
|
||||
import { ToastProvider } from '../app/components/base/toast'
|
||||
|
||||
import '../app/styles/globals.css'
|
||||
import '../app/styles/markdown.scss'
|
||||
import './storybook.css'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const decorators = [
|
||||
withThemeByDataAttribute({
|
||||
themes: {
|
||||
|
|
@ -17,9 +26,15 @@ export const decorators = [
|
|||
attributeName: 'data-theme',
|
||||
}),
|
||||
(Story) => {
|
||||
return <I18nServer>
|
||||
<Story />
|
||||
</I18nServer>
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18N locale="en-US">
|
||||
<ToastProvider>
|
||||
<Story />
|
||||
</ToastProvider>
|
||||
</I18N>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -31,7 +46,11 @@ const preview: Preview = {
|
|||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
toc: true,
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default preview
|
||||
|
|
|
|||
|
|
@ -160,8 +160,7 @@ describe('Navigation Utilities', () => {
|
|||
page: 1,
|
||||
limit: '',
|
||||
keyword: 'test',
|
||||
empty: null,
|
||||
undefined,
|
||||
filter: '',
|
||||
})
|
||||
|
||||
expect(path).toBe('/datasets/123/documents?page=1&keyword=test')
|
||||
|
|
|
|||
|
|
@ -39,28 +39,38 @@ const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = fa
|
|||
const isDarkQuery = DARK_MODE_MEDIA_QUERY.test(query)
|
||||
const matches = isDarkQuery ? systemPrefersDark : false
|
||||
|
||||
const handleAddListener = (listener: (event: MediaQueryListEvent) => void) => {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
const handleRemoveListener = (listener: (event: MediaQueryListEvent) => void) => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
|
||||
const handleAddEventListener = (_event: string, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.add(listener as (event: MediaQueryListEvent) => void)
|
||||
}
|
||||
|
||||
const handleRemoveEventListener = (_event: string, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.delete(listener as (event: MediaQueryListEvent) => void)
|
||||
}
|
||||
|
||||
const handleDispatchEvent = (event: Event) => {
|
||||
listeners.forEach(listener => listener(event as MediaQueryListEvent))
|
||||
return true
|
||||
}
|
||||
|
||||
const mediaQueryList: MediaQueryList = {
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: (listener: MediaQueryListListener) => {
|
||||
listeners.add(listener)
|
||||
},
|
||||
removeListener: (listener: MediaQueryListListener) => {
|
||||
listeners.delete(listener)
|
||||
},
|
||||
addEventListener: (_event, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.add(listener as MediaQueryListListener)
|
||||
},
|
||||
removeEventListener: (_event, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.delete(listener as MediaQueryListListener)
|
||||
},
|
||||
dispatchEvent: (event: Event) => {
|
||||
listeners.forEach(listener => listener(event as MediaQueryListEvent))
|
||||
return true
|
||||
},
|
||||
addListener: handleAddListener,
|
||||
removeListener: handleRemoveListener,
|
||||
addEventListener: handleAddEventListener,
|
||||
removeEventListener: handleRemoveEventListener,
|
||||
dispatchEvent: handleDispatchEvent,
|
||||
}
|
||||
|
||||
return mediaQueryList
|
||||
|
|
@ -69,6 +79,121 @@ const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = fa
|
|||
jest.spyOn(window, 'matchMedia').mockImplementation(mockMatchMedia)
|
||||
}
|
||||
|
||||
// Helper function to create timing page component
|
||||
const createTimingPageComponent = (
|
||||
timingData: Array<{ phase: string; timestamp: number; styles: { backgroundColor: string; color: string } }>,
|
||||
) => {
|
||||
const recordTiming = (phase: string, styles: { backgroundColor: string; color: string }) => {
|
||||
timingData.push({
|
||||
phase,
|
||||
timestamp: performance.now(),
|
||||
styles,
|
||||
})
|
||||
}
|
||||
|
||||
const TimingPageComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const isDark = mounted ? theme === 'dark' : false
|
||||
|
||||
const currentStyles = {
|
||||
backgroundColor: isDark ? '#1f2937' : '#ffffff',
|
||||
color: isDark ? '#ffffff' : '#000000',
|
||||
}
|
||||
|
||||
recordTiming(mounted ? 'CSR' : 'Initial', currentStyles)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="timing-page"
|
||||
style={currentStyles}
|
||||
>
|
||||
<div data-testid="timing-status">
|
||||
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return TimingPageComponent
|
||||
}
|
||||
|
||||
// Helper function to create CSS test component
|
||||
const createCSSTestComponent = (
|
||||
cssStates: Array<{ className: string; timestamp: number }>,
|
||||
) => {
|
||||
const recordCSSState = (className: string) => {
|
||||
cssStates.push({
|
||||
className,
|
||||
timestamp: performance.now(),
|
||||
})
|
||||
}
|
||||
|
||||
const CSSTestComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const isDark = mounted ? theme === 'dark' : false
|
||||
|
||||
const className = `min-h-screen ${isDark ? 'bg-gray-900 text-white' : 'bg-white text-black'}`
|
||||
|
||||
recordCSSState(className)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="css-component"
|
||||
className={className}
|
||||
>
|
||||
<div data-testid="css-classes">Classes: {className}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return CSSTestComponent
|
||||
}
|
||||
|
||||
// Helper function to create performance test component
|
||||
const createPerformanceTestComponent = (
|
||||
performanceMarks: Array<{ event: string; timestamp: number }>,
|
||||
) => {
|
||||
const recordPerformanceMark = (event: string) => {
|
||||
performanceMarks.push({ event, timestamp: performance.now() })
|
||||
}
|
||||
|
||||
const PerformanceTestComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
recordPerformanceMark('component-render')
|
||||
|
||||
useEffect(() => {
|
||||
recordPerformanceMark('mount-start')
|
||||
setMounted(true)
|
||||
recordPerformanceMark('mount-complete')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (theme)
|
||||
recordPerformanceMark('theme-available')
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<div data-testid="performance-test">
|
||||
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return PerformanceTestComponent
|
||||
}
|
||||
|
||||
// Simulate real page component based on Dify's actual theme usage
|
||||
const PageComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
|
@ -227,39 +352,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
setupMockEnvironment('dark')
|
||||
|
||||
const timingData: Array<{ phase: string; timestamp: number; styles: any }> = []
|
||||
|
||||
const TimingPageComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const isDark = mounted ? theme === 'dark' : false
|
||||
|
||||
// Record timing and styles for each render phase
|
||||
const currentStyles = {
|
||||
backgroundColor: isDark ? '#1f2937' : '#ffffff',
|
||||
color: isDark ? '#ffffff' : '#000000',
|
||||
}
|
||||
|
||||
timingData.push({
|
||||
phase: mounted ? 'CSR' : 'Initial',
|
||||
timestamp: performance.now(),
|
||||
styles: currentStyles,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="timing-page"
|
||||
style={currentStyles}
|
||||
>
|
||||
<div data-testid="timing-status">
|
||||
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const TimingPageComponent = createTimingPageComponent(timingData)
|
||||
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
|
|
@ -295,33 +388,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
setupMockEnvironment('dark')
|
||||
|
||||
const cssStates: Array<{ className: string; timestamp: number }> = []
|
||||
|
||||
const CSSTestComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const isDark = mounted ? theme === 'dark' : false
|
||||
|
||||
// Simulate Tailwind CSS class application
|
||||
const className = `min-h-screen ${isDark ? 'bg-gray-900 text-white' : 'bg-white text-black'}`
|
||||
|
||||
cssStates.push({
|
||||
className,
|
||||
timestamp: performance.now(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="css-component"
|
||||
className={className}
|
||||
>
|
||||
<div data-testid="css-classes">Classes: {className}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const CSSTestComponent = createCSSTestComponent(cssStates)
|
||||
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
|
|
@ -413,34 +480,12 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
test('verifies ThemeProvider position fix reduces initialization delay', async () => {
|
||||
const performanceMarks: Array<{ event: string; timestamp: number }> = []
|
||||
|
||||
const PerformanceTestComponent = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
performanceMarks.push({ event: 'component-render', timestamp: performance.now() })
|
||||
|
||||
useEffect(() => {
|
||||
performanceMarks.push({ event: 'mount-start', timestamp: performance.now() })
|
||||
setMounted(true)
|
||||
performanceMarks.push({ event: 'mount-complete', timestamp: performance.now() })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (theme)
|
||||
performanceMarks.push({ event: 'theme-available', timestamp: performance.now() })
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<div data-testid="performance-test">
|
||||
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
expect(window.localStorage.getItem('theme')).toBe('dark')
|
||||
|
||||
const PerformanceTestComponent = createPerformanceTestComponent(performanceMarks)
|
||||
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
<PerformanceTestComponent />
|
||||
|
|
|
|||
|
|
@ -70,14 +70,18 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
})
|
||||
|
||||
describe('Fallback Logic (from layout-main.tsx)', () => {
|
||||
type Tag = { id: string; name: string }
|
||||
type AppDetail = { tags: Tag[] }
|
||||
type FallbackResult = { tags?: Tag[] } | null
|
||||
// no-op
|
||||
it('should trigger fallback when tags are missing or empty', () => {
|
||||
const appDetailWithoutTags = { tags: [] }
|
||||
const appDetailWithTags = { tags: [{ id: 'tag1' }] }
|
||||
const appDetailWithUndefinedTags = { tags: undefined as any }
|
||||
const appDetailWithoutTags: AppDetail = { tags: [] }
|
||||
const appDetailWithTags: AppDetail = { tags: [{ id: 'tag1', name: 't' }] }
|
||||
const appDetailWithUndefinedTags: { tags: Tag[] | undefined } = { tags: undefined }
|
||||
|
||||
// This simulates the condition in layout-main.tsx
|
||||
const shouldFallback1 = !appDetailWithoutTags.tags || appDetailWithoutTags.tags.length === 0
|
||||
const shouldFallback2 = !appDetailWithTags.tags || appDetailWithTags.tags.length === 0
|
||||
const shouldFallback1 = appDetailWithoutTags.tags.length === 0
|
||||
const shouldFallback2 = appDetailWithTags.tags.length === 0
|
||||
const shouldFallback3 = !appDetailWithUndefinedTags.tags || appDetailWithUndefinedTags.tags.length === 0
|
||||
|
||||
expect(shouldFallback1).toBe(true) // Empty array should trigger fallback
|
||||
|
|
@ -86,24 +90,26 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
})
|
||||
|
||||
it('should preserve tags when fallback succeeds', () => {
|
||||
const originalAppDetail = { tags: [] as any[] }
|
||||
const fallbackResult = { tags: [{ id: 'tag1', name: 'fallback-tag' }] }
|
||||
const originalAppDetail: AppDetail = { tags: [] }
|
||||
const fallbackResult: { tags?: Tag[] } = { tags: [{ id: 'tag1', name: 'fallback-tag' }] }
|
||||
|
||||
// This simulates the successful fallback in layout-main.tsx
|
||||
if (fallbackResult?.tags)
|
||||
originalAppDetail.tags = fallbackResult.tags
|
||||
const tags = fallbackResult.tags
|
||||
if (tags)
|
||||
originalAppDetail.tags = tags
|
||||
|
||||
expect(originalAppDetail.tags).toEqual(fallbackResult.tags)
|
||||
expect(originalAppDetail.tags.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should continue with empty tags when fallback fails', () => {
|
||||
const originalAppDetail: { tags: any[] } = { tags: [] }
|
||||
const fallbackResult: { tags?: any[] } | null = null
|
||||
const originalAppDetail: AppDetail = { tags: [] }
|
||||
const fallbackResult = null as FallbackResult
|
||||
|
||||
// This simulates fallback failure in layout-main.tsx
|
||||
if (fallbackResult?.tags)
|
||||
originalAppDetail.tags = fallbackResult.tags
|
||||
const tags: Tag[] | undefined = fallbackResult && 'tags' in fallbackResult ? fallbackResult.tags : undefined
|
||||
if (tags)
|
||||
originalAppDetail.tags = tags
|
||||
|
||||
expect(originalAppDetail.tags).toEqual([])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import AppPublisher from '@/app/components/app/app-publisher'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import copy from 'copy-to-clipboard'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiErrorWarningFill,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfirmAddVar from './confirm-add-var'
|
||||
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ChangeEvent, FC } from 'react'
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import ConfigSelect from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useMemo, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import Panel from '../base/feature-panel'
|
||||
import EditModal from './config-modal'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ParamConfig from './param-config'
|
||||
import { Vision } from '@/app/components/base/icons/src/vender/features'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import ParamItem from '@/app/components/base/param-item'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/features'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
||||
import { Document } from '@/app/components/base/icons/src/vender/features'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useFormattingChangedDispatcher } from '../debug/hooks'
|
||||
import DatasetConfig from '../dataset-config'
|
||||
import HistoryPanel from '../config-prompt/conversation-history/history-panel'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useCallback, useMemo } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { intersectionBy } from 'lodash-es'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useFormattingChangedDispatcher } from '../debug/hooks'
|
||||
import FeaturePanel from '../base/feature-panel'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import produce, { setAutoFreeze } from 'immer'
|
||||
import { produce, setAutoFreeze } from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
RiAddLine,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { clone } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { basePath } from '@/utils/var'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import { clone, isEqual } from 'lodash-es'
|
||||
import { CodeBracketIcon } from '@heroicons/react/20/solid'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { fn } from '@storybook/test'
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
|
||||
import { RocketLaunchIcon } from '@heroicons/react/20/solid'
|
||||
import { Button } from '.'
|
||||
|
|
@ -20,8 +19,7 @@ const meta = {
|
|||
},
|
||||
args: {
|
||||
variant: 'ghost',
|
||||
onClick: fn(),
|
||||
children: 'adsf',
|
||||
children: 'Button',
|
||||
},
|
||||
} satisfies Meta<typeof Button>
|
||||
|
||||
|
|
@ -33,6 +31,9 @@ export const Default: Story = {
|
|||
variant: 'primary',
|
||||
loading: false,
|
||||
children: 'Primary Button',
|
||||
styleCss: {},
|
||||
spinnerClassName: '',
|
||||
destructive: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
Callback,
|
||||
ChatConfig,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
|
||||
import type { ChatItem } from '../../types'
|
||||
import { mockedWorkflowProcess } from './__mocks__/workflowProcess'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
|
||||
import type { ChatItem } from '../types'
|
||||
import Question from './question'
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { useState } from 'react'
|
||||
import Confirm from '.'
|
||||
import Button from '../button'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Confirm',
|
||||
component: Confirm,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Confirmation dialog component that supports warning and info types, with customizable button text and behavior.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['info', 'warning'],
|
||||
description: 'Dialog type',
|
||||
},
|
||||
isShow: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show the dialog',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Dialog title',
|
||||
},
|
||||
content: {
|
||||
control: 'text',
|
||||
description: 'Dialog content',
|
||||
},
|
||||
confirmText: {
|
||||
control: 'text',
|
||||
description: 'Confirm button text',
|
||||
},
|
||||
cancelText: {
|
||||
control: 'text',
|
||||
description: 'Cancel button text',
|
||||
},
|
||||
isLoading: {
|
||||
control: 'boolean',
|
||||
description: 'Confirm button loading state',
|
||||
},
|
||||
isDisabled: {
|
||||
control: 'boolean',
|
||||
description: 'Confirm button disabled state',
|
||||
},
|
||||
showConfirm: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show confirm button',
|
||||
},
|
||||
showCancel: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show cancel button',
|
||||
},
|
||||
maskClosable: {
|
||||
control: 'boolean',
|
||||
description: 'Whether clicking mask closes dialog',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Confirm>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Interactive demo wrapper
|
||||
const ConfirmDemo = (args: any) => {
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="primary" onClick={() => setIsShow(true)}>
|
||||
Open Dialog
|
||||
</Button>
|
||||
<Confirm
|
||||
{...args}
|
||||
isShow={isShow}
|
||||
onConfirm={() => {
|
||||
console.log('✅ User clicked confirm')
|
||||
setIsShow(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
console.log('❌ User clicked cancel')
|
||||
setIsShow(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Basic warning dialog - Delete action
|
||||
export const WarningDialog: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Delete Confirmation',
|
||||
content: 'Are you sure you want to delete this project? This action cannot be undone.',
|
||||
},
|
||||
}
|
||||
|
||||
// Info dialog
|
||||
export const InfoDialog: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Notice',
|
||||
content: 'Your changes have been saved. Do you want to proceed to the next step?',
|
||||
},
|
||||
}
|
||||
|
||||
// Custom button text
|
||||
export const CustomButtonText: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Exit Editor',
|
||||
content: 'You have unsaved changes. Are you sure you want to exit?',
|
||||
confirmText: 'Discard Changes',
|
||||
cancelText: 'Continue Editing',
|
||||
},
|
||||
}
|
||||
|
||||
// Loading state
|
||||
export const LoadingState: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Deleting...',
|
||||
content: 'Please wait while we delete the file...',
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
export const DisabledState: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Verification Required',
|
||||
content: 'Please complete email verification before proceeding.',
|
||||
isDisabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Alert style - Confirm button only
|
||||
export const AlertStyle: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Success',
|
||||
content: 'Your settings have been updated!',
|
||||
showCancel: false,
|
||||
confirmText: 'Got it',
|
||||
},
|
||||
}
|
||||
|
||||
// Dangerous action - Long content
|
||||
export const DangerousAction: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Permanently Delete Account',
|
||||
content: 'This action will permanently delete your account and all associated data, including: all projects and files, collaboration history, and personal settings. This action cannot be reversed!',
|
||||
confirmText: 'Delete My Account',
|
||||
cancelText: 'Keep My Account',
|
||||
},
|
||||
}
|
||||
|
||||
// Non-closable mask
|
||||
export const NotMaskClosable: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Important Action',
|
||||
content: 'This action requires your explicit choice. Clicking outside will not close this dialog.',
|
||||
maskClosable: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Full feature demo - Playground
|
||||
export const Playground: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'This is a title',
|
||||
content: 'This is the dialog content text...',
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
showConfirm: true,
|
||||
showCancel: true,
|
||||
maskClosable: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiEqualizer2Line, RiExternalLinkLine } from '@remixicon/react'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { Citations } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiEditLine } from '@remixicon/react'
|
||||
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { FolderUpload } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { VirtualAssistant } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiEqualizer2Line, RiImage2Fill } from '@remixicon/react'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import SettingModal from '@/app/components/base/features/new-feature-panel/file-upload/setting-modal'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { ContentModeration } from '@/app/components/base/icons/src/vender/features'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiSparklingFill } from '@remixicon/react'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { TextToAudio } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import useSWR from 'swr'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import React, { Fragment } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
useState,
|
||||
} from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FileEntity } from './types'
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="route" clip-path="url(#clip0_3167_28693)">
|
||||
<path id="Icon" d="M6.70866 2.91699H6.96206C8.73962 2.91699 9.6284 2.91699 9.96578 3.23624C10.2574 3.51221 10.3867 3.91874 10.3079 4.31245C10.2168 4.76792 9.49122 5.28116 8.03999 6.30763L5.66899 7.98468C4.21777 9.01116 3.49215 9.5244 3.40106 9.97987C3.32233 10.3736 3.45157 10.7801 3.7432 11.0561C4.08059 11.3753 4.96937 11.3753 6.74693 11.3753H7.29199M4.66699 2.91699C4.66699 3.88349 3.88349 4.66699 2.91699 4.66699C1.95049 4.66699 1.16699 3.88349 1.16699 2.91699C1.16699 1.95049 1.95049 1.16699 2.91699 1.16699C3.88349 1.16699 4.66699 1.95049 4.66699 2.91699ZM12.8337 11.0837C12.8337 12.0502 12.0502 12.8337 11.0837 12.8337C10.1172 12.8337 9.33366 12.0502 9.33366 11.0837C9.33366 10.1172 10.1172 9.33366 11.0837 9.33366C12.0502 9.33366 12.8337 10.1172 12.8337 11.0837Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3167_28693">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,8 +0,0 @@
|
|||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<g id="Solid">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.39498 2.71706C6.90587 2.57557 7.44415 2.49996 8.00008 2.49996C9.30806 2.49996 10.5183 2.91849 11.5041 3.62893C10.9796 3.97562 10.5883 4.35208 10.3171 4.75458C9.90275 5.36959 9.79654 6.00558 9.88236 6.58587C9.96571 7.1494 10.2245 7.63066 10.4965 7.98669C10.7602 8.33189 11.0838 8.6206 11.3688 8.76305C12.0863 9.12177 12.9143 9.30141 13.5334 9.39399C14.0933 9.47774 15.2805 9.75802 15.3244 8.86608C15.3304 8.74474 15.3334 8.62267 15.3334 8.49996C15.3334 4.44987 12.0502 1.16663 8.00008 1.16663C3.94999 1.16663 0.666748 4.44987 0.666748 8.49996C0.666748 12.55 3.94999 15.8333 8.00008 15.8333C8.1228 15.8333 8.24486 15.8303 8.3662 15.8243C8.73395 15.8062 9.01738 15.4934 8.99927 15.1256C8.98117 14.7579 8.66837 14.4745 8.30063 14.4926C8.20111 14.4975 8.10091 14.5 8.00008 14.5C5.6605 14.5 3.63367 13.1609 2.6442 11.2074L3.28991 10.8346L5.67171 11.2804C6.28881 11.3959 6.85846 10.9208 6.85566 10.293L6.84632 8.19093L8.06357 6.10697C8.26079 5.76932 8.24312 5.3477 8.01833 5.02774L6.39498 2.71706Z" fill="#1570EF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29718 8.93736C9.05189 8.84432 8.77484 8.90379 8.58934 9.08929C8.40383 9.27479 8.34437 9.55184 8.43741 9.79713L10.5486 15.363C10.6461 15.6199 10.8912 15.7908 11.166 15.7932C11.4408 15.7956 11.689 15.6292 11.791 15.374L12.6714 13.1714L14.874 12.2909C15.1292 12.1889 15.2957 11.9408 15.2932 11.666C15.2908 11.3912 15.12 11.146 14.863 11.0486L9.29718 8.93736Z" fill="#1570EF"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="route-sep">
|
||||
<path id="Icon" d="M6.08303 2.5H6.30023C7.82386 2.5 8.58567 2.5 8.87485 2.77364C9.12483 3.01018 9.23561 3.35864 9.16812 3.69611C9.09004 4.08651 8.46809 4.52643 7.22418 5.40627L5.19189 6.84373C3.94799 7.72357 3.32603 8.16349 3.24795 8.55389C3.18046 8.89136 3.29124 9.23982 3.54122 9.47636C3.8304 9.75 4.59221 9.75 6.11584 9.75H6.58303" stroke="#F79009" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Icon_2" d="M2.83301 4C3.66143 4 4.33301 3.32843 4.33301 2.5C4.33301 1.67157 3.66143 1 2.83301 1C2.00458 1 1.33301 1.67157 1.33301 2.5C1.33301 3.32843 2.00458 4 2.83301 4Z" fill="#F79009"/>
|
||||
<path id="Icon_3" d="M9.83301 11C10.6614 11 11.333 10.3284 11.333 9.5C11.333 8.67157 10.6614 8 9.83301 8C9.00458 8 8.33301 8.67157 8.33301 9.5C8.33301 10.3284 9.00458 11 9.83301 11Z" fill="#F79009"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 927 B |
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 14 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "ai-text"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M2.33301 10.5H4.08301M2.33301 7H5.24967M2.33301 3.5H11.6663M9.91634 5.83333L10.7913 7.875L12.833 8.75L10.7913 9.625L9.91634 11.6667L9.04134 9.625L6.99967 8.75L9.04134 7.875L9.91634 5.83333Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AiText"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './AiText.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AiText'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { default as AiText } from './AiText'
|
||||
export { default as ChatBotSlim } from './ChatBotSlim'
|
||||
export { default as ChatBot } from './ChatBot'
|
||||
export { default as CuteRobot } from './CuteRobot'
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 14 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "route",
|
||||
"clip-path": "url(#clip0_3167_28693)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M6.70866 2.91699H6.96206C8.73962 2.91699 9.6284 2.91699 9.96578 3.23624C10.2574 3.51221 10.3867 3.91874 10.3079 4.31245C10.2168 4.76792 9.49122 5.28116 8.03999 6.30763L5.66899 7.98468C4.21777 9.01116 3.49215 9.5244 3.40106 9.97987C3.32233 10.3736 3.45157 10.7801 3.7432 11.0561C4.08059 11.3753 4.96937 11.3753 6.74693 11.3753H7.29199M4.66699 2.91699C4.66699 3.88349 3.88349 4.66699 2.91699 4.66699C1.95049 4.66699 1.16699 3.88349 1.16699 2.91699C1.16699 1.95049 1.95049 1.16699 2.91699 1.16699C3.88349 1.16699 4.66699 1.95049 4.66699 2.91699ZM12.8337 11.0837C12.8337 12.0502 12.0502 12.8337 11.0837 12.8337C10.1172 12.8337 9.33366 12.0502 9.33366 11.0837C9.33366 10.1172 10.1172 9.33366 11.0837 9.33366C12.0502 9.33366 12.8337 10.1172 12.8337 11.0837Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_3167_28693"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Route"
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 5C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M17.9191 9.60608C17.7616 9.2384 17.4 9 17 9C16.6 9 16.2384 9.2384 16.0809 9.60608L14.7384 12.7384L11.6061 14.0809C11.2384 14.2384 11 14.6 11 15C11 15.4 11.2384 15.7616 11.6061 15.9191L14.7384 17.2616L16.0809 20.3939C16.2384 20.7616 16.6 21 17 21C17.4 21 17.7616 20.7616 17.9191 20.3939L19.2616 17.2616L22.3939 15.9191C22.7616 15.7616 23 15.4 23 15C23 14.6 22.7616 14.2384 22.3939 14.0809L19.2616 12.7384L17.9191 9.60608Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H9C9.55228 13 10 12.5523 10 12C10 11.4477 9.55228 11 9 11H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 17C3.44772 17 3 17.4477 3 18C3 18.5523 3.44772 19 4 19H7C7.55228 19 8 18.5523 8 18C8 17.4477 7.55228 17 7 17H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AiText"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './AiText.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AiText'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { default as AiText } from './AiText'
|
||||
export { default as BubbleTextMod } from './BubbleTextMod'
|
||||
export { default as ChatBot } from './ChatBot'
|
||||
export { default as CuteRobot } from './CuteRobot'
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "17",
|
||||
"viewBox": "0 0 16 17",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Solid"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M6.39498 2.71706C6.90587 2.57557 7.44415 2.49996 8.00008 2.49996C9.30806 2.49996 10.5183 2.91849 11.5041 3.62893C10.9796 3.97562 10.5883 4.35208 10.3171 4.75458C9.90275 5.36959 9.79654 6.00558 9.88236 6.58587C9.96571 7.1494 10.2245 7.63066 10.4965 7.98669C10.7602 8.33189 11.0838 8.6206 11.3688 8.76305C12.0863 9.12177 12.9143 9.30141 13.5334 9.39399C14.0933 9.47774 15.2805 9.75802 15.3244 8.86608C15.3304 8.74474 15.3334 8.62267 15.3334 8.49996C15.3334 4.44987 12.0502 1.16663 8.00008 1.16663C3.94999 1.16663 0.666748 4.44987 0.666748 8.49996C0.666748 12.55 3.94999 15.8333 8.00008 15.8333C8.1228 15.8333 8.24486 15.8303 8.3662 15.8243C8.73395 15.8062 9.01738 15.4934 8.99927 15.1256C8.98117 14.7579 8.66837 14.4745 8.30063 14.4926C8.20111 14.4975 8.10091 14.5 8.00008 14.5C5.6605 14.5 3.63367 13.1609 2.6442 11.2074L3.28991 10.8346L5.67171 11.2804C6.28881 11.3959 6.85846 10.9208 6.85566 10.293L6.84632 8.19093L8.06357 6.10697C8.26079 5.76932 8.24312 5.3477 8.01833 5.02774L6.39498 2.71706Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M9.29718 8.93736C9.05189 8.84432 8.77484 8.90379 8.58934 9.08929C8.40383 9.27479 8.34437 9.55184 8.43741 9.79713L10.5486 15.363C10.6461 15.6199 10.8912 15.7908 11.166 15.7932C11.4408 15.7956 11.689 15.6292 11.791 15.374L12.6714 13.1714L14.874 12.2909C15.1292 12.1889 15.2957 11.9408 15.2932 11.666C15.2908 11.3912 15.12 11.146 14.863 11.0486L9.29718 8.93736Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Globe06"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Globe06.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'Globe06'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "13",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 13 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "route-sep"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M6.08303 2.5H6.30023C7.82386 2.5 8.58567 2.5 8.87485 2.77364C9.12483 3.01018 9.23561 3.35864 9.16812 3.69611C9.09004 4.08651 8.46809 4.52643 7.22418 5.40627L5.19189 6.84373C3.94799 7.72357 3.32603 8.16349 3.24795 8.55389C3.18046 8.89136 3.29124 9.23982 3.54122 9.47636C3.8304 9.75 4.59221 9.75 6.11584 9.75H6.58303",
|
||||
"stroke": "currentColor",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_2",
|
||||
"d": "M2.83301 4C3.66143 4 4.33301 3.32843 4.33301 2.5C4.33301 1.67157 3.66143 1 2.83301 1C2.00458 1 1.33301 1.67157 1.33301 2.5C1.33301 3.32843 2.00458 4 2.83301 4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_3",
|
||||
"d": "M9.83301 11C10.6614 11 11.333 10.3284 11.333 9.5C11.333 8.67157 10.6614 8 9.83301 8C9.00458 8 8.33301 8.67157 8.33301 9.5C8.33301 10.3284 9.00458 11 9.83301 11Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Route"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Route.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'Route'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Globe06 } from './Globe06'
|
||||
export { default as Route } from './Route'
|
||||
|
|
@ -3,13 +3,13 @@ import React from 'react'
|
|||
export type AbstractNode = {
|
||||
name: string
|
||||
attributes: {
|
||||
[key: string]: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
children?: AbstractNode[]
|
||||
}
|
||||
|
||||
export type Attrs = {
|
||||
[key: string]: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
|
||||
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
|
||||
|
|
@ -24,6 +24,9 @@ export function normalizeAttrs(attrs: Attrs = {}): Attrs {
|
|||
return acc
|
||||
|
||||
const val = attrs[key]
|
||||
if (val === undefined)
|
||||
return acc
|
||||
|
||||
key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase())
|
||||
key = key.replace(/([:]\w)/g, (g: string) => g[1].toUpperCase())
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.18055 6.45828C7.52291 6.45828 8.61111 5.37008 8.61111 4.02772C8.61111 2.68536 7.52291 1.59717 6.18055 1.59717C4.8382 1.59717 3.75 2.68536 3.75 4.02772C3.75 5.37008 4.8382 6.45828 6.18055 6.45828Z" fill="#EF6820"/>
|
||||
<path d="M13.8192 6.45828C15.1616 6.45828 16.2498 5.37008 16.2498 4.02772C16.2498 2.68536 15.1616 1.59717 13.8192 1.59717C12.4769 1.59717 11.3887 2.68536 11.3887 4.02772C11.3887 5.37008 12.4769 6.45828 13.8192 6.45828Z" fill="#EF6820"/>
|
||||
<path d="M13.8193 7.84719C13.0627 7.84805 12.3185 8.03933 11.6552 8.40341C10.992 8.7675 10.4311 9.29267 10.0241 9.93053C10.5745 9.93695 11.1 10.1609 11.4858 10.5535C11.8716 10.9461 12.0864 11.4755 12.0831 12.0259C12.0799 12.5763 11.859 13.1031 11.4687 13.4911C11.0783 13.8792 10.5503 14.097 9.99984 14.097C9.44942 14.097 8.92135 13.8792 8.53101 13.4911C8.14066 13.1031 7.91976 12.5763 7.91655 12.0259C7.91334 11.4755 8.12808 10.9461 8.51387 10.5535C8.89966 10.1609 9.42515 9.93695 9.97554 9.93053C9.45127 9.10686 8.67371 8.47572 7.75983 8.13205C6.84596 7.78839 5.84519 7.75078 4.9081 8.0249C3.97101 8.29902 3.14828 8.87003 2.56368 9.65203C1.97908 10.434 1.66424 11.3847 1.66652 12.3611V16.875C1.66652 17.0591 1.73968 17.2358 1.86991 17.366C2.00015 17.4962 2.17678 17.5694 2.36096 17.5694H7.22207V15.8333L4.72207 13.9583C4.64911 13.9036 4.58765 13.835 4.54118 13.7566C4.49472 13.6781 4.46417 13.5912 4.45127 13.501C4.42522 13.3186 4.47267 13.1334 4.58318 12.9861C4.69369 12.8387 4.8582 12.7413 5.04053 12.7153C5.22285 12.6892 5.40806 12.7367 5.5554 12.8472L8.14776 14.7916H11.8519L14.4443 12.8472C14.5916 12.7367 14.7768 12.6892 14.9592 12.7153C15.1415 12.7413 15.306 12.8387 15.4165 12.9861C15.527 13.1334 15.5745 13.3186 15.5484 13.501C15.5224 13.6833 15.425 13.8478 15.2776 13.9583L12.7776 15.8333V17.5694H17.6387C17.8229 17.5694 17.9995 17.4962 18.1298 17.366C18.26 17.2358 18.3332 17.0591 18.3332 16.875V12.3611C18.3317 11.1644 17.8557 10.0171 17.0095 9.17091C16.1633 8.32471 15.016 7.84867 13.8193 7.84719Z" fill="#EF6820"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Effect" opacity="0.8" filter="url(#filter0_f_481_16338)">
|
||||
<circle cx="32" cy="32" r="28" fill="#EF6820"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_481_16338" x="-156" y="-156" width="376" height="376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="80" result="effect1_foregroundBlur_481_16338"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
|
|
@ -13,7 +13,7 @@ import { DataSourceProvider, type NotionPage } from '@/models/common'
|
|||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useGetDefaultDataSourceListAuth } from '@/service/use-datasource'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import {
|
|||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import SettingCog from '../assets/setting-gear-mod.svg'
|
||||
import OrangeEffect from '../assets/option-card-effect-orange.svg'
|
||||
import FamilyMod from '../assets/family-mod.svg'
|
||||
import BlueEffect from '../assets/option-card-effect-blue.svg'
|
||||
import { ParentChildChunk } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import Note from '../assets/note-mod.svg'
|
||||
import FileList from '../assets/file-list-3-fill.svg'
|
||||
import { indexMethodIcon } from '../icons'
|
||||
|
|
@ -733,9 +733,10 @@ const StepTwo = ({
|
|||
)
|
||||
&& <OptionCard
|
||||
title={t('datasetCreation.stepTwo.parentChild')}
|
||||
icon={<Image width={20} height={20} src={FamilyMod} alt={t('datasetCreation.stepTwo.parentChild')} />}
|
||||
effectImg={OrangeEffect.src}
|
||||
activeHeaderClassName='bg-dataset-option-card-orange-gradient'
|
||||
icon={<ParentChildChunk className='h-[20px] w-[20px]' />}
|
||||
effectImg={BlueEffect.src}
|
||||
className='text-util-colors-blue-light-blue-light-500'
|
||||
activeHeaderClassName='bg-dataset-option-card-blue-gradient'
|
||||
description={t('datasetCreation.stepTwo.parentChildTip')}
|
||||
isActive={currentDocForm === ChunkingMode.parentChild}
|
||||
onSwitched={() => handleChangeDocform(ChunkingMode.parentChild)}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { Theme } from '@/types/app'
|
|||
import useTheme from '@/hooks/use-theme'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const SimplePieChart = dynamic(() => import('@/app/components/base/simple-pie-chart'), { ssr: false })
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } fro
|
|||
import Toast from '@/app/components/base/toast'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
|
||||
import { convertOnlineDriveData } from './utils'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useGetDataSourceAuth } from '@/service/use-datasource'
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
|||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import useCheckMetadataName from '../hooks/use-check-metadata-name'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const AppCard = ({
|
|||
background={appBasicInfo.icon_background}
|
||||
imageUrl={appBasicInfo.icon_url}
|
||||
/>
|
||||
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-[4px] border border-divider-regular outline outline-components-panel-on-panel-item-bg'
|
||||
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm'
|
||||
className='h-3 w-3' type={appBasicInfo.mode} />
|
||||
</div>
|
||||
<div className='w-0 grow py-[1px]'>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { useParams } from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { flatten } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiRobot2Fill,
|
||||
RiRobot2Line,
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ import { useRouter } from 'next/navigation'
|
|||
import { debounce } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { AiText, BubbleTextMod, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import type { AppIconType, AppMode } from '@/types/app'
|
||||
|
||||
export type NavItem = {
|
||||
id: string
|
||||
|
|
@ -26,7 +25,7 @@ export type NavItem = {
|
|||
icon: string
|
||||
icon_background: string | null
|
||||
icon_url: string | null
|
||||
mode?: string
|
||||
mode?: AppMode
|
||||
}
|
||||
export type INavSelectorProps = {
|
||||
navigationItems: NavItem[]
|
||||
|
|
@ -92,25 +91,7 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
|||
imageUrl={nav.icon_url}
|
||||
/>
|
||||
{!!nav.mode && (
|
||||
<span className={cn(
|
||||
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm',
|
||||
)}>
|
||||
{nav.mode === 'advanced-chat' && (
|
||||
<BubbleTextMod className='h-2.5 w-2.5 text-[#1570EF]' />
|
||||
)}
|
||||
{nav.mode === 'agent-chat' && (
|
||||
<CuteRobot className='h-2.5 w-2.5 text-indigo-600' />
|
||||
)}
|
||||
{nav.mode === 'chat' && (
|
||||
<ChatBot className='h-2.5 w-2.5 text-[#1570EF]' />
|
||||
)}
|
||||
{nav.mode === 'completion' && (
|
||||
<AiText className='h-2.5 w-2.5 text-[#0E9384]' />
|
||||
)}
|
||||
{nav.mode === 'workflow' && (
|
||||
<Route className='h-2.5 w-2.5 text-[#f79009]' />
|
||||
)}
|
||||
</span>
|
||||
<AppTypeIcon type={nav.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 shadow-sm' className='h-2.5 w-2.5' />
|
||||
)}
|
||||
</div>
|
||||
<div className='truncate'>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import MarketplaceItem from '../item/marketplace-item'
|
|||
import GithubItem from '../item/github-item'
|
||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import PackageItem from '../item/package-item'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBracesLine,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, RefObject } from 'react'
|
||||
import {
|
||||
useMemo,
|
||||
useRef,
|
||||
|
|
@ -17,7 +17,7 @@ import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks'
|
|||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type PluginPageContextValue = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
containerRef: RefObject<HTMLDivElement | null>
|
||||
currentPluginID: string | undefined
|
||||
setCurrentPluginID: (pluginID?: string) => void
|
||||
filters: FilterState
|
||||
|
|
@ -27,8 +27,10 @@ export type PluginPageContextValue = {
|
|||
options: Array<{ value: string, text: string }>
|
||||
}
|
||||
|
||||
const emptyContainerRef: RefObject<HTMLDivElement | null> = { current: null }
|
||||
|
||||
export const PluginPageContext = createContext<PluginPageContextValue>({
|
||||
containerRef: { current: null },
|
||||
containerRef: emptyContainerRef,
|
||||
currentPluginID: undefined,
|
||||
setCurrentPluginID: noop,
|
||||
filters: {
|
||||
|
|
@ -53,7 +55,7 @@ export function usePluginPageContext(selector: (value: PluginPageContextValue) =
|
|||
export const PluginPageContextProvider = ({
|
||||
children,
|
||||
}: PluginPageContextProviderProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
categories: [],
|
||||
tags: [],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
type UploaderHookProps = {
|
||||
onFileChange: (file: File | null) => void
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
containerRef: RefObject<HTMLDivElement | null>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const i18nPrefix = 'plugin.upgrade'
|
|||
|
||||
type Props = {
|
||||
payload: UpdateFromMarketPlacePayload
|
||||
pluginId: string
|
||||
pluginId?: string
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
isShowDowngradeWarningModal?: boolean
|
||||
|
|
@ -113,9 +113,11 @@ const UpdatePluginModal: FC<Props> = ({
|
|||
const { mutateAsync } = useRemoveAutoUpgrade()
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const handleExcludeAndDownload = async () => {
|
||||
await mutateAsync({
|
||||
plugin_id: pluginId,
|
||||
})
|
||||
if (pluginId) {
|
||||
await mutateAsync({
|
||||
plugin_id: pluginId,
|
||||
})
|
||||
}
|
||||
invalidateReferenceSettings()
|
||||
handleConfirm()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
||||
|
|
@ -145,6 +145,9 @@ export const usePipelineRun = () => {
|
|||
} = workflowStore.getState()
|
||||
setWorkflowRunningData({
|
||||
result: {
|
||||
inputs_truncated: false,
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
tracing: [],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { t } from 'i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiCloseLine,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
import { RiSettings2Line } from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { LinkExternal02 } from '../../base/icons/src/vender/line/general'
|
||||
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
|
||||
import { AuthHeaderPrefix, AuthType } from '../types'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
|
||||
import { AuthHeaderPrefix, AuthType } from '../types'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import I18n from '@/context/i18n'
|
|||
import { getLanguage } from '@/i18n-config/language'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
tool: Tool
|
||||
|
|
@ -16,6 +17,32 @@ const MCPToolItem = ({
|
|||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const renderParameters = () => {
|
||||
const parameters = tool.parameters
|
||||
|
||||
if (parameters.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='title-xs-semi-bold mb-1 text-text-primary'>{t('tools.mcp.toolItem.parameters')}:</div>
|
||||
<ul className='space-y-1'>
|
||||
{parameters.map((parameter) => {
|
||||
const descriptionContent = parameter.human_description[language] || t('tools.mcp.toolItem.noDescription')
|
||||
return (
|
||||
<li key={parameter.name} className='pl-2'>
|
||||
<span className='system-xs-regular font-bold text-text-secondary'>{parameter.name}</span>
|
||||
<span className='system-xs-regular mr-1 text-text-tertiary'>({parameter.type}):</span>
|
||||
<span className='system-xs-regular text-text-tertiary'>{descriptionContent}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
@ -26,6 +53,7 @@ const MCPToolItem = ({
|
|||
<div>
|
||||
<div className='title-xs-semi-bold mb-1 text-text-primary'>{tool.label[language]}</div>
|
||||
<div className='body-xs-regular text-text-secondary'>{tool.description[language]}</div>
|
||||
{renderParameters()}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
|
@ -164,6 +164,9 @@ export const useWorkflowRun = () => {
|
|||
} = workflowStore.getState()
|
||||
setWorkflowRunningData({
|
||||
result: {
|
||||
inputs_truncated: false,
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
tracing: [],
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const WorkflowAppWithAdditionalContext = () => {
|
|||
if (!parsedInputs)
|
||||
return
|
||||
|
||||
const userInputs: Record<string, string> = {}
|
||||
const userInputs: Record<string, string | number | boolean> = {}
|
||||
Object.entries(parsedInputs).forEach(([key, value]) => {
|
||||
if (key.startsWith('sys.'))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { noop } from 'lodash-es'
|
|||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
export type ListProps = {
|
||||
wrapElemRef: React.RefObject<HTMLElement>
|
||||
wrapElemRef: React.RefObject<HTMLElement | null>
|
||||
list: Plugin[]
|
||||
searchText: string
|
||||
tags: string[]
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue