diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index cc8eb552b0..62f7ee3b4d 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -7,6 +7,7 @@ pipx install uv echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc +echo 'alias start-web-prod="cd /workspaces/dify/web && pnpm build && pnpm start"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc diff --git a/README.md b/README.md index ef654ced1e..ca09adec08 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media ## Community & contact -- [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. +- [GitHub Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. - [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). - [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. - [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. diff --git a/README_AR.md b/README_AR.md index 1310a9d802..df288fd33c 100644 --- a/README_AR.md +++ b/README_AR.md @@ -223,7 +223,7 @@ docker compose up -d ## المجتمع والاتصال -- [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة. +- [مناقشة GitHub](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة. - [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). - [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. - [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. diff --git a/README_BN.md b/README_BN.md index be1f8cc72e..4a5b5f3928 100644 --- a/README_BN.md +++ b/README_BN.md @@ -234,7 +234,7 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন ## কমিউনিটি এবং যোগাযোগ -- [Github Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম। +- [GitHub Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম। - [GitHub Issues](https://github.com/langgenius/dify/issues). Dify.AI ব্যবহার করে আপনি যেসব বাগের সম্মুখীন হন এবং ফিচার প্রস্তাবনা। আমাদের [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) দেখুন। - [Discord](https://discord.gg/FngNHpbcY7) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম। - [X(Twitter)](https://twitter.com/dify_ai) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম। diff --git a/README_CN.md b/README_CN.md index 1d4d51e856..ba7ee0006d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -243,7 +243,7 @@ docker compose up -d 我们欢迎您为 Dify 做出贡献,以帮助改善 Dify。包括:提交代码、问题、新想法,或分享您基于 Dify 创建的有趣且有用的 AI 应用程序。同时,我们也欢迎您在不同的活动、会议和社交媒体上分享 Dify。 -- [Github Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。 +- [GitHub Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。 - [GitHub Issues](https://github.com/langgenius/dify/issues)。👉:使用 Dify.AI 时遇到的错误和问题,请参阅[贡献指南](CONTRIBUTING.md)。 - [电子邮件支持](mailto:hello@dify.ai?subject=[GitHub]Questions%20About%20Dify)。👉:关于使用 Dify.AI 的问题。 - [Discord](https://discord.gg/FngNHpbcY7)。👉:分享您的应用程序并与社区交流。 diff --git a/README_DE.md b/README_DE.md index 7cc2710fde..f6023a3935 100644 --- a/README_DE.md +++ b/README_DE.md @@ -230,7 +230,7 @@ Falls Sie Code beitragen möchten, lesen Sie bitte unseren [Contribution Guide]( ## Gemeinschaft & Kontakt -* [Github Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen. +* [GitHub Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen. * [GitHub Issues](https://github.com/langgenius/dify/issues). Am besten für: Fehler, auf die Sie bei der Verwendung von Dify.AI stoßen, und Funktionsvorschläge. Siehe unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [Discord](https://discord.gg/FngNHpbcY7). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community. * [X(Twitter)](https://twitter.com/dify_ai). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community. diff --git a/README_JA.md b/README_JA.md index be5d8644e0..26703f3958 100644 --- a/README_JA.md +++ b/README_JA.md @@ -236,7 +236,7 @@ docker compose up -d ## コミュニティ & お問い合わせ -* [Github Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。 +* [GitHub Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。 * [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください * [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。 * [X(Twitter)](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。 diff --git a/README_KL.md b/README_KL.md index 3ebd756929..ea91baa5aa 100644 --- a/README_KL.md +++ b/README_KL.md @@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media ## Community & Contact -* [Github Discussion](https://github.com/langgenius/dify/discussions +* [GitHub Discussion](https://github.com/langgenius/dify/discussions ). Best for: sharing feedback and asking questions. * [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_KR.md b/README_KR.md index ecbe2f6b74..89301e8b2c 100644 --- a/README_KR.md +++ b/README_KR.md @@ -229,7 +229,7 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 ## 커뮤니티 & 연락처 -* [Github 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다. +* [GitHub 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다. * [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. * [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. * [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. diff --git a/README_SI.md b/README_SI.md index 45aec75dcd..14de1ea792 100644 --- a/README_SI.md +++ b/README_SI.md @@ -229,7 +229,7 @@ Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkra ## Skupnost in stik -* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj. +* [GitHub Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj. * [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. * [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. diff --git a/README_TR.md b/README_TR.md index 94ab6ee338..563a05af3c 100644 --- a/README_TR.md +++ b/README_TR.md @@ -227,7 +227,7 @@ Aynı zamanda, lütfen Dify'ı sosyal medyada, etkinliklerde ve konferanslarda p ## Topluluk & iletişim -* [Github Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için. +* [GitHub Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için. * [GitHub Sorunları](https://github.com/langgenius/dify/issues). En uygun: Dify.AI kullanırken karşılaştığınız hatalar ve özellik önerileri için. [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakın. * [Discord](https://discord.gg/FngNHpbcY7). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için. * [X(Twitter)](https://twitter.com/dify_ai). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için. diff --git a/README_TW.md b/README_TW.md index c252292e44..f4a76ac109 100644 --- a/README_TW.md +++ b/README_TW.md @@ -233,7 +233,7 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify ## 社群與聯絡方式 -- [Github Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。 +- [GitHub Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。 - [GitHub Issues](https://github.com/langgenius/dify/issues):最適合報告使用 Dify.AI 時遇到的問題和提出功能建議。請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。 - [Discord](https://discord.gg/FngNHpbcY7):最適合分享您的應用程式並與社群互動。 - [X(Twitter)](https://twitter.com/dify_ai):最適合分享您的應用程式並與社群互動。 diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index e5dbc30689..e01896a491 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -51,15 +51,19 @@ class LLMGenerator: response = cast( LLMResult, model_instance.invoke_llm( - prompt_messages=list(prompts), model_parameters={"max_tokens": 100, "temperature": 1}, stream=False + prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False ), ) answer = cast(str, response.message.content) cleaned_answer = re.sub(r"^.*(\{.*\}).*$", r"\1", answer, flags=re.DOTALL) if cleaned_answer is None: return "" - result_dict = json.loads(cleaned_answer) - answer = result_dict["Your Output"] + try: + result_dict = json.loads(cleaned_answer) + answer = result_dict["Your Output"] + except json.JSONDecodeError as e: + logging.exception("Failed to generate name after answer, use query instead") + answer = query name = answer.strip() if len(name) > 75: diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index 8fb1baec89..84abac7b15 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -366,7 +366,7 @@ def _extract_text_from_excel(file_content: bytes) -> str: df = excel_file.parse(sheet_name=sheet_name) df.dropna(how="all", inplace=True) # Create Markdown table two times to separate tables with a newline - markdown_table += df.to_markdown(index=False) + "\n\n" + markdown_table += df.to_markdown(index=False, floatfmt="") + "\n\n" except Exception as e: continue return markdown_table diff --git a/api/extensions/ext_login.py b/api/extensions/ext_login.py index 80fee7ccd8..6a5721c021 100644 --- a/api/extensions/ext_login.py +++ b/api/extensions/ext_login.py @@ -6,10 +6,11 @@ from flask_login import user_loaded_from_request, user_logged_in from werkzeug.exceptions import NotFound, Unauthorized import contexts +from configs import dify_config from dify_app import DifyApp from extensions.ext_database import db from libs.passport import PassportService -from models.account import Account +from models.account import Account, Tenant, TenantAccountJoin from models.model import EndUser from services.account_service import AccountService @@ -32,6 +33,26 @@ def load_user_from_request(request_from_flask_login): else: auth_token = request.args.get("_token") + # Check for admin API key authentication first + if dify_config.ADMIN_API_KEY_ENABLE and auth_header: + admin_api_key = dify_config.ADMIN_API_KEY + if admin_api_key and admin_api_key == auth_token: + workspace_id = request.headers.get("X-WORKSPACE-ID") + if workspace_id: + tenant_account_join = ( + db.session.query(Tenant, TenantAccountJoin) + .filter(Tenant.id == workspace_id) + .filter(TenantAccountJoin.tenant_id == Tenant.id) + .filter(TenantAccountJoin.role == "owner") + .one_or_none() + ) + if tenant_account_join: + tenant, ta = tenant_account_join + account = db.session.query(Account).filter_by(id=ta.account_id).first() + if account: + account.current_tenant = tenant + return account + if request.blueprint in {"console", "inner_api"}: if not auth_token: raise Unauthorized("Invalid Authorization token.") diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 416870cafa..500ca47c7e 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -100,6 +100,8 @@ app_partial_fields = { "updated_at": TimestampField, "tags": fields.List(fields.Nested(tag_fields)), "access_mode": fields.String, + "create_user_name": fields.String, + "author_name": fields.String, } diff --git a/api/libs/login.py b/api/libs/login.py index be9478e850..e3a7fe2948 100644 --- a/api/libs/login.py +++ b/api/libs/login.py @@ -2,14 +2,11 @@ from functools import wraps from typing import Any from flask import current_app, g, has_request_context, request -from flask_login import user_logged_in # type: ignore from flask_login.config import EXEMPT_METHODS # type: ignore -from werkzeug.exceptions import Unauthorized from werkzeug.local import LocalProxy from configs import dify_config -from extensions.ext_database import db -from models.account import Account, Tenant, TenantAccountJoin +from models.account import Account from models.model import EndUser #: A proxy for the current user. If no user is logged in, this will be an @@ -53,36 +50,6 @@ def login_required(func): @wraps(func) def decorated_view(*args, **kwargs): - auth_header = request.headers.get("Authorization") - if dify_config.ADMIN_API_KEY_ENABLE: - if auth_header: - if " " not in auth_header: - raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") - auth_scheme, auth_token = auth_header.split(None, 1) - auth_scheme = auth_scheme.lower() - if auth_scheme != "bearer": - raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") - - admin_api_key = dify_config.ADMIN_API_KEY - if admin_api_key: - if admin_api_key == auth_token: - workspace_id = request.headers.get("X-WORKSPACE-ID") - if workspace_id: - tenant_account_join = ( - db.session.query(Tenant, TenantAccountJoin) - .filter(Tenant.id == workspace_id) - .filter(TenantAccountJoin.tenant_id == Tenant.id) - .filter(TenantAccountJoin.role == "owner") - .one_or_none() - ) - if tenant_account_join: - tenant, ta = tenant_account_join - account = db.session.query(Account).filter_by(id=ta.account_id).first() - # Login admin - if account: - account.current_tenant = tenant - current_app.login_manager._update_request_context_with_user(account) # type: ignore - user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED: pass elif not current_user.is_authenticated: diff --git a/api/models/model.py b/api/models/model.py index ee79fbd6b5..92a5c0d121 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -294,6 +294,15 @@ class App(Base): return tags or [] + @property + def author_name(self): + if self.created_by: + account = db.session.query(Account).filter(Account.id == self.created_by).first() + if account: + return account.name + + return None + class AppModelConfig(Base): __tablename__ = "app_model_configs" diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py index de739ce9f5..1e8aec7f88 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py @@ -10,6 +10,7 @@ from core.workflow.entities.node_entities import NodeRunResult from core.workflow.nodes.document_extractor import DocumentExtractorNode, DocumentExtractorNodeData from core.workflow.nodes.document_extractor.node import ( _extract_text_from_docx, + _extract_text_from_excel, _extract_text_from_pdf, _extract_text_from_plain_text, ) @@ -182,3 +183,181 @@ def test_extract_text_from_docx(mock_document): def test_node_type(document_extractor_node): assert document_extractor_node._node_type == NodeType.DOCUMENT_EXTRACTOR + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_single_sheet(mock_excel_file): + """Test extracting text from Excel file with single sheet.""" + # Mock DataFrame + mock_df = Mock() + mock_df.dropna = Mock() + mock_df.to_markdown.return_value = "| Name | Age |\n|------|-----|\n| John | 25 |" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["Sheet1"] + mock_excel_instance.parse.return_value = mock_df + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_content" + result = _extract_text_from_excel(file_content) + + expected = "| Name | Age |\n|------|-----|\n| John | 25 |\n\n" + assert result == expected + mock_excel_file.assert_called_once() + mock_df.dropna.assert_called_once_with(how="all", inplace=True) + mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="") + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_multiple_sheets(mock_excel_file): + """Test extracting text from Excel file with multiple sheets.""" + # Mock DataFrames for different sheets + mock_df1 = Mock() + mock_df1.dropna = Mock() + mock_df1.to_markdown.return_value = "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |" + + mock_df2 = Mock() + mock_df2.dropna = Mock() + mock_df2.to_markdown.return_value = "| City | Population |\n|------|------------|\n| NYC | 8000000 |" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["Products", "Cities"] + mock_excel_instance.parse.side_effect = [mock_df1, mock_df2] + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_content_multiple_sheets" + result = _extract_text_from_excel(file_content) + + expected = ( + "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |\n\n" + "| City | Population |\n|------|------------|\n| NYC | 8000000 |\n\n" + ) + assert result == expected + assert mock_excel_instance.parse.call_count == 2 + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_empty_sheets(mock_excel_file): + """Test extracting text from Excel file with empty sheets.""" + # Mock empty DataFrame + mock_df = Mock() + mock_df.dropna = Mock() + mock_df.to_markdown.return_value = "" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["EmptySheet"] + mock_excel_instance.parse.return_value = mock_df + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_empty_content" + result = _extract_text_from_excel(file_content) + + expected = "\n\n" + assert result == expected + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_sheet_parse_error(mock_excel_file): + """Test handling of sheet parsing errors - should continue with other sheets.""" + # Mock DataFrames - one successful, one that raises exception + mock_df_success = Mock() + mock_df_success.dropna = Mock() + mock_df_success.to_markdown.return_value = "| Data | Value |\n|------|-------|\n| Test | 123 |" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["GoodSheet", "BadSheet"] + mock_excel_instance.parse.side_effect = [mock_df_success, Exception("Parse error")] + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_mixed_content" + result = _extract_text_from_excel(file_content) + + expected = "| Data | Value |\n|------|-------|\n| Test | 123 |\n\n" + assert result == expected + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_file_error(mock_excel_file): + """Test handling of Excel file reading errors.""" + mock_excel_file.side_effect = Exception("Invalid Excel file") + + file_content = b"invalid_excel_content" + + with pytest.raises(Exception) as exc_info: + _extract_text_from_excel(file_content) + + # Note: The function should raise TextExtractionError, but since it's not imported in the test, + # we check for the general Exception pattern + assert "Failed to extract text from Excel file" in str(exc_info.value) + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_io_bytesio_usage(mock_excel_file): + """Test that BytesIO is properly used with the file content.""" + import io + + # Mock DataFrame + mock_df = Mock() + mock_df.dropna = Mock() + mock_df.to_markdown.return_value = "| Test | Data |\n|------|------|\n| 1 | A |" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["TestSheet"] + mock_excel_instance.parse.return_value = mock_df + mock_excel_file.return_value = mock_excel_instance + + file_content = b"test_excel_bytes" + result = _extract_text_from_excel(file_content) + + # Verify that ExcelFile was called with a BytesIO object + mock_excel_file.assert_called_once() + call_args = mock_excel_file.call_args[0][0] + assert isinstance(call_args, io.BytesIO) + + expected = "| Test | Data |\n|------|------|\n| 1 | A |\n\n" + assert result == expected + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_all_sheets_fail(mock_excel_file): + """Test when all sheets fail to parse - should return empty string.""" + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["BadSheet1", "BadSheet2"] + mock_excel_instance.parse.side_effect = [Exception("Error 1"), Exception("Error 2")] + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_all_bad_sheets" + result = _extract_text_from_excel(file_content) + + # Should return empty string when all sheets fail + assert result == "" + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_markdown_formatting(mock_excel_file): + """Test that markdown formatting parameters are correctly applied.""" + # Mock DataFrame + mock_df = Mock() + mock_df.dropna = Mock() + mock_df.to_markdown.return_value = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |" + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["NumberSheet"] + mock_excel_instance.parse.return_value = mock_df + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_numbers" + result = _extract_text_from_excel(file_content) + + # Verify to_markdown was called with correct parameters + mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="") + + expected = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |\n\n" + assert result == expected diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 468c8244c0..42967b96f4 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -2,7 +2,7 @@ import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react' import cn from '@/utils/classnames' @@ -35,6 +35,7 @@ import Tooltip from '@/app/components/base/tooltip' import AccessControl from '@/app/components/app/app-access-control' import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' +import { formatTime } from '@/utils/time' export type AppCardProps = { app: App @@ -296,6 +297,15 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { setTags(app.tags) }, [app.tags]) + const EditTimeText = useMemo(() => { + const timeText = formatTime({ + date: (app.updated_at || app.created_at) * 1000, + dateFormat: 'MM/DD/YYYY h:mm', + }) + return `${t('datasetDocuments.segment.editedAt')} ${timeText}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [app.updated_at, app.created_at]) + return ( <>
{
{app.name}
-
- {app.mode === 'advanced-chat' &&
{t('app.types.advanced').toUpperCase()}
} - {app.mode === 'chat' &&
{t('app.types.chatbot').toUpperCase()}
} - {app.mode === 'agent-chat' &&
{t('app.types.agent').toUpperCase()}
} - {app.mode === 'workflow' &&
{t('app.types.workflow').toUpperCase()}
} - {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
} +
+
{app.author_name}
+
·
+
{EditTimeText}
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 7ce164c01b..3062e3a911 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -32,7 +32,6 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import TextGeneration from '@/app/components/app/text-generate/item' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import MessageLogModal from '@/app/components/base/message-log-modal' -import PromptLogModal from '@/app/components/base/prompt-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' import { useAppContext } from '@/context/app-context' import useTimestamp from '@/hooks/use-timestamp' @@ -191,13 +190,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const { userProfile: { timezone } } = useAppContext() const { formatTime } = useTimestamp() const { onClose, appDetail } = useContext(DrawerContext) - const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ + const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, showMessageLogModal: state.showMessageLogModal, setShowMessageLogModal: state.setShowMessageLogModal, - showPromptLogModal: state.showPromptLogModal, - setShowPromptLogModal: state.setShowPromptLogModal, currentLogModalActiveTab: state.currentLogModalActiveTab, }))) const { t } = useTranslation() @@ -518,16 +515,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { defaultTab={currentLogModalActiveTab} /> )} - {showPromptLogModal && ( - { - setCurrentLogItem() - setShowPromptLogModal(false) - }} - /> - )}
) } diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index a0a9323729..3722556931 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -234,6 +234,4 @@ const Answer: FC = ({ ) } -export default memo(Answer, (prevProps, nextProps) => - prevProps.responding === false && nextProps.responding === false, -) +export default memo(Answer) diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx index a484261a51..a0332ce819 100644 --- a/web/app/components/base/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -91,6 +91,11 @@ const initMermaid = () => { numberSectionStyles: 4, axisFormat: '%Y-%m-%d', }, + mindmap: { + useMaxWidth: true, + padding: 10, + diagramPadding: 20, + }, maxTextSize: 50000, }) isMermaidInitialized = true @@ -289,11 +294,12 @@ const Flowchart = React.forwardRef((props: { try { let finalCode: string - // Check if it's a gantt chart + // Check if it's a gantt chart or mindmap const isGanttChart = primitiveCode.trim().startsWith('gantt') + const isMindMap = primitiveCode.trim().startsWith('mindmap') - if (isGanttChart) { - // For gantt charts, ensure each task is on its own line + if (isGanttChart || isMindMap) { + // For gantt charts and mindmaps, ensure each task is on its own line // and preserve exact whitespace/format finalCode = primitiveCode.trim() } @@ -352,6 +358,11 @@ const Flowchart = React.forwardRef((props: { numberSectionStyles: 4, axisFormat: '%Y-%m-%d', }, + mindmap: { + useMaxWidth: true, + padding: 10, + diagramPadding: 20, + }, } if (look === 'classic') { @@ -476,15 +487,15 @@ const Flowchart = React.forwardRef((props: { 'bg-white': currentTheme === Theme.light, 'bg-slate-900': currentTheme === Theme.dark, }), - mermaidDiv: cn('mermaid relative h-auto w-full cursor-pointer', { + mermaidDiv: cn('mermaid cursor-pointer h-auto w-full relative', { 'bg-white': currentTheme === Theme.light, 'bg-slate-900': currentTheme === Theme.dark, }), - errorMessage: cn('px-[26px] py-4', { + errorMessage: cn('py-4 px-[26px]', { 'text-red-500': currentTheme === Theme.light, 'text-red-400': currentTheme === Theme.dark, }), - errorIcon: cn('h-6 w-6', { + errorIcon: cn('w-6 h-6', { 'text-red-500': currentTheme === Theme.light, 'text-red-400': currentTheme === Theme.dark, }), @@ -492,7 +503,7 @@ const Flowchart = React.forwardRef((props: { 'text-gray-700': currentTheme === Theme.light, 'text-gray-300': currentTheme === Theme.dark, }), - themeToggle: cn('flex h-10 w-10 items-center justify-center rounded-full shadow-md backdrop-blur-sm transition-all duration-300', { + themeToggle: cn('flex items-center justify-center w-10 h-10 rounded-full transition-all duration-300 shadow-md backdrop-blur-sm', { 'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light, 'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark, }), @@ -501,7 +512,7 @@ const Flowchart = React.forwardRef((props: { // Style classes for look options const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => { return cn( - 'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary', + 'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary', look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary', currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300', look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white', @@ -512,7 +523,7 @@ const Flowchart = React.forwardRef((props: {
} className={themeClasses.container}>
-