diff --git a/api/apps/file_app.py b/api/apps/file_app.py index 8b5383a66..e262b3d7b 100644 --- a/api/apps/file_app.py +++ b/api/apps/file_app.py @@ -125,8 +125,8 @@ async def upload(): @validate_request("name") async def create(): req = await request.json - pf_id = await request.json.get("parent_id") - input_file_type = await request.json.get("type") + pf_id = req.get("parent_id") + input_file_type = req.get("type") if not pf_id: root_folder = FileService.get_root_folder(current_user.id) pf_id = root_folder["id"] diff --git a/common/constants.py b/common/constants.py index 7956276f8..189ae9dbd 100644 --- a/common/constants.py +++ b/common/constants.py @@ -119,6 +119,7 @@ class FileSource(StrEnum): SLACK = "slack" TEAMS = "teams" WEBDAV = "webdav" + MOODLE = "moodle" class PipelineTaskType(StrEnum): diff --git a/common/data_source/__init__.py b/common/data_source/__init__.py index 1678bf9c3..66d393884 100644 --- a/common/data_source/__init__.py +++ b/common/data_source/__init__.py @@ -15,6 +15,7 @@ from .jira.connector import JiraConnector from .sharepoint_connector import SharePointConnector from .teams_connector import TeamsConnector from .webdav_connector import WebDAVConnector +from .moodle_connector import MoodleConnector from .config import BlobType, DocumentSource from .models import Document, TextSection, ImageSection, BasicExpertInfo from .exceptions import ( @@ -38,6 +39,7 @@ __all__ = [ "SharePointConnector", "TeamsConnector", "WebDAVConnector", + "MoodleConnector", "BlobType", "DocumentSource", "Document", diff --git a/common/data_source/config.py b/common/data_source/config.py index f1b671fa4..5998aa41d 100644 --- a/common/data_source/config.py +++ b/common/data_source/config.py @@ -50,6 +50,8 @@ class DocumentSource(str, Enum): GMAIL = "gmail" DISCORD = "discord" WEBDAV = "webdav" + MOODLE = "moodle" + S3_COMPATIBLE = "s3_compatible" class FileOrigin(str, Enum): diff --git a/common/data_source/moodle_connector.py b/common/data_source/moodle_connector.py new file mode 100644 index 000000000..427e7e86c --- /dev/null +++ b/common/data_source/moodle_connector.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import logging +import os +from collections.abc import Generator +from datetime import datetime, timezone +from retry import retry +from typing import Any, Optional + +from markdownify import markdownify as md +from moodle import Moodle as MoodleClient, MoodleException + +from common.data_source.config import INDEX_BATCH_SIZE +from common.data_source.exceptions import ( + ConnectorMissingCredentialError, + CredentialExpiredError, + InsufficientPermissionsError, + ConnectorValidationError, +) +from common.data_source.interfaces import LoadConnector, PollConnector, SecondsSinceUnixEpoch +from common.data_source.models import Document +from common.data_source.utils import batch_generator, rl_requests + +logger = logging.getLogger(__name__) + + +class MoodleConnector(LoadConnector, PollConnector): + """Moodle LMS connector for accessing course content""" + + def __init__(self, moodle_url: str, batch_size: int = INDEX_BATCH_SIZE) -> None: + self.moodle_url = moodle_url.rstrip("/") + self.batch_size = batch_size + self.moodle_client: Optional[MoodleClient] = None + + def _add_token_to_url(self, file_url: str) -> str: + """Append Moodle token to URL if missing""" + if not self.moodle_client: + return file_url + token = getattr(self.moodle_client, "token", "") + if "token=" in file_url.lower(): + return file_url + delimiter = "&" if "?" in file_url else "?" + return f"{file_url}{delimiter}token={token}" + + def _log_error(self, context: str, error: Exception, level: str = "warning") -> None: + """Simplified logging wrapper""" + msg = f"{context}: {error}" + if level == "error": + logger.error(msg) + else: + logger.warning(msg) + + def _get_latest_timestamp(self, *timestamps: int) -> int: + """Return latest valid timestamp""" + return max((t for t in timestamps if t and t > 0), default=0) + + def _yield_in_batches( + self, generator: Generator[Document, None, None] + ) -> Generator[list[Document], None, None]: + for batch in batch_generator(generator, self.batch_size): + yield batch + + def load_credentials(self, credentials: dict[str, Any]) -> None: + token = credentials.get("moodle_token") + if not token: + raise ConnectorMissingCredentialError("Moodle API token is required") + + try: + self.moodle_client = MoodleClient( + self.moodle_url + "/webservice/rest/server.php", token + ) + self.moodle_client.core.webservice.get_site_info() + except MoodleException as e: + if "invalidtoken" in str(e).lower(): + raise CredentialExpiredError("Moodle token is invalid or expired") + raise ConnectorMissingCredentialError(f"Failed to initialize Moodle client: {e}") + + def validate_connector_settings(self) -> None: + if not self.moodle_client: + raise ConnectorMissingCredentialError("Moodle client not initialized") + + try: + site_info = self.moodle_client.core.webservice.get_site_info() + if not site_info.sitename: + raise InsufficientPermissionsError("Invalid Moodle API response") + except MoodleException as e: + msg = str(e).lower() + if "invalidtoken" in msg: + raise CredentialExpiredError("Moodle token is invalid or expired") + if "accessexception" in msg: + raise InsufficientPermissionsError( + "Insufficient permissions. Ensure web services are enabled and permissions are correct." + ) + raise ConnectorValidationError(f"Moodle validation error: {e}") + except Exception as e: + raise ConnectorValidationError(f"Unexpected validation error: {e}") + + # ------------------------------------------------------------------------- + # Data loading & polling + # ------------------------------------------------------------------------- + + def load_from_state(self) -> Generator[list[Document], None, None]: + if not self.moodle_client: + raise ConnectorMissingCredentialError("Moodle client not initialized") + + logger.info("Starting full load from Moodle workspace") + courses = self._get_enrolled_courses() + if not courses: + logger.warning("No courses found to process") + return + + yield from self._yield_in_batches(self._process_courses(courses)) + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> Generator[list[Document], None, None]: + if not self.moodle_client: + raise ConnectorMissingCredentialError("Moodle client not initialized") + + logger.info( + f"Polling Moodle updates between {datetime.fromtimestamp(start)} and {datetime.fromtimestamp(end)}" + ) + courses = self._get_enrolled_courses() + if not courses: + logger.warning("No courses found to poll") + return + + yield from self._yield_in_batches(self._get_updated_content(courses, start, end)) + + @retry(tries=3, delay=1, backoff=2) + def _get_enrolled_courses(self) -> list: + if not self.moodle_client: + raise ConnectorMissingCredentialError("Moodle client not initialized") + + try: + return self.moodle_client.core.course.get_courses() + except MoodleException as e: + self._log_error("fetching courses", e, "error") + raise ConnectorValidationError(f"Failed to fetch courses: {e}") + + @retry(tries=3, delay=1, backoff=2) + def _get_course_contents(self, course_id: int): + if not self.moodle_client: + raise ConnectorMissingCredentialError("Moodle client not initialized") + + try: + return self.moodle_client.core.course.get_contents(courseid=course_id) + except MoodleException as e: + self._log_error(f"fetching course contents for {course_id}", e) + return [] + + def _process_courses(self, courses) -> Generator[Document, None, None]: + for course in courses: + try: + contents = self._get_course_contents(course.id) + for section in contents: + for module in section.modules: + doc = self._process_module(course, section, module) + if doc: + yield doc + except Exception as e: + self._log_error(f"processing course {course.fullname}", e) + + def _get_updated_content( + self, courses, start: float, end: float + ) -> Generator[Document, None, None]: + for course in courses: + try: + contents = self._get_course_contents(course.id) + for section in contents: + for module in section.modules: + times = [ + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + ] + if hasattr(module, "contents"): + times.extend( + getattr(c, "timemodified", 0) + for c in module.contents + if c and getattr(c, "timemodified", 0) + ) + last_mod = self._get_latest_timestamp(*times) + if start < last_mod <= end: + doc = self._process_module(course, section, module) + if doc: + yield doc + except Exception as e: + self._log_error(f"polling course {course.fullname}", e) + + def _process_module( + self, course, section, module + ) -> Optional[Document]: + try: + mtype = module.modname + if mtype in ["label", "url"]: + return None + if mtype == "resource": + return self._process_resource(course, section, module) + if mtype == "forum": + return self._process_forum(course, section, module) + if mtype == "page": + return self._process_page(course, section, module) + if mtype in ["assign", "quiz"]: + return self._process_activity(course, section, module) + if mtype == "book": + return self._process_book(course, section, module) + except Exception as e: + self._log_error(f"processing module {getattr(module, 'name', '?')}", e) + return None + + def _process_resource(self, course, section, module) -> Optional[Document]: + if not getattr(module, "contents", None): + return None + + file_info = module.contents[0] + if not getattr(file_info, "fileurl", None): + return None + + file_name = os.path.basename(file_info.filename) + ts = self._get_latest_timestamp( + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + getattr(file_info, "timemodified", 0), + ) + + try: + resp = rl_requests.get(self._add_token_to_url(file_info.fileurl), timeout=60) + resp.raise_for_status() + blob = resp.content + ext = os.path.splitext(file_name)[1] or ".bin" + semantic_id = f"{course.fullname} / {section.name} / {file_name}" + return Document( + id=f"moodle_resource_{module.id}", + source="moodle", + semantic_identifier=semantic_id, + extension=ext, + blob=blob, + doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc), + size_bytes=len(blob), + ) + except Exception as e: + self._log_error(f"downloading resource {file_name}", e, "error") + return None + + def _process_forum(self, course, section, module) -> Optional[Document]: + if not self.moodle_client or not getattr(module, "instance", None): + return None + + try: + result = self.moodle_client.mod.forum.get_forum_discussions(forumid=module.instance) + disc_list = getattr(result, "discussions", []) + if not disc_list: + return None + + markdown = [f"# {module.name}\n"] + latest_ts = self._get_latest_timestamp( + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + ) + + for d in disc_list: + markdown.append(f"## {d.name}\n\n{md(d.message or '')}\n\n---\n") + latest_ts = max(latest_ts, getattr(d, "timemodified", 0)) + + blob = "\n".join(markdown).encode("utf-8") + semantic_id = f"{course.fullname} / {section.name} / {module.name}" + return Document( + id=f"moodle_forum_{module.id}", + source="moodle", + semantic_identifier=semantic_id, + extension=".md", + blob=blob, + doc_updated_at=datetime.fromtimestamp(latest_ts or 0, tz=timezone.utc), + size_bytes=len(blob), + ) + except Exception as e: + self._log_error(f"processing forum {module.name}", e) + return None + + def _process_page(self, course, section, module) -> Optional[Document]: + if not getattr(module, "contents", None): + return None + + file_info = module.contents[0] + if not getattr(file_info, "fileurl", None): + return None + + file_name = os.path.basename(file_info.filename) + ts = self._get_latest_timestamp( + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + getattr(file_info, "timemodified", 0), + ) + + try: + resp = rl_requests.get(self._add_token_to_url(file_info.fileurl), timeout=60) + resp.raise_for_status() + blob = resp.content + ext = os.path.splitext(file_name)[1] or ".html" + semantic_id = f"{course.fullname} / {section.name} / {module.name}" + return Document( + id=f"moodle_page_{module.id}", + source="moodle", + semantic_identifier=semantic_id, + extension=ext, + blob=blob, + doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc), + size_bytes=len(blob), + ) + except Exception as e: + self._log_error(f"processing page {file_name}", e, "error") + return None + + def _process_activity(self, course, section, module) -> Optional[Document]: + desc = getattr(module, "description", "") + if not desc: + return None + + mtype, mname = module.modname, module.name + markdown = f"# {mname}\n\n**Type:** {mtype.capitalize()}\n\n{md(desc)}" + ts = self._get_latest_timestamp( + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + getattr(module, "added", 0), + ) + + semantic_id = f"{course.fullname} / {section.name} / {mname}" + blob = markdown.encode("utf-8") + return Document( + id=f"moodle_{mtype}_{module.id}", + source="moodle", + semantic_identifier=semantic_id, + extension=".md", + blob=blob, + doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc), + size_bytes=len(blob), + ) + + def _process_book(self, course, section, module) -> Optional[Document]: + if not getattr(module, "contents", None): + return None + + contents = module.contents + chapters = [ + c for c in contents + if getattr(c, "fileurl", None) and os.path.basename(c.filename) == "index.html" + ] + if not chapters: + return None + + latest_ts = self._get_latest_timestamp( + getattr(module, "timecreated", 0), + getattr(module, "timemodified", 0), + *[getattr(c, "timecreated", 0) for c in contents], + *[getattr(c, "timemodified", 0) for c in contents], + ) + + markdown_parts = [f"# {module.name}\n"] + for ch in chapters: + try: + resp = rl_requests.get(self._add_token_to_url(ch.fileurl), timeout=60) + resp.raise_for_status() + html = resp.content.decode("utf-8", errors="ignore") + markdown_parts.append(md(html) + "\n\n---\n") + except Exception as e: + self._log_error(f"processing book chapter {ch.filename}", e) + + blob = "\n".join(markdown_parts).encode("utf-8") + semantic_id = f"{course.fullname} / {section.name} / {module.name}" + return Document( + id=f"moodle_book_{module.id}", + source="moodle", + semantic_identifier=semantic_id, + extension=".md", + blob=blob, + doc_updated_at=datetime.fromtimestamp(latest_ts or 0, tz=timezone.utc), + size_bytes=len(blob), + ) diff --git a/common/settings.py b/common/settings.py index ac2b13e00..9df0c0cd2 100644 --- a/common/settings.py +++ b/common/settings.py @@ -139,7 +139,7 @@ def _get_or_create_secret_key(): import logging new_key = secrets.token_hex(32) - logging.warning(f"SECURITY WARNING: Using auto-generated SECRET_KEY. Generated key: {new_key}") + logging.warning("SECURITY WARNING: Using auto-generated SECRET_KEY.") return new_key class StorageFactory: diff --git a/docker/nginx/ragflow.https.conf b/docker/nginx/ragflow.https.conf index 71cd241e7..2ef4cd005 100644 --- a/docker/nginx/ragflow.https.conf +++ b/docker/nginx/ragflow.https.conf @@ -23,12 +23,12 @@ server { gzip_disable "MSIE [1-6]\."; location ~ ^/api/v1/admin { - proxy_pass http://ragflow:9381; + proxy_pass http://localhost:9381; include proxy.conf; } location ~ ^/(v1|api) { - proxy_pass http://ragflow:9380; + proxy_pass http://localhost:9380; include proxy.conf; } diff --git a/pyproject.toml b/pyproject.toml index 1bcc99ccf..afbfd5519 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "arxiv==2.1.3", "aspose-slides>=25.10.0,<26.0.0; platform_machine == 'x86_64' or (sys_platform == 'darwin' and platform_machine == 'arm64')", "atlassian-python-api==4.0.7", - "beartype>=0.18.5,<0.19.0", + "beartype>=0.20.0,<1.0.0", "bio==1.7.1", "blinker==1.7.0", "boto3==1.34.140", @@ -128,13 +128,13 @@ dependencies = [ "google-generativeai>=0.8.1,<0.9.0", # Needed for cv_model and embedding_model "python-docx>=1.1.2,<2.0.0", "pypdf2>=3.0.1,<4.0.0", - "graspologic>=3.4.1,<4.0.0", + "graspologic @ git+https://github.com/yuzhichang/graspologic.git@38e680cab72bc9fb68a7992c3bcc2d53b24e42fd", "mini-racer>=0.12.4,<0.13.0", "pyodbc>=5.2.0,<6.0.0", "pyicu>=2.15.3,<3.0.0", "flasgger>=0.9.7.1,<0.10.0", "xxhash>=3.5.0,<4.0.0", - "trio>=0.29.0", + "trio>=0.17.0,<0.29.0", "langfuse>=2.60.0", "debugpy>=1.8.13", "mcp>=1.9.4", @@ -149,6 +149,7 @@ dependencies = [ "markdownify>=1.2.0", "captcha>=0.7.1", "pip>=25.2", + "moodlepy>=0.23.0", "pypandoc>=1.16", "pyobvector==0.2.18", ] diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py index 856d23b01..cce5b2454 100644 --- a/rag/llm/chat_model.py +++ b/rag/llm/chat_model.py @@ -1635,6 +1635,15 @@ class LiteLLMBase(ABC): provider_cfg["allow_fallbacks"] = False extra_body["provider"] = provider_cfg completion_args.update({"extra_body": extra_body}) + + # Ollama deployments commonly sit behind a reverse proxy that enforces + # Bearer auth. Ensure the Authorization header is set when an API key + # is provided, while respecting any user-supplied headers. #11350 + extra_headers = deepcopy(completion_args.get("extra_headers") or {}) + if self.provider == SupportedLiteLLMProvider.Ollama and self.api_key and "Authorization" not in extra_headers: + extra_headers["Authorization"] = f"Bearer {self.api_key}" + if extra_headers: + completion_args["extra_headers"] = extra_headers return completion_args def chat_with_tools(self, system: str, history: list, gen_conf: dict = {}): diff --git a/rag/svr/sync_data_source.py b/rag/svr/sync_data_source.py index 66df335fa..83bd7a4eb 100644 --- a/rag/svr/sync_data_source.py +++ b/rag/svr/sync_data_source.py @@ -35,19 +35,9 @@ import trio from api.db.services.connector_service import ConnectorService, SyncLogsService from api.db.services.knowledgebase_service import KnowledgebaseService -from common.log_utils import init_root_logger -from common.config_utils import show_configs -from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, WebDAVConnector -import logging -import os -from datetime import datetime, timezone -import signal -import trio -import faulthandler -from common.constants import FileSource, TaskStatus from common import settings from common.config_utils import show_configs -from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, JiraConnector +from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, MoodleConnector, JiraConnector, WebDAVConnector from common.constants import FileSource, TaskStatus from common.data_source.config import INDEX_BATCH_SIZE from common.data_source.confluence_connector import ConfluenceConnector @@ -470,6 +460,36 @@ class WebDAV(SyncBase): begin_info )) return document_batch_generator + +class Moodle(SyncBase): + SOURCE_NAME: str = FileSource.MOODLE + + async def _generate(self, task: dict): + self.connector = MoodleConnector( + moodle_url=self.conf["moodle_url"], + batch_size=self.conf.get("batch_size", INDEX_BATCH_SIZE) + ) + + self.connector.load_credentials(self.conf["credentials"]) + + # Determine the time range for synchronization based on reindex or poll_range_start + if task["reindex"] == "1" or not task.get("poll_range_start"): + document_generator = self.connector.load_from_state() + begin_info = "totally" + else: + poll_start = task["poll_range_start"] + if poll_start is None: + document_generator = self.connector.load_from_state() + begin_info = "totally" + else: + document_generator = self.connector.poll_source( + poll_start.timestamp(), + datetime.now(timezone.utc).timestamp() + ) + begin_info = "from {}".format(poll_start) + + logging.info("Connect to Moodle: {} {}".format(self.conf["moodle_url"], begin_info)) + return document_generator func_factory = { @@ -483,7 +503,8 @@ func_factory = { FileSource.SHAREPOINT: SharePoint, FileSource.SLACK: Slack, FileSource.TEAMS: Teams, - FileSource.WEBDAV: WebDAV + FileSource.WEBDAV: WebDAV, + FileSource.MOODLE: Moodle } diff --git a/uv.lock b/uv.lock index 2c476f8a6..0c652efdf 100644 --- a/uv.lock +++ b/uv.lock @@ -343,11 +343,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.3.0" +version = "22.2.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/31/3f468da74c7de4fcf9b25591e682856389b3400b4b62f201e65f15ea3e07/attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99", size = 215900, upload-time = "2022-12-21T09:48:51.773Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/6e/6f83bf616d2becdf333a1640f1d463fef3150e2e926b7010cb0f81c95e88/attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", size = 60018, upload-time = "2022-12-21T09:48:49.401Z" }, ] [[package]] @@ -456,11 +456,11 @@ wheels = [ [[package]] name = "beartype" -version = "0.18.5" +version = "0.22.6" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/15/4e623478a9628ad4cee2391f19aba0b16c1dd6fedcb2a399f0928097b597/beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927", size = 1193506, upload-time = "2024-04-21T07:25:58.64Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/e2/105ceb1704cb80fe4ab3872529ab7b6f365cf7c74f725e6132d0efcf1560/beartype-0.22.6.tar.gz", hash = "sha256:97fbda69c20b48c5780ac2ca60ce3c1bb9af29b3a1a0216898ffabdd523e48f4", size = 1588975, upload-time = "2025-11-20T04:47:14.736Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/43/7a1259741bd989723272ac7d381a43be932422abcff09a1d9f7ba212cb74/beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089", size = 917762, upload-time = "2024-04-21T07:25:55.758Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/c9/ceecc71fe2c9495a1d8e08d44f5f31f5bca1350d5b2e27a4b6265424f59e/beartype-0.22.6-py3-none-any.whl", hash = "sha256:0584bc46a2ea2a871509679278cda992eadde676c01356ab0ac77421f3c9a093", size = 1324807, upload-time = "2025-11-20T04:47:11.837Z" }, ] [[package]] @@ -668,6 +668,19 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/ff/3f0982ecd37c2d6a7266c22e7ea2e47d0773fe449984184c5316459d2776/captcha-0.7.1-py3-none-any.whl", hash = "sha256:8b73b5aba841ad1e5bdb856205bf5f09560b728ee890eb9dae42901219c8c599", size = 147606, upload-time = "2025-03-01T05:00:10.433Z" }, ] +[[package]] +name = "cattrs" +version = "22.2.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/da/ff3239eb4241cbc6f8b69f53d4ca27a178d51f9e5a954f1a3588c8227dc5/cattrs-22.2.0.tar.gz", hash = "sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d", size = 30050, upload-time = "2022-10-03T11:00:37.889Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/3b/1d34fc4449174dfd2bc5ad7047a23edb6558b2e4b5a41b25a8ad6655c6c7/cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21", size = 35673, upload-time = "2022-10-03T11:00:36.109Z" }, +] + [[package]] name = "cbor" version = "1.0.0" @@ -2177,11 +2190,12 @@ wheels = [ [[package]] name = "graspologic" -version = "3.4.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +version = "0.1.dev847+g38e680cab" +source = { git = "https://github.com/yuzhichang/graspologic.git?rev=38e680cab72bc9fb68a7992c3bcc2d53b24e42fd#38e680cab72bc9fb68a7992c3bcc2d53b24e42fd" } dependencies = [ { name = "anytree" }, { name = "beartype" }, + { name = "future" }, { name = "gensim" }, { name = "graspologic-native" }, { name = "hyppo" }, @@ -2198,10 +2212,6 @@ dependencies = [ { name = "typing-extensions" }, { name = "umap-learn" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/de/83d653cc8029dc8c5f75bc5aea68f6b1e834230f05525fb3e7ac4aeae226/graspologic-3.4.1.tar.gz", hash = "sha256:7561f0b852a2bccd351bff77e8db07d9892f9dfa35a420fdec01690e4fdc8075", size = 5134018, upload-time = "2024-05-22T22:54:42.797Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/0b/9a167cec9cc4555b59cd282e8669998a50cb3f929a9a503965b24fa58a20/graspologic-3.4.1-py3-none-any.whl", hash = "sha256:c6563e087eda599bad1de831d4b7321c0daa7a82f4e85a7d7737ff67e07cdda2", size = 5200768, upload-time = "2024-05-22T22:54:39.259Z" }, -] [[package]] name = "graspologic-native" @@ -2557,17 +2567,22 @@ wheels = [ [[package]] name = "hyppo" -version = "0.4.0" +version = "0.5.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "autograd" }, + { name = "future" }, { name = "numba" }, { name = "numpy" }, + { name = "pandas" }, + { name = "patsy" }, { name = "scikit-learn" }, { name = "scipy" }, + { name = "statsmodels" }, ] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/a6/0d84fe8486a1447da8bdb8ebb249d525fd8c1d0fe038bceb003c6e0513f9/hyppo-0.5.2.tar.gz", hash = "sha256:4634d15516248a43d25c241ed18beeb79bb3210360f7253693b3f154fe8c9879", size = 125115, upload-time = "2025-05-24T18:33:27.418Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/87/7940713f929d0280cff1bde207479cb588a0d3a4dd49a0e2e69bfff46363/hyppo-0.4.0-py3-none-any.whl", hash = "sha256:4e75565b8deb601485cd7bc1b5c3f44e6ddf329136fc81e65d011f9b4e95132f", size = 146607, upload-time = "2023-05-24T13:50:04.441Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/c4/d46858cfac3c0aad314a1fc378beae5c8cac499b677650a34b5a6a3d4328/hyppo-0.5.2-py3-none-any.whl", hash = "sha256:5cc18f9e158fe2cf1804c9a1e979e807118ee89a303f29dc5cb8891d92d44ef3", size = 192272, upload-time = "2025-05-24T18:33:25.904Z" }, ] [[package]] @@ -3383,6 +3398,20 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, ] +[[package]] +name = "moodlepy" +version = "0.24.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "requests" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/83/d03072735fc822b225efefa7f7646a4ed6cd73d1c717a338871b9958ce5d/moodlepy-0.24.1.tar.gz", hash = "sha256:94d361e4da56748d29910e01979e4652a42220994112b4f07589f200cb7915e3", size = 82174, upload-time = "2024-10-11T11:53:45.433Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/6b/c526e3230e20171d7791f0fb2137aa4b49c82d9878959f089be1162a7c72/moodlepy-0.24.1-py3-none-any.whl", hash = "sha256:2809ece7a167d7ecc2a744cde188f0af66e1db58863e7a2ed77d1a0b08ff82e2", size = 153323, upload-time = "2024-10-11T11:53:44.149Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -5375,6 +5404,7 @@ dependencies = [ { name = "mini-racer" }, { name = "minio" }, { name = "mistralai" }, + { name = "moodlepy" }, { name = "mypy-boto3-s3" }, { name = "nltk" }, { name = "numpy" }, @@ -5478,7 +5508,7 @@ requires-dist = [ { name = "azure-identity", specifier = "==1.17.1" }, { name = "azure-storage-blob", specifier = "==12.22.0" }, { name = "azure-storage-file-datalake", specifier = "==12.16.0" }, - { name = "beartype", specifier = ">=0.18.5,<0.19.0" }, + { name = "beartype", specifier = ">=0.20.0,<1.0.0" }, { name = "bio", specifier = "==1.7.1" }, { name = "blinker", specifier = "==1.7.0" }, { name = "boto3", specifier = "==1.34.140" }, @@ -5514,7 +5544,7 @@ requires-dist = [ { name = "google-genai", specifier = ">=1.41.0,<2.0.0" }, { name = "google-generativeai", specifier = ">=0.8.1,<0.9.0" }, { name = "google-search-results", specifier = "==2.4.2" }, - { name = "graspologic", specifier = ">=3.4.1,<4.0.0" }, + { name = "graspologic", git = "https://github.com/yuzhichang/graspologic.git?rev=38e680cab72bc9fb68a7992c3bcc2d53b24e42fd" }, { name = "groq", specifier = "==0.9.0" }, { name = "hanziconv", specifier = "==0.3.2" }, { name = "html-text", specifier = "==0.6.2" }, @@ -5536,6 +5566,7 @@ requires-dist = [ { name = "mini-racer", specifier = ">=0.12.4,<0.13.0" }, { name = "minio", specifier = "==7.2.4" }, { name = "mistralai", specifier = "==0.4.2" }, + { name = "moodlepy", specifier = ">=0.23.0" }, { name = "mypy-boto3-s3", specifier = "==1.40.26" }, { name = "nltk", specifier = "==3.9.1" }, { name = "numpy", specifier = ">=1.26.0,<2.0.0" }, @@ -5598,7 +5629,7 @@ requires-dist = [ { name = "tencentcloud-sdk-python", specifier = "==3.0.1478" }, { name = "tika", specifier = "==2.6.0" }, { name = "tiktoken", specifier = "==0.7.0" }, - { name = "trio", specifier = ">=0.29.0" }, + { name = "trio", specifier = ">=0.17.0,<0.29.0" }, { name = "umap-learn", specifier = "==0.5.6" }, { name = "valkey", specifier = "==6.0.2" }, { name = "vertexai", specifier = "==1.70.0" }, @@ -6882,7 +6913,7 @@ wheels = [ [[package]] name = "trio" -version = "0.30.0" +version = "0.24.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "attrs" }, @@ -6893,9 +6924,9 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/f3/07c152213222c615fe2391b8e1fea0f5af83599219050a549c20fcbd9ba2/trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d", size = 545131, upload-time = "2024-01-10T03:29:21.671Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/fb/9299cf74953f473a15accfdbe2c15218e766bae8c796f2567c83bae03e98/trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c", size = 460205, upload-time = "2024-01-10T03:29:20.165Z" }, ] [[package]] diff --git a/web/src/assets/svg/data-source/moodle.svg b/web/src/assets/svg/data-source/moodle.svg new file mode 100644 index 000000000..d268572cd --- /dev/null +++ b/web/src/assets/svg/data-source/moodle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index e3cec6240..4b87792d7 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -47,6 +47,7 @@ export type SelectWithSearchFlagProps = { allowClear?: boolean; disabled?: boolean; placeholder?: string; + emptyData?: string; }; function findLabelWithoutOptions( @@ -78,6 +79,7 @@ export const SelectWithSearch = forwardRef< allowClear = false, disabled = false, placeholder = t('common.selectPlaceholder'), + emptyData = t('common.noDataFound'), }, ref, ) => { @@ -183,8 +185,8 @@ export const SelectWithSearch = forwardRef< className=" placeholder:text-text-disabled" /> )} - - {t('common.noDataFound')} + + {emptyData} {options.map((group, idx) => { if (group.options) { return ( @@ -196,6 +198,9 @@ export const SelectWithSearch = forwardRef< value={option.value} disabled={option.disabled} onSelect={handleSelect} + className={ + value === option.value ? 'bg-bg-card' : '' + } > {option.label} diff --git a/web/src/components/ui/label.tsx b/web/src/components/ui/label.tsx index 64a56584c..ce1e55565 100644 --- a/web/src/components/ui/label.tsx +++ b/web/src/components/ui/label.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; const labelVariants = cva( - 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-secondary', + 'text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-secondary', ); const Label = React.forwardRef< diff --git a/web/src/hooks/common-hooks.tsx b/web/src/hooks/common-hooks.tsx index 1d8ca4c42..7f7729ade 100644 --- a/web/src/hooks/common-hooks.tsx +++ b/web/src/hooks/common-hooks.tsx @@ -88,18 +88,18 @@ export const useShowDeleteConfirm = () => { ({ title, content, onOk, onCancel }: IProps): Promise => { return new Promise((resolve, reject) => { Modal.show({ - title: title ?? t('common.deleteModalTitle'), + // title: title ?? t('common.deleteModalTitle'), + closable: false, visible: true, onVisibleChange: () => { Modal.hide(); }, footer: null, - closable: true, maskClosable: false, okText: t('common.yes'), cancelText: t('common.no'), style: { - width: '400px', + width: '450px', }, okButtonClassName: 'bg-state-error text-white hover:bg-state-error hover:text-white', @@ -116,7 +116,14 @@ export const useShowDeleteConfirm = () => { onCancel?.(); Modal.hide(); }, - children: content, + children: ( +
+
+ {title ?? t('common.deleteModalTitle')} +
+
{content}
+
+ ), }); }); }, diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 18c7dbc4a..a7cf380cd 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -696,6 +696,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`, }, setting: { + modelEmptyTip: + 'No models available. Please add models from the panel on the right.', + sourceEmptyTip: 'No data sources added yet. Select one below to connect.', seconds: 'seconds', minutes: 'minutes', edit: 'Edit', @@ -716,7 +719,7 @@ Example: https://fsn1.your-objectstorage.com`, deleteSourceModalTitle: 'Delete data source', deleteSourceModalContent: `

Are you sure you want to delete this data source link?

`, - deleteSourceModalConfirmText: 'Comfirm', + deleteSourceModalConfirmText: 'Confirm', errorMsg: 'Error message', newDocs: 'New Docs', timeStarted: 'Time started', @@ -739,9 +742,15 @@ Example: https://fsn1.your-objectstorage.com`, google_drivePrimaryAdminTip: 'Email address that has access to the Drive content being synced.', google_driveMyDriveEmailsTip: - 'Comma-separated emails whose “My Drive” contents should be indexed (include the primary admin).', + 'Comma-separated emails whose "My Drive" contents should be indexed (include the primary admin).', google_driveSharedFoldersTip: 'Comma-separated Google Drive folder links to crawl.', + moodleDescription: + 'Connect to your Moodle LMS to sync course content, forums, and resources.', + moodleUrlTip: + 'The base URL of your Moodle instance (e.g., https://moodle.university.edu). Do not include /webservice or /login.', + moodleTokenTip: + 'Generate a web service token in Moodle: Go to Site administration → Server → Web services → Manage tokens. The user must be enrolled in the courses you want to sync.', jiraDescription: 'Connect your Jira workspace to sync issues, comments, and attachments.', jiraBaseUrlTip: diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 70a78c825..d2f4b1d16 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -685,6 +685,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 tocEnhanceTip: `解析文档时生成了目录信息(见General方法的‘启用目录抽取’),让大模型返回和用户问题相关的目录项,从而利用目录项拿到相关chunk,对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`, }, setting: { + modelEmptyTip: '暂无可用模型,请先在右侧面板添加模型。', + sourceEmptyTip: '暂未添加任何数据源,请从下方选择一个进行连接。', seconds: '秒', minutes: '分', edit: '编辑', diff --git a/web/src/pages/next-chats/chat/conversation-dropdown.tsx b/web/src/pages/next-chats/chat/conversation-dropdown.tsx index 83193f57c..27bed0a7e 100644 --- a/web/src/pages/next-chats/chat/conversation-dropdown.tsx +++ b/web/src/pages/next-chats/chat/conversation-dropdown.tsx @@ -14,16 +14,28 @@ import { useTranslation } from 'react-i18next'; export function ConversationDropdown({ children, conversation, + removeTemporaryConversation, }: PropsWithChildren & { conversation: IConversation; + removeTemporaryConversation?: (conversationId: string) => void; }) { const { t } = useTranslation(); const { removeConversation } = useRemoveConversation(); const handleDelete: MouseEventHandler = useCallback(() => { - removeConversation([conversation.id]); - }, [conversation.id, removeConversation]); + if (conversation.is_new && removeTemporaryConversation) { + removeTemporaryConversation(conversation.id); + removeConversation([]); + } else { + removeConversation([conversation.id]); + } + }, [ + conversation.id, + conversation.is_new, + removeConversation, + removeTemporaryConversation, + ]); return ( diff --git a/web/src/pages/next-chats/chat/sessions.tsx b/web/src/pages/next-chats/chat/sessions.tsx index 98b707374..15433b38d 100644 --- a/web/src/pages/next-chats/chat/sessions.tsx +++ b/web/src/pages/next-chats/chat/sessions.tsx @@ -29,6 +29,7 @@ export function Sessions({ const { list: conversationList, addTemporaryConversation, + removeTemporaryConversation, handleInputChange, searchString, } = useSelectDerivedConversationList(); @@ -97,7 +98,10 @@ export function Sessions({ >
{x.name}
- +
diff --git a/web/src/pages/next-chats/hooks/use-select-conversation-list.ts b/web/src/pages/next-chats/hooks/use-select-conversation-list.ts index a40c57a0c..a37d3078b 100644 --- a/web/src/pages/next-chats/hooks/use-select-conversation-list.ts +++ b/web/src/pages/next-chats/hooks/use-select-conversation-list.ts @@ -80,6 +80,14 @@ export const useSelectDerivedConversationList = () => { }); }, [conversationList, dialogId, prologue, t, setNewConversationRouteParams]); + const removeTemporaryConversation = useCallback((conversationId: string) => { + setList((prevList) => { + return prevList.filter( + (conversation) => conversation.id !== conversationId, + ); + }); + }, []); + // When you first enter the page, select the top conversation card useEffect(() => { @@ -89,6 +97,7 @@ export const useSelectDerivedConversationList = () => { return { list, addTemporaryConversation, + removeTemporaryConversation, loading, handleInputChange, searchString, diff --git a/web/src/pages/user-setting/components/user-setting-header/index.tsx b/web/src/pages/user-setting/components/user-setting-header/index.tsx index 931e2681d..d54e57769 100644 --- a/web/src/pages/user-setting/components/user-setting-header/index.tsx +++ b/web/src/pages/user-setting/components/user-setting-header/index.tsx @@ -10,7 +10,7 @@ export const UserSettingHeader = ({ }) => { return ( <> -
+
{name}
{description && (
{description}
diff --git a/web/src/pages/user-setting/data-source/contant.tsx b/web/src/pages/user-setting/data-source/contant.tsx index 2aed46b27..78cc63ff0 100644 --- a/web/src/pages/user-setting/data-source/contant.tsx +++ b/web/src/pages/user-setting/data-source/contant.tsx @@ -9,7 +9,8 @@ export enum DataSourceKey { NOTION = 'notion', DISCORD = 'discord', GOOGLE_DRIVE = 'google_drive', - // GMAIL = 'gmail', + MOODLE = 'moodle', + // GMAIL = 'gmail', JIRA = 'jira', WEBDAV = 'webdav', // SHAREPOINT = 'sharepoint', @@ -43,6 +44,11 @@ export const DataSourceInfo = { description: t(`setting.${DataSourceKey.GOOGLE_DRIVE}Description`), icon: , }, + [DataSourceKey.MOODLE]: { + name: 'Moodle', + description: t(`setting.${DataSourceKey.MOODLE}Description`), + icon: , + }, [DataSourceKey.JIRA]: { name: 'Jira', description: t(`setting.${DataSourceKey.JIRA}Description`), @@ -122,7 +128,7 @@ export const DataSourceFormFields = { required: false, placeholder: 'https://fsn1.your-objectstorage.com', tooltip: t('setting.S3CompatibleEndpointUrlTip'), - shouldRender: (formValues) => { + shouldRender: (formValues: any) => { return formValues?.config?.bucket_type === 's3_compatible'; }, }, @@ -293,6 +299,21 @@ export const DataSourceFormFields = { defaultValue: 'uploaded', }, ], + [DataSourceKey.MOODLE]: [ + { + label: 'Moodle URL', + name: 'config.moodle_url', + type: FormFieldType.Text, + required: true, + placeholder: 'https://moodle.example.com', + }, + { + label: 'API Token', + name: 'config.credentials.moodle_token', + type: FormFieldType.Password, + required: true, + }, + ], [DataSourceKey.JIRA]: [ { label: 'Jira Base URL', @@ -491,6 +512,16 @@ export const DataSourceFormDefaultValues = { }, }, }, + [DataSourceKey.MOODLE]: { + name: '', + source: DataSourceKey.MOODLE, + config: { + moodle_url: '', + credentials: { + moodle_token: '', + }, + }, + }, [DataSourceKey.JIRA]: { name: '', source: DataSourceKey.JIRA, diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx index 1962cf07c..fe54dda64 100644 --- a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx +++ b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx @@ -170,7 +170,7 @@ const SourceDetailPage = () => { - +
{ defaultValues={defaultValues} />
-
-
{t('setting.log')}
+
+
+ {t('setting.log')} +
diff --git a/web/src/pages/user-setting/data-source/index.tsx b/web/src/pages/user-setting/data-source/index.tsx index b6ec14a88..b3d1ac438 100644 --- a/web/src/pages/user-setting/data-source/index.tsx +++ b/web/src/pages/user-setting/data-source/index.tsx @@ -44,6 +44,12 @@ const dataSourceTemplates = [ description: DataSourceInfo[DataSourceKey.NOTION].description, icon: DataSourceInfo[DataSourceKey.NOTION].icon, }, + { + id: DataSourceKey.MOODLE, + name: DataSourceInfo[DataSourceKey.MOODLE].name, + description: DataSourceInfo[DataSourceKey.MOODLE].description, + icon: DataSourceInfo[DataSourceKey.MOODLE].icon, + }, { id: DataSourceKey.JIRA, name: DataSourceInfo[DataSourceKey.JIRA].name, @@ -120,6 +126,11 @@ const DataSource = () => {
+ {categorizedList?.length <= 0 && ( +
+ {t('setting.sourceEmptyTip')} +
+ )} {categorizedList.map((item, index) => ( ))} @@ -127,9 +138,9 @@ const DataSource = () => {
{/* */} - + {t('setting.availableSources')} -
+
{t('setting.availableSourcesDescription')}
diff --git a/web/src/pages/user-setting/setting-model/components/system-setting.tsx b/web/src/pages/user-setting/setting-model/components/system-setting.tsx index a8714b998..97906a421 100644 --- a/web/src/pages/user-setting/setting-model/components/system-setting.tsx +++ b/web/src/pages/user-setting/setting-model/components/system-setting.tsx @@ -161,6 +161,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => { options={options} onChange={(value) => handleFieldChange(id, value)} placeholder={t('selectModelPlaceholder')} + emptyData={t('modelEmptyTip')} />
); diff --git a/web/src/pages/user-setting/sidebar/hooks.tsx b/web/src/pages/user-setting/sidebar/hooks.tsx index fcfedcfca..47415a4ea 100644 --- a/web/src/pages/user-setting/sidebar/hooks.tsx +++ b/web/src/pages/user-setting/sidebar/hooks.tsx @@ -1,12 +1,19 @@ import { useLogout } from '@/hooks/login-hooks'; import { Routes } from '@/routes'; -import { useCallback, useState } from 'react'; -import { useNavigate } from 'umi'; +import { useCallback, useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'umi'; export const useHandleMenuClick = () => { const navigate = useNavigate(); const [active, setActive] = useState(); const { logout } = useLogout(); + const location = useLocation(); + useEffect(() => { + const path = (location.pathname.split('/')?.[2] || '') as Routes; + if (path) { + setActive(('/' + path) as Routes); + } + }, [location]); const handleMenuClick = useCallback( (key: Routes) => () => { @@ -20,5 +27,5 @@ export const useHandleMenuClick = () => { [logout, navigate], ); - return { handleMenuClick, active }; + return { handleMenuClick, active, setActive }; };