solve merge conflicts and fix linter errors

This commit is contained in:
Jonah879 2025-11-24 08:32:47 +00:00
commit 30fc8f08cd
26 changed files with 612 additions and 63 deletions

View file

@ -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"]

View file

@ -119,6 +119,7 @@ class FileSource(StrEnum):
SLACK = "slack"
TEAMS = "teams"
WEBDAV = "webdav"
MOODLE = "moodle"
class PipelineTaskType(StrEnum):

View file

@ -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",

View file

@ -50,6 +50,8 @@ class DocumentSource(str, Enum):
GMAIL = "gmail"
DISCORD = "discord"
WEBDAV = "webdav"
MOODLE = "moodle"
S3_COMPATIBLE = "s3_compatible"
class FileOrigin(str, Enum):

View file

@ -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),
)

View file

@ -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:

View file

@ -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;
}

View file

@ -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",
]

View file

@ -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 = {}):

View file

@ -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
}

71
uv.lock generated
View file

@ -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]]

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1230.87 315.18">
<path fill="#f98012" d="M289.61 309.77V201.51q0-33.94-28-33.95t-28.06 33.95v108.26H178.4V201.51q0-33.94-27.57-33.95-28.05 0-28 33.95v108.26H67.67V195.12q0-35.43 24.6-53.63 21.66-16.25 58.56-16.25 37.41 0 55.12 19.19 15.26-19.19 55.62-19.19 36.9 0 58.54 16.25 24.6 18.19 24.61 53.63v114.65Zm675.49-.5V0h55.16v309.27Zm-70.3 0v-18.22q-7.39 9.84-25.11 15.76a92.81 92.81 0 0 1-30.05 5.41q-39.4 0-63.28-27.09t-23.89-67c0-26.25 7.76-48.3 23.4-66 13.85-15.65 36.35-26.59 62.29-26.59 29.22 0 46.28 11 56.64 23.63V0h53.68v309.27Zm0-102.92q0-14.78-14-28.33T852 164.47q-21.16 0-33.48 17.24-10.85 15.3-10.84 37.43 0 21.68 10.84 36.94 12.3 17.75 33.48 17.73 12.81 0 27.83-12.07t15-24.86ZM648.57 314.19q-41.87 0-69.19-26.59T552 219.14q0-41.83 27.34-68.45t69.19-26.59q41.85 0 69.44 26.59t27.58 68.45q0 41.88-27.58 68.46t-69.4 26.59Zm0-145.77q-19.94 0-30.65 15.1t-10.71 35.88q0 20.78 10 35.13 11.46 16.34 31.4 16.32T680 254.53q10.46-14.34 10.46-35.13t-10-35.13q-11.46-15.86-31.89-15.85ZM449.13 314.19q-41.86 0-69.2-26.59t-27.33-68.46q0-41.83 27.33-68.45t69.2-26.59q41.83 0 69.44 26.59t27.57 68.45q0 41.88-27.57 68.46t-69.44 26.59Zm0-145.77q-19.94 0-30.66 15.1t-10.71 35.88q0 20.78 10 35.13 11.46 16.34 31.41 16.32t31.39-16.32Q491 240.19 491 219.4t-10-35.13q-11.44-15.86-31.87-15.85Zm636.45 67.47c1.18 13.13 18.25 41.37 46.31 41.37 27.31 0 40.23-15.77 40.87-22.16l58.11-.5c-6.34 19.39-32.1 60.58-100 60.58-28.24 0-54.08-8.79-72.64-26.35s-27.82-40.45-27.82-68.7q0-43.83 27.82-69.68t72.16-25.85q48.25 0 75.34 32 25.13 29.53 25.12 79.28Zm90.13-34c-2.3-11.83-7.23-21.49-14.77-29.06q-12.82-12.3-29.55-12.31-17.25 0-28.82 11.82t-15.5 29.55Z"/>
<path fill="#333" d="m174.74 116.9 54.74-40-.7-2.44C130 86.57 85.08 95.15 0 144.47l.79 2.24 6.76.07c-.62 6.81-1.7 23.64-.32 48.95-9.44 27.32-.24 45.88 8.4 66.07 1.37-21 1.23-44-5.22-66.89-1.35-25.14-.24-41.67.37-48.1l56.4.54a258 258 0 0 0 1.67 33.06c50.4 17.71 101.09-.06 128-43.72-7.47-8.37-22.11-19.79-22.11-19.79Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -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"
/>
)}
<CommandList className="mt-2">
<CommandEmpty>{t('common.noDataFound')}</CommandEmpty>
<CommandList className="mt-2 outline-none">
<CommandEmpty>{emptyData}</CommandEmpty>
{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' : ''
}
>
<span className="leading-none">{option.label}</span>

