diff --git a/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md b/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md new file mode 100644 index 0000000000..f03c41cc25 --- /dev/null +++ b/agent-notes/api/core/model_runtime/model_providers/__base/large_language_model.py.md @@ -0,0 +1,27 @@ +# Notes: `large_language_model.py` + +## Purpose + +Provides the base `LargeLanguageModel` implementation used by the model runtime to invoke plugin-backed LLMs and to +bridge plugin daemon streaming semantics back into API-layer entities (`LLMResult`, `LLMResultChunk`). + +## Key behaviors / invariants + +- `invoke(..., stream=False)` still calls the plugin in streaming mode and then synthesizes a single `LLMResult` from + the first yielded `LLMResultChunk`. +- Plugin invocation is wrapped by `_invoke_llm_via_plugin(...)`, and `stream=False` normalization is handled by + `_normalize_non_stream_plugin_result(...)` / `_build_llm_result_from_first_chunk(...)`. +- Tool call deltas are merged incrementally via `_increase_tool_call(...)` to support multiple provider chunking + patterns (IDs anchored to first chunk, every chunk, or missing entirely). +- A tool-call delta with an empty `id` requires at least one existing tool call; otherwise we raise `ValueError` to + surface invalid delta sequences explicitly. +- Callback invocation is centralized in `_run_callbacks(...)` to ensure consistent error handling/logging. +- For compatibility with dify issue `#17799`, `prompt_messages` may be removed by the plugin daemon in chunks and must + be re-attached in this layer before callbacks/consumers use them. +- Callback hooks (`on_before_invoke`, `on_new_chunk`, `on_after_invoke`, `on_invoke_error`) must not break invocation + unless `callback.raise_error` is true. + +## Test focus + +- `api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py` validates tool-call delta merging and + patches `_gen_tool_call_id` for deterministic IDs. diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py index ed22ef045d..e1ea007232 100644 --- a/api/controllers/console/setup.py +++ b/api/controllers/console/setup.py @@ -1,20 +1,19 @@ +from typing import Literal + from flask import request -from flask_restx import Resource, fields from pydantic import BaseModel, Field, field_validator from configs import dify_config +from controllers.fastopenapi import console_router from libs.helper import EmailStr, extract_remote_ip from libs.password import valid_password from models.model import DifySetup, db from services.account_service import RegisterService, TenantService -from . import console_ns from .error import AlreadySetupError, NotInitValidateError from .init_validate import get_init_validate_status from .wraps import only_edition_self_hosted -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" - class SetupRequestPayload(BaseModel): email: EmailStr = Field(..., description="Admin email address") @@ -28,78 +27,66 @@ class SetupRequestPayload(BaseModel): return valid_password(value) -console_ns.schema_model( - SetupRequestPayload.__name__, - SetupRequestPayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), +class SetupStatusResponse(BaseModel): + step: Literal["not_started", "finished"] = Field(description="Setup step status") + setup_at: str | None = Field(default=None, description="Setup completion time (ISO format)") + + +class SetupResponse(BaseModel): + result: str = Field(description="Setup result", examples=["success"]) + + +@console_router.get( + "/setup", + response_model=SetupStatusResponse, + tags=["console"], ) +def get_setup_status_api() -> SetupStatusResponse: + """Get system setup status.""" + if dify_config.EDITION == "SELF_HOSTED": + setup_status = get_setup_status() + if setup_status and not isinstance(setup_status, bool): + return SetupStatusResponse(step="finished", setup_at=setup_status.setup_at.isoformat()) + if setup_status: + return SetupStatusResponse(step="finished") + return SetupStatusResponse(step="not_started") + return SetupStatusResponse(step="finished") -@console_ns.route("/setup") -class SetupApi(Resource): - @console_ns.doc("get_setup_status") - @console_ns.doc(description="Get system setup status") - @console_ns.response( - 200, - "Success", - console_ns.model( - "SetupStatusResponse", - { - "step": fields.String(description="Setup step status", enum=["not_started", "finished"]), - "setup_at": fields.String(description="Setup completion time (ISO format)", required=False), - }, - ), +@console_router.post( + "/setup", + response_model=SetupResponse, + tags=["console"], + status_code=201, +) +@only_edition_self_hosted +def setup_system(payload: SetupRequestPayload) -> SetupResponse: + """Initialize system setup with admin account.""" + if get_setup_status(): + raise AlreadySetupError() + + tenant_count = TenantService.get_tenant_count() + if tenant_count > 0: + raise AlreadySetupError() + + if not get_init_validate_status(): + raise NotInitValidateError() + + normalized_email = payload.email.lower() + + RegisterService.setup( + email=normalized_email, + name=payload.name, + password=payload.password, + ip_address=extract_remote_ip(request), + language=payload.language, ) - def get(self): - """Get system setup status""" - if dify_config.EDITION == "SELF_HOSTED": - setup_status = get_setup_status() - # Check if setup_status is a DifySetup object rather than a bool - if setup_status and not isinstance(setup_status, bool): - return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()} - elif setup_status: - return {"step": "finished"} - return {"step": "not_started"} - return {"step": "finished"} - @console_ns.doc("setup_system") - @console_ns.doc(description="Initialize system setup with admin account") - @console_ns.expect(console_ns.models[SetupRequestPayload.__name__]) - @console_ns.response( - 201, "Success", console_ns.model("SetupResponse", {"result": fields.String(description="Setup result")}) - ) - @console_ns.response(400, "Already setup or validation failed") - @only_edition_self_hosted - def post(self): - """Initialize system setup with admin account""" - # is set up - if get_setup_status(): - raise AlreadySetupError() - - # is tenant created - tenant_count = TenantService.get_tenant_count() - if tenant_count > 0: - raise AlreadySetupError() - - if not get_init_validate_status(): - raise NotInitValidateError() - - args = SetupRequestPayload.model_validate(console_ns.payload) - normalized_email = args.email.lower() - - # setup - RegisterService.setup( - email=normalized_email, - name=args.name, - password=args.password, - ip_address=extract_remote_ip(request), - language=args.language, - ) - - return {"result": "success"}, 201 + return SetupResponse(result="success") -def get_setup_status(): +def get_setup_status() -> DifySetup | bool | None: if dify_config.EDITION == "SELF_HOSTED": return db.session.query(DifySetup).first() - else: - return True + + return True diff --git a/api/controllers/console/version.py b/api/controllers/console/version.py index 419261ba2a..fdb23acf52 100644 --- a/api/controllers/console/version.py +++ b/api/controllers/console/version.py @@ -1,15 +1,11 @@ -import json import logging import httpx -from flask import request -from flask_restx import Resource, fields from packaging import version from pydantic import BaseModel, Field from configs import dify_config - -from . import console_ns +from controllers.fastopenapi import console_router logger = logging.getLogger(__name__) @@ -18,69 +14,61 @@ class VersionQuery(BaseModel): current_version: str = Field(..., description="Current application version") -console_ns.schema_model( - VersionQuery.__name__, - VersionQuery.model_json_schema(ref_template="#/definitions/{model}"), +class VersionFeatures(BaseModel): + can_replace_logo: bool = Field(description="Whether logo replacement is supported") + model_load_balancing_enabled: bool = Field(description="Whether model load balancing is enabled") + + +class VersionResponse(BaseModel): + version: str = Field(description="Latest version number") + release_date: str = Field(description="Release date of latest version") + release_notes: str = Field(description="Release notes for latest version") + can_auto_update: bool = Field(description="Whether auto-update is supported") + features: VersionFeatures = Field(description="Feature flags and capabilities") + + +@console_router.get( + "/version", + response_model=VersionResponse, + tags=["console"], ) +def check_version_update(query: VersionQuery) -> VersionResponse: + """Check for application version updates.""" + check_update_url = dify_config.CHECK_UPDATE_URL - -@console_ns.route("/version") -class VersionApi(Resource): - @console_ns.doc("check_version_update") - @console_ns.doc(description="Check for application version updates") - @console_ns.expect(console_ns.models[VersionQuery.__name__]) - @console_ns.response( - 200, - "Success", - console_ns.model( - "VersionResponse", - { - "version": fields.String(description="Latest version number"), - "release_date": fields.String(description="Release date of latest version"), - "release_notes": fields.String(description="Release notes for latest version"), - "can_auto_update": fields.Boolean(description="Whether auto-update is supported"), - "features": fields.Raw(description="Feature flags and capabilities"), - }, + result = VersionResponse( + version=dify_config.project.version, + release_date="", + release_notes="", + can_auto_update=False, + features=VersionFeatures( + can_replace_logo=dify_config.CAN_REPLACE_LOGO, + model_load_balancing_enabled=dify_config.MODEL_LB_ENABLED, ), ) - def get(self): - """Check for application version updates""" - args = VersionQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore - check_update_url = dify_config.CHECK_UPDATE_URL - result = { - "version": dify_config.project.version, - "release_date": "", - "release_notes": "", - "can_auto_update": False, - "features": { - "can_replace_logo": dify_config.CAN_REPLACE_LOGO, - "model_load_balancing_enabled": dify_config.MODEL_LB_ENABLED, - }, - } - - if not check_update_url: - return result - - try: - response = httpx.get( - check_update_url, - params={"current_version": args.current_version}, - timeout=httpx.Timeout(timeout=10.0, connect=3.0), - ) - except Exception as error: - logger.warning("Check update version error: %s.", str(error)) - result["version"] = args.current_version - return result - - content = json.loads(response.content) - if _has_new_version(latest_version=content["version"], current_version=f"{args.current_version}"): - result["version"] = content["version"] - result["release_date"] = content["releaseDate"] - result["release_notes"] = content["releaseNotes"] - result["can_auto_update"] = content["canAutoUpdate"] + if not check_update_url: return result + try: + response = httpx.get( + check_update_url, + params={"current_version": query.current_version}, + timeout=httpx.Timeout(timeout=10.0, connect=3.0), + ) + content = response.json() + except Exception as error: + logger.warning("Check update version error: %s.", str(error)) + result.version = query.current_version + return result + latest_version = content.get("version", result.version) + if _has_new_version(latest_version=latest_version, current_version=f"{query.current_version}"): + result.version = latest_version + result.release_date = content.get("releaseDate", "") + result.release_notes = content.get("releaseNotes", "") + result.can_auto_update = content.get("canAutoUpdate", False) + return result + def _has_new_version(*, latest_version: str, current_version: str) -> bool: try: diff --git a/api/core/model_runtime/model_providers/__base/large_language_model.py b/api/core/model_runtime/model_providers/__base/large_language_model.py index c0f4c504d9..7a0757f219 100644 --- a/api/core/model_runtime/model_providers/__base/large_language_model.py +++ b/api/core/model_runtime/model_providers/__base/large_language_model.py @@ -1,7 +1,7 @@ import logging import time import uuid -from collections.abc import Generator, Sequence +from collections.abc import Callable, Generator, Iterator, Sequence from typing import Union from pydantic import ConfigDict @@ -30,6 +30,142 @@ def _gen_tool_call_id() -> str: return f"chatcmpl-tool-{str(uuid.uuid4().hex)}" +def _run_callbacks(callbacks: Sequence[Callback] | None, *, event: str, invoke: Callable[[Callback], None]) -> None: + if not callbacks: + return + + for callback in callbacks: + try: + invoke(callback) + except Exception as e: + if callback.raise_error: + raise + logger.warning("Callback %s %s failed with error %s", callback.__class__.__name__, event, e) + + +def _get_or_create_tool_call( + existing_tools_calls: list[AssistantPromptMessage.ToolCall], + tool_call_id: str, +) -> AssistantPromptMessage.ToolCall: + """ + Get or create a tool call by ID. + + If `tool_call_id` is empty, returns the most recently created tool call. + """ + if not tool_call_id: + if not existing_tools_calls: + raise ValueError("tool_call_id is empty but no existing tool call is available to apply the delta") + return existing_tools_calls[-1] + + tool_call = next((tool_call for tool_call in existing_tools_calls if tool_call.id == tool_call_id), None) + if tool_call is None: + tool_call = AssistantPromptMessage.ToolCall( + id=tool_call_id, + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="", arguments=""), + ) + existing_tools_calls.append(tool_call) + + return tool_call + + +def _merge_tool_call_delta( + tool_call: AssistantPromptMessage.ToolCall, + delta: AssistantPromptMessage.ToolCall, +) -> None: + if delta.id: + tool_call.id = delta.id + if delta.type: + tool_call.type = delta.type + if delta.function.name: + tool_call.function.name = delta.function.name + if delta.function.arguments: + tool_call.function.arguments += delta.function.arguments + + +def _build_llm_result_from_first_chunk( + model: str, + prompt_messages: Sequence[PromptMessage], + chunks: Iterator[LLMResultChunk], +) -> LLMResult: + """ + Build a single `LLMResult` from the first returned chunk. + + This is used for `stream=False` because the plugin side may still implement the response via a chunked stream. + """ + content = "" + content_list: list[PromptMessageContentUnionTypes] = [] + usage = LLMUsage.empty_usage() + system_fingerprint: str | None = None + tools_calls: list[AssistantPromptMessage.ToolCall] = [] + + first_chunk = next(chunks, None) + if first_chunk is not None: + if isinstance(first_chunk.delta.message.content, str): + content += first_chunk.delta.message.content + elif isinstance(first_chunk.delta.message.content, list): + content_list.extend(first_chunk.delta.message.content) + + if first_chunk.delta.message.tool_calls: + _increase_tool_call(first_chunk.delta.message.tool_calls, tools_calls) + + usage = first_chunk.delta.usage or LLMUsage.empty_usage() + system_fingerprint = first_chunk.system_fingerprint + + return LLMResult( + model=model, + prompt_messages=prompt_messages, + message=AssistantPromptMessage( + content=content or content_list, + tool_calls=tools_calls, + ), + usage=usage, + system_fingerprint=system_fingerprint, + ) + + +def _invoke_llm_via_plugin( + *, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + model: str, + credentials: dict, + model_parameters: dict, + prompt_messages: Sequence[PromptMessage], + tools: list[PromptMessageTool] | None, + stop: Sequence[str] | None, + stream: bool, +) -> Union[LLMResult, Generator[LLMResultChunk, None, None]]: + from core.plugin.impl.model import PluginModelClient + + plugin_model_manager = PluginModelClient() + return plugin_model_manager.invoke_llm( + tenant_id=tenant_id, + user_id=user_id, + plugin_id=plugin_id, + provider=provider, + model=model, + credentials=credentials, + model_parameters=model_parameters, + prompt_messages=list(prompt_messages), + tools=tools, + stop=list(stop) if stop else None, + stream=stream, + ) + + +def _normalize_non_stream_plugin_result( + model: str, + prompt_messages: Sequence[PromptMessage], + result: Union[LLMResult, Iterator[LLMResultChunk]], +) -> LLMResult: + if isinstance(result, LLMResult): + return result + return _build_llm_result_from_first_chunk(model=model, prompt_messages=prompt_messages, chunks=result) + + def _increase_tool_call( new_tool_calls: list[AssistantPromptMessage.ToolCall], existing_tools_calls: list[AssistantPromptMessage.ToolCall] ): @@ -40,42 +176,13 @@ def _increase_tool_call( :param existing_tools_calls: List of existing tool calls to be modified IN-PLACE. """ - def get_tool_call(tool_call_id: str): - """ - Get or create a tool call by ID - - :param tool_call_id: tool call ID - :return: existing or new tool call - """ - if not tool_call_id: - return existing_tools_calls[-1] - - _tool_call = next((_tool_call for _tool_call in existing_tools_calls if _tool_call.id == tool_call_id), None) - if _tool_call is None: - _tool_call = AssistantPromptMessage.ToolCall( - id=tool_call_id, - type="function", - function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="", arguments=""), - ) - existing_tools_calls.append(_tool_call) - - return _tool_call - for new_tool_call in new_tool_calls: # generate ID for tool calls with function name but no ID to track them if new_tool_call.function.name and not new_tool_call.id: new_tool_call.id = _gen_tool_call_id() - # get tool call - tool_call = get_tool_call(new_tool_call.id) - # update tool call - if new_tool_call.id: - tool_call.id = new_tool_call.id - if new_tool_call.type: - tool_call.type = new_tool_call.type - if new_tool_call.function.name: - tool_call.function.name = new_tool_call.function.name - if new_tool_call.function.arguments: - tool_call.function.arguments += new_tool_call.function.arguments + + tool_call = _get_or_create_tool_call(existing_tools_calls, new_tool_call.id) + _merge_tool_call_delta(tool_call, new_tool_call) class LargeLanguageModel(AIModel): @@ -141,10 +248,7 @@ class LargeLanguageModel(AIModel): result: Union[LLMResult, Generator[LLMResultChunk, None, None]] try: - from core.plugin.impl.model import PluginModelClient - - plugin_model_manager = PluginModelClient() - result = plugin_model_manager.invoke_llm( + result = _invoke_llm_via_plugin( tenant_id=self.tenant_id, user_id=user or "unknown", plugin_id=self.plugin_id, @@ -154,38 +258,13 @@ class LargeLanguageModel(AIModel): model_parameters=model_parameters, prompt_messages=prompt_messages, tools=tools, - stop=list(stop) if stop else None, + stop=stop, stream=stream, ) if not stream: - content = "" - content_list = [] - usage = LLMUsage.empty_usage() - system_fingerprint = None - tools_calls: list[AssistantPromptMessage.ToolCall] = [] - - for chunk in result: - if isinstance(chunk.delta.message.content, str): - content += chunk.delta.message.content - elif isinstance(chunk.delta.message.content, list): - content_list.extend(chunk.delta.message.content) - if chunk.delta.message.tool_calls: - _increase_tool_call(chunk.delta.message.tool_calls, tools_calls) - - usage = chunk.delta.usage or LLMUsage.empty_usage() - system_fingerprint = chunk.system_fingerprint - break - - result = LLMResult( - model=model, - prompt_messages=prompt_messages, - message=AssistantPromptMessage( - content=content or content_list, - tool_calls=tools_calls, - ), - usage=usage, - system_fingerprint=system_fingerprint, + result = _normalize_non_stream_plugin_result( + model=model, prompt_messages=prompt_messages, result=result ) except Exception as e: self._trigger_invoke_error_callbacks( @@ -425,27 +504,21 @@ class LargeLanguageModel(AIModel): :param user: unique user id :param callbacks: callbacks """ - if callbacks: - for callback in callbacks: - try: - callback.on_before_invoke( - llm_instance=self, - model=model, - credentials=credentials, - prompt_messages=prompt_messages, - model_parameters=model_parameters, - tools=tools, - stop=stop, - stream=stream, - user=user, - ) - except Exception as e: - if callback.raise_error: - raise e - else: - logger.warning( - "Callback %s on_before_invoke failed with error %s", callback.__class__.__name__, e - ) + _run_callbacks( + callbacks, + event="on_before_invoke", + invoke=lambda callback: callback.on_before_invoke( + llm_instance=self, + model=model, + credentials=credentials, + prompt_messages=prompt_messages, + model_parameters=model_parameters, + tools=tools, + stop=stop, + stream=stream, + user=user, + ), + ) def _trigger_new_chunk_callbacks( self, @@ -473,26 +546,22 @@ class LargeLanguageModel(AIModel): :param stream: is stream response :param user: unique user id """ - if callbacks: - for callback in callbacks: - try: - callback.on_new_chunk( - llm_instance=self, - chunk=chunk, - model=model, - credentials=credentials, - prompt_messages=prompt_messages, - model_parameters=model_parameters, - tools=tools, - stop=stop, - stream=stream, - user=user, - ) - except Exception as e: - if callback.raise_error: - raise e - else: - logger.warning("Callback %s on_new_chunk failed with error %s", callback.__class__.__name__, e) + _run_callbacks( + callbacks, + event="on_new_chunk", + invoke=lambda callback: callback.on_new_chunk( + llm_instance=self, + chunk=chunk, + model=model, + credentials=credentials, + prompt_messages=prompt_messages, + model_parameters=model_parameters, + tools=tools, + stop=stop, + stream=stream, + user=user, + ), + ) def _trigger_after_invoke_callbacks( self, @@ -521,28 +590,22 @@ class LargeLanguageModel(AIModel): :param user: unique user id :param callbacks: callbacks """ - if callbacks: - for callback in callbacks: - try: - callback.on_after_invoke( - llm_instance=self, - result=result, - model=model, - credentials=credentials, - prompt_messages=prompt_messages, - model_parameters=model_parameters, - tools=tools, - stop=stop, - stream=stream, - user=user, - ) - except Exception as e: - if callback.raise_error: - raise e - else: - logger.warning( - "Callback %s on_after_invoke failed with error %s", callback.__class__.__name__, e - ) + _run_callbacks( + callbacks, + event="on_after_invoke", + invoke=lambda callback: callback.on_after_invoke( + llm_instance=self, + result=result, + model=model, + credentials=credentials, + prompt_messages=prompt_messages, + model_parameters=model_parameters, + tools=tools, + stop=stop, + stream=stream, + user=user, + ), + ) def _trigger_invoke_error_callbacks( self, @@ -571,25 +634,19 @@ class LargeLanguageModel(AIModel): :param user: unique user id :param callbacks: callbacks """ - if callbacks: - for callback in callbacks: - try: - callback.on_invoke_error( - llm_instance=self, - ex=ex, - model=model, - credentials=credentials, - prompt_messages=prompt_messages, - model_parameters=model_parameters, - tools=tools, - stop=stop, - stream=stream, - user=user, - ) - except Exception as e: - if callback.raise_error: - raise e - else: - logger.warning( - "Callback %s on_invoke_error failed with error %s", callback.__class__.__name__, e - ) + _run_callbacks( + callbacks, + event="on_invoke_error", + invoke=lambda callback: callback.on_invoke_error( + llm_instance=self, + ex=ex, + model=model, + credentials=credentials, + prompt_messages=prompt_messages, + model_parameters=model_parameters, + tools=tools, + stop=stop, + stream=stream, + user=user, + ), + ) diff --git a/api/extensions/ext_fastopenapi.py b/api/extensions/ext_fastopenapi.py index 0ef1513e11..5f98aa7b67 100644 --- a/api/extensions/ext_fastopenapi.py +++ b/api/extensions/ext_fastopenapi.py @@ -28,8 +28,10 @@ def init_app(app: DifyApp) -> None: # Ensure route decorators are evaluated. import controllers.console.ping as ping_module + from controllers.console import setup _ = ping_module + _ = setup router.include_router(console_router, prefix="/console/api") CORS( diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index da22464d39..edcb2a7870 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -781,15 +781,16 @@ class AppDslService: return dependencies @classmethod - def get_leaked_dependencies(cls, tenant_id: str, dsl_dependencies: list[dict]) -> list[PluginDependency]: + def get_leaked_dependencies( + cls, tenant_id: str, dsl_dependencies: list[PluginDependency] + ) -> list[PluginDependency]: """ Returns the leaked dependencies in current workspace """ - dependencies = [PluginDependency.model_validate(dep) for dep in dsl_dependencies] - if not dependencies: + if not dsl_dependencies: return [] - return DependenciesAnalysisService.get_leaked_dependencies(tenant_id=tenant_id, dependencies=dependencies) + return DependenciesAnalysisService.get_leaked_dependencies(tenant_id=tenant_id, dependencies=dsl_dependencies) @staticmethod def _generate_aes_key(tenant_id: str) -> bytes: diff --git a/api/services/rag_pipeline/rag_pipeline_dsl_service.py b/api/services/rag_pipeline/rag_pipeline_dsl_service.py index 06f294863d..c1c6e204fb 100644 --- a/api/services/rag_pipeline/rag_pipeline_dsl_service.py +++ b/api/services/rag_pipeline/rag_pipeline_dsl_service.py @@ -870,15 +870,16 @@ class RagPipelineDslService: return dependencies @classmethod - def get_leaked_dependencies(cls, tenant_id: str, dsl_dependencies: list[dict]) -> list[PluginDependency]: + def get_leaked_dependencies( + cls, tenant_id: str, dsl_dependencies: list[PluginDependency] + ) -> list[PluginDependency]: """ Returns the leaked dependencies in current workspace """ - dependencies = [PluginDependency.model_validate(dep) for dep in dsl_dependencies] - if not dependencies: + if not dsl_dependencies: return [] - return DependenciesAnalysisService.get_leaked_dependencies(tenant_id=tenant_id, dependencies=dependencies) + return DependenciesAnalysisService.get_leaked_dependencies(tenant_id=tenant_id, dependencies=dsl_dependencies) def _generate_aes_key(self, tenant_id: str) -> bytes: """Generate AES key based on tenant_id""" diff --git a/api/services/rag_pipeline/rag_pipeline_transform_service.py b/api/services/rag_pipeline/rag_pipeline_transform_service.py index 84f97907c0..8ea365e907 100644 --- a/api/services/rag_pipeline/rag_pipeline_transform_service.py +++ b/api/services/rag_pipeline/rag_pipeline_transform_service.py @@ -44,7 +44,7 @@ class RagPipelineTransformService: doc_form = dataset.doc_form if not doc_form: return self._transform_to_empty_pipeline(dataset) - retrieval_model = dataset.retrieval_model + retrieval_model = RetrievalSetting.model_validate(dataset.retrieval_model) if dataset.retrieval_model else None pipeline_yaml = self._get_transform_yaml(doc_form, datasource_type, indexing_technique) # deal dependencies self._deal_dependencies(pipeline_yaml, dataset.tenant_id) @@ -154,7 +154,12 @@ class RagPipelineTransformService: return node def _deal_knowledge_index( - self, dataset: Dataset, doc_form: str, indexing_technique: str | None, retrieval_model: dict, node: dict + self, + dataset: Dataset, + doc_form: str, + indexing_technique: str | None, + retrieval_model: RetrievalSetting | None, + node: dict, ): knowledge_configuration_dict = node.get("data", {}) knowledge_configuration = KnowledgeConfiguration.model_validate(knowledge_configuration_dict) @@ -163,10 +168,9 @@ class RagPipelineTransformService: knowledge_configuration.embedding_model = dataset.embedding_model knowledge_configuration.embedding_model_provider = dataset.embedding_model_provider if retrieval_model: - retrieval_setting = RetrievalSetting.model_validate(retrieval_model) if indexing_technique == "economy": - retrieval_setting.search_method = RetrievalMethod.KEYWORD_SEARCH - knowledge_configuration.retrieval_model = retrieval_setting + retrieval_model.search_method = RetrievalMethod.KEYWORD_SEARCH + knowledge_configuration.retrieval_model = retrieval_model else: dataset.retrieval_model = knowledge_configuration.retrieval_model.model_dump() diff --git a/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py b/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py new file mode 100644 index 0000000000..385539b6f3 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/test_fastopenapi_setup.py @@ -0,0 +1,56 @@ +import builtins +from unittest.mock import patch + +import pytest +from flask import Flask +from flask.views import MethodView + +from extensions import ext_fastopenapi + +if not hasattr(builtins, "MethodView"): + builtins.MethodView = MethodView # type: ignore[attr-defined] + + +@pytest.fixture +def app() -> Flask: + app = Flask(__name__) + app.config["TESTING"] = True + return app + + +def test_console_setup_fastopenapi_get_not_started(app: Flask): + ext_fastopenapi.init_app(app) + + with ( + patch("controllers.console.setup.dify_config.EDITION", "SELF_HOSTED"), + patch("controllers.console.setup.get_setup_status", return_value=None), + ): + client = app.test_client() + response = client.get("/console/api/setup") + + assert response.status_code == 200 + assert response.get_json() == {"step": "not_started", "setup_at": None} + + +def test_console_setup_fastopenapi_post_success(app: Flask): + ext_fastopenapi.init_app(app) + + payload = { + "email": "admin@example.com", + "name": "Admin", + "password": "Passw0rd1", + "language": "en-US", + } + + with ( + patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"), + patch("controllers.console.setup.get_setup_status", return_value=None), + patch("controllers.console.setup.TenantService.get_tenant_count", return_value=0), + patch("controllers.console.setup.get_init_validate_status", return_value=True), + patch("controllers.console.setup.RegisterService.setup"), + ): + client = app.test_client() + response = client.post("/console/api/setup", json=payload) + + assert response.status_code == 201 + assert response.get_json() == {"result": "success"} diff --git a/api/tests/unit_tests/controllers/console/test_fastopenapi_version.py b/api/tests/unit_tests/controllers/console/test_fastopenapi_version.py new file mode 100644 index 0000000000..c5b4e0dfcf --- /dev/null +++ b/api/tests/unit_tests/controllers/console/test_fastopenapi_version.py @@ -0,0 +1,35 @@ +import builtins +from unittest.mock import patch + +import pytest +from flask import Flask +from flask.views import MethodView + +from configs import dify_config +from extensions import ext_fastopenapi + +if not hasattr(builtins, "MethodView"): + builtins.MethodView = MethodView # type: ignore[attr-defined] + + +@pytest.fixture +def app() -> Flask: + app = Flask(__name__) + app.config["TESTING"] = True + return app + + +def test_console_version_fastopenapi_returns_current_version(app: Flask): + ext_fastopenapi.init_app(app) + + with patch("controllers.console.version.dify_config.CHECK_UPDATE_URL", None): + client = app.test_client() + response = client.get("/console/api/version", query_string={"current_version": "0.0.0"}) + + assert response.status_code == 200 + data = response.get_json() + assert data["version"] == dify_config.project.version + assert data["release_date"] == "" + assert data["release_notes"] == "" + assert data["can_auto_update"] is False + assert "features" in data diff --git a/api/tests/unit_tests/controllers/console/test_setup.py b/api/tests/unit_tests/controllers/console/test_setup.py deleted file mode 100644 index e7882dcd2b..0000000000 --- a/api/tests/unit_tests/controllers/console/test_setup.py +++ /dev/null @@ -1,39 +0,0 @@ -from types import SimpleNamespace -from unittest.mock import patch - -from controllers.console.setup import SetupApi - - -class TestSetupApi: - def test_post_lowercases_email_before_register(self): - """Ensure setup registration normalizes email casing.""" - payload = { - "email": "Admin@Example.com", - "name": "Admin User", - "password": "ValidPass123!", - "language": "en-US", - } - setup_api = SetupApi(api=None) - - mock_console_ns = SimpleNamespace(payload=payload) - - with ( - patch("controllers.console.setup.console_ns", mock_console_ns), - patch("controllers.console.setup.get_setup_status", return_value=False), - patch("controllers.console.setup.TenantService.get_tenant_count", return_value=0), - patch("controllers.console.setup.get_init_validate_status", return_value=True), - patch("controllers.console.setup.extract_remote_ip", return_value="127.0.0.1"), - patch("controllers.console.setup.request", object()), - patch("controllers.console.setup.RegisterService.setup") as mock_register, - ): - response, status = setup_api.post() - - assert response == {"result": "success"} - assert status == 201 - mock_register.assert_called_once_with( - email="admin@example.com", - name=payload["name"], - password=payload["password"], - ip_address="127.0.0.1", - language=payload["language"], - ) diff --git a/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py b/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py index 93d8a20cac..5fbdabceed 100644 --- a/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py +++ b/api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +import pytest + from core.model_runtime.entities.message_entities import AssistantPromptMessage from core.model_runtime.model_providers.__base.large_language_model import _increase_tool_call @@ -97,3 +99,14 @@ def test__increase_tool_call(): mock_id_generator.side_effect = [_exp_case.id for _exp_case in EXPECTED_CASE_4] with patch("core.model_runtime.model_providers.__base.large_language_model._gen_tool_call_id", mock_id_generator): _run_case(INPUTS_CASE_4, EXPECTED_CASE_4) + + +def test__increase_tool_call__no_id_no_name_first_delta_should_raise(): + inputs = [ + ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')), + ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments='"value"}')), + ] + actual: list[ToolCall] = [] + with patch("core.model_runtime.model_providers.__base.large_language_model._gen_tool_call_id", MagicMock()): + with pytest.raises(ValueError): + _increase_tool_call(inputs, actual) diff --git a/api/tests/unit_tests/core/model_runtime/__base/test_large_language_model_non_stream_parsing.py b/api/tests/unit_tests/core/model_runtime/__base/test_large_language_model_non_stream_parsing.py new file mode 100644 index 0000000000..91352b2a5f --- /dev/null +++ b/api/tests/unit_tests/core/model_runtime/__base/test_large_language_model_non_stream_parsing.py @@ -0,0 +1,103 @@ +from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from core.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + TextPromptMessageContent, + UserPromptMessage, +) +from core.model_runtime.model_providers.__base.large_language_model import _normalize_non_stream_plugin_result + + +def _make_chunk( + *, + model: str = "test-model", + content: str | list[TextPromptMessageContent] | None, + tool_calls: list[AssistantPromptMessage.ToolCall] | None = None, + usage: LLMUsage | None = None, + system_fingerprint: str | None = None, +) -> LLMResultChunk: + message = AssistantPromptMessage(content=content, tool_calls=tool_calls or []) + delta = LLMResultChunkDelta(index=0, message=message, usage=usage) + return LLMResultChunk(model=model, delta=delta, system_fingerprint=system_fingerprint) + + +def test__normalize_non_stream_plugin_result__from_first_chunk_str_content_and_tool_calls(): + prompt_messages = [UserPromptMessage(content="hi")] + + tool_calls = [ + AssistantPromptMessage.ToolCall( + id="1", + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="func_foo", arguments=""), + ), + AssistantPromptMessage.ToolCall( + id="", + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="", arguments='{"arg1": '), + ), + AssistantPromptMessage.ToolCall( + id="", + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="", arguments='"value"}'), + ), + ] + + usage = LLMUsage.empty_usage().model_copy(update={"prompt_tokens": 1, "total_tokens": 1}) + chunk = _make_chunk(content="hello", tool_calls=tool_calls, usage=usage, system_fingerprint="fp-1") + + result = _normalize_non_stream_plugin_result( + model="test-model", prompt_messages=prompt_messages, result=iter([chunk]) + ) + + assert result.model == "test-model" + assert result.prompt_messages == prompt_messages + assert result.message.content == "hello" + assert result.usage.prompt_tokens == 1 + assert result.system_fingerprint == "fp-1" + assert result.message.tool_calls == [ + AssistantPromptMessage.ToolCall( + id="1", + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}'), + ) + ] + + +def test__normalize_non_stream_plugin_result__from_first_chunk_list_content(): + prompt_messages = [UserPromptMessage(content="hi")] + + content_list = [TextPromptMessageContent(data="a"), TextPromptMessageContent(data="b")] + chunk = _make_chunk(content=content_list, usage=LLMUsage.empty_usage()) + + result = _normalize_non_stream_plugin_result( + model="test-model", prompt_messages=prompt_messages, result=iter([chunk]) + ) + + assert result.message.content == content_list + + +def test__normalize_non_stream_plugin_result__passthrough_llm_result(): + prompt_messages = [UserPromptMessage(content="hi")] + llm_result = LLMResult( + model="test-model", + prompt_messages=prompt_messages, + message=AssistantPromptMessage(content="ok"), + usage=LLMUsage.empty_usage(), + ) + + assert ( + _normalize_non_stream_plugin_result(model="test-model", prompt_messages=prompt_messages, result=llm_result) + == llm_result + ) + + +def test__normalize_non_stream_plugin_result__empty_iterator_defaults(): + prompt_messages = [UserPromptMessage(content="hi")] + + result = _normalize_non_stream_plugin_result(model="test-model", prompt_messages=prompt_messages, result=iter([])) + + assert result.model == "test-model" + assert result.prompt_messages == prompt_messages + assert result.message.content == [] + assert result.message.tool_calls == [] + assert result.usage == LLMUsage.empty_usage() + assert result.system_fingerprint is None diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index ca56261431..918860c786 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -1,27 +1,15 @@ -import type { StorybookConfig } from '@storybook/nextjs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const storybookDir = path.dirname(fileURLToPath(import.meta.url)) +import type { StorybookConfig } from '@storybook/nextjs-vite' const config: StorybookConfig = { stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - '@storybook/addon-onboarding', + // Not working with Storybook Vite framework + // '@storybook/addon-onboarding', '@storybook/addon-links', '@storybook/addon-docs', '@chromatic-com/storybook', ], - framework: { - name: '@storybook/nextjs', - options: { - builder: { - useSWC: true, - lazyCompilation: false, - }, - nextConfigPath: undefined, - }, - }, + framework: '@storybook/nextjs-vite', staticDirs: ['../public'], core: { disableWhatsNewNotifications: true, @@ -29,17 +17,5 @@ const config: StorybookConfig = { docs: { defaultName: 'Documentation', }, - webpackFinal: async (config) => { - // Add alias to mock problematic modules with circular dependencies - config.resolve = config.resolve || {} - config.resolve.alias = { - ...config.resolve.alias, - // Mock the plugin index files to avoid circular dependencies - [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/context-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/context-block.tsx'), - [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/history-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/history-block.tsx'), - [path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/query-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/query-block.tsx'), - } - return config - }, } export default config diff --git a/web/app/components/app/overview/settings/index.spec.tsx b/web/app/components/app/overview/settings/index.spec.tsx index 776c55d149..c9cbe0b724 100644 --- a/web/app/components/app/overview/settings/index.spec.tsx +++ b/web/app/components/app/overview/settings/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { ReactNode } from 'react' import type { ModalContextState } from '@/context/modal-context' import type { ProviderContextState } from '@/context/provider-context' diff --git a/web/app/components/base/action-button/index.stories.tsx b/web/app/components/base/action-button/index.stories.tsx index 07e0592374..d6f0767faa 100644 --- a/web/app/components/base/action-button/index.stories.tsx +++ b/web/app/components/base/action-button/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { RiAddLine, RiDeleteBinLine, RiEditLine, RiMore2Fill, RiSaveLine, RiShareLine } from '@remixicon/react' import ActionButton, { ActionButtonState } from '.' diff --git a/web/app/components/base/agent-log-modal/index.stories.tsx b/web/app/components/base/agent-log-modal/index.stories.tsx index 781782af8d..87318848b4 100644 --- a/web/app/components/base/agent-log-modal/index.stories.tsx +++ b/web/app/components/base/agent-log-modal/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { useEffect, useRef } from 'react' diff --git a/web/app/components/base/answer-icon/index.stories.tsx b/web/app/components/base/answer-icon/index.stories.tsx index 0928d9cda6..d5de350a40 100644 --- a/web/app/components/base/answer-icon/index.stories.tsx +++ b/web/app/components/base/answer-icon/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ReactNode } from 'react' import AnswerIcon from '.' diff --git a/web/app/components/base/app-icon-picker/index.stories.tsx b/web/app/components/base/app-icon-picker/index.stories.tsx index 43abfccc39..08e9d69f32 100644 --- a/web/app/components/base/app-icon-picker/index.stories.tsx +++ b/web/app/components/base/app-icon-picker/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { AppIconSelection } from '.' import { useState } from 'react' import AppIconPicker from '.' diff --git a/web/app/components/base/app-icon/index.stories.tsx b/web/app/components/base/app-icon/index.stories.tsx index 9fdffb54b0..a645471254 100644 --- a/web/app/components/base/app-icon/index.stories.tsx +++ b/web/app/components/base/app-icon/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ComponentProps } from 'react' import AppIcon from '.' diff --git a/web/app/components/base/audio-btn/index.stories.tsx b/web/app/components/base/audio-btn/index.stories.tsx index e560b9af99..c760e1366d 100644 --- a/web/app/components/base/audio-btn/index.stories.tsx +++ b/web/app/components/base/audio-btn/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ComponentProps } from 'react' import { useEffect } from 'react' import AudioBtn from '.' diff --git a/web/app/components/base/audio-gallery/index.stories.tsx b/web/app/components/base/audio-gallery/index.stories.tsx index 539ab9e332..cf22058c9a 100644 --- a/web/app/components/base/audio-gallery/index.stories.tsx +++ b/web/app/components/base/audio-gallery/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import AudioGallery from '.' const AUDIO_SOURCES = [ diff --git a/web/app/components/base/auto-height-textarea/index.stories.tsx b/web/app/components/base/auto-height-textarea/index.stories.tsx index d0f36e4736..f5239a49ca 100644 --- a/web/app/components/base/auto-height-textarea/index.stories.tsx +++ b/web/app/components/base/auto-height-textarea/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import AutoHeightTextarea from '.' diff --git a/web/app/components/base/avatar/index.stories.tsx b/web/app/components/base/avatar/index.stories.tsx index fdc9bf8281..5e392640ca 100644 --- a/web/app/components/base/avatar/index.stories.tsx +++ b/web/app/components/base/avatar/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Avatar from '.' const meta = { diff --git a/web/app/components/base/badge/index.stories.tsx b/web/app/components/base/badge/index.stories.tsx index e1fe8cb271..b2ab794087 100644 --- a/web/app/components/base/badge/index.stories.tsx +++ b/web/app/components/base/badge/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Badge from '../badge' const meta = { diff --git a/web/app/components/base/block-input/index.stories.tsx b/web/app/components/base/block-input/index.stories.tsx index d05cc221b6..484b917c75 100644 --- a/web/app/components/base/block-input/index.stories.tsx +++ b/web/app/components/base/block-input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import BlockInput from '.' diff --git a/web/app/components/base/button/add-button.stories.tsx b/web/app/components/base/button/add-button.stories.tsx index edd52b2b78..5181309f2c 100644 --- a/web/app/components/base/button/add-button.stories.tsx +++ b/web/app/components/base/button/add-button.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import AddButton from './add-button' const meta = { diff --git a/web/app/components/base/button/index.stories.tsx b/web/app/components/base/button/index.stories.tsx index 02d20b4af4..25bd5957e1 100644 --- a/web/app/components/base/button/index.stories.tsx +++ b/web/app/components/base/button/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { RocketLaunchIcon } from '@heroicons/react/20/solid' import { Button } from '.' diff --git a/web/app/components/base/button/sync-button.stories.tsx b/web/app/components/base/button/sync-button.stories.tsx index dcfbf6daf3..5a5c078ec1 100644 --- a/web/app/components/base/button/sync-button.stories.tsx +++ b/web/app/components/base/button/sync-button.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import SyncButton from './sync-button' const meta = { diff --git a/web/app/components/base/chat/chat/answer/index.stories.tsx b/web/app/components/base/chat/chat/answer/index.stories.tsx index a8e42b7ad3..f39746efb2 100644 --- a/web/app/components/base/chat/chat/answer/index.stories.tsx +++ b/web/app/components/base/chat/chat/answer/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ChatItem } from '../../types' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import Answer from '.' diff --git a/web/app/components/base/chat/chat/question.stories.tsx b/web/app/components/base/chat/chat/question.stories.tsx index 4542dc3ac6..308a096864 100644 --- a/web/app/components/base/chat/chat/question.stories.tsx +++ b/web/app/components/base/chat/chat/question.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ChatItem } from '../types' import { User } from '@/app/components/base/icons/src/public/avatar' diff --git a/web/app/components/base/checkbox/index.stories.tsx b/web/app/components/base/checkbox/index.stories.tsx index 580d731a7e..0dac4cae15 100644 --- a/web/app/components/base/checkbox/index.stories.tsx +++ b/web/app/components/base/checkbox/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Checkbox from '.' diff --git a/web/app/components/base/chip/index.stories.tsx b/web/app/components/base/chip/index.stories.tsx index fc43ae8724..5812f97d98 100644 --- a/web/app/components/base/chip/index.stories.tsx +++ b/web/app/components/base/chip/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Item } from '.' import { useState } from 'react' import Chip from '.' diff --git a/web/app/components/base/confirm/index.stories.tsx b/web/app/components/base/confirm/index.stories.tsx index 12cb46d9e4..6d22bbe87b 100644 --- a/web/app/components/base/confirm/index.stories.tsx +++ b/web/app/components/base/confirm/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Confirm from '.' import Button from '../button' diff --git a/web/app/components/base/content-dialog/index.stories.tsx b/web/app/components/base/content-dialog/index.stories.tsx index aaebcad1b7..8ddd5c667d 100644 --- a/web/app/components/base/content-dialog/index.stories.tsx +++ b/web/app/components/base/content-dialog/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useEffect, useState } from 'react' import ContentDialog from '.' diff --git a/web/app/components/base/copy-feedback/index.stories.tsx b/web/app/components/base/copy-feedback/index.stories.tsx index 3bab620aec..aa535993f8 100644 --- a/web/app/components/base/copy-feedback/index.stories.tsx +++ b/web/app/components/base/copy-feedback/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import CopyFeedback, { CopyFeedbackNew } from '.' diff --git a/web/app/components/base/copy-icon/index.stories.tsx b/web/app/components/base/copy-icon/index.stories.tsx index 5962773792..dd13343819 100644 --- a/web/app/components/base/copy-icon/index.stories.tsx +++ b/web/app/components/base/copy-icon/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import CopyIcon from '.' const meta = { diff --git a/web/app/components/base/corner-label/index.stories.tsx b/web/app/components/base/corner-label/index.stories.tsx index 1592f94259..dbfab31da0 100644 --- a/web/app/components/base/corner-label/index.stories.tsx +++ b/web/app/components/base/corner-label/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import CornerLabel from '.' const meta = { diff --git a/web/app/components/base/date-and-time-picker/index.stories.tsx b/web/app/components/base/date-and-time-picker/index.stories.tsx index ad057f7969..1ed35afe88 100644 --- a/web/app/components/base/date-and-time-picker/index.stories.tsx +++ b/web/app/components/base/date-and-time-picker/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { DatePickerProps } from './types' import { useState } from 'react' import { fn } from 'storybook/test' diff --git a/web/app/components/base/dialog/index.stories.tsx b/web/app/components/base/dialog/index.stories.tsx index f573b856d3..af2e669535 100644 --- a/web/app/components/base/dialog/index.stories.tsx +++ b/web/app/components/base/dialog/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useEffect, useState } from 'react' import Dialog from '.' diff --git a/web/app/components/base/divider/index.stories.tsx b/web/app/components/base/divider/index.stories.tsx index c634173202..2ae00eca47 100644 --- a/web/app/components/base/divider/index.stories.tsx +++ b/web/app/components/base/divider/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Divider from '.' const meta = { diff --git a/web/app/components/base/drawer-plus/index.stories.tsx b/web/app/components/base/drawer-plus/index.stories.tsx index c79dd8af8a..4bdfef2ab3 100644 --- a/web/app/components/base/drawer-plus/index.stories.tsx +++ b/web/app/components/base/drawer-plus/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { fn } from 'storybook/test' import DrawerPlus from '.' diff --git a/web/app/components/base/drawer/index.stories.tsx b/web/app/components/base/drawer/index.stories.tsx index cfcfbf6a2e..ca7b3bc243 100644 --- a/web/app/components/base/drawer/index.stories.tsx +++ b/web/app/components/base/drawer/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { fn } from 'storybook/test' import Drawer from '.' diff --git a/web/app/components/base/dropdown/index.stories.tsx b/web/app/components/base/dropdown/index.stories.tsx index 4b08d54c47..7cb7f820f6 100644 --- a/web/app/components/base/dropdown/index.stories.tsx +++ b/web/app/components/base/dropdown/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Item } from '.' import { useState } from 'react' import { fn } from 'storybook/test' diff --git a/web/app/components/base/effect/index.stories.tsx b/web/app/components/base/effect/index.stories.tsx index a7f316fe7e..36a0e668cf 100644 --- a/web/app/components/base/effect/index.stories.tsx +++ b/web/app/components/base/effect/index.stories.tsx @@ -1,5 +1,5 @@ /* eslint-disable tailwindcss/classnames-order */ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Effect from '.' const meta = { diff --git a/web/app/components/base/emoji-picker/Inner.stories.tsx b/web/app/components/base/emoji-picker/Inner.stories.tsx index 642b4092e8..be0e993cce 100644 --- a/web/app/components/base/emoji-picker/Inner.stories.tsx +++ b/web/app/components/base/emoji-picker/Inner.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import EmojiPickerInner from './Inner' diff --git a/web/app/components/base/emoji-picker/index.stories.tsx b/web/app/components/base/emoji-picker/index.stories.tsx index beadcc0898..0649f32e68 100644 --- a/web/app/components/base/emoji-picker/index.stories.tsx +++ b/web/app/components/base/emoji-picker/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import EmojiPicker from '.' diff --git a/web/app/components/base/features/index.stories.tsx b/web/app/components/base/features/index.stories.tsx index c94d4faa1d..99d8df097b 100644 --- a/web/app/components/base/features/index.stories.tsx +++ b/web/app/components/base/features/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Features } from './types' import { useState } from 'react' import { FeaturesProvider } from '.' diff --git a/web/app/components/base/file-icon/index.stories.tsx b/web/app/components/base/file-icon/index.stories.tsx index 21f9c3111c..4f0ec61c94 100644 --- a/web/app/components/base/file-icon/index.stories.tsx +++ b/web/app/components/base/file-icon/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import FileIcon from '.' const meta = { diff --git a/web/app/components/base/file-uploader/file-image-render.stories.tsx b/web/app/components/base/file-uploader/file-image-render.stories.tsx index 132c0b61a3..ca051e4b27 100644 --- a/web/app/components/base/file-uploader/file-image-render.stories.tsx +++ b/web/app/components/base/file-uploader/file-image-render.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import FileImageRender from './file-image-render' const SAMPLE_IMAGE = 'data:image/svg+xml;utf8,Preview' diff --git a/web/app/components/base/file-uploader/file-list.stories.tsx b/web/app/components/base/file-uploader/file-list.stories.tsx index 37c828c7f7..560202779a 100644 --- a/web/app/components/base/file-uploader/file-list.stories.tsx +++ b/web/app/components/base/file-uploader/file-list.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FileEntity } from './types' import { useState } from 'react' import { SupportUploadFileTypes } from '@/app/components/workflow/types' diff --git a/web/app/components/base/file-uploader/file-type-icon.stories.tsx b/web/app/components/base/file-uploader/file-type-icon.stories.tsx index c317afab68..6a6df069b1 100644 --- a/web/app/components/base/file-uploader/file-type-icon.stories.tsx +++ b/web/app/components/base/file-uploader/file-type-icon.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import FileTypeIcon from './file-type-icon' import { FileAppearanceTypeEnum } from './types' diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx index aa53ff17d9..f10fce1173 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FileEntity } from '../types' import type { FileUpload } from '@/app/components/base/features/types' import { useState } from 'react' diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx index e094a48803..632fc40136 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FileEntity } from '../types' import type { FileUpload } from '@/app/components/base/features/types' import { useState } from 'react' diff --git a/web/app/components/base/float-right-container/index.stories.tsx b/web/app/components/base/float-right-container/index.stories.tsx index dcc55d4cf6..5887afd1e3 100644 --- a/web/app/components/base/float-right-container/index.stories.tsx +++ b/web/app/components/base/float-right-container/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { fn } from 'storybook/test' import FloatRightContainer from '.' diff --git a/web/app/components/base/form/index.stories.tsx b/web/app/components/base/form/index.stories.tsx index 41e2e9deb8..3a6e052f0e 100644 --- a/web/app/components/base/form/index.stories.tsx +++ b/web/app/components/base/form/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FormStoryRender } from '../../../../.storybook/utils/form-story-wrapper' import type { FormSchema } from './types' import { useStore } from '@tanstack/react-form' diff --git a/web/app/components/base/fullscreen-modal/index.stories.tsx b/web/app/components/base/fullscreen-modal/index.stories.tsx index 72fd28df66..3285b1c4ea 100644 --- a/web/app/components/base/fullscreen-modal/index.stories.tsx +++ b/web/app/components/base/fullscreen-modal/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import FullScreenModal from '.' diff --git a/web/app/components/base/grid-mask/index.stories.tsx b/web/app/components/base/grid-mask/index.stories.tsx index 1b67a1510d..24028f4347 100644 --- a/web/app/components/base/grid-mask/index.stories.tsx +++ b/web/app/components/base/grid-mask/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import GridMask from '.' const meta = { diff --git a/web/app/components/base/icons/icon-gallery.stories.tsx b/web/app/components/base/icons/icon-gallery.stories.tsx index 55322a7ea3..15206f2735 100644 --- a/web/app/components/base/icons/icon-gallery.stories.tsx +++ b/web/app/components/base/icons/icon-gallery.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import * as React from 'react' declare const require: any diff --git a/web/app/components/base/image-gallery/index.stories.tsx b/web/app/components/base/image-gallery/index.stories.tsx index c1b463170c..d3be60fd56 100644 --- a/web/app/components/base/image-gallery/index.stories.tsx +++ b/web/app/components/base/image-gallery/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import ImageGallery from '.' const IMAGE_SOURCES = [ diff --git a/web/app/components/base/image-uploader/image-list.stories.tsx b/web/app/components/base/image-uploader/image-list.stories.tsx index eabdb27aab..efafa6b4f0 100644 --- a/web/app/components/base/image-uploader/image-list.stories.tsx +++ b/web/app/components/base/image-uploader/image-list.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ImageFile } from '@/types/app' import { useMemo, useState } from 'react' import { TransferMethod } from '@/types/app' diff --git a/web/app/components/base/inline-delete-confirm/index.stories.tsx b/web/app/components/base/inline-delete-confirm/index.stories.tsx index 56fa5a9431..c352c512a5 100644 --- a/web/app/components/base/inline-delete-confirm/index.stories.tsx +++ b/web/app/components/base/inline-delete-confirm/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { fn } from 'storybook/test' import InlineDeleteConfirm from '.' diff --git a/web/app/components/base/input-number/index.stories.tsx b/web/app/components/base/input-number/index.stories.tsx index 997003d53f..4b7bebf216 100644 --- a/web/app/components/base/input-number/index.stories.tsx +++ b/web/app/components/base/input-number/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { InputNumber } from '.' diff --git a/web/app/components/base/input/index.stories.tsx b/web/app/components/base/input/index.stories.tsx index 59861435ef..860f65dfc7 100644 --- a/web/app/components/base/input/index.stories.tsx +++ b/web/app/components/base/input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Input from '.' diff --git a/web/app/components/base/linked-apps-panel/index.stories.tsx b/web/app/components/base/linked-apps-panel/index.stories.tsx index 07787173e0..fb9d7c4fba 100644 --- a/web/app/components/base/linked-apps-panel/index.stories.tsx +++ b/web/app/components/base/linked-apps-panel/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { RelatedApp } from '@/models/datasets' import { AppModeEnum } from '@/types/app' import LinkedAppsPanel from '.' diff --git a/web/app/components/base/list-empty/index.stories.tsx b/web/app/components/base/list-empty/index.stories.tsx index 36c0e3c7a7..5079337687 100644 --- a/web/app/components/base/list-empty/index.stories.tsx +++ b/web/app/components/base/list-empty/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import ListEmpty from '.' const meta = { diff --git a/web/app/components/base/loading/index.stories.tsx b/web/app/components/base/loading/index.stories.tsx index f22f87516c..22fe746709 100644 --- a/web/app/components/base/loading/index.stories.tsx +++ b/web/app/components/base/loading/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Loading from '.' const meta = { diff --git a/web/app/components/base/logo/index.stories.tsx b/web/app/components/base/logo/index.stories.tsx index 7bd63151e4..8347bc48da 100644 --- a/web/app/components/base/logo/index.stories.tsx +++ b/web/app/components/base/logo/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ReactNode } from 'react' import { ThemeProvider } from 'next-themes' import DifyLogo from './dify-logo' diff --git a/web/app/components/base/markdown-blocks/code-block.stories.tsx b/web/app/components/base/markdown-blocks/code-block.stories.tsx index 98473bdf57..b9e92ada22 100644 --- a/web/app/components/base/markdown-blocks/code-block.stories.tsx +++ b/web/app/components/base/markdown-blocks/code-block.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import CodeBlock from './code-block' const SAMPLE_CODE = `const greet = (name: string) => { diff --git a/web/app/components/base/markdown-blocks/think-block.stories.tsx b/web/app/components/base/markdown-blocks/think-block.stories.tsx index 6d5c8dc418..23713fb263 100644 --- a/web/app/components/base/markdown-blocks/think-block.stories.tsx +++ b/web/app/components/base/markdown-blocks/think-block.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { ChatContextProvider } from '@/app/components/base/chat/chat/context' import ThinkBlock from './think-block' diff --git a/web/app/components/base/markdown/index.stories.tsx b/web/app/components/base/markdown/index.stories.tsx index 8c940e01cf..289dfda147 100644 --- a/web/app/components/base/markdown/index.stories.tsx +++ b/web/app/components/base/markdown/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { Markdown } from '.' diff --git a/web/app/components/base/mermaid/index.stories.tsx b/web/app/components/base/mermaid/index.stories.tsx index 73030d7905..70c259db08 100644 --- a/web/app/components/base/mermaid/index.stories.tsx +++ b/web/app/components/base/mermaid/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Flowchart from '.' diff --git a/web/app/components/base/message-log-modal/index.stories.tsx b/web/app/components/base/message-log-modal/index.stories.tsx index 6c29584f7d..e370bd3338 100644 --- a/web/app/components/base/message-log-modal/index.stories.tsx +++ b/web/app/components/base/message-log-modal/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow' diff --git a/web/app/components/base/modal-like-wrap/index.stories.tsx b/web/app/components/base/modal-like-wrap/index.stories.tsx index c7d66b8e6a..9e5ecd6d15 100644 --- a/web/app/components/base/modal-like-wrap/index.stories.tsx +++ b/web/app/components/base/modal-like-wrap/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import ModalLikeWrap from '.' const meta = { diff --git a/web/app/components/base/modal/index.stories.tsx b/web/app/components/base/modal/index.stories.tsx index c0ea31eb42..91bb851f20 100644 --- a/web/app/components/base/modal/index.stories.tsx +++ b/web/app/components/base/modal/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useEffect, useState } from 'react' import Modal from '.' diff --git a/web/app/components/base/modal/modal.stories.tsx b/web/app/components/base/modal/modal.stories.tsx index adb80aebe6..2ddf706866 100644 --- a/web/app/components/base/modal/modal.stories.tsx +++ b/web/app/components/base/modal/modal.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useEffect, useState } from 'react' import Modal from './modal' diff --git a/web/app/components/base/new-audio-button/index.stories.tsx b/web/app/components/base/new-audio-button/index.stories.tsx index 0bc8accec1..44a7e2616a 100644 --- a/web/app/components/base/new-audio-button/index.stories.tsx +++ b/web/app/components/base/new-audio-button/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ComponentProps } from 'react' import { useEffect } from 'react' import AudioBtn from '.' diff --git a/web/app/components/base/notion-connector/index.stories.tsx b/web/app/components/base/notion-connector/index.stories.tsx index eb8b17df3f..d43e4a2ae6 100644 --- a/web/app/components/base/notion-connector/index.stories.tsx +++ b/web/app/components/base/notion-connector/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import NotionConnector from '.' const meta = { diff --git a/web/app/components/base/notion-icon/index.stories.tsx b/web/app/components/base/notion-icon/index.stories.tsx index 5389a6f935..68f400f363 100644 --- a/web/app/components/base/notion-icon/index.stories.tsx +++ b/web/app/components/base/notion-icon/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import NotionIcon from '.' const meta = { diff --git a/web/app/components/base/notion-page-selector/index.stories.tsx b/web/app/components/base/notion-page-selector/index.stories.tsx index 9b2c44687a..d338793363 100644 --- a/web/app/components/base/notion-page-selector/index.stories.tsx +++ b/web/app/components/base/notion-page-selector/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { DataSourceCredential } from '@/app/components/header/account-setting/data-source-page-new/types' import type { NotionPage } from '@/models/common' import { useEffect, useMemo, useState } from 'react' diff --git a/web/app/components/base/pagination/index.stories.tsx b/web/app/components/base/pagination/index.stories.tsx index 4ad5488b96..e53f285bb2 100644 --- a/web/app/components/base/pagination/index.stories.tsx +++ b/web/app/components/base/pagination/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useMemo, useState } from 'react' import Pagination from '.' diff --git a/web/app/components/base/param-item/index.stories.tsx b/web/app/components/base/param-item/index.stories.tsx index 1b5b233f6d..0cf6c40146 100644 --- a/web/app/components/base/param-item/index.stories.tsx +++ b/web/app/components/base/param-item/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import ParamItem from '.' diff --git a/web/app/components/base/popover/index.stories.tsx b/web/app/components/base/popover/index.stories.tsx index ab57bc15cc..0076c1852b 100644 --- a/web/app/components/base/popover/index.stories.tsx +++ b/web/app/components/base/popover/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import CustomPopover from '.' diff --git a/web/app/components/base/portal-to-follow-elem/index.stories.tsx b/web/app/components/base/portal-to-follow-elem/index.stories.tsx index bbe5e9d206..c9c43f34c6 100644 --- a/web/app/components/base/portal-to-follow-elem/index.stories.tsx +++ b/web/app/components/base/portal-to-follow-elem/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import { PortalToFollowElem, diff --git a/web/app/components/base/portal-to-follow-elem/index.tsx b/web/app/components/base/portal-to-follow-elem/index.tsx index a656ab5308..c57fba9dd0 100644 --- a/web/app/components/base/portal-to-follow-elem/index.tsx +++ b/web/app/components/base/portal-to-follow-elem/index.tsx @@ -61,9 +61,12 @@ export function usePortalToFollowElem({ }), shift({ padding: 5 }), size({ - apply({ rects, elements }) { - if (triggerPopupSameWidth) - elements.floating.style.width = `${rects.reference.width}px` + apply({ rects, elements, availableHeight }) { + Object.assign(elements.floating.style, { + maxHeight: `${Math.max(0, availableHeight)}px`, + overflowY: 'auto', + ...(triggerPopupSameWidth && { width: `${rects.reference.width}px` }), + }) }, }), ], diff --git a/web/app/components/base/premium-badge/index.stories.tsx b/web/app/components/base/premium-badge/index.stories.tsx index c1f6ede869..9d892cbf61 100644 --- a/web/app/components/base/premium-badge/index.stories.tsx +++ b/web/app/components/base/premium-badge/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import PremiumBadge from '.' const colors: Array['color']>> = ['blue', 'indigo', 'gray', 'orange'] diff --git a/web/app/components/base/progress-bar/progress-circle.stories.tsx b/web/app/components/base/progress-bar/progress-circle.stories.tsx index 10f8ce6c28..1dd52d5683 100644 --- a/web/app/components/base/progress-bar/progress-circle.stories.tsx +++ b/web/app/components/base/progress-bar/progress-circle.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import ProgressCircle from './progress-circle' diff --git a/web/app/components/base/prompt-editor/index.stories.tsx b/web/app/components/base/prompt-editor/index.stories.tsx index f7b0812ff7..92f23345be 100644 --- a/web/app/components/base/prompt-editor/index.stories.tsx +++ b/web/app/components/base/prompt-editor/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' // Mock component to avoid complex initialization issues diff --git a/web/app/components/base/prompt-log-modal/index.stories.tsx b/web/app/components/base/prompt-log-modal/index.stories.tsx index 39fab32030..42f90e6a57 100644 --- a/web/app/components/base/prompt-log-modal/index.stories.tsx +++ b/web/app/components/base/prompt-log-modal/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { IChatItem } from '@/app/components/base/chat/chat/type' import { useEffect } from 'react' import { useStore } from '@/app/components/app/store' diff --git a/web/app/components/base/qrcode/index.stories.tsx b/web/app/components/base/qrcode/index.stories.tsx index 312dc6a5a8..700a71fceb 100644 --- a/web/app/components/base/qrcode/index.stories.tsx +++ b/web/app/components/base/qrcode/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import ShareQRCode from '.' const QRDemo = ({ diff --git a/web/app/components/base/radio-card/index.stories.tsx b/web/app/components/base/radio-card/index.stories.tsx index 3adccfaf0d..40ef7069f5 100644 --- a/web/app/components/base/radio-card/index.stories.tsx +++ b/web/app/components/base/radio-card/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { RiCloudLine, RiCpuLine, RiDatabase2Line, RiLightbulbLine, RiRocketLine, RiShieldLine } from '@remixicon/react' import { useState } from 'react' import RadioCard from '.' diff --git a/web/app/components/base/radio/index.stories.tsx b/web/app/components/base/radio/index.stories.tsx index 1f9f7173fd..61449f1b5f 100644 --- a/web/app/components/base/radio/index.stories.tsx +++ b/web/app/components/base/radio/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Radio from '.' diff --git a/web/app/components/base/search-input/index.stories.tsx b/web/app/components/base/search-input/index.stories.tsx index 4b5323a8db..b27a6c2fb5 100644 --- a/web/app/components/base/search-input/index.stories.tsx +++ b/web/app/components/base/search-input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import SearchInput from '.' diff --git a/web/app/components/base/segmented-control/index.stories.tsx b/web/app/components/base/segmented-control/index.stories.tsx index d7b41b3921..6ccb3e293a 100644 --- a/web/app/components/base/segmented-control/index.stories.tsx +++ b/web/app/components/base/segmented-control/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { RiLineChartLine, RiListCheck2, RiRobot2Line } from '@remixicon/react' import { useState } from 'react' import { SegmentedControl } from '.' diff --git a/web/app/components/base/select/index.stories.tsx b/web/app/components/base/select/index.stories.tsx index 793f21343a..5a7fae6cf7 100644 --- a/web/app/components/base/select/index.stories.tsx +++ b/web/app/components/base/select/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Item } from '.' import { useState } from 'react' import Select, { PortalSelect, SimpleSelect } from '.' diff --git a/web/app/components/base/simple-pie-chart/index.stories.tsx b/web/app/components/base/simple-pie-chart/index.stories.tsx index d08c8fa0ce..05bf603629 100644 --- a/web/app/components/base/simple-pie-chart/index.stories.tsx +++ b/web/app/components/base/simple-pie-chart/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useMemo, useState } from 'react' import SimplePieChart from '.' diff --git a/web/app/components/base/skeleton/index.stories.tsx b/web/app/components/base/skeleton/index.stories.tsx index b5ea649b34..e767852406 100644 --- a/web/app/components/base/skeleton/index.stories.tsx +++ b/web/app/components/base/skeleton/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { SkeletonContainer, SkeletonPoint, diff --git a/web/app/components/base/slider/index.stories.tsx b/web/app/components/base/slider/index.stories.tsx index 7640e06c09..bde937ffad 100644 --- a/web/app/components/base/slider/index.stories.tsx +++ b/web/app/components/base/slider/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Slider from '.' diff --git a/web/app/components/base/sort/index.stories.tsx b/web/app/components/base/sort/index.stories.tsx index 3ecf9983a0..46b46e8d1e 100644 --- a/web/app/components/base/sort/index.stories.tsx +++ b/web/app/components/base/sort/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useMemo, useState } from 'react' import Sort from '.' diff --git a/web/app/components/base/spinner/index.stories.tsx b/web/app/components/base/spinner/index.stories.tsx index d4a481e55a..f5a8e83059 100644 --- a/web/app/components/base/spinner/index.stories.tsx +++ b/web/app/components/base/spinner/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Spinner from '.' diff --git a/web/app/components/base/svg-gallery/index.stories.tsx b/web/app/components/base/svg-gallery/index.stories.tsx index 65da97d243..ccbf320b52 100644 --- a/web/app/components/base/svg-gallery/index.stories.tsx +++ b/web/app/components/base/svg-gallery/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import SVGRenderer from '.' const SAMPLE_SVG = ` diff --git a/web/app/components/base/svg/index.stories.tsx b/web/app/components/base/svg/index.stories.tsx index 3c6a7ca0a3..3d215dad5a 100644 --- a/web/app/components/base/svg/index.stories.tsx +++ b/web/app/components/base/svg/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import SVGBtn from '.' diff --git a/web/app/components/base/switch/index.stories.tsx b/web/app/components/base/switch/index.stories.tsx index 941ebaf172..7fe7d1fbec 100644 --- a/web/app/components/base/switch/index.stories.tsx +++ b/web/app/components/base/switch/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Switch from '.' diff --git a/web/app/components/base/tab-header/index.stories.tsx b/web/app/components/base/tab-header/index.stories.tsx index 3d7f2bf31b..2b45907788 100644 --- a/web/app/components/base/tab-header/index.stories.tsx +++ b/web/app/components/base/tab-header/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ITabHeaderProps } from '.' import { useState } from 'react' import TabHeader from '.' diff --git a/web/app/components/base/tab-slider-new/index.stories.tsx b/web/app/components/base/tab-slider-new/index.stories.tsx index d0a412732a..56f9df4e27 100644 --- a/web/app/components/base/tab-slider-new/index.stories.tsx +++ b/web/app/components/base/tab-slider-new/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { RiSparklingFill, RiTerminalBoxLine } from '@remixicon/react' import { useState } from 'react' import TabSliderNew from '.' diff --git a/web/app/components/base/tab-slider-plain/index.stories.tsx b/web/app/components/base/tab-slider-plain/index.stories.tsx index dd8c7e0d30..e621ba43aa 100644 --- a/web/app/components/base/tab-slider-plain/index.stories.tsx +++ b/web/app/components/base/tab-slider-plain/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import TabSliderPlain from '.' diff --git a/web/app/components/base/tab-slider/index.stories.tsx b/web/app/components/base/tab-slider/index.stories.tsx index 703116fe19..0db53491bd 100644 --- a/web/app/components/base/tab-slider/index.stories.tsx +++ b/web/app/components/base/tab-slider/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useEffect, useState } from 'react' import TabSlider from '.' diff --git a/web/app/components/base/tag-input/index.stories.tsx b/web/app/components/base/tag-input/index.stories.tsx index cb9551702e..ef1b2e3365 100644 --- a/web/app/components/base/tag-input/index.stories.tsx +++ b/web/app/components/base/tag-input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import TagInput from '.' diff --git a/web/app/components/base/tag-management/index.stories.tsx b/web/app/components/base/tag-management/index.stories.tsx index e6a088c267..cb62965901 100644 --- a/web/app/components/base/tag-management/index.stories.tsx +++ b/web/app/components/base/tag-management/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Tag } from './constant' import { useEffect, useRef } from 'react' import { ToastProvider } from '@/app/components/base/toast' diff --git a/web/app/components/base/tag/index.stories.tsx b/web/app/components/base/tag/index.stories.tsx index 8ca15c0c8b..219ed4f9e8 100644 --- a/web/app/components/base/tag/index.stories.tsx +++ b/web/app/components/base/tag/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Tag from '.' const COLORS: Array['color']>> = ['green', 'yellow', 'red', 'gray'] diff --git a/web/app/components/base/textarea/index.stories.tsx b/web/app/components/base/textarea/index.stories.tsx index 7d584368ef..0474e4c93f 100644 --- a/web/app/components/base/textarea/index.stories.tsx +++ b/web/app/components/base/textarea/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' import Textarea from '.' diff --git a/web/app/components/base/toast/index.stories.tsx b/web/app/components/base/toast/index.stories.tsx index 6ef65475cb..4ab9138070 100644 --- a/web/app/components/base/toast/index.stories.tsx +++ b/web/app/components/base/toast/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useCallback } from 'react' import Toast, { ToastProvider, useToastContext } from '.' diff --git a/web/app/components/base/tooltip/index.stories.tsx b/web/app/components/base/tooltip/index.stories.tsx index aeca69464f..9e2ce9977b 100644 --- a/web/app/components/base/tooltip/index.stories.tsx +++ b/web/app/components/base/tooltip/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Tooltip from '.' const TooltipGrid = () => { diff --git a/web/app/components/base/video-gallery/index.stories.tsx b/web/app/components/base/video-gallery/index.stories.tsx index 7e17ee208c..93aba599ef 100644 --- a/web/app/components/base/video-gallery/index.stories.tsx +++ b/web/app/components/base/video-gallery/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import VideoGallery from '.' const VIDEO_SOURCES = [ diff --git a/web/app/components/base/voice-input/index.stories.tsx b/web/app/components/base/voice-input/index.stories.tsx index e368f4bd51..169c62014d 100644 --- a/web/app/components/base/voice-input/index.stories.tsx +++ b/web/app/components/base/voice-input/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { useState } from 'react' // Mock component since VoiceInput requires browser APIs and service dependencies diff --git a/web/app/components/base/with-input-validation/index.stories.tsx b/web/app/components/base/with-input-validation/index.stories.tsx index 167fa73e84..cb06d45956 100644 --- a/web/app/components/base/with-input-validation/index.stories.tsx +++ b/web/app/components/base/with-input-validation/index.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { z } from 'zod' import withValidation from '.' diff --git a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx index 4bb8267cea..646c59eb75 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { Mock } from 'vitest' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' import { fireEvent, render, screen, waitFor } from '@testing-library/react' diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx new file mode 100644 index 0000000000..aa3e300322 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx @@ -0,0 +1,499 @@ +import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context' +import type { ChildChunkDetail, ChunkingMode, ParentMode } from '@/models/datasets' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import ChildSegmentList from './child-segment-list' + +// ============================================================================ +// Hoisted Mocks +// ============================================================================ + +const { + mockParentMode, + mockCurrChildChunk, +} = vi.hoisted(() => ({ + mockParentMode: { current: 'paragraph' as ParentMode }, + mockCurrChildChunk: { current: { childChunkInfo: undefined, showModal: false } as { childChunkInfo?: ChildChunkDetail, showModal: boolean } }, +})) + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { count?: number, ns?: string }) => { + if (key === 'segment.childChunks') + return options?.count === 1 ? 'child chunk' : 'child chunks' + if (key === 'segment.searchResults') + return 'search results' + if (key === 'segment.edited') + return 'edited' + if (key === 'operation.add') + return 'Add' + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, + }), +})) + +// Mock document context +vi.mock('../context', () => ({ + useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => { + const value: DocumentContextValue = { + datasetId: 'test-dataset-id', + documentId: 'test-document-id', + docForm: 'text' as ChunkingMode, + parentMode: mockParentMode.current, + } + return selector(value) + }, +})) + +// Mock segment list context +vi.mock('./index', () => ({ + useSegmentListContext: (selector: (value: { currChildChunk: { childChunkInfo?: ChildChunkDetail, showModal: boolean } }) => unknown) => { + return selector({ currChildChunk: mockCurrChildChunk.current }) + }, +})) + +// Mock skeleton component +vi.mock('./skeleton/full-doc-list-skeleton', () => ({ + default: () =>
Loading...
, +})) + +// Mock Empty component +vi.mock('./common/empty', () => ({ + default: ({ onClearFilter }: { onClearFilter: () => void }) => ( +
+ +
+ ), +})) + +// Mock FormattedText and EditSlice +vi.mock('../../../formatted-text/formatted', () => ({ + FormattedText: ({ children, className }: { children: React.ReactNode, className?: string }) => ( +
{children}
+ ), +})) + +vi.mock('../../../formatted-text/flavours/edit-slice', () => ({ + EditSlice: ({ label, text, onDelete, onClick, labelClassName, contentClassName }: { + label: string + text: string + onDelete: () => void + onClick: (e: React.MouseEvent) => void + labelClassName?: string + contentClassName?: string + }) => ( +
+ {label} + {text} + +
+ ), +})) + +// ============================================================================ +// Test Data Factories +// ============================================================================ + +const createMockChildChunk = (overrides: Partial = {}): ChildChunkDetail => ({ + id: `child-${Math.random().toString(36).substr(2, 9)}`, + position: 1, + segment_id: 'segment-1', + content: 'Child chunk content', + word_count: 100, + created_at: 1700000000, + updated_at: 1700000000, + type: 'automatic', + ...overrides, +}) + +// ============================================================================ +// Tests +// ============================================================================ + +describe('ChildSegmentList', () => { + const defaultProps = { + childChunks: [] as ChildChunkDetail[], + parentChunkId: 'parent-1', + enabled: true, + } + + beforeEach(() => { + vi.clearAllMocks() + mockParentMode.current = 'paragraph' + mockCurrChildChunk.current = { childChunkInfo: undefined, showModal: false } + }) + + describe('Rendering', () => { + it('should render with empty child chunks', () => { + render() + + expect(screen.getByText(/child chunks/i)).toBeInTheDocument() + }) + + it('should render child chunks when provided', () => { + const childChunks = [ + createMockChildChunk({ id: 'child-1', position: 1, content: 'First chunk' }), + createMockChildChunk({ id: 'child-2', position: 2, content: 'Second chunk' }), + ] + + render() + + // In paragraph mode, content is collapsed by default + expect(screen.getByText(/2 child chunks/i)).toBeInTheDocument() + }) + + it('should render total count correctly with total prop in full-doc mode', () => { + mockParentMode.current = 'full-doc' + const childChunks = [createMockChildChunk()] + + // Pass inputValue="" to ensure isSearching is false + render() + + expect(screen.getByText(/5 child chunks/i)).toBeInTheDocument() + }) + + it('should render loading skeleton in full-doc mode when loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByTestId('full-doc-list-skeleton')).toBeInTheDocument() + }) + + it('should not render loading skeleton when not loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.queryByTestId('full-doc-list-skeleton')).not.toBeInTheDocument() + }) + }) + + describe('Paragraph Mode', () => { + beforeEach(() => { + mockParentMode.current = 'paragraph' + }) + + it('should show collapse icon in paragraph mode', () => { + const childChunks = [createMockChildChunk()] + + render() + + // Check for collapse/expand behavior + const totalRow = screen.getByText(/1 child chunk/i).closest('div') + expect(totalRow).toBeInTheDocument() + }) + + it('should toggle collapsed state when clicked', () => { + const childChunks = [createMockChildChunk({ content: 'Test content' })] + + render() + + // Initially collapsed in paragraph mode - content should not be visible + expect(screen.queryByTestId('formatted-text')).not.toBeInTheDocument() + + // Find and click the toggle area + const toggleArea = screen.getByText(/1 child chunk/i).closest('div') + + // Click to expand + if (toggleArea) + fireEvent.click(toggleArea) + + // After expansion, content should be visible + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should apply opacity when disabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).toHaveClass('opacity-50') + }) + + it('should not apply opacity when enabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Full-Doc Mode', () => { + beforeEach(() => { + mockParentMode.current = 'full-doc' + }) + + it('should show content by default in full-doc mode', () => { + const childChunks = [createMockChildChunk({ content: 'Full doc content' })] + + render() + + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should render search input in full-doc mode', () => { + render() + + const input = document.querySelector('input') + expect(input).toBeInTheDocument() + }) + + it('should call handleInputChange when input changes', () => { + const handleInputChange = vi.fn() + + render() + + const input = document.querySelector('input') + if (input) { + fireEvent.change(input, { target: { value: 'test search' } }) + expect(handleInputChange).toHaveBeenCalledWith('test search') + } + }) + + it('should show search results text when searching', () => { + render() + + expect(screen.getByText(/3 search results/i)).toBeInTheDocument() + }) + + it('should show empty component when no results and searching', () => { + render( + , + ) + + expect(screen.getByTestId('empty-component')).toBeInTheDocument() + }) + + it('should call onClearFilter when clear button clicked in empty state', () => { + const onClearFilter = vi.fn() + + render( + , + ) + + const clearButton = screen.getByText('Clear Filter') + fireEvent.click(clearButton) + + expect(onClearFilter).toHaveBeenCalled() + }) + }) + + describe('Child Chunk Items', () => { + it('should render edited label when chunk is edited', () => { + mockParentMode.current = 'full-doc' + const editedChunk = createMockChildChunk({ + id: 'edited-chunk', + position: 1, + created_at: 1700000000, + updated_at: 1700000001, // Different from created_at + }) + + render() + + expect(screen.getByText(/C-1 · edited/i)).toBeInTheDocument() + }) + + it('should not show edited label when chunk is not edited', () => { + mockParentMode.current = 'full-doc' + const normalChunk = createMockChildChunk({ + id: 'normal-chunk', + position: 2, + created_at: 1700000000, + updated_at: 1700000000, // Same as created_at + }) + + render() + + expect(screen.getByText('C-2')).toBeInTheDocument() + expect(screen.queryByText(/edited/i)).not.toBeInTheDocument() + }) + + it('should call onClickSlice when chunk is clicked', () => { + mockParentMode.current = 'full-doc' + const onClickSlice = vi.fn() + const chunk = createMockChildChunk({ id: 'clickable-chunk' }) + + render( + , + ) + + const editSlice = screen.getByTestId('edit-slice') + fireEvent.click(editSlice) + + expect(onClickSlice).toHaveBeenCalledWith(chunk) + }) + + it('should call onDelete when delete button is clicked', () => { + mockParentMode.current = 'full-doc' + const onDelete = vi.fn() + const chunk = createMockChildChunk({ id: 'deletable-chunk', segment_id: 'seg-1' }) + + render( + , + ) + + const deleteButton = screen.getByTestId('delete-button') + fireEvent.click(deleteButton) + + expect(onDelete).toHaveBeenCalledWith('seg-1', 'deletable-chunk') + }) + + it('should apply focused styles when chunk is currently selected', () => { + mockParentMode.current = 'full-doc' + const chunk = createMockChildChunk({ id: 'focused-chunk' }) + mockCurrChildChunk.current = { childChunkInfo: chunk, showModal: true } + + render() + + const label = screen.getByTestId('edit-slice-label') + expect(label).toHaveClass('bg-state-accent-solid') + }) + }) + + describe('Add Button', () => { + it('should call handleAddNewChildChunk when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + + render( + , + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-123') + }) + + it('should disable Add button when loading in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + const addButton = screen.getByText('Add') + expect(addButton).toBeDisabled() + }) + + it('should stop propagation when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + const parentClickHandler = vi.fn() + + render( +
+ +
, + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalled() + // Parent should not be called due to stopPropagation + }) + }) + + describe('computeTotalInfo function', () => { + it('should return search results when searching in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByText(/10 search results/i)).toBeInTheDocument() + }) + + it('should return "--" when total is 0 in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + // When total is 0, displayText is '--' + expect(screen.getByText(/--/)).toBeInTheDocument() + }) + + it('should use childChunks length in paragraph mode', () => { + mockParentMode.current = 'paragraph' + const childChunks = [ + createMockChildChunk(), + createMockChildChunk(), + createMockChildChunk(), + ] + + render() + + expect(screen.getByText(/3 child chunks/i)).toBeInTheDocument() + }) + }) + + describe('Focused State', () => { + it('should not apply opacity when focused even if disabled', () => { + const { container } = render( + , + ) + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Input clear button', () => { + it('should call handleInputChange with empty string when clear is clicked', () => { + mockParentMode.current = 'full-doc' + const handleInputChange = vi.fn() + + render( + , + ) + + // Find the clear button (it's the showClearIcon button in Input) + const input = document.querySelector('input') + if (input) { + // Trigger clear by simulating the input's onClear + const clearButton = document.querySelector('[class*="cursor-pointer"]') + if (clearButton) + fireEvent.click(clearButton) + } + }) + }) +}) diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx index b23aac6af9..fd6fd338d0 100644 --- a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import type { ChildChunkDetail } from '@/models/datasets' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' @@ -29,6 +29,37 @@ type IChildSegmentCardProps = { focused?: boolean } +function computeTotalInfo( + isFullDocMode: boolean, + isSearching: boolean, + total: number | undefined, + childChunksLength: number, +): { displayText: string, count: number, translationKey: 'segment.searchResults' | 'segment.childChunks' } { + if (isSearching) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.searchResults', + } + } + + if (isFullDocMode) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.childChunks', + } + } + + return { + displayText: String(formatNumber(childChunksLength)), + count: childChunksLength, + translationKey: 'segment.childChunks', + } +} + const ChildSegmentList: FC = ({ childChunks, parentChunkId, @@ -49,59 +80,87 @@ const ChildSegmentList: FC = ({ const [collapsed, setCollapsed] = useState(true) - const toggleCollapse = () => { - setCollapsed(!collapsed) + const isParagraphMode = parentMode === 'paragraph' + const isFullDocMode = parentMode === 'full-doc' + const isSearching = inputValue !== '' && isFullDocMode + const contentOpacity = (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' + const { displayText, count, translationKey } = computeTotalInfo(isFullDocMode, isSearching, total, childChunks.length) + const totalText = `${displayText} ${t(translationKey, { ns: 'datasetDocuments', count })}` + + const toggleCollapse = () => setCollapsed(prev => !prev) + const showContent = (isFullDocMode && !isLoading) || !collapsed + const hoverVisibleClass = isParagraphMode ? 'hidden group-hover/card:inline-block' : '' + + const renderCollapseIcon = () => { + if (!isParagraphMode) + return null + const Icon = collapsed ? RiArrowRightSLine : RiArrowDownSLine + return } - const isParagraphMode = useMemo(() => { - return parentMode === 'paragraph' - }, [parentMode]) + const renderChildChunkItem = (childChunk: ChildChunkDetail) => { + const isEdited = childChunk.updated_at !== childChunk.created_at + const isFocused = currChildChunk?.childChunkInfo?.id === childChunk.id + const label = isEdited + ? `C-${childChunk.position} · ${t('segment.edited', { ns: 'datasetDocuments' })}` + : `C-${childChunk.position}` - const isFullDocMode = useMemo(() => { - return parentMode === 'full-doc' - }, [parentMode]) + return ( + onDelete?.(childChunk.segment_id, childChunk.id)} + className="child-chunk" + labelClassName={isFocused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} + labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" + contentClassName={cn('!leading-6', isFocused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} + showDivider={false} + onClick={(e) => { + e.stopPropagation() + onClickSlice?.(childChunk) + }} + offsetOptions={({ rects }) => ({ + mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, + crossAxis: (20 - rects.floating.height) / 2, + })} + /> + ) + } - const contentOpacity = useMemo(() => { - return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' - }, [enabled, focused]) - - const totalText = useMemo(() => { - const isSearch = inputValue !== '' && isFullDocMode - if (!isSearch) { - const text = isFullDocMode - ? !total - ? '--' - : formatNumber(total) - : formatNumber(childChunks.length) - const count = isFullDocMode - ? text === '--' - ? 0 - : total - : childChunks.length - return `${text} ${t('segment.childChunks', { ns: 'datasetDocuments', count })}` + const renderContent = () => { + if (childChunks.length > 0) { + return ( + + {childChunks.map(renderChildChunkItem)} + + ) } - else { - const text = !total ? '--' : formatNumber(total) - const count = text === '--' ? 0 : total - return `${count} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}` + if (inputValue !== '') { + return ( +
+ +
+ ) } - }, [isFullDocMode, total, childChunks.length, inputValue]) + return null + } return (
- {isFullDocMode ? : null} -
+ {isFullDocMode && } +
{ @@ -109,23 +168,15 @@ const ChildSegmentList: FC = ({ toggleCollapse() }} > - { - isParagraphMode - ? collapsed - ? ( - - ) - : () - : null - } + {renderCollapseIcon()} {totalText} - · + ·
- {isFullDocMode - ? ( - handleInputChange?.(e.target.value)} - onClear={() => handleInputChange?.('')} - /> - ) - : null} + {isFullDocMode && ( + handleInputChange?.(e.target.value)} + onClear={() => handleInputChange?.('')} + /> + )}
- {isLoading ? : null} - {((isFullDocMode && !isLoading) || !collapsed) - ? ( -
- {isParagraphMode && ( -
- -
- )} - {childChunks.length > 0 - ? ( - - {childChunks.map((childChunk) => { - const edited = childChunk.updated_at !== childChunk.created_at - const focused = currChildChunk?.childChunkInfo?.id === childChunk.id - return ( - onDelete?.(childChunk.segment_id, childChunk.id)} - className="child-chunk" - labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} - labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" - contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} - showDivider={false} - onClick={(e) => { - e.stopPropagation() - onClickSlice?.(childChunk) - }} - offsetOptions={({ rects }) => { - return { - mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, - crossAxis: (20 - rects.floating.height) / 2, - } - }} - /> - ) - })} - - ) - : inputValue !== '' - ? ( -
- -
- ) - : null} + {isLoading && } + {showContent && ( +
+ {isParagraphMode && ( +
+
- ) - : null} + )} + {renderContent()} +
+ )}
) } diff --git a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx index dc1b7192c3..a68742890a 100644 --- a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx @@ -17,6 +17,31 @@ type DrawerProps = { needCheckChunks?: boolean } +const SIDE_POSITION_CLASS = { + right: 'right-0', + left: 'left-0', + bottom: 'bottom-0', + top: 'top-0', +} as const + +function containsTarget(selector: string, target: Node | null): boolean { + const elements = document.querySelectorAll(selector) + return Array.from(elements).some(el => el?.contains(target)) +} + +function shouldReopenChunkDetail( + isClickOnChunk: boolean, + isClickOnChildChunk: boolean, + segmentModalOpen: boolean, + childChunkModalOpen: boolean, +): boolean { + if (segmentModalOpen && isClickOnChildChunk) + return true + if (childChunkModalOpen && isClickOnChunk && !isClickOnChildChunk) + return true + return !isClickOnChunk && !isClickOnChildChunk +} + const Drawer = ({ open, onClose, @@ -41,22 +66,22 @@ const Drawer = ({ const shouldCloseDrawer = useCallback((target: Node | null) => { const panelContent = panelContentRef.current - if (!panelContent) + if (!panelContent || !target) return false - const chunks = document.querySelectorAll('.chunk-card') - const childChunks = document.querySelectorAll('.child-chunk') - const imagePreviewer = document.querySelector('.image-previewer') - const isClickOnChunk = Array.from(chunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const isClickOnChildChunk = Array.from(childChunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const reopenChunkDetail = (currSegment.showModal && isClickOnChildChunk) - || (currChildChunk.showModal && isClickOnChunk && !isClickOnChildChunk) || (!isClickOnChunk && !isClickOnChildChunk) - const isClickOnImagePreviewer = imagePreviewer && imagePreviewer.contains(target) - return target && !panelContent.contains(target) && (!needCheckChunks || reopenChunkDetail) && !isClickOnImagePreviewer - }, [currSegment, currChildChunk, needCheckChunks]) + + if (panelContent.contains(target)) + return false + + if (containsTarget('.image-previewer', target)) + return false + + if (!needCheckChunks) + return true + + const isClickOnChunk = containsTarget('.chunk-card', target) + const isClickOnChildChunk = containsTarget('.child-chunk', target) + return shouldReopenChunkDetail(isClickOnChunk, isClickOnChildChunk, currSegment.showModal, currChildChunk.showModal) + }, [currSegment.showModal, currChildChunk.showModal, needCheckChunks]) const onDownCapture = useCallback((e: PointerEvent) => { if (!open || modal) @@ -77,32 +102,27 @@ const Drawer = ({ const isHorizontal = side === 'left' || side === 'right' + const overlayPointerEvents = modal && open ? 'pointer-events-auto' : 'pointer-events-none' + const content = (
- {showOverlay - ? ( -