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 dd24b4ead..1c3404786 100644 --- a/common/constants.py +++ b/common/constants.py @@ -118,6 +118,7 @@ class FileSource(StrEnum): SHAREPOINT = "sharepoint" SLACK = "slack" TEAMS = "teams" + MOODLE = "moodle" class PipelineTaskType(StrEnum): diff --git a/common/data_source/__init__.py b/common/data_source/__init__.py index 611c3c61a..e6480e65f 100644 --- a/common/data_source/__init__.py +++ b/common/data_source/__init__.py @@ -14,6 +14,7 @@ from .google_drive.connector import GoogleDriveConnector from .jira.connector import JiraConnector from .sharepoint_connector import SharePointConnector from .teams_connector import TeamsConnector +from .moodle_connector import MoodleConnector from .config import BlobType, DocumentSource from .models import Document, TextSection, ImageSection, BasicExpertInfo from .exceptions import ( @@ -36,6 +37,7 @@ __all__ = [ "JiraConnector", "SharePointConnector", "TeamsConnector", + "MoodleConnector", "BlobType", "DocumentSource", "Document", diff --git a/common/data_source/config.py b/common/data_source/config.py index 6cd497527..0c038c6d7 100644 --- a/common/data_source/config.py +++ b/common/data_source/config.py @@ -48,6 +48,7 @@ class DocumentSource(str, Enum): GOOGLE_DRIVE = "google_drive" GMAIL = "gmail" DISCORD = "discord" + MOODLE = "moodle" S3_COMPATIBLE = "s3_compatible" 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 f8902d00f..4b03b0eb3 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", @@ -127,13 +127,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", @@ -148,6 +148,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 6925eb5f7..b29ad15de 100644 --- a/rag/svr/sync_data_source.py +++ b/rag/svr/sync_data_source.py @@ -37,14 +37,8 @@ from api.db.services.connector_service import ConnectorService, SyncLogsService from api.db.services.knowledgebase_service import KnowledgebaseService from common import settings from common.config_utils import show_configs +from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, MoodleConnector, JiraConnector from common.constants import FileSource, TaskStatus -from common.data_source import ( - BlobStorageConnector, - DiscordConnector, - GoogleDriveConnector, - JiraConnector, - NotionConnector, -) from common.data_source.config import INDEX_BATCH_SIZE from common.data_source.confluence_connector import ConfluenceConnector from common.data_source.interfaces import CheckpointOutputWrapper @@ -418,6 +412,37 @@ class Teams(SyncBase): pass +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 = { FileSource.S3: S3, FileSource.NOTION: Notion, @@ -429,6 +454,7 @@ func_factory = { FileSource.SHAREPOINT: SharePoint, FileSource.SLACK: Slack, FileSource.TEAMS: Teams, + FileSource.MOODLE: Moodle } diff --git a/uv.lock b/uv.lock index 6a6728adc..4d5747b2e 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" }, @@ -5477,7 +5507,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" }, @@ -5513,7 +5543,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" }, @@ -5535,6 +5565,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" }, @@ -5597,7 +5628,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" }, @@ -6880,7 +6911,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" }, @@ -6891,9 +6922,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/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index c74e4c381..ca0b08763 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -356,6 +356,13 @@ const DynamicForm = { ...combinedErrors, ...fieldErrors, } as any; + + console.log('combinedErrors', combinedErrors); + for (const key in combinedErrors) { + if (Array.isArray(combinedErrors[key])) { + combinedErrors[key] = combinedErrors[key][0]; + } + } console.log('combinedErrors', combinedErrors); return { values: Object.keys(combinedErrors).length ? {} : data, @@ -720,9 +727,7 @@ const DynamicForm = { type="button" disabled={submitLoading} onClick={() => { - console.log('form submit'); (async () => { - console.log('form submit2'); try { let beValid = await form.formControl.trigger(); console.log('form valid', beValid, form, form.formControl); 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 8a3488cee..44eff8144 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', @@ -736,9 +739,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: @@ -1046,7 +1055,7 @@ Example: https://fsn1.your-objectstorage.com`, downloadFileType: 'Download file type', formatTypeError: 'Format or type error', variableNameMessage: - 'Variable name can only contain letters and underscores', + 'Variable name can only contain letters and underscores and numbers', variableDescription: 'Variable Description', defaultValue: 'Default Value', conversationVariable: 'Conversation variable', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 375b2fadc..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: '编辑', @@ -980,7 +982,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 downloadFileTypeTip: '文件下载的类型', downloadFileType: '文件类型', formatTypeError: '格式或类型错误', - variableNameMessage: '名称只能包含字母和下划线', + variableNameMessage: '名称只能包含字母,数字和下划线', variableDescription: '变量的描述', defaultValue: '默认值', conversationVariable: '会话变量', diff --git a/web/src/pages/agent/gobal-variable-sheet/constant.ts b/web/src/pages/agent/gobal-variable-sheet/constant.ts index 935540c15..8470ffa86 100644 --- a/web/src/pages/agent/gobal-variable-sheet/constant.ts +++ b/web/src/pages/agent/gobal-variable-sheet/constant.ts @@ -18,7 +18,7 @@ export const GlobalFormFields = [ placeholder: t('common.namePlaceholder'), required: true, validation: { - pattern: /^[a-zA-Z_]+$/, + pattern: /^[a-zA-Z_0-9]+$/, message: t('flow.variableNameMessage'), }, type: FormFieldType.Text, diff --git a/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx b/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx index 7e60a7aec..c41e766f2 100644 --- a/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx +++ b/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx @@ -3,6 +3,7 @@ import { BlockButton, Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Segmented } from '@/components/ui/segmented'; import { t } from 'i18next'; +import { isEmpty } from 'lodash'; import { Trash2, X } from 'lucide-react'; import { useCallback } from 'react'; import { FieldValues } from 'react-hook-form'; @@ -36,14 +37,19 @@ export const useObjectFields = () => { path: (string | number)[] = [], ): Array<{ path: (string | number)[]; message: string }> => { const errors: Array<{ path: (string | number)[]; message: string }> = []; - - if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) { + if (typeof obj === 'object' && !Array.isArray(obj)) { + if (isEmpty(obj)) { + errors.push({ + path: [...path], + message: 'No empty parameters are allowed.', + }); + } for (const key in obj) { if (obj.hasOwnProperty(key)) { - if (!/^[a-zA-Z_]+$/.test(key)) { + if (!/^[a-zA-Z_0-9]+$/.test(key)) { errors.push({ path: [...path, key], - message: `Key "${key}" is invalid. Keys can only contain letters and underscores.`, + message: `Key "${key}" is invalid. Keys can only contain letters and underscores and numbers.`, }); } const nestedErrors = validateKeys(obj[key], [...path, key]); @@ -108,6 +114,21 @@ export const useObjectFields = () => { } }, []); + const arrayObjectValidate = useCallback((value: any) => { + try { + if (validateKeys(value, [])?.length > 0) { + throw new Error(t('flow.formatTypeError')); + } + if (value && typeof value === 'string' && !JSON.parse(value)) { + throw new Error(t('flow.formatTypeError')); + } + return true; + } catch (e) { + console.log('object-render-error', e, value); + throw new Error(t('flow.formatTypeError')); + } + }, []); + const arrayStringRender = useCallback((field: FieldValues, type = 'text') => { const values = Array.isArray(field.value) ? field.value @@ -253,8 +274,9 @@ export const useObjectFields = () => { const handleCustomValidate = (value: TypesWithArray) => { switch (value) { case TypesWithArray.Object: - case TypesWithArray.ArrayObject: return objectValidate; + case TypesWithArray.ArrayObject: + return arrayObjectValidate; case TypesWithArray.ArrayString: return arrayStringValidate; case TypesWithArray.ArrayNumber: @@ -284,6 +306,7 @@ export const useObjectFields = () => { return { objectRender, objectValidate, + arrayObjectValidate, arrayStringRender, arrayStringValidate, arrayNumberRender, 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 a1f0241fa..cc45ad869 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', // SHAREPOINT = 'sharepoint', // SLACK = 'slack', @@ -42,6 +43,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`), @@ -116,7 +122,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'; }, }, @@ -287,6 +293,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', @@ -456,6 +477,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 9cb58672a..2ba7cecd0 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, @@ -114,6 +120,11 @@ const DataSource = () => {
+ {categorizedList?.length <= 0 && ( +
+ {t('setting.sourceEmptyTip')} +
+ )} {categorizedList.map((item, index) => ( ))} @@ -121,9 +132,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 }; };