View file

@ -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<

View file

@ -88,18 +88,18 @@ export const useShowDeleteConfirm = () => {
({ title, content, onOk, onCancel }: IProps): Promise<number> => {
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: (
<div className="flex flex-col justify-start items-start mt-3">
<div className="text-lg font-medium">
{title ?? t('common.deleteModalTitle')}
</div>
<div className="text-base font-normal">{content}</div>
</div>
),
});
});
},

View file

@ -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: `
<p>Are you sure you want to delete this data source link?</p>`,
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:

View file

@ -685,6 +685,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tocEnhanceTip: `解析文档时生成了目录信息见General方法的启用目录抽取让大模型返回和用户问题相关的目录项从而利用目录项拿到相关chunk对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`,
},
setting: {
modelEmptyTip: '暂无可用模型,请先在右侧面板添加模型。',
sourceEmptyTip: '暂未添加任何数据源,请从下方选择一个进行连接。',
seconds: '秒',
minutes: '分',
edit: '编辑',

View file

@ -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<HTMLDivElement> = 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 (
<DropdownMenu>

View file

@ -29,6 +29,7 @@ export function Sessions({
const {
list: conversationList,
addTemporaryConversation,
removeTemporaryConversation,
handleInputChange,
searchString,
} = useSelectDerivedConversationList();
@ -97,7 +98,10 @@ export function Sessions({
>
<CardContent className="px-3 py-2 flex justify-between items-center group gap-1">
<div className="truncate">{x.name}</div>
<ConversationDropdown conversation={x}>
<ConversationDropdown
conversation={x}
removeTemporaryConversation={removeTemporaryConversation}
>
<MoreButton></MoreButton>
</ConversationDropdown>
</CardContent>

View file

@ -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,

View file

@ -10,7 +10,7 @@ export const UserSettingHeader = ({
}) => {
return (
<>
<header className="flex flex-col gap-1 justify-between items-start p-0">
<header className="flex flex-col gap-1.5 justify-between items-start p-0">
<div className="text-2xl font-medium text-text-primary">{name}</div>
{description && (
<div className="text-sm text-text-secondary ">{description}</div>

View file

@ -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: <SvgIcon name={'data-source/google-drive'} width={38} />,
},
[DataSourceKey.MOODLE]: {
name: 'Moodle',
description: t(`setting.${DataSourceKey.MOODLE}Description`),
icon: <SvgIcon name={'data-source/moodle'} width={38} />,
},
[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,

View file

@ -170,7 +170,7 @@ const SourceDetailPage = () => {
</CardTitle>
</CardHeader>
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
<CardContent className="p-2 flex flex-col gap-2 max-h-[calc(100vh-190px)] overflow-y-auto scrollbar-auto">
<CardContent className="p-2 flex flex-col gap-10 max-h-[calc(100vh-190px)] overflow-y-auto scrollbar-auto">
<div className="max-w-[1200px]">
<DynamicForm.Root
ref={formRef}
@ -181,8 +181,10 @@ const SourceDetailPage = () => {
defaultValues={defaultValues}
/>
</div>
<section className="flex flex-col gap-2 mt-6">
<div className="text-2xl text-text-primary">{t('setting.log')}</div>
<section className="flex flex-col gap-2">
<div className="text-2xl text-text-primary mb-2">
{t('setting.log')}
</div>
<DataSourceLogsTable refresh_freq={detail?.refresh_freq || false} />
</section>
</CardContent>

View file

@ -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 = () => {
<div className="relative">
<div className=" flex flex-col gap-4 max-h-[calc(100vh-230px)] overflow-y-auto overflow-x-hidden scrollbar-auto">
<div className="flex flex-col gap-3">
{categorizedList?.length <= 0 && (
<div className="text-text-secondary w-full flex justify-center items-center h-20">
{t('setting.sourceEmptyTip')}
</div>
)}
{categorizedList.map((item, index) => (
<AddedSourceCard key={index} {...item} />
))}
@ -127,9 +138,9 @@ const DataSource = () => {
<section className="bg-transparent border-none mt-8">
<header className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-2xl font-semibold">
<CardTitle className="text-2xl font-semibold ">
{t('setting.availableSources')}
<div className="text-sm text-text-secondary font-normal">
<div className="text-sm text-text-secondary font-normal mt-1.5">
{t('setting.availableSourcesDescription')}
</div>
</CardTitle>

View file

@ -161,6 +161,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
options={options}
onChange={(value) => handleFieldChange(id, value)}
placeholder={t('selectModelPlaceholder')}
emptyData={t('modelEmptyTip')}
/>
</div>
);

View file

@ -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<Routes>();
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 };
};