mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-04 09:35:06 +08:00
Compare commits
11 Commits
e8cb1d8fc4
...
43ea312144
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ea312144 | |||
| ce05696d95 | |||
| 0f62bfda21 | |||
| 70ffe2b4e8 | |||
| e76db6e222 | |||
| 7b664b5a84 | |||
| 8a41057236 | |||
| 447041d265 | |||
| f0375c4acd | |||
| 8af769de41 | |||
| f808bc32ba |
@ -135,7 +135,7 @@ releases! 🌟
|
|||||||
## 🔎 System Architecture
|
## 🔎 System Architecture
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 Get Started
|
## 🎬 Get Started
|
||||||
|
|||||||
@ -129,7 +129,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
|
|||||||
## 🔎 Arsitektur Sistem
|
## 🔎 Arsitektur Sistem
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 Mulai
|
## 🎬 Mulai
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
## 🔎 システム構成
|
## 🔎 システム構成
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 初期設定
|
## 🎬 初期設定
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
## 🔎 시스템 아키텍처
|
## 🔎 시스템 아키텍처
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 시작하기
|
## 🎬 시작하기
|
||||||
|
|||||||
@ -129,7 +129,7 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
|
|||||||
## 🔎 Arquitetura do Sistema
|
## 🔎 Arquitetura do Sistema
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 Primeiros Passos
|
## 🎬 Primeiros Passos
|
||||||
|
|||||||
@ -132,7 +132,7 @@
|
|||||||
## 🔎 系統架構
|
## 🔎 系統架構
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 快速開始
|
## 🎬 快速開始
|
||||||
|
|||||||
@ -132,7 +132,7 @@
|
|||||||
## 🔎 系统架构
|
## 🔎 系统架构
|
||||||
|
|
||||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/d6ac5664-c237-4200-a7c2-a4a00691b485" width="1000"/>
|
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🎬 快速开始
|
## 🎬 快速开始
|
||||||
|
|||||||
@ -48,7 +48,7 @@ It consists of a server-side Service and a command-line client (CLI), both imple
|
|||||||
1. Ensure the Admin Service is running.
|
1. Ensure the Admin Service is running.
|
||||||
2. Install ragflow-cli.
|
2. Install ragflow-cli.
|
||||||
```bash
|
```bash
|
||||||
pip install ragflow-cli
|
pip install ragflow-cli==0.21.0
|
||||||
```
|
```
|
||||||
3. Launch the CLI client:
|
3. Launch the CLI client:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -60,7 +60,7 @@ def list_chunk():
|
|||||||
}
|
}
|
||||||
if "available_int" in req:
|
if "available_int" in req:
|
||||||
query["available_int"] = int(req["available_int"])
|
query["available_int"] = int(req["available_int"])
|
||||||
sres = settings.retriever.search(query, search.index_name(tenant_id), kb_ids, highlight=True)
|
sres = settings.retriever.search(query, search.index_name(tenant_id), kb_ids, highlight=["content_ltks"])
|
||||||
res = {"total": sres.total, "chunks": [], "doc": doc.to_dict()}
|
res = {"total": sres.total, "chunks": [], "doc": doc.to_dict()}
|
||||||
for id in sres.ids:
|
for id in sres.ids:
|
||||||
d = {
|
d = {
|
||||||
|
|||||||
@ -15,11 +15,14 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import string
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import redirect, request, session
|
from flask import redirect, request, session, Response
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
@ -46,6 +49,19 @@ from api.utils.api_utils import (
|
|||||||
validate_request,
|
validate_request,
|
||||||
)
|
)
|
||||||
from api.utils.crypt import decrypt
|
from api.utils.crypt import decrypt
|
||||||
|
from rag.utils.redis_conn import REDIS_CONN
|
||||||
|
from api.apps import smtp_mail_server
|
||||||
|
from api.utils.web_utils import (
|
||||||
|
send_email_html,
|
||||||
|
OTP_LENGTH,
|
||||||
|
OTP_TTL_SECONDS,
|
||||||
|
ATTEMPT_LIMIT,
|
||||||
|
ATTEMPT_LOCK_SECONDS,
|
||||||
|
RESEND_COOLDOWN_SECONDS,
|
||||||
|
otp_keys,
|
||||||
|
hash_code,
|
||||||
|
captcha_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/login", methods=["POST", "GET"]) # noqa: F821
|
@manager.route("/login", methods=["POST", "GET"]) # noqa: F821
|
||||||
@ -825,3 +841,170 @@ def set_tenant_info():
|
|||||||
return get_json_result(data=True)
|
return get_json_result(data=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.route("/forget/captcha", methods=["GET"]) # noqa: F821
|
||||||
|
def forget_get_captcha():
|
||||||
|
"""
|
||||||
|
GET /forget/captcha?email=<email>
|
||||||
|
- Generate an image captcha and cache it in Redis under key captcha:{email} with TTL = OTP_TTL_SECONDS.
|
||||||
|
- Returns the captcha as a PNG image.
|
||||||
|
"""
|
||||||
|
email = (request.args.get("email") or "")
|
||||||
|
if not email:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.ARGUMENT_ERROR, message="email is required")
|
||||||
|
|
||||||
|
users = UserService.query(email=email)
|
||||||
|
if not users:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.DATA_ERROR, message="invalid email")
|
||||||
|
|
||||||
|
# Generate captcha text
|
||||||
|
allowed = string.ascii_uppercase + string.digits
|
||||||
|
captcha_text = "".join(secrets.choice(allowed) for _ in range(OTP_LENGTH))
|
||||||
|
REDIS_CONN.set(captcha_key(email), captcha_text, 60) # Valid for 60 seconds
|
||||||
|
|
||||||
|
from captcha.image import ImageCaptcha
|
||||||
|
image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70])
|
||||||
|
img_bytes = image.generate(captcha_text).read()
|
||||||
|
return Response(img_bytes, mimetype="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
@manager.route("/forget/otp", methods=["POST"]) # noqa: F821
|
||||||
|
def forget_send_otp():
|
||||||
|
"""
|
||||||
|
POST /forget/otp
|
||||||
|
- Verify the image captcha stored at captcha:{email} (case-insensitive).
|
||||||
|
- On success, generate an email OTP (A–Z with length = OTP_LENGTH), store hash + salt (and timestamp) in Redis with TTL, reset attempts and cooldown, and send the OTP via email.
|
||||||
|
"""
|
||||||
|
req = request.get_json()
|
||||||
|
email = req.get("email") or ""
|
||||||
|
captcha = (req.get("captcha") or "").strip()
|
||||||
|
|
||||||
|
if not email or not captcha:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.ARGUMENT_ERROR, message="email and captcha required")
|
||||||
|
|
||||||
|
users = UserService.query(email=email)
|
||||||
|
if not users:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.DATA_ERROR, message="invalid email")
|
||||||
|
|
||||||
|
stored_captcha = REDIS_CONN.get(captcha_key(email))
|
||||||
|
if not stored_captcha:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.NOT_EFFECTIVE, message="invalid or expired captcha")
|
||||||
|
if (stored_captcha or "").strip().lower() != captcha.lower():
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.AUTHENTICATION_ERROR, message="invalid or expired captcha")
|
||||||
|
|
||||||
|
# Delete captcha to prevent reuse
|
||||||
|
REDIS_CONN.delete(captcha_key(email))
|
||||||
|
|
||||||
|
k_code, k_attempts, k_last, k_lock = otp_keys(email)
|
||||||
|
now = int(time.time())
|
||||||
|
last_ts = REDIS_CONN.get(k_last)
|
||||||
|
if last_ts:
|
||||||
|
try:
|
||||||
|
elapsed = now - int(last_ts)
|
||||||
|
except Exception:
|
||||||
|
elapsed = RESEND_COOLDOWN_SECONDS
|
||||||
|
remaining = RESEND_COOLDOWN_SECONDS - elapsed
|
||||||
|
if remaining > 0:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.NOT_EFFECTIVE, message=f"you still have to wait {remaining} seconds")
|
||||||
|
|
||||||
|
# Generate OTP (uppercase letters only) and store hashed
|
||||||
|
otp = "".join(secrets.choice(string.ascii_uppercase) for _ in range(OTP_LENGTH))
|
||||||
|
salt = os.urandom(16)
|
||||||
|
code_hash = hash_code(otp, salt)
|
||||||
|
REDIS_CONN.set(k_code, f"{code_hash}:{salt.hex()}", OTP_TTL_SECONDS)
|
||||||
|
REDIS_CONN.set(k_attempts, 0, OTP_TTL_SECONDS)
|
||||||
|
REDIS_CONN.set(k_last, now, OTP_TTL_SECONDS)
|
||||||
|
REDIS_CONN.delete(k_lock)
|
||||||
|
|
||||||
|
ttl_min = OTP_TTL_SECONDS // 60
|
||||||
|
|
||||||
|
if not smtp_mail_server:
|
||||||
|
logging.warning("SMTP mail server not initialized; skip sending email.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
send_email_html(
|
||||||
|
subject="Your Password Reset Code",
|
||||||
|
to_email=email,
|
||||||
|
template_key="reset_code",
|
||||||
|
code=otp,
|
||||||
|
ttl_min=ttl_min,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.SERVER_ERROR, message="failed to send email")
|
||||||
|
|
||||||
|
return get_json_result(data=True, code=settings.RetCode.SUCCESS, message="verification passed, email sent")
|
||||||
|
|
||||||
|
|
||||||
|
@manager.route("/forget", methods=["POST"]) # noqa: F821
|
||||||
|
def forget():
|
||||||
|
"""
|
||||||
|
POST: Verify email + OTP and reset password, then log the user in.
|
||||||
|
Request JSON: { email, otp, new_password, confirm_new_password }
|
||||||
|
"""
|
||||||
|
req = request.get_json()
|
||||||
|
email = req.get("email") or ""
|
||||||
|
otp = (req.get("otp") or "").strip()
|
||||||
|
new_pwd = req.get("new_password")
|
||||||
|
new_pwd2 = req.get("confirm_new_password")
|
||||||
|
|
||||||
|
if not all([email, otp, new_pwd, new_pwd2]):
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.ARGUMENT_ERROR, message="email, otp and passwords are required")
|
||||||
|
|
||||||
|
# For reset, passwords are provided as-is (no decrypt needed)
|
||||||
|
if new_pwd != new_pwd2:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.ARGUMENT_ERROR, message="passwords do not match")
|
||||||
|
|
||||||
|
users = UserService.query(email=email)
|
||||||
|
if not users:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.DATA_ERROR, message="invalid email")
|
||||||
|
|
||||||
|
user = users[0]
|
||||||
|
# Verify OTP from Redis
|
||||||
|
k_code, k_attempts, k_last, k_lock = otp_keys(email)
|
||||||
|
if REDIS_CONN.get(k_lock):
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.NOT_EFFECTIVE, message="too many attempts, try later")
|
||||||
|
|
||||||
|
stored = REDIS_CONN.get(k_code)
|
||||||
|
if not stored:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.NOT_EFFECTIVE, message="expired otp")
|
||||||
|
|
||||||
|
try:
|
||||||
|
stored_hash, salt_hex = str(stored).split(":", 1)
|
||||||
|
salt = bytes.fromhex(salt_hex)
|
||||||
|
except Exception:
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.EXCEPTION_ERROR, message="otp storage corrupted")
|
||||||
|
|
||||||
|
# Case-insensitive verification: OTP generated uppercase
|
||||||
|
calc = hash_code(otp.upper(), salt)
|
||||||
|
if calc != stored_hash:
|
||||||
|
# bump attempts
|
||||||
|
try:
|
||||||
|
attempts = int(REDIS_CONN.get(k_attempts) or 0) + 1
|
||||||
|
except Exception:
|
||||||
|
attempts = 1
|
||||||
|
REDIS_CONN.set(k_attempts, attempts, OTP_TTL_SECONDS)
|
||||||
|
if attempts >= ATTEMPT_LIMIT:
|
||||||
|
REDIS_CONN.set(k_lock, int(time.time()), ATTEMPT_LOCK_SECONDS)
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.AUTHENTICATION_ERROR, message="expired otp")
|
||||||
|
|
||||||
|
# Success: consume OTP and reset password
|
||||||
|
REDIS_CONN.delete(k_code)
|
||||||
|
REDIS_CONN.delete(k_attempts)
|
||||||
|
REDIS_CONN.delete(k_last)
|
||||||
|
REDIS_CONN.delete(k_lock)
|
||||||
|
|
||||||
|
try:
|
||||||
|
UserService.update_user_password(user.id, new_pwd)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return get_json_result(data=False, code=settings.RetCode.EXCEPTION_ERROR, message="failed to reset password")
|
||||||
|
|
||||||
|
# Auto login (reuse login flow)
|
||||||
|
user.access_token = get_uuid()
|
||||||
|
login_user(user)
|
||||||
|
user.update_time = (current_timestamp(),)
|
||||||
|
user.update_date = (datetime_format(datetime.now()),)
|
||||||
|
user.save()
|
||||||
|
msg = "Password reset successful. Logged in."
|
||||||
|
return construct_response(data=user.to_json(), auth=user.get_id(), message=msg)
|
||||||
|
|||||||
@ -313,9 +313,75 @@ class RetryingPooledMySQLDatabase(PooledMySQLDatabase):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class RetryingPooledPostgresqlDatabase(PooledPostgresqlDatabase):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.max_retries = kwargs.pop("max_retries", 5)
|
||||||
|
self.retry_delay = kwargs.pop("retry_delay", 1)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def execute_sql(self, sql, params=None, commit=True):
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
|
try:
|
||||||
|
return super().execute_sql(sql, params, commit)
|
||||||
|
except (OperationalError, InterfaceError) as e:
|
||||||
|
# PostgreSQL specific error codes
|
||||||
|
# 57P01: admin_shutdown
|
||||||
|
# 57P02: crash_shutdown
|
||||||
|
# 57P03: cannot_connect_now
|
||||||
|
# 08006: connection_failure
|
||||||
|
# 08003: connection_does_not_exist
|
||||||
|
# 08000: connection_exception
|
||||||
|
error_messages = ['connection', 'server closed', 'connection refused',
|
||||||
|
'no connection to the server', 'terminating connection']
|
||||||
|
|
||||||
|
should_retry = any(msg in str(e).lower() for msg in error_messages)
|
||||||
|
|
||||||
|
if should_retry and attempt < self.max_retries:
|
||||||
|
logging.warning(
|
||||||
|
f"PostgreSQL connection issue (attempt {attempt+1}/{self.max_retries}): {e}"
|
||||||
|
)
|
||||||
|
self._handle_connection_loss()
|
||||||
|
time.sleep(self.retry_delay * (2 ** attempt))
|
||||||
|
else:
|
||||||
|
logging.error(f"PostgreSQL execution failure: {e}")
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_connection_loss(self):
|
||||||
|
try:
|
||||||
|
self.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to reconnect to PostgreSQL: {e}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def begin(self):
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
|
try:
|
||||||
|
return super().begin()
|
||||||
|
except (OperationalError, InterfaceError) as e:
|
||||||
|
error_messages = ['connection', 'server closed', 'connection refused',
|
||||||
|
'no connection to the server', 'terminating connection']
|
||||||
|
|
||||||
|
should_retry = any(msg in str(e).lower() for msg in error_messages)
|
||||||
|
|
||||||
|
if should_retry and attempt < self.max_retries:
|
||||||
|
logging.warning(
|
||||||
|
f"PostgreSQL connection lost during transaction (attempt {attempt+1}/{self.max_retries})"
|
||||||
|
)
|
||||||
|
self._handle_connection_loss()
|
||||||
|
time.sleep(self.retry_delay * (2 ** attempt))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class PooledDatabase(Enum):
|
class PooledDatabase(Enum):
|
||||||
MYSQL = RetryingPooledMySQLDatabase
|
MYSQL = RetryingPooledMySQLDatabase
|
||||||
POSTGRES = PooledPostgresqlDatabase
|
POSTGRES = RetryingPooledPostgresqlDatabase
|
||||||
|
|
||||||
|
|
||||||
class DatabaseMigrator(Enum):
|
class DatabaseMigrator(Enum):
|
||||||
|
|||||||
25
api/utils/email_templates.py
Normal file
25
api/utils/email_templates.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Reusable HTML email templates and registry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Invitation email template
|
||||||
|
INVITE_EMAIL_TMPL = """
|
||||||
|
<p>Hi {{email}},</p>
|
||||||
|
<p>{{inviter}} has invited you to join their team (ID: {{tenant_id}}).</p>
|
||||||
|
<p>Click the link below to complete your registration:<br>
|
||||||
|
<a href="{{invite_url}}">{{invite_url}}</a></p>
|
||||||
|
<p>If you did not request this, please ignore this email.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Password reset code template
|
||||||
|
RESET_CODE_EMAIL_TMPL = """
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>Your password reset code is: <b>{{ code }}</b></p>
|
||||||
|
<p>This code will expire in {{ ttl_min }} minutes.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Template registry
|
||||||
|
EMAIL_TEMPLATES = {
|
||||||
|
"invite": INVITE_EMAIL_TMPL,
|
||||||
|
"reset_code": RESET_CODE_EMAIL_TMPL,
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ from urllib.parse import urlparse
|
|||||||
from api.apps import smtp_mail_server
|
from api.apps import smtp_mail_server
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from flask import render_template_string
|
from flask import render_template_string
|
||||||
|
from api.utils.email_templates import EMAIL_TEMPLATES
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.common.exceptions import TimeoutException
|
from selenium.common.exceptions import TimeoutException
|
||||||
from selenium.webdriver.chrome.options import Options
|
from selenium.webdriver.chrome.options import Options
|
||||||
@ -34,6 +35,12 @@ from selenium.webdriver.support.ui import WebDriverWait
|
|||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
|
||||||
|
|
||||||
|
OTP_LENGTH = 8
|
||||||
|
OTP_TTL_SECONDS = 5 * 60
|
||||||
|
ATTEMPT_LIMIT = 5
|
||||||
|
ATTEMPT_LOCK_SECONDS = 30 * 60
|
||||||
|
RESEND_COOLDOWN_SECONDS = 60
|
||||||
|
|
||||||
|
|
||||||
CONTENT_TYPE_MAP = {
|
CONTENT_TYPE_MAP = {
|
||||||
# Office
|
# Office
|
||||||
@ -178,24 +185,49 @@ def get_float(req: dict, key: str, default: float | int = 10.0) -> float:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
INVITE_EMAIL_TMPL = """
|
def send_email_html(subject: str, to_email: str, template_key: str, **context):
|
||||||
<p>Hi {{email}},</p>
|
"""Generic HTML email sender using shared templates.
|
||||||
<p>{{inviter}} has invited you to join their team (ID: {{tenant_id}}).</p>
|
template_key must exist in EMAIL_TEMPLATES.
|
||||||
<p>Click the link below to complete your registration:<br>
|
"""
|
||||||
<a href="{{invite_url}}">{{invite_url}}</a></p>
|
from api.apps import app
|
||||||
<p>If you did not request this, please ignore this email.</p>
|
tmpl = EMAIL_TEMPLATES.get(template_key)
|
||||||
"""
|
if not tmpl:
|
||||||
|
raise ValueError(f"Unknown email template: {template_key}")
|
||||||
|
with app.app_context():
|
||||||
|
msg = Message(subject=subject, recipients=[to_email])
|
||||||
|
msg.html = render_template_string(tmpl, **context)
|
||||||
|
smtp_mail_server.send(msg)
|
||||||
|
|
||||||
|
|
||||||
def send_invite_email(to_email, invite_url, tenant_id, inviter):
|
def send_invite_email(to_email, invite_url, tenant_id, inviter):
|
||||||
from api.apps import app
|
# Reuse the generic HTML sender with 'invite' template
|
||||||
with app.app_context():
|
send_email_html(
|
||||||
msg = Message(subject="RAGFlow Invitation",
|
subject="RAGFlow Invitation",
|
||||||
recipients=[to_email])
|
to_email=to_email,
|
||||||
msg.html = render_template_string(
|
template_key="invite",
|
||||||
INVITE_EMAIL_TMPL,
|
email=to_email,
|
||||||
email=to_email,
|
invite_url=invite_url,
|
||||||
invite_url=invite_url,
|
tenant_id=tenant_id,
|
||||||
tenant_id=tenant_id,
|
inviter=inviter,
|
||||||
inviter=inviter,
|
)
|
||||||
)
|
|
||||||
smtp_mail_server.send(msg)
|
|
||||||
|
def otp_keys(email: str):
|
||||||
|
email = (email or "").strip().lower()
|
||||||
|
return (
|
||||||
|
f"otp:{email}",
|
||||||
|
f"otp_attempts:{email}",
|
||||||
|
f"otp_last_sent:{email}",
|
||||||
|
f"otp_lock:{email}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_code(code: str, salt: bytes) -> str:
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
return hmac.new(salt, (code or "").encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def captcha_key(email: str) -> str:
|
||||||
|
return f"captcha:{email}"
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,6 @@
|
|||||||
"entities_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"entities_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
"pagerank_fea": {"type": "integer", "default": 0},
|
"pagerank_fea": {"type": "integer", "default": 0},
|
||||||
"tag_feas": {"type": "varchar", "default": "", "analyzer": "rankfeatures"},
|
"tag_feas": {"type": "varchar", "default": "", "analyzer": "rankfeatures"},
|
||||||
|
|
||||||
"from_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"from_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
"to_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"to_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
"entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
@ -39,6 +38,6 @@
|
|||||||
"source_id": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"source_id": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
"n_hop_with_weight": {"type": "varchar", "default": ""},
|
"n_hop_with_weight": {"type": "varchar", "default": ""},
|
||||||
"removed_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
"removed_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
|
"doc_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||||
"doc_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}
|
"toc_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ The Admin CLI and Admin Service form a client-server architectural suite for RAG
|
|||||||
2. Install ragflow-cli.
|
2. Install ragflow-cli.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install ragflow-cli
|
pip install ragflow-cli==0.21.0
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Launch the CLI client:
|
3. Launch the CLI client:
|
||||||
|
|||||||
@ -114,7 +114,7 @@ dependencies = [
|
|||||||
"xpinyin==0.7.6",
|
"xpinyin==0.7.6",
|
||||||
"yfinance==0.2.65",
|
"yfinance==0.2.65",
|
||||||
"zhipuai==2.0.1",
|
"zhipuai==2.0.1",
|
||||||
"google-generativeai>=0.8.1,<0.9.0", # Needed for cv_model and embedding_model
|
"google-generativeai>=0.8.1,<0.9.0", # Needed for cv_model and embedding_model
|
||||||
"python-docx>=1.1.2,<2.0.0",
|
"python-docx>=1.1.2,<2.0.0",
|
||||||
"pypdf2>=3.0.1,<4.0.0",
|
"pypdf2>=3.0.1,<4.0.0",
|
||||||
"graspologic>=3.4.1,<4.0.0",
|
"graspologic>=3.4.1,<4.0.0",
|
||||||
@ -136,6 +136,7 @@ dependencies = [
|
|||||||
"lark>=1.2.2",
|
"lark>=1.2.2",
|
||||||
"mammoth>=1.11.0",
|
"mammoth>=1.11.0",
|
||||||
"markdownify>=1.2.0",
|
"markdownify>=1.2.0",
|
||||||
|
"captcha>=0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@ -72,7 +72,7 @@ class Dealer:
|
|||||||
def search(self, req, idx_names: str | list[str],
|
def search(self, req, idx_names: str | list[str],
|
||||||
kb_ids: list[str],
|
kb_ids: list[str],
|
||||||
emb_mdl=None,
|
emb_mdl=None,
|
||||||
highlight=False,
|
highlight: bool | list = False,
|
||||||
rank_feature: dict | None = None
|
rank_feature: dict | None = None
|
||||||
):
|
):
|
||||||
filters = self.get_filters(req)
|
filters = self.get_filters(req)
|
||||||
@ -101,7 +101,11 @@ class Dealer:
|
|||||||
total = self.dataStore.getTotal(res)
|
total = self.dataStore.getTotal(res)
|
||||||
logging.debug("Dealer.search TOTAL: {}".format(total))
|
logging.debug("Dealer.search TOTAL: {}".format(total))
|
||||||
else:
|
else:
|
||||||
highlightFields = ["content_ltks", "title_tks"] if highlight else []
|
highlightFields = ["content_ltks", "title_tks"]
|
||||||
|
if not highlight:
|
||||||
|
highlightFields = []
|
||||||
|
elif isinstance(highlight, list):
|
||||||
|
highlightFields = highlight
|
||||||
matchText, keywords = self.qryr.question(qst, min_match=0.3)
|
matchText, keywords = self.qryr.question(qst, min_match=0.3)
|
||||||
if emb_mdl is None:
|
if emb_mdl is None:
|
||||||
matchExprs = [matchText]
|
matchExprs = [matchText]
|
||||||
|
|||||||
@ -447,7 +447,7 @@ def build_TOC(task, docs, progress_callback):
|
|||||||
d["content_with_weight"] = json.dumps(toc, ensure_ascii=False)
|
d["content_with_weight"] = json.dumps(toc, ensure_ascii=False)
|
||||||
d["toc_kwd"] = "toc"
|
d["toc_kwd"] = "toc"
|
||||||
d["available_int"] = 0
|
d["available_int"] = 0
|
||||||
d["page_num_int"] = 100000000
|
d["page_num_int"] = [100000000]
|
||||||
d["id"] = xxhash.xxh64((d["content_with_weight"] + str(d["doc_id"])).encode("utf-8", "surrogatepass")).hexdigest()
|
d["id"] = xxhash.xxh64((d["content_with_weight"] + str(d["doc_id"])).encode("utf-8", "surrogatepass")).hexdigest()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|||||||
14
uv.lock
generated
14
uv.lock
generated
@ -658,6 +658,18 @@ wheels = [
|
|||||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/2b/a64c2d25a37aeb921fddb929111413049fc5f8b9a4c1aefaffaafe768d54/cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", size = 9325, upload-time = "2024-02-26T20:33:20.308Z" },
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/2b/a64c2d25a37aeb921fddb929111413049fc5f8b9a4c1aefaffaafe768d54/cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", size = 9325, upload-time = "2024-02-26T20:33:20.308Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "captcha"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/65/8e186bb798f33ba390eab897c995b0fcee92bc030e0f40cb8ea01f34dd07/captcha-0.7.1.tar.gz", hash = "sha256:a1b462bcc633a64d8db5efa7754548a877c698d98f87716c620a707364cabd6b", size = 226561, upload-time = "2025-03-01T05:00:13.395Z" }
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "cbor"
|
name = "cbor"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -5471,6 +5483,7 @@ dependencies = [
|
|||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
{ name = "cachetools" },
|
{ name = "cachetools" },
|
||||||
|
{ name = "captcha" },
|
||||||
{ name = "chardet" },
|
{ name = "chardet" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "cn2an" },
|
{ name = "cn2an" },
|
||||||
@ -5628,6 +5641,7 @@ requires-dist = [
|
|||||||
{ name = "boto3", specifier = "==1.34.140" },
|
{ name = "boto3", specifier = "==1.34.140" },
|
||||||
{ name = "botocore", specifier = "==1.34.140" },
|
{ name = "botocore", specifier = "==1.34.140" },
|
||||||
{ name = "cachetools", specifier = "==5.3.3" },
|
{ name = "cachetools", specifier = "==5.3.3" },
|
||||||
|
{ name = "captcha", specifier = ">=0.7.1" },
|
||||||
{ name = "chardet", specifier = "==5.2.0" },
|
{ name = "chardet", specifier = "==5.2.0" },
|
||||||
{ name = "click", specifier = ">=8.1.8" },
|
{ name = "click", specifier = ">=8.1.8" },
|
||||||
{ name = "cn2an", specifier = "==0.5.22" },
|
{ name = "cn2an", specifier = "==0.5.22" },
|
||||||
|
|||||||
@ -340,7 +340,9 @@ export function ChunkMethodDialog({
|
|||||||
show={showAutoKeywords(selectedTag) || showExcelToHtml}
|
show={showAutoKeywords(selectedTag) || showExcelToHtml}
|
||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
>
|
>
|
||||||
<EnableTocToggle />
|
{selectedTag === DocumentParserType.Naive && (
|
||||||
|
<EnableTocToggle />
|
||||||
|
)}
|
||||||
{showAutoKeywords(selectedTag) && (
|
{showAutoKeywords(selectedTag) && (
|
||||||
<>
|
<>
|
||||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import {
|
|||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { CollapsibleProps } from '@radix-ui/react-collapsible';
|
import { CollapsibleProps } from '@radix-ui/react-collapsible';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -67,3 +69,53 @@ export function Collapse({
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NodeCollapsibleProps<T extends any[]> = {
|
||||||
|
items?: T;
|
||||||
|
children: (item: T[0], idx: number) => ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
export function NodeCollapsible<T extends any[]>({
|
||||||
|
items = [] as unknown as T,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: NodeCollapsibleProps<T>) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const nextClassName = cn('space-y-2', className);
|
||||||
|
|
||||||
|
const nextItems = items.every((x) => Array.isArray(x)) ? items.flat() : items;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
className={cn('relative', nextClassName)}
|
||||||
|
>
|
||||||
|
{nextItems.slice(0, 3).map(children)}
|
||||||
|
<CollapsibleContent className={nextClassName}>
|
||||||
|
{nextItems.slice(3).map(children)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
{nextItems.length > 3 && (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
asChild
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="absolute left-1/2 -translate-x-1/2 bottom-0 translate-y-1/2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'size-3 bg-text-secondary rounded-full flex items-center justify-center',
|
||||||
|
{ 'bg-text-primary': isOpen },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronUp className="stroke-bg-component" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="stroke-bg-component" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
)}
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const buttonVariants = cva(
|
|||||||
outline:
|
outline:
|
||||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-bg-input text-secondary-foreground shadow-xs hover:bg-bg-input/80',
|
'bg-bg-input text-text-primary shadow-xs hover:bg-bg-input/80 border border-border-button',
|
||||||
ghost:
|
ghost:
|
||||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export interface ModalProps {
|
|||||||
okText?: ReactNode | string;
|
okText?: ReactNode | string;
|
||||||
onOk?: () => void;
|
onOk?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
export interface ModalType extends FC<ModalProps> {
|
export interface ModalType extends FC<ModalProps> {
|
||||||
show: typeof modalIns.show;
|
show: typeof modalIns.show;
|
||||||
@ -55,6 +56,7 @@ const Modal: ModalType = ({
|
|||||||
confirmLoading,
|
confirmLoading,
|
||||||
cancelText,
|
cancelText,
|
||||||
okText,
|
okText,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
small: 'max-w-md',
|
small: 'max-w-md',
|
||||||
@ -86,7 +88,7 @@ const Modal: ModalType = ({
|
|||||||
const handleChange = (open: boolean) => {
|
const handleChange = (open: boolean) => {
|
||||||
onOpenChange?.(open);
|
onOpenChange?.(open);
|
||||||
console.log('open', open, onOpenChange);
|
console.log('open', open, onOpenChange);
|
||||||
if (open) {
|
if (open && !disabled) {
|
||||||
onOk?.();
|
onOk?.();
|
||||||
}
|
}
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -112,7 +114,7 @@ const Modal: ModalType = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={confirmLoading}
|
disabled={confirmLoading || disabled}
|
||||||
onClick={() => handleOk()}
|
onClick={() => handleOk()}
|
||||||
className="px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
className="px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -291,7 +291,7 @@ export const RAGFlowSelect = forwardRef<
|
|||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
allowClear={allowClear}
|
allowClear={allowClear}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(triggerClassName, 'bg-bg-base')}
|
className={cn('bg-bg-base', triggerClassName)}
|
||||||
>
|
>
|
||||||
<SelectValue placeholder={placeholder}>{label}</SelectValue>
|
<SelectValue placeholder={placeholder}>{label}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@ -161,7 +161,7 @@ export type IIterationNode = BaseNode;
|
|||||||
export type IIterationStartNode = BaseNode;
|
export type IIterationStartNode = BaseNode;
|
||||||
export type IKeywordNode = BaseNode;
|
export type IKeywordNode = BaseNode;
|
||||||
export type ICodeNode = BaseNode<ICodeForm>;
|
export type ICodeNode = BaseNode<ICodeForm>;
|
||||||
export type IAgentNode = BaseNode;
|
export type IAgentNode<T = any> = BaseNode<T>;
|
||||||
|
|
||||||
export type RAGFlowNodeType =
|
export type RAGFlowNodeType =
|
||||||
| IBeginNode
|
| IBeginNode
|
||||||
|
|||||||
@ -258,7 +258,6 @@ export default {
|
|||||||
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
|
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
|
||||||
},
|
},
|
||||||
knowledgeConfiguration: {
|
knowledgeConfiguration: {
|
||||||
tocExtraction: '目录增强',
|
|
||||||
tocExtractionTip:
|
tocExtractionTip:
|
||||||
'对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`目录增强`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。',
|
'对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`目录增强`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。',
|
||||||
deleteGenerateModalContent: `
|
deleteGenerateModalContent: `
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import LLMLabel from '@/components/llm-select/llm-label';
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
import { IAgentNode } from '@/interfaces/database/flow';
|
import { IAgentNode } from '@/interfaces/database/flow';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AgentExceptionMethod, NodeHandleId } from '../../constant';
|
import { AgentExceptionMethod, NodeHandleId } from '../../constant';
|
||||||
|
import { AgentFormSchemaType } from '../../form/agent-form';
|
||||||
import useGraphStore from '../../store';
|
import useGraphStore from '../../store';
|
||||||
import { isBottomSubAgent } from '../../utils';
|
import { hasSubAgent, isBottomSubAgent } from '../../utils';
|
||||||
import { CommonHandle, LeftEndHandle } from './handle';
|
import { CommonHandle, LeftEndHandle } from './handle';
|
||||||
import { RightHandleStyle } from './handle-icon';
|
import { RightHandleStyle } from './handle-icon';
|
||||||
import NodeHeader from './node-header';
|
import NodeHeader from './node-header';
|
||||||
@ -18,7 +20,7 @@ function InnerAgentNode({
|
|||||||
data,
|
data,
|
||||||
isConnectable = true,
|
isConnectable = true,
|
||||||
selected,
|
selected,
|
||||||
}: NodeProps<IAgentNode>) {
|
}: NodeProps<IAgentNode<AgentFormSchemaType>>) {
|
||||||
const edges = useGraphStore((state) => state.edges);
|
const edges = useGraphStore((state) => state.edges);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -30,6 +32,12 @@ function InnerAgentNode({
|
|||||||
return get(data, 'form.exception_method');
|
return get(data, 'form.exception_method');
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const hasTools = useMemo(() => {
|
||||||
|
const tools = get(data, 'form.tools', []);
|
||||||
|
const mcp = get(data, 'form.mcp', []);
|
||||||
|
return tools.length > 0 || mcp.length > 0;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const isGotoMethod = useMemo(() => {
|
const isGotoMethod = useMemo(() => {
|
||||||
return exceptionMethod === AgentExceptionMethod.Goto;
|
return exceptionMethod === AgentExceptionMethod.Goto;
|
||||||
}, [exceptionMethod]);
|
}, [exceptionMethod]);
|
||||||
@ -51,7 +59,6 @@ function InnerAgentNode({
|
|||||||
></CommonHandle>
|
></CommonHandle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHeadAgent || (
|
{isHeadAgent || (
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
@ -67,7 +74,9 @@ function InnerAgentNode({
|
|||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
id={NodeHandleId.AgentBottom}
|
id={NodeHandleId.AgentBottom}
|
||||||
style={{ left: 180 }}
|
style={{ left: 180 }}
|
||||||
className="!bg-accent-primary !size-2"
|
className={cn('!bg-accent-primary !size-2 invisible', {
|
||||||
|
visible: hasSubAgent(edges, id),
|
||||||
|
})}
|
||||||
></Handle>
|
></Handle>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
@ -75,7 +84,9 @@ function InnerAgentNode({
|
|||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
id={NodeHandleId.Tool}
|
id={NodeHandleId.Tool}
|
||||||
style={{ left: 20 }}
|
style={{ left: 20 }}
|
||||||
className="!bg-accent-primary !size-2"
|
className={cn('!bg-accent-primary !size-2 invisible', {
|
||||||
|
visible: hasTools,
|
||||||
|
})}
|
||||||
></Handle>
|
></Handle>
|
||||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
<section className="flex flex-col gap-2">
|
<section className="flex flex-col gap-2">
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||||
import { IRetrievalNode } from '@/interfaces/database/flow';
|
import { IRetrievalNode } from '@/interfaces/database/flow';
|
||||||
@ -44,8 +45,8 @@ function InnerRetrievalNode({
|
|||||||
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
|
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
|
||||||
})}
|
})}
|
||||||
></NodeHeader>
|
></NodeHeader>
|
||||||
<section className="flex flex-col gap-2">
|
<NodeCollapsible items={knowledgeBaseIds}>
|
||||||
{knowledgeBaseIds.map((id) => {
|
{(id) => {
|
||||||
const item = knowledgeList.find((y) => id === y.id);
|
const item = knowledgeList.find((y) => id === y.id);
|
||||||
const label = getLabel(id);
|
const label = getLabel(id);
|
||||||
|
|
||||||
@ -63,8 +64,8 @@ function InnerRetrievalNode({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}}
|
||||||
</section>
|
</NodeCollapsible>
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
|
import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
|
||||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
@ -51,32 +52,38 @@ function InnerToolNode({
|
|||||||
isConnectable={isConnectable}
|
isConnectable={isConnectable}
|
||||||
className="!bg-accent-primary !size-2"
|
className="!bg-accent-primary !size-2"
|
||||||
></Handle>
|
></Handle>
|
||||||
<ul className="space-y-2">
|
<NodeCollapsible items={[tools, mcpList]}>
|
||||||
{tools.map((x) => (
|
{(x) => {
|
||||||
<ToolCard
|
if ('mcp_id' in x) {
|
||||||
key={x.component_name}
|
const mcp = x as unknown as IAgentForm['mcp'][number];
|
||||||
onClick={handleClick(x.component_name)}
|
return (
|
||||||
className="cursor-pointer"
|
<ToolCard
|
||||||
data-tool={x.component_name}
|
onClick={handleClick(mcp.mcp_id)}
|
||||||
>
|
className="cursor-pointer"
|
||||||
<div className="flex gap-1 items-center pointer-events-none">
|
data-tool={x.mcp_id}
|
||||||
<OperatorIcon name={x.component_name as Operator}></OperatorIcon>
|
>
|
||||||
{x.component_name}
|
{findMcpById(mcp.mcp_id)?.name}
|
||||||
</div>
|
</ToolCard>
|
||||||
</ToolCard>
|
);
|
||||||
))}
|
}
|
||||||
|
|
||||||
{mcpList.map((x) => (
|
const tool = x as unknown as IAgentForm['tools'][number];
|
||||||
<ToolCard
|
return (
|
||||||
key={x.mcp_id}
|
<ToolCard
|
||||||
onClick={handleClick(x.mcp_id)}
|
onClick={handleClick(tool.component_name)}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
data-tool={x.mcp_id}
|
data-tool={tool.component_name}
|
||||||
>
|
>
|
||||||
{findMcpById(x.mcp_id)?.name}
|
<div className="flex gap-1 items-center pointer-events-none">
|
||||||
</ToolCard>
|
<OperatorIcon
|
||||||
))}
|
name={tool.component_name as Operator}
|
||||||
</ul>
|
></OperatorIcon>
|
||||||
|
{tool.component_name}
|
||||||
|
</div>
|
||||||
|
</ToolCard>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</NodeCollapsible>
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,8 @@ const FormSchema = z.object({
|
|||||||
cite: z.boolean().optional(),
|
cite: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type AgentFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
const outputList = buildOutputList(initialAgentValues.outputs);
|
const outputList = buildOutputList(initialAgentValues.outputs);
|
||||||
|
|
||||||
function AgentForm({ node }: INextOperatorForm) {
|
function AgentForm({ node }: INextOperatorForm) {
|
||||||
@ -92,7 +94,7 @@ function AgentForm({ node }: INextOperatorForm) {
|
|||||||
return isBottomSubAgent(edges, node?.id);
|
return isBottomSubAgent(edges, node?.id);
|
||||||
}, [edges, node?.id]);
|
}, [edges, node?.id]);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
const form = useForm<AgentFormSchemaType>({
|
||||||
defaultValues: defaultValues,
|
defaultValues: defaultValues,
|
||||||
resolver: zodResolver(FormSchema),
|
resolver: zodResolver(FormSchema),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function useBuildPromptExtraPromptOptions(
|
|||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
label: key,
|
label: key,
|
||||||
value: wrapPromptWithTag(value, key),
|
value: wrapPromptWithTag(value, key),
|
||||||
|
icon: null,
|
||||||
}))
|
}))
|
||||||
.filter((x) => {
|
.filter((x) => {
|
||||||
if (!has) {
|
if (!has) {
|
||||||
|
|||||||
@ -162,6 +162,13 @@ export function hasSubAgentOrTool(edges: Edge[], nodeId?: string) {
|
|||||||
return !!edge;
|
return !!edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasSubAgent(edges: Edge[], nodeId?: string) {
|
||||||
|
const edge = edges.find(
|
||||||
|
(x) => x.source === nodeId && x.sourceHandle === NodeHandleId.AgentBottom,
|
||||||
|
);
|
||||||
|
return !!edge;
|
||||||
|
}
|
||||||
|
|
||||||
// construct a dsl based on the node information of the graph
|
// construct a dsl based on the node information of the graph
|
||||||
export const buildDslComponentsByGraph = (
|
export const buildDslComponentsByGraph = (
|
||||||
nodes: RAGFlowNodeType[],
|
nodes: RAGFlowNodeType[],
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { Switch } from '@/components/ui/switch';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useFetchChunk } from '@/hooks/chunk-hooks';
|
import { useFetchChunk } from '@/hooks/chunk-hooks';
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { Trash2 } from 'lucide-react';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -194,9 +193,9 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
|||||||
{t('chunk.enabled')}
|
{t('chunk.enabled')}
|
||||||
<Switch checked={checked} onCheckedChange={handleCheck} />
|
<Switch checked={checked} onCheckedChange={handleCheck} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1" onClick={handleRemove}>
|
{/* <div className="flex items-center gap-1" onClick={handleRemove}>
|
||||||
<Trash2 size={16} /> {t('common.delete')}
|
<Trash2 size={16} /> {t('common.delete')}
|
||||||
</div>
|
</div> */}
|
||||||
</Space>
|
</Space>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import {
|
|||||||
useNavigatePage,
|
useNavigatePage,
|
||||||
} from '@/hooks/logic-hooks/navigate-hooks';
|
} from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||||
import { useGetDocumentUrl } from '../../../knowledge-chunk/components/document-preview/hooks';
|
import { useGetDocumentUrl } from './components/document-preview/hooks';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const Chunk = () => {
|
const Chunk = () => {
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
.image {
|
|
||||||
width: 100px !important;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imagePreview {
|
|
||||||
max-width: 50vw;
|
|
||||||
max-height: 50vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
.chunkText;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentEllipsis {
|
|
||||||
.multipleLineEllipsis(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentText {
|
|
||||||
word-break: break-all !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chunkCard {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardSelected {
|
|
||||||
background-color: @selectedBackgroundColor;
|
|
||||||
}
|
|
||||||
.cardSelectedDark {
|
|
||||||
background-color: #ffffff2f;
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import Image from '@/components/image';
|
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
|
||||||
import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useTheme } from '@/components/theme-provider';
|
|
||||||
import { ChunkTextMode } from '../../constant';
|
|
||||||
import styles from './index.less';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
item: IChunk;
|
|
||||||
checked: boolean;
|
|
||||||
switchChunk: (available?: number, chunkIds?: string[]) => void;
|
|
||||||
editChunk: (chunkId: string) => void;
|
|
||||||
handleCheckboxClick: (chunkId: string, checked: boolean) => void;
|
|
||||||
selected: boolean;
|
|
||||||
clickChunkCard: (chunkId: string) => void;
|
|
||||||
textMode: ChunkTextMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChunkCard = ({
|
|
||||||
item,
|
|
||||||
checked,
|
|
||||||
handleCheckboxClick,
|
|
||||||
editChunk,
|
|
||||||
switchChunk,
|
|
||||||
selected,
|
|
||||||
clickChunkCard,
|
|
||||||
textMode,
|
|
||||||
}: IProps) => {
|
|
||||||
const available = Number(item.available_int);
|
|
||||||
const [enabled, setEnabled] = useState(false);
|
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const onChange = (checked: boolean) => {
|
|
||||||
setEnabled(checked);
|
|
||||||
switchChunk(available === 0 ? 1 : 0, [item.chunk_id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheck: CheckboxProps['onChange'] = (e) => {
|
|
||||||
handleCheckboxClick(item.chunk_id, e.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContentDoubleClick = () => {
|
|
||||||
editChunk(item.chunk_id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContentClick = () => {
|
|
||||||
clickChunkCard(item.chunk_id);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setEnabled(available === 1);
|
|
||||||
}, [available]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={classNames(styles.chunkCard, {
|
|
||||||
[`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]:
|
|
||||||
selected,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Flex gap={'middle'} justify={'space-between'}>
|
|
||||||
<Checkbox onChange={handleCheck} checked={checked}></Checkbox>
|
|
||||||
{item.image_id && (
|
|
||||||
<Popover
|
|
||||||
placement="right"
|
|
||||||
content={
|
|
||||||
<Image id={item.image_id} className={styles.imagePreview}></Image>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Image id={item.image_id} className={styles.image}></Image>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section
|
|
||||||
onDoubleClick={handleContentDoubleClick}
|
|
||||||
onClick={handleContentClick}
|
|
||||||
className={styles.content}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: DOMPurify.sanitize(item.content_with_weight),
|
|
||||||
}}
|
|
||||||
className={classNames(styles.contentText, {
|
|
||||||
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
|
|
||||||
})}
|
|
||||||
></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Switch checked={enabled} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChunkCard;
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
import EditTag from '@/components/edit-tag';
|
|
||||||
import { useFetchChunk } from '@/hooks/chunk-hooks';
|
|
||||||
import { IModalProps } from '@/interfaces/common';
|
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
|
||||||
import { DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { Divider, Form, Input, Modal, Space, Switch } from 'antd';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDeleteChunkByIds } from '../../hooks';
|
|
||||||
import {
|
|
||||||
transformTagFeaturesArrayToObject,
|
|
||||||
transformTagFeaturesObjectToArray,
|
|
||||||
} from '../../utils';
|
|
||||||
import { TagFeatureItem } from './tag-feature-item';
|
|
||||||
|
|
||||||
type FieldType = Pick<
|
|
||||||
IChunk,
|
|
||||||
'content_with_weight' | 'tag_kwd' | 'question_kwd' | 'important_kwd'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface kFProps {
|
|
||||||
doc_id: string;
|
|
||||||
chunkId: string | undefined;
|
|
||||||
parserId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
|
||||||
doc_id,
|
|
||||||
chunkId,
|
|
||||||
hideModal,
|
|
||||||
onOk,
|
|
||||||
loading,
|
|
||||||
parserId,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [checked, setChecked] = useState(false);
|
|
||||||
const { removeChunk } = useDeleteChunkByIds();
|
|
||||||
const { data } = useFetchChunk(chunkId);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const isTagParser = parserId === 'tag';
|
|
||||||
|
|
||||||
const handleOk = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
console.log('🚀 ~ handleOk ~ values:', values);
|
|
||||||
|
|
||||||
onOk?.({
|
|
||||||
...values,
|
|
||||||
tag_feas: transformTagFeaturesArrayToObject(values.tag_feas),
|
|
||||||
available_int: checked ? 1 : 0, // available_int
|
|
||||||
});
|
|
||||||
} catch (errorInfo) {
|
|
||||||
console.log('Failed:', errorInfo);
|
|
||||||
}
|
|
||||||
}, [checked, form, onOk]);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
|
||||||
if (chunkId) {
|
|
||||||
return removeChunk([chunkId], doc_id);
|
|
||||||
}
|
|
||||||
}, [chunkId, doc_id, removeChunk]);
|
|
||||||
|
|
||||||
const handleCheck = useCallback(() => {
|
|
||||||
setChecked(!checked);
|
|
||||||
}, [checked]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.code === 0) {
|
|
||||||
const { available_int, tag_feas } = data.data;
|
|
||||||
form.setFieldsValue({
|
|
||||||
...(data.data || {}),
|
|
||||||
tag_feas: transformTagFeaturesObjectToArray(tag_feas),
|
|
||||||
});
|
|
||||||
|
|
||||||
setChecked(available_int !== 0);
|
|
||||||
}
|
|
||||||
}, [data, form, chunkId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`}
|
|
||||||
open={true}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={hideModal}
|
|
||||||
okButtonProps={{ loading }}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<Form form={form} autoComplete="off" layout={'vertical'}>
|
|
||||||
<Form.Item<FieldType>
|
|
||||||
label={t('chunk.chunk')}
|
|
||||||
name="content_with_weight"
|
|
||||||
rules={[{ required: true, message: t('chunk.chunkMessage') }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item<FieldType> label={t('chunk.keyword')} name="important_kwd">
|
|
||||||
<EditTag></EditTag>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FieldType>
|
|
||||||
label={t('chunk.question')}
|
|
||||||
name="question_kwd"
|
|
||||||
tooltip={t('chunk.questionTip')}
|
|
||||||
>
|
|
||||||
<EditTag></EditTag>
|
|
||||||
</Form.Item>
|
|
||||||
{isTagParser && (
|
|
||||||
<Form.Item<FieldType>
|
|
||||||
label={t('knowledgeConfiguration.tagName')}
|
|
||||||
name="tag_kwd"
|
|
||||||
>
|
|
||||||
<EditTag></EditTag>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isTagParser && <TagFeatureItem></TagFeatureItem>}
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{chunkId && (
|
|
||||||
<section>
|
|
||||||
<Divider></Divider>
|
|
||||||
<Space size={'large'}>
|
|
||||||
<Switch
|
|
||||||
checkedChildren={t('chunk.enabled')}
|
|
||||||
unCheckedChildren={t('chunk.disabled')}
|
|
||||||
onChange={handleCheck}
|
|
||||||
checked={checked}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span onClick={handleRemove}>
|
|
||||||
<DeleteOutlined /> {t('common.delete')}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default ChunkCreatingModal;
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import {
|
|
||||||
useFetchKnowledgeBaseConfiguration,
|
|
||||||
useFetchTagListByKnowledgeIds,
|
|
||||||
} from '@/hooks/knowledge-hooks';
|
|
||||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, Form, InputNumber, Select } from 'antd';
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FormListItem } from '../../utils';
|
|
||||||
|
|
||||||
const FieldKey = 'tag_feas';
|
|
||||||
|
|
||||||
export const TagFeatureItem = () => {
|
|
||||||
const form = Form.useFormInstance();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration();
|
|
||||||
|
|
||||||
const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds();
|
|
||||||
|
|
||||||
const tagKnowledgeIds = useMemo(() => {
|
|
||||||
return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? [];
|
|
||||||
}, [knowledgeConfiguration?.parser_config?.tag_kb_ids]);
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
return list.map((x) => ({
|
|
||||||
value: x[0],
|
|
||||||
label: x[0],
|
|
||||||
}));
|
|
||||||
}, [list]);
|
|
||||||
|
|
||||||
const filterOptions = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const tags: FormListItem[] = form.getFieldValue(FieldKey) ?? [];
|
|
||||||
|
|
||||||
// Exclude it's own current data
|
|
||||||
const list = tags
|
|
||||||
.filter((x, idx) => x && index !== idx)
|
|
||||||
.map((x) => x.tag);
|
|
||||||
|
|
||||||
// Exclude the selected data from other options from one's own options.
|
|
||||||
return options.filter((x) => !list.some((y) => x.value === y));
|
|
||||||
},
|
|
||||||
[form, options],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setKnowledgeIds(tagKnowledgeIds);
|
|
||||||
}, [setKnowledgeIds, tagKnowledgeIds]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Item label={t('knowledgeConfiguration.tags')}>
|
|
||||||
<Form.List name={FieldKey} initialValue={[]}>
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
|
||||||
<div key={key} className="flex gap-3 items-center">
|
|
||||||
<div className="flex flex-1 gap-8">
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 'tag']}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: t('common.pleaseSelect') },
|
|
||||||
]}
|
|
||||||
className="w-2/3"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
showSearch
|
|
||||||
placeholder={t('knowledgeConfiguration.tagName')}
|
|
||||||
options={filterOptions(name)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
{...restField}
|
|
||||||
name={[name, 'frequency']}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: t('common.pleaseInput') },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder={t('knowledgeConfiguration.frequency')}
|
|
||||||
max={10}
|
|
||||||
min={0}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<MinusCircleOutlined
|
|
||||||
onClick={() => remove(name)}
|
|
||||||
className="mb-6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => add()}
|
|
||||||
block
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
>
|
|
||||||
{t('knowledgeConfiguration.addTag')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
|
|
||||||
import { KnowledgeRouteKey } from '@/constants/knowledge';
|
|
||||||
import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks';
|
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
|
||||||
import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks';
|
|
||||||
import {
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
CloseCircleOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
DownOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
Menu,
|
|
||||||
MenuProps,
|
|
||||||
Popover,
|
|
||||||
Radio,
|
|
||||||
RadioChangeEvent,
|
|
||||||
Segmented,
|
|
||||||
SegmentedProps,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { Link } from 'umi';
|
|
||||||
import { ChunkTextMode } from '../../constant';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface IProps
|
|
||||||
extends Pick<
|
|
||||||
IChunkListResult,
|
|
||||||
'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable'
|
|
||||||
> {
|
|
||||||
checked: boolean;
|
|
||||||
selectAllChunk: (checked: boolean) => void;
|
|
||||||
createChunk: () => void;
|
|
||||||
removeChunk: () => void;
|
|
||||||
switchChunk: (available: number) => void;
|
|
||||||
changeChunkTextMode(mode: ChunkTextMode): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChunkToolBar = ({
|
|
||||||
selectAllChunk,
|
|
||||||
checked,
|
|
||||||
createChunk,
|
|
||||||
removeChunk,
|
|
||||||
switchChunk,
|
|
||||||
changeChunkTextMode,
|
|
||||||
available,
|
|
||||||
handleSetAvailable,
|
|
||||||
searchString,
|
|
||||||
handleInputChange,
|
|
||||||
}: IProps) => {
|
|
||||||
const data = useSelectChunkList();
|
|
||||||
const documentInfo = data?.documentInfo;
|
|
||||||
const knowledgeBaseId = useKnowledgeBaseId();
|
|
||||||
const [isShowSearchBox, setIsShowSearchBox] = useState(false);
|
|
||||||
const { t } = useTranslate('chunk');
|
|
||||||
|
|
||||||
const handleSelectAllCheck = useCallback(
|
|
||||||
(e: any) => {
|
|
||||||
selectAllChunk(e.target.checked);
|
|
||||||
},
|
|
||||||
[selectAllChunk],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearchIconClick = () => {
|
|
||||||
setIsShowSearchBox(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearchBlur = () => {
|
|
||||||
if (!searchString?.trim()) {
|
|
||||||
setIsShowSearchBox(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
|
||||||
removeChunk();
|
|
||||||
}, [removeChunk]);
|
|
||||||
|
|
||||||
const handleEnabledClick = useCallback(() => {
|
|
||||||
switchChunk(1);
|
|
||||||
}, [switchChunk]);
|
|
||||||
|
|
||||||
const handleDisabledClick = useCallback(() => {
|
|
||||||
switchChunk(0);
|
|
||||||
}, [switchChunk]);
|
|
||||||
|
|
||||||
const items: MenuProps['items'] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
<Checkbox onChange={handleSelectAllCheck} checked={checked}>
|
|
||||||
<b>{t('selectAll')}</b>
|
|
||||||
</Checkbox>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ type: 'divider' },
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
label: (
|
|
||||||
<Space onClick={handleEnabledClick}>
|
|
||||||
<CheckCircleOutlined />
|
|
||||||
<b>{t('enabledSelected')}</b>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '3',
|
|
||||||
label: (
|
|
||||||
<Space onClick={handleDisabledClick}>
|
|
||||||
<CloseCircleOutlined />
|
|
||||||
<b>{t('disabledSelected')}</b>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ type: 'divider' },
|
|
||||||
{
|
|
||||||
key: '4',
|
|
||||||
label: (
|
|
||||||
<Space onClick={handleDelete}>
|
|
||||||
<DeleteOutlined />
|
|
||||||
<b>{t('deleteSelected')}</b>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [
|
|
||||||
checked,
|
|
||||||
handleSelectAllCheck,
|
|
||||||
handleDelete,
|
|
||||||
handleEnabledClick,
|
|
||||||
handleDisabledClick,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<Menu style={{ width: 200 }} items={items} selectable={false} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterChange = (e: RadioChangeEvent) => {
|
|
||||||
selectAllChunk(false);
|
|
||||||
handleSetAvailable(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterContent = (
|
|
||||||
<Radio.Group onChange={handleFilterChange} value={available}>
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Radio value={undefined}>{t('all')}</Radio>
|
|
||||||
<Radio value={1}>{t('enabled')}</Radio>
|
|
||||||
<Radio value={0}>{t('disabled')}</Radio>
|
|
||||||
</Space>
|
|
||||||
</Radio.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex justify="space-between" align="center">
|
|
||||||
<Space size={'middle'}>
|
|
||||||
<Link
|
|
||||||
to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`}
|
|
||||||
>
|
|
||||||
<ArrowLeftOutlined />
|
|
||||||
</Link>
|
|
||||||
<FilePdfOutlined />
|
|
||||||
<Text ellipsis={{ tooltip: documentInfo?.name }} style={{ width: 150 }}>
|
|
||||||
{documentInfo?.name}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<Segmented
|
|
||||||
options={[
|
|
||||||
{ label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
|
|
||||||
{ label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
|
|
||||||
]}
|
|
||||||
onChange={changeChunkTextMode as SegmentedProps['onChange']}
|
|
||||||
/>
|
|
||||||
<Popover content={content} placement="bottom" arrow={false}>
|
|
||||||
<Button>
|
|
||||||
{t('bulk')}
|
|
||||||
<DownOutlined />
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
{isShowSearchBox ? (
|
|
||||||
<Input
|
|
||||||
size="middle"
|
|
||||||
placeholder={t('search')}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
allowClear
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onBlur={handleSearchBlur}
|
|
||||||
value={searchString}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button icon={<SearchOutlined />} onClick={handleSearchIconClick} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Popover content={filterContent} placement="bottom" arrow={false}>
|
|
||||||
<Button icon={<FilterIcon />} />
|
|
||||||
</Popover>
|
|
||||||
<Button
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
onClick={() => createChunk()}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChunkToolBar;
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
|
||||||
import { api_host } from '@/utils/api';
|
|
||||||
import { useSize } from 'ahooks';
|
|
||||||
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
export const useDocumentResizeObserver = () => {
|
|
||||||
const [containerWidth, setContainerWidth] = useState<number>();
|
|
||||||
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
|
|
||||||
const size = useSize(containerRef);
|
|
||||||
|
|
||||||
const onResize = useCallback((width?: number) => {
|
|
||||||
if (width) {
|
|
||||||
setContainerWidth(width);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onResize(size?.width);
|
|
||||||
}, [size?.width, onResize]);
|
|
||||||
|
|
||||||
return { containerWidth, setContainerRef };
|
|
||||||
};
|
|
||||||
|
|
||||||
function highlightPattern(text: string, pattern: string, pageNumber: number) {
|
|
||||||
if (pageNumber === 2) {
|
|
||||||
return `<mark>${text}</mark>`;
|
|
||||||
}
|
|
||||||
if (text.trim() !== '' && pattern.match(text)) {
|
|
||||||
// return pattern.replace(text, (value) => `<mark>${value}</mark>`);
|
|
||||||
return `<mark>${text}</mark>`;
|
|
||||||
}
|
|
||||||
return text.replace(pattern, (value) => `<mark>${value}</mark>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useHighlightText = (searchText: string = '') => {
|
|
||||||
const textRenderer: CustomTextRenderer = useCallback(
|
|
||||||
(textItem) => {
|
|
||||||
return highlightPattern(textItem.str, searchText, textItem.pageNumber);
|
|
||||||
},
|
|
||||||
[searchText],
|
|
||||||
);
|
|
||||||
|
|
||||||
return textRenderer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetDocumentUrl = () => {
|
|
||||||
const { documentId } = useGetKnowledgeSearchParams();
|
|
||||||
|
|
||||||
const url = useMemo(() => {
|
|
||||||
return `${api_host}/document/get/${documentId}`;
|
|
||||||
}, [documentId]);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
.documentContainer {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 284px);
|
|
||||||
position: relative;
|
|
||||||
:global(.PdfHighlighter) {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
:global(.Highlight--scrolledTo .Highlight__part) {
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-color: rgba(255, 226, 143, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { Skeleton } from 'antd';
|
|
||||||
import { memo, useEffect, useRef } from 'react';
|
|
||||||
import {
|
|
||||||
AreaHighlight,
|
|
||||||
Highlight,
|
|
||||||
IHighlight,
|
|
||||||
PdfHighlighter,
|
|
||||||
PdfLoader,
|
|
||||||
Popup,
|
|
||||||
} from 'react-pdf-highlighter';
|
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
|
|
||||||
import FileError from '@/pages/document-viewer/file-error';
|
|
||||||
import styles from './index.less';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
highlights: IHighlight[];
|
|
||||||
setWidthAndHeight: (width: number, height: number) => void;
|
|
||||||
}
|
|
||||||
const HighlightPopup = ({
|
|
||||||
comment,
|
|
||||||
}: {
|
|
||||||
comment: { text: string; emoji: string };
|
|
||||||
}) =>
|
|
||||||
comment.text ? (
|
|
||||||
<div className="Highlight__popup">
|
|
||||||
{comment.emoji} {comment.text}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
// TODO: merge with DocumentPreviewer
|
|
||||||
const Preview = ({ highlights: state, setWidthAndHeight }: IProps) => {
|
|
||||||
const url = useGetDocumentUrl();
|
|
||||||
|
|
||||||
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
|
||||||
const error = useCatchDocumentError(url);
|
|
||||||
|
|
||||||
const resetHash = () => {};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.length > 0) {
|
|
||||||
ref?.current(state[0]);
|
|
||||||
}
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.documentContainer}>
|
|
||||||
<PdfLoader
|
|
||||||
url={url}
|
|
||||||
beforeLoad={<Skeleton active />}
|
|
||||||
workerSrc="/pdfjs-dist/pdf.worker.min.js"
|
|
||||||
errorMessage={<FileError>{error}</FileError>}
|
|
||||||
>
|
|
||||||
{(pdfDocument) => {
|
|
||||||
pdfDocument.getPage(1).then((page) => {
|
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
|
||||||
const width = viewport.width;
|
|
||||||
const height = viewport.height;
|
|
||||||
setWidthAndHeight(width, height);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PdfHighlighter
|
|
||||||
pdfDocument={pdfDocument}
|
|
||||||
enableAreaSelection={(event) => event.altKey}
|
|
||||||
onScrollChange={resetHash}
|
|
||||||
scrollRef={(scrollTo) => {
|
|
||||||
ref.current = scrollTo;
|
|
||||||
}}
|
|
||||||
onSelectionFinished={() => null}
|
|
||||||
highlightTransform={(
|
|
||||||
highlight,
|
|
||||||
index,
|
|
||||||
setTip,
|
|
||||||
hideTip,
|
|
||||||
viewportToScaled,
|
|
||||||
screenshot,
|
|
||||||
isScrolledTo,
|
|
||||||
) => {
|
|
||||||
const isTextHighlight = !Boolean(
|
|
||||||
highlight.content && highlight.content.image,
|
|
||||||
);
|
|
||||||
|
|
||||||
const component = isTextHighlight ? (
|
|
||||||
<Highlight
|
|
||||||
isScrolledTo={isScrolledTo}
|
|
||||||
position={highlight.position}
|
|
||||||
comment={highlight.comment}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AreaHighlight
|
|
||||||
isScrolledTo={isScrolledTo}
|
|
||||||
highlight={highlight}
|
|
||||||
onChange={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popup
|
|
||||||
popupContent={<HighlightPopup {...highlight} />}
|
|
||||||
onMouseOver={(popupContent) =>
|
|
||||||
setTip(highlight, () => popupContent)
|
|
||||||
}
|
|
||||||
onMouseOut={hideTip}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{component}
|
|
||||||
</Popup>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
highlights={state}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</PdfLoader>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Preview);
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export enum ChunkTextMode {
|
|
||||||
Full = 'full',
|
|
||||||
Ellipse = 'ellipse',
|
|
||||||
}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
import {
|
|
||||||
useCreateChunk,
|
|
||||||
useDeleteChunk,
|
|
||||||
useSelectChunkList,
|
|
||||||
} from '@/hooks/chunk-hooks';
|
|
||||||
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
|
|
||||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
|
||||||
import { buildChunkHighlights } from '@/utils/document-util';
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { IHighlight } from 'react-pdf-highlighter';
|
|
||||||
import { ChunkTextMode } from './constant';
|
|
||||||
|
|
||||||
export const useHandleChunkCardClick = () => {
|
|
||||||
const [selectedChunkId, setSelectedChunkId] = useState<string>('');
|
|
||||||
|
|
||||||
const handleChunkCardClick = useCallback((chunkId: string) => {
|
|
||||||
setSelectedChunkId(chunkId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { handleChunkCardClick, selectedChunkId };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetSelectedChunk = (selectedChunkId: string) => {
|
|
||||||
const data = useSelectChunkList();
|
|
||||||
return (
|
|
||||||
data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetChunkHighlights = (selectedChunkId: string) => {
|
|
||||||
const [size, setSize] = useState({ width: 849, height: 1200 });
|
|
||||||
const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId);
|
|
||||||
|
|
||||||
const highlights: IHighlight[] = useMemo(() => {
|
|
||||||
return buildChunkHighlights(selectedChunk, size);
|
|
||||||
}, [selectedChunk, size]);
|
|
||||||
|
|
||||||
const setWidthAndHeight = useCallback((width: number, height: number) => {
|
|
||||||
setSize((pre) => {
|
|
||||||
if (pre.height !== height || pre.width !== width) {
|
|
||||||
return { height, width };
|
|
||||||
}
|
|
||||||
return pre;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { highlights, setWidthAndHeight };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Switch chunk text to be fully displayed or ellipse
|
|
||||||
export const useChangeChunkTextMode = () => {
|
|
||||||
const [textMode, setTextMode] = useState<ChunkTextMode>(ChunkTextMode.Full);
|
|
||||||
|
|
||||||
const changeChunkTextMode = useCallback((mode: ChunkTextMode) => {
|
|
||||||
setTextMode(mode);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { textMode, changeChunkTextMode };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeleteChunkByIds = (): {
|
|
||||||
removeChunk: (chunkIds: string[], documentId: string) => Promise<number>;
|
|
||||||
} => {
|
|
||||||
const { deleteChunk } = useDeleteChunk();
|
|
||||||
const showDeleteConfirm = useShowDeleteConfirm();
|
|
||||||
|
|
||||||
const removeChunk = useCallback(
|
|
||||||
(chunkIds: string[], documentId: string) => () => {
|
|
||||||
return deleteChunk({ chunkIds, doc_id: documentId });
|
|
||||||
},
|
|
||||||
[deleteChunk],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRemoveChunk = useCallback(
|
|
||||||
(chunkIds: string[], documentId: string): Promise<number> => {
|
|
||||||
return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) });
|
|
||||||
},
|
|
||||||
[removeChunk, showDeleteConfirm],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
removeChunk: onRemoveChunk,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateChunk = () => {
|
|
||||||
const [chunkId, setChunkId] = useState<string | undefined>('');
|
|
||||||
const {
|
|
||||||
visible: chunkUpdatingVisible,
|
|
||||||
hideModal: hideChunkUpdatingModal,
|
|
||||||
showModal,
|
|
||||||
} = useSetModalState();
|
|
||||||
const { createChunk, loading } = useCreateChunk();
|
|
||||||
const { documentId } = useGetKnowledgeSearchParams();
|
|
||||||
|
|
||||||
const onChunkUpdatingOk = useCallback(
|
|
||||||
async (params: IChunk) => {
|
|
||||||
const code = await createChunk({
|
|
||||||
...params,
|
|
||||||
doc_id: documentId,
|
|
||||||
chunk_id: chunkId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
hideChunkUpdatingModal();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[createChunk, hideChunkUpdatingModal, chunkId, documentId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleShowChunkUpdatingModal = useCallback(
|
|
||||||
async (id?: string) => {
|
|
||||||
setChunkId(id);
|
|
||||||
showModal();
|
|
||||||
},
|
|
||||||
[showModal],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
chunkUpdatingLoading: loading,
|
|
||||||
onChunkUpdatingOk,
|
|
||||||
chunkUpdatingVisible,
|
|
||||||
hideChunkUpdatingModal,
|
|
||||||
showChunkUpdatingModal: handleShowChunkUpdatingModal,
|
|
||||||
chunkId,
|
|
||||||
documentId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
.chunkPage {
|
|
||||||
padding: 24px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
// height: calc(100vh - 112px);
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
margin: 10px 0;
|
|
||||||
display: flex;
|
|
||||||
height: 32px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagePdfWrapper {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageWrapper {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageContent {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 12px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.spin {
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.documentPreview {
|
|
||||||
width: 40%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chunkContainer {
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh - 332px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chunkOtherContainer {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageFooter {
|
|
||||||
padding-top: 10px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
height: 100px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.context {
|
|
||||||
flex: 1;
|
|
||||||
// width: 207px;
|
|
||||||
height: 88px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
:global {
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
import { useFetchNextChunkList, useSwitchChunk } from '@/hooks/chunk-hooks';
|
|
||||||
import type { PaginationProps } from 'antd';
|
|
||||||
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import ChunkCard from './components/chunk-card';
|
|
||||||
import CreatingModal from './components/chunk-creating-modal';
|
|
||||||
import ChunkToolBar from './components/chunk-toolbar';
|
|
||||||
import DocumentPreview from './components/document-preview/preview';
|
|
||||||
import {
|
|
||||||
useChangeChunkTextMode,
|
|
||||||
useDeleteChunkByIds,
|
|
||||||
useGetChunkHighlights,
|
|
||||||
useHandleChunkCardClick,
|
|
||||||
useUpdateChunk,
|
|
||||||
} from './hooks';
|
|
||||||
|
|
||||||
import styles from './index.less';
|
|
||||||
|
|
||||||
const Chunk = () => {
|
|
||||||
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
|
|
||||||
const { removeChunk } = useDeleteChunkByIds();
|
|
||||||
const {
|
|
||||||
data: { documentInfo, data = [], total },
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
searchString,
|
|
||||||
handleInputChange,
|
|
||||||
available,
|
|
||||||
handleSetAvailable,
|
|
||||||
} = useFetchNextChunkList();
|
|
||||||
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
|
|
||||||
const isPdf = documentInfo?.type === 'pdf';
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { changeChunkTextMode, textMode } = useChangeChunkTextMode();
|
|
||||||
const { switchChunk } = useSwitchChunk();
|
|
||||||
const {
|
|
||||||
chunkUpdatingLoading,
|
|
||||||
onChunkUpdatingOk,
|
|
||||||
showChunkUpdatingModal,
|
|
||||||
hideChunkUpdatingModal,
|
|
||||||
chunkId,
|
|
||||||
chunkUpdatingVisible,
|
|
||||||
documentId,
|
|
||||||
} = useUpdateChunk();
|
|
||||||
|
|
||||||
const onPaginationChange: PaginationProps['onShowSizeChange'] = (
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
) => {
|
|
||||||
setSelectedChunkIds([]);
|
|
||||||
pagination.onChange?.(page, size);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectAllChunk = useCallback(
|
|
||||||
(checked: boolean) => {
|
|
||||||
setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []);
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSingleCheckboxClick = useCallback(
|
|
||||||
(chunkId: string, checked: boolean) => {
|
|
||||||
setSelectedChunkIds((previousIds) => {
|
|
||||||
const idx = previousIds.findIndex((x) => x === chunkId);
|
|
||||||
const nextIds = [...previousIds];
|
|
||||||
if (checked && idx === -1) {
|
|
||||||
nextIds.push(chunkId);
|
|
||||||
} else if (!checked && idx !== -1) {
|
|
||||||
nextIds.splice(idx, 1);
|
|
||||||
}
|
|
||||||
return nextIds;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showSelectedChunkWarning = useCallback(() => {
|
|
||||||
message.warning(t('message.pleaseSelectChunk'));
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const handleRemoveChunk = useCallback(async () => {
|
|
||||||
if (selectedChunkIds.length > 0) {
|
|
||||||
const resCode: number = await removeChunk(selectedChunkIds, documentId);
|
|
||||||
if (resCode === 0) {
|
|
||||||
setSelectedChunkIds([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showSelectedChunkWarning();
|
|
||||||
}
|
|
||||||
}, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]);
|
|
||||||
|
|
||||||
const handleSwitchChunk = useCallback(
|
|
||||||
async (available?: number, chunkIds?: string[]) => {
|
|
||||||
let ids = chunkIds;
|
|
||||||
if (!chunkIds) {
|
|
||||||
ids = selectedChunkIds;
|
|
||||||
if (selectedChunkIds.length === 0) {
|
|
||||||
showSelectedChunkWarning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resCode: number = await switchChunk({
|
|
||||||
chunk_ids: ids,
|
|
||||||
available_int: available,
|
|
||||||
doc_id: documentId,
|
|
||||||
});
|
|
||||||
if (!chunkIds && resCode === 0) {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[switchChunk, documentId, selectedChunkIds, showSelectedChunkWarning],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { highlights, setWidthAndHeight } =
|
|
||||||
useGetChunkHighlights(selectedChunkId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.chunkPage}>
|
|
||||||
<ChunkToolBar
|
|
||||||
selectAllChunk={selectAllChunk}
|
|
||||||
createChunk={showChunkUpdatingModal}
|
|
||||||
removeChunk={handleRemoveChunk}
|
|
||||||
checked={selectedChunkIds.length === data.length}
|
|
||||||
switchChunk={handleSwitchChunk}
|
|
||||||
changeChunkTextMode={changeChunkTextMode}
|
|
||||||
searchString={searchString}
|
|
||||||
handleInputChange={handleInputChange}
|
|
||||||
available={available}
|
|
||||||
handleSetAvailable={handleSetAvailable}
|
|
||||||
></ChunkToolBar>
|
|
||||||
<Divider></Divider>
|
|
||||||
<Flex flex={1} gap={'middle'}>
|
|
||||||
<Flex
|
|
||||||
vertical
|
|
||||||
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper}
|
|
||||||
>
|
|
||||||
<Spin spinning={loading} className={styles.spin} size="large">
|
|
||||||
<div className={styles.pageContent}>
|
|
||||||
<Space
|
|
||||||
direction="vertical"
|
|
||||||
size={'middle'}
|
|
||||||
className={classNames(styles.chunkContainer, {
|
|
||||||
[styles.chunkOtherContainer]: !isPdf,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{data.map((item) => (
|
|
||||||
<ChunkCard
|
|
||||||
item={item}
|
|
||||||
key={item.chunk_id}
|
|
||||||
editChunk={showChunkUpdatingModal}
|
|
||||||
checked={selectedChunkIds.some(
|
|
||||||
(x) => x === item.chunk_id,
|
|
||||||
)}
|
|
||||||
handleCheckboxClick={handleSingleCheckboxClick}
|
|
||||||
switchChunk={handleSwitchChunk}
|
|
||||||
clickChunkCard={handleChunkCardClick}
|
|
||||||
selected={item.chunk_id === selectedChunkId}
|
|
||||||
textMode={textMode}
|
|
||||||
></ChunkCard>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
<div className={styles.pageFooter}>
|
|
||||||
<Pagination
|
|
||||||
{...pagination}
|
|
||||||
total={total}
|
|
||||||
size={'small'}
|
|
||||||
onChange={onPaginationChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
{isPdf && (
|
|
||||||
<section className={styles.documentPreview}>
|
|
||||||
<DocumentPreview
|
|
||||||
highlights={highlights}
|
|
||||||
setWidthAndHeight={setWidthAndHeight}
|
|
||||||
></DocumentPreview>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
{chunkUpdatingVisible && (
|
|
||||||
<CreatingModal
|
|
||||||
doc_id={documentId}
|
|
||||||
chunkId={chunkId}
|
|
||||||
hideModal={hideChunkUpdatingModal}
|
|
||||||
visible={chunkUpdatingVisible}
|
|
||||||
loading={chunkUpdatingLoading}
|
|
||||||
onOk={onChunkUpdatingOk}
|
|
||||||
parserId={documentInfo.parser_id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Chunk;
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
export type FormListItem = {
|
|
||||||
frequency: number;
|
|
||||||
tag: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function transformTagFeaturesArrayToObject(
|
|
||||||
list: Array<FormListItem> = [],
|
|
||||||
) {
|
|
||||||
return list.reduce<Record<string, number>>((pre, cur) => {
|
|
||||||
pre[cur.tag] = cur.frequency;
|
|
||||||
|
|
||||||
return pre;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformTagFeaturesObjectToArray(
|
|
||||||
object: Record<string, number> = {},
|
|
||||||
) {
|
|
||||||
return Object.keys(object).reduce<Array<FormListItem>>((pre, key) => {
|
|
||||||
pre.push({ frequency: object[key], tag: key });
|
|
||||||
|
|
||||||
return pre;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
import { BaseNode } from '@/interfaces/database/agent';
|
import { BaseNode } from '@/interfaces/database/agent';
|
||||||
import { NodeProps, Position } from '@xyflow/react';
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -37,17 +38,18 @@ function ParserNode({
|
|||||||
isConnectableEnd={false}
|
isConnectableEnd={false}
|
||||||
></CommonHandle>
|
></CommonHandle>
|
||||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
<section className="space-y-2">
|
|
||||||
{data.form?.setups.map((x, idx) => (
|
<NodeCollapsible items={data.form?.setups}>
|
||||||
|
{(x, idx) => (
|
||||||
<LabelCard
|
<LabelCard
|
||||||
key={idx}
|
key={idx}
|
||||||
className="flex justify- flex-col text-text-primary gap-1"
|
className="flex flex-col text-text-primary gap-1"
|
||||||
>
|
>
|
||||||
<span className="text-text-secondary">Parser {idx + 1}</span>
|
<span className="text-text-secondary">Parser {idx + 1}</span>
|
||||||
{t(`dataflow.fileFormatOptions.${x.fileFormat}`)}
|
{t(`dataflow.fileFormatOptions.${x.fileFormat}`)}
|
||||||
</LabelCard>
|
</LabelCard>
|
||||||
))}
|
)}
|
||||||
</section>
|
</NodeCollapsible>
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
|||||||
import { INextOperatorForm } from '../../interface';
|
import { INextOperatorForm } from '../../interface';
|
||||||
import { buildOutputList } from '../../utils/build-output-list';
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
import { Output } from '../components/output';
|
import { Output } from '../components/output';
|
||||||
|
import { OutputFormatFormField } from './common-form-fields';
|
||||||
import { EmailFormFields } from './email-form-fields';
|
import { EmailFormFields } from './email-form-fields';
|
||||||
import { ImageFormFields } from './image-form-fields';
|
import { ImageFormFields } from './image-form-fields';
|
||||||
import { PdfFormFields } from './pdf-form-fields';
|
import { PdfFormFields } from './pdf-form-fields';
|
||||||
@ -146,10 +147,12 @@ function ParserItem({
|
|||||||
)}
|
)}
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
|
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
|
||||||
{/* <OutputFormatFormField
|
<div className="hidden">
|
||||||
prefix={prefix}
|
<OutputFormatFormField
|
||||||
fileType={fileFormat as FileType}
|
prefix={prefix}
|
||||||
/> */}
|
fileType={fileFormat as FileType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{index < fieldLength - 1 && <Separator />}
|
{index < fieldLength - 1 && <Separator />}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import {
|
|||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||||
|
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
|
|
||||||
export function AudioConfiguration() {
|
export function AudioConfiguration() {
|
||||||
return (
|
return (
|
||||||
<ConfigurationFormContainer>
|
<ConfigurationFormContainer>
|
||||||
@ -14,7 +12,7 @@ export function AudioConfiguration() {
|
|||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<TagItems></TagItems>
|
{/* <TagItems></TagItems> */}
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import {
|
import {
|
||||||
ConfigurationFormContainer,
|
ConfigurationFormContainer,
|
||||||
MainContainer,
|
MainContainer,
|
||||||
@ -20,9 +19,9 @@ export function BookConfiguration() {
|
|||||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
<ConfigurationFormContainer>
|
{/* <ConfigurationFormContainer>
|
||||||
<TagItems></TagItems>
|
<TagItems></TagItems>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer> */}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Radio } from '@/components/ui/radio';
|
import { Radio } from '@/components/ui/radio';
|
||||||
import { RAGFlowSelect } from '@/components/ui/select';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@ -46,7 +45,7 @@ export function ChunkMethodItem(props: IProps) {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className={line === 1 ? 'w-3/4 ' : 'w-full'}>
|
<div className={line === 1 ? 'w-3/4 ' : 'w-full'}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RAGFlowSelect
|
<SelectWithSearch
|
||||||
{...field}
|
{...field}
|
||||||
options={parserList}
|
options={parserList}
|
||||||
placeholder={t('chunkMethodPlaceholder')}
|
placeholder={t('chunkMethodPlaceholder')}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import {
|
|||||||
AutoKeywordsFormField,
|
AutoKeywordsFormField,
|
||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||||
|
|
||||||
export function EmailConfiguration() {
|
export function EmailConfiguration() {
|
||||||
@ -12,7 +11,7 @@ export function EmailConfiguration() {
|
|||||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</>
|
</>
|
||||||
<TagItems></TagItems>
|
{/* <TagItems></TagItems> */}
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import {
|
import {
|
||||||
ConfigurationFormContainer,
|
ConfigurationFormContainer,
|
||||||
MainContainer,
|
MainContainer,
|
||||||
@ -21,9 +20,9 @@ export function LawsConfiguration() {
|
|||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
|
|
||||||
<ConfigurationFormContainer>
|
{/* <ConfigurationFormContainer>
|
||||||
<TagItems></TagItems>
|
<TagItems></TagItems>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer> */}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import {
|
import {
|
||||||
ConfigurationFormContainer,
|
ConfigurationFormContainer,
|
||||||
MainContainer,
|
MainContainer,
|
||||||
@ -21,7 +20,7 @@ export function ManualConfiguration() {
|
|||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
|
|
||||||
<TagItems></TagItems>
|
{/* <TagItems></TagItems> */}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||||
|
|
||||||
export function OneConfiguration() {
|
export function OneConfiguration() {
|
||||||
@ -15,7 +14,7 @@ export function OneConfiguration() {
|
|||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<TagItems></TagItems>
|
{/* <TagItems></TagItems> */}
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import {
|
import {
|
||||||
ConfigurationFormContainer,
|
ConfigurationFormContainer,
|
||||||
MainContainer,
|
MainContainer,
|
||||||
@ -20,9 +19,9 @@ export function PaperConfiguration() {
|
|||||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
<ConfigurationFormContainer>
|
{/* <ConfigurationFormContainer>
|
||||||
<TagItems></TagItems>
|
<TagItems></TagItems>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer> */}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import {
|
|||||||
AutoKeywordsFormField,
|
AutoKeywordsFormField,
|
||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||||
|
|
||||||
export function PictureConfiguration() {
|
export function PictureConfiguration() {
|
||||||
@ -12,7 +11,7 @@ export function PictureConfiguration() {
|
|||||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</>
|
</>
|
||||||
<TagItems></TagItems>
|
{/* <TagItems></TagItems> */}
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
AutoQuestionsFormField,
|
AutoQuestionsFormField,
|
||||||
} from '@/components/auto-keywords-form-field';
|
} from '@/components/auto-keywords-form-field';
|
||||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import {
|
import {
|
||||||
ConfigurationFormContainer,
|
ConfigurationFormContainer,
|
||||||
MainContainer,
|
MainContainer,
|
||||||
@ -21,9 +20,9 @@ export function PresentationConfiguration() {
|
|||||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
|
|
||||||
<ConfigurationFormContainer>
|
{/* <ConfigurationFormContainer>
|
||||||
<TagItems></TagItems>
|
<TagItems></TagItems>
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer> */}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
|
||||||
|
|
||||||
export function QAConfiguration() {
|
export function QAConfiguration() {
|
||||||
return (
|
return (
|
||||||
<ConfigurationFormContainer>
|
<></>
|
||||||
<TagItems></TagItems>
|
// <ConfigurationFormContainer>
|
||||||
</ConfigurationFormContainer>
|
// <TagItems></TagItems>
|
||||||
|
// </ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { TagItems } from '../components/tag-item';
|
|
||||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
|
||||||
|
|
||||||
export function ResumeConfiguration() {
|
export function ResumeConfiguration() {
|
||||||
return (
|
return (
|
||||||
<ConfigurationFormContainer>
|
<></>
|
||||||
<TagItems></TagItems>
|
// <ConfigurationFormContainer>
|
||||||
</ConfigurationFormContainer>
|
// <TagItems></TagItems>
|
||||||
|
// </ConfigurationFormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
165
web/src/stories/node-collapsible.stories.tsx
Normal file
165
web/src/stories/node-collapsible.stories.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
|
const meta = {
|
||||||
|
title: 'Example/NodeCollapsible',
|
||||||
|
component: NodeCollapsible,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
NodeCollapsible is a specialized component for displaying collapsible content within nodes.
|
||||||
|
It automatically shows only the first 3 items and provides a toggle button to expand/collapse the rest.
|
||||||
|
The component is designed to work within the application's node-based UI, such as in agent or data flow canvases.
|
||||||
|
|
||||||
|
The toggle button is displayed as a small circle at the bottom center of the component when there are more than 3 items.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
items: {
|
||||||
|
control: 'object',
|
||||||
|
description: 'Array of items to display in the collapsible component',
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
control: false,
|
||||||
|
description: 'Function to render each item',
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Additional CSS classes to apply to the component',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NodeCollapsible>;
|
||||||
|
|
||||||
|
// Form wrapper decorator
|
||||||
|
const WithFormProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {},
|
||||||
|
resolver: zodResolver(z.object({})),
|
||||||
|
});
|
||||||
|
return <Form {...form}>{children}</Form>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withFormProvider = (Story: any) => (
|
||||||
|
<WithFormProvider>
|
||||||
|
<Story />
|
||||||
|
</WithFormProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
|
export const Default: Story = {
|
||||||
|
decorators: [withFormProvider],
|
||||||
|
args: {
|
||||||
|
items: [
|
||||||
|
'Document Analysis Parser',
|
||||||
|
'Web Search Parser',
|
||||||
|
'Database Query Parser',
|
||||||
|
'Image Recognition Parser',
|
||||||
|
'Audio Transcription Parser',
|
||||||
|
'Video Processing Parser',
|
||||||
|
'Code Analysis Parser',
|
||||||
|
'Spreadsheet Parser',
|
||||||
|
],
|
||||||
|
children: (item: string) => (
|
||||||
|
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
By default, the NodeCollapsible component shows the first 3 items and collapses the rest.
|
||||||
|
A toggle button appears at the bottom when there are more than 3 items.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
|
|
||||||
|
<NodeCollapsible
|
||||||
|
items={[
|
||||||
|
'Document Analysis Parser',
|
||||||
|
'Web Search Parser',
|
||||||
|
'Database Query Parser',
|
||||||
|
'Image Recognition Parser',
|
||||||
|
'Audio Transcription Parser',
|
||||||
|
'Video Processing Parser',
|
||||||
|
'Code Analysis Parser',
|
||||||
|
'Spreadsheet Parser'
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<div className="px-4 py-2 border rounded-md bg-bg-component">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NodeCollapsible>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithFewItems: Story = {
|
||||||
|
decorators: [withFormProvider],
|
||||||
|
args: {
|
||||||
|
items: ['Single Item'],
|
||||||
|
children: (item: string) => (
|
||||||
|
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
When there are 3 or fewer items, no toggle button is shown.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithManyItems: Story = {
|
||||||
|
decorators: [withFormProvider],
|
||||||
|
args: {
|
||||||
|
items: [
|
||||||
|
'Item 1',
|
||||||
|
'Item 2',
|
||||||
|
'Item 3',
|
||||||
|
'Item 4',
|
||||||
|
'Item 5',
|
||||||
|
'Item 6',
|
||||||
|
'Item 7',
|
||||||
|
'Item 8',
|
||||||
|
],
|
||||||
|
children: (item: string) => (
|
||||||
|
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
When there are more than 3 items, a toggle button is shown at the bottom center.
|
||||||
|
Clicking it will expand to show all items.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user