Compare commits

..

9 Commits

Author SHA1 Message Date
d874683ae4 Fix the bug in enablePrologue under agent task mode (#9487)
### What problem does this PR solve?

There is a problem with the implementation of the Agent begin-form:
although the enablePrologue switch and the prologue input box are hidden
in Task mode, these values are still saved in the form data. If the user
first enables the opening and sets the content in Conversational mode,
and then switches to Task mode, these values will still be saved and may
be used in some scenarios.
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-15 20:29:02 +08:00
f9e5caa8ed feat(search): Added app embedding functionality and optimized search page #3221 (#9499)
### What problem does this PR solve?
feat(search): Added app embedding functionality and optimized search
page #3221

- Added an Embed App button and related functionality
- Optimized the layout and interaction of the search settings interface
- Adjusted the search result display method
- Refactored some code to support new features
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-15 18:25:00 +08:00
99df0766fe Feat: add SMTP support for user invitation emails (#9479)
### What problem does this PR solve?

Add SMTP support for user invitation emails

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-15 18:12:20 +08:00
3b50688228 Docs: Miscellaneous updates. (#9506)
### What problem does this PR solve?

### Type of change

- [x] Documentation Update
2025-08-15 18:10:11 +08:00
ffc095bd50 Feat: conversation completion can specify different model (#9485)
### What problem does this PR solve?

Conversation completion can specify different model

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-15 17:44:58 +08:00
799c57287c Feat: Add metadata configuration for new chats #3221 (#9502)
### What problem does this PR solve?

Feat: Add metadata configuration for new chats #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-15 17:40:16 +08:00
eef43fa25c Fix: unexpected truncated Excel files (#9500)
### What problem does this PR solve?

Handle unexpected truncated Excel files.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-15 17:00:34 +08:00
5a4dfecfbe Refactor:Standardize image conf and add private registry support (#9496)
- Unified configuration format: All services now use the same image
configuration structure for consistency.

- Private registry support: Added imagePullSecrets to enable pulling
images from private registries.

- Per-service flexibility: Each service can override image-related
parameters independently.

### What problem does this PR solve?

_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):
2025-08-15 16:05:33 +08:00
7f237fee16 Fix:HTTPs component re.error: bad escape \u (#9480)
### What problem does this PR solve?

When calling HTTP to request data, if the JSON string returned by the
interface contains an unasked back slash like '\u', Python's RE module
will escape 'u' as Unicode, but there is no valid 4-digit hexadecimal
number at the end, so it will directly report an error. Error: re.
error: bad escape \ u at position 26
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-15 15:48:10 +08:00
65 changed files with 1967 additions and 764 deletions

View File

@ -529,8 +529,12 @@ class ComponentBase(ABC):
@staticmethod
def string_format(content: str, kv: dict[str, str]) -> str:
for n, v in kv.items():
def repl(_match, val=v):
return str(val) if val is not None else ""
content = re.sub(
r"\{%s\}" % re.escape(n), v, content
r"\{%s\}" % re.escape(n),
repl,
content
)
return content

View File

@ -29,6 +29,7 @@ from api.db.db_models import close_connection
from api.db.services import UserService
from api.utils import CustomJSONEncoder, commands
from flask_mail import Mail
from flask_session import Session
from flask_login import LoginManager
from api import settings
@ -40,6 +41,7 @@ __all__ = ["app"]
Request.json = property(lambda self: self.get_json(force=True, silent=True))
app = Flask(__name__)
smtp_mail_server = Mail()
# Add this at the beginning of your file to configure Swagger UI
swagger_config = {
@ -146,16 +148,16 @@ def load_user(web_request):
if authorization:
try:
access_token = str(jwt.loads(authorization))
if not access_token or not access_token.strip():
logging.warning("Authentication attempt with empty access token")
return None
# Access tokens should be UUIDs (32 hex characters)
if len(access_token.strip()) < 32:
logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
return None
user = UserService.query(
access_token=access_token, status=StatusEnum.VALID.value
)

View File

@ -29,7 +29,8 @@ from api.db.services.conversation_service import ConversationService, structure_
from api.db.services.dialog_service import DialogService, ask, chat
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.llm_service import LLMBundle
from api.db.services.user_service import UserTenantService, TenantService
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.user_service import TenantService, UserTenantService
from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request
from graphrag.general.mind_map_extractor import MindMapExtractor
from rag.app.tag import label_question
@ -66,8 +67,14 @@ def set_conversation():
e, dia = DialogService.get_by_id(req["dialog_id"])
if not e:
return get_data_error_result(message="Dialog not found")
conv = {"id": conv_id, "dialog_id": req["dialog_id"], "name": name, "message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}],"user_id": current_user.id,
"reference":[],}
conv = {
"id": conv_id,
"dialog_id": req["dialog_id"],
"name": name,
"message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}],
"user_id": current_user.id,
"reference": [],
}
ConversationService.save(**conv)
return get_json_result(data=conv)
except Exception as e:
@ -174,6 +181,21 @@ def completion():
continue
msg.append(m)
message_id = msg[-1].get("id")
chat_model_id = req.get("llm_id", "")
req.pop("llm_id", None)
chat_model_config = {}
for model_config in [
"temperature",
"top_p",
"frequency_penalty",
"presence_penalty",
"max_tokens",
]:
config = req.get(model_config)
if config:
chat_model_config[model_config] = config
try:
e, conv = ConversationService.get_by_id(req["conversation_id"])
if not e:
@ -190,13 +212,23 @@ def completion():
conv.reference = [r for r in conv.reference if r]
conv.reference.append({"chunks": [], "doc_aggs": []})
if chat_model_id:
if not TenantLLMService.get_api_key(tenant_id=dia.tenant_id, model_name=chat_model_id):
req.pop("chat_model_id", None)
req.pop("chat_model_config", None)
return get_data_error_result(message=f"Cannot use specified model {chat_model_id}.")
dia.llm_id = chat_model_id
dia.llm_setting = chat_model_config
is_embedded = bool(chat_model_id)
def stream():
nonlocal dia, msg, req, conv
try:
for ans in chat(dia, msg, True, **req):
ans = structure_answer(conv, ans, message_id, conv.id)
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
ConversationService.update_by_id(conv.id, conv.to_dict())
if not is_embedded:
ConversationService.update_by_id(conv.id, conv.to_dict())
except Exception as e:
traceback.print_exc()
yield "data:" + json.dumps({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "reference": []}}, ensure_ascii=False) + "\n\n"
@ -214,7 +246,8 @@ def completion():
answer = None
for ans in chat(dia, msg, **req):
answer = structure_answer(conv, ans, message_id, conv.id)
ConversationService.update_by_id(conv.id, conv.to_dict())
if not is_embedded:
ConversationService.update_by_id(conv.id, conv.to_dict())
break
return get_json_result(data=answer)
except Exception as e:

View File

@ -18,12 +18,14 @@ from flask import request
from flask_login import login_required, current_user
from api import settings
from api.apps import smtp_mail_server
from api.db import UserTenantRole, StatusEnum
from api.db.db_models import UserTenant
from api.db.services.user_service import UserTenantService, UserService
from api.utils import get_uuid, delta_seconds
from api.utils.api_utils import get_json_result, validate_request, server_error_response, get_data_error_result
from api.utils.web_utils import send_invite_email
@manager.route("/<tenant_id>/user/list", methods=["GET"]) # noqa: F821
@ -78,6 +80,20 @@ def create(tenant_id):
role=UserTenantRole.INVITE,
status=StatusEnum.VALID.value)
if smtp_mail_server and settings.SMTP_CONF:
from threading import Thread
user_name = ""
_, user = UserService.get_by_id(current_user.id)
if user:
user_name = user.nickname
Thread(
target=send_invite_email,
args=(invite_user_email, settings.MAIL_FRONTEND_URL, tenant_id, user_name or current_user.email),
daemon=True
).start()
usr = invite_users[0].to_dict()
usr = {k: v for k, v in usr.items() if k in ["id", "avatar", "email", "nickname"]}

View File

@ -881,11 +881,12 @@ class Search(DataBaseModel):
# chat settings
"summary": False,
"chat_id": "",
# Leave it here for reference, don't need to set default values
"llm_setting": {
"temperature": 0.1,
"top_p": 0.3,
"frequency_penalty": 0.7,
"presence_penalty": 0.4,
# "temperature": 0.1,
# "top_p": 0.3,
# "frequency_penalty": 0.7,
# "presence_penalty": 0.4,
},
"chat_settingcross_languages": [],
"highlight": False,
@ -1020,4 +1021,4 @@ def migrate_db():
migrate(migrator.add_column("dialog", "meta_data_filter", JSONField(null=True, default={})))
except Exception:
pass
logging.disable(logging.NOTSET)
logging.disable(logging.NOTSET)

View File

@ -99,7 +99,6 @@ class DialogService(CommonService):
return list(chats.dicts())
@classmethod
@DB.connection_context()
def get_by_tenant_ids(cls, joined_tenant_ids, user_id, page_number, items_per_page, orderby, desc, keywords, parser_id=None):
@ -256,9 +255,10 @@ def repair_bad_citation_formats(answer: str, kbinfos: dict, idx: set):
def meta_filter(metas: dict, filters: list[dict]):
doc_ids = []
def filter_out(v2docs, operator, value):
nonlocal doc_ids
for input,docids in v2docs.items():
for input, docids in v2docs.items():
try:
input = float(input)
value = float(value)
@ -389,7 +389,17 @@ def chat(dialog, messages, stream=True, **kwargs):
reasoner = DeepResearcher(
chat_mdl,
prompt_config,
partial(retriever.retrieval, embd_mdl=embd_mdl, tenant_ids=tenant_ids, kb_ids=dialog.kb_ids, page=1, page_size=dialog.top_n, similarity_threshold=0.2, vector_similarity_weight=0.3, doc_ids=attachments),
partial(
retriever.retrieval,
embd_mdl=embd_mdl,
tenant_ids=tenant_ids,
kb_ids=dialog.kb_ids,
page=1,
page_size=dialog.top_n,
similarity_threshold=0.2,
vector_similarity_weight=0.3,
doc_ids=attachments,
),
)
for think in reasoner.thinking(kbinfos, " ".join(questions)):

View File

@ -33,7 +33,7 @@ import uuid
from werkzeug.serving import run_simple
from api import settings
from api.apps import app
from api.apps import app, smtp_mail_server
from api.db.runtime_config import RuntimeConfig
from api.db.services.document_service import DocumentService
from api import utils
@ -74,11 +74,11 @@ def signal_handler(sig, frame):
if __name__ == '__main__':
logging.info(r"""
____ ___ ______ ______ __
____ ___ ______ ______ __
/ __ \ / | / ____// ____// /____ _ __
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
""")
logging.info(
@ -137,6 +137,18 @@ if __name__ == '__main__':
else:
threading.Timer(1.0, delayed_start_update_progress).start()
# init smtp server
if settings.SMTP_CONF:
app.config["MAIL_SERVER"] = settings.MAIL_SERVER
app.config["MAIL_PORT"] = settings.MAIL_PORT
app.config["MAIL_USE_SSL"] = settings.MAIL_USE_SSL
app.config["MAIL_USE_TLS"] = settings.MAIL_USE_TLS
app.config["MAIL_USERNAME"] = settings.MAIL_USERNAME
app.config["MAIL_PASSWORD"] = settings.MAIL_PASSWORD
app.config["MAIL_DEFAULT_SENDER"] = settings.MAIL_DEFAULT_SENDER
smtp_mail_server.init_app(app)
# start http server
try:
logging.info("RAGFlow HTTP server start...")

View File

@ -79,6 +79,16 @@ STRONG_TEST_COUNT = int(os.environ.get("STRONG_TEST_COUNT", "8"))
BUILTIN_EMBEDDING_MODELS = ["BAAI/bge-large-zh-v1.5@BAAI", "maidalun1020/bce-embedding-base_v1@Youdao"]
SMTP_CONF = None
MAIL_SERVER = ""
MAIL_PORT = 000
MAIL_USE_SSL= True
MAIL_USE_TLS = False
MAIL_USERNAME = ""
MAIL_PASSWORD = ""
MAIL_DEFAULT_SENDER = ()
MAIL_FRONTEND_URL = ""
def get_or_create_secret_key():
secret_key = os.environ.get("RAGFLOW_SECRET_KEY")
@ -186,6 +196,21 @@ def init_settings():
global SANDBOX_HOST
SANDBOX_HOST = os.environ.get("SANDBOX_HOST", "sandbox-executor-manager")
global SMTP_CONF, MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, MAIL_USE_TLS
global MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, MAIL_FRONTEND_URL
SMTP_CONF = get_base_config("smtp", {})
MAIL_SERVER = SMTP_CONF.get("mail_server", "")
MAIL_PORT = SMTP_CONF.get("mail_port", 000)
MAIL_USE_SSL = SMTP_CONF.get("mail_use_ssl", True)
MAIL_USE_TLS = SMTP_CONF.get("mail_use_tls", False)
MAIL_USERNAME = SMTP_CONF.get("mail_username", "")
MAIL_PASSWORD = SMTP_CONF.get("mail_password", "")
mail_default_sender = SMTP_CONF.get("mail_default_sender", [])
if mail_default_sender and len(mail_default_sender) >= 2:
MAIL_DEFAULT_SENDER = (mail_default_sender[0], mail_default_sender[1])
MAIL_FRONTEND_URL = SMTP_CONF.get("mail_frontend_url", "")
class CustomEnum(Enum):
@classmethod

View File

@ -21,6 +21,9 @@ import re
import socket
from urllib.parse import urlparse
from api.apps import smtp_mail_server
from flask_mail import Message
from flask import render_template_string
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.chrome.options import Options
@ -31,6 +34,7 @@ from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
CONTENT_TYPE_MAP = {
# Office
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@ -172,3 +176,26 @@ def get_float(req: dict, key: str, default: float | int = 10.0) -> float:
return parsed if parsed > 0 else default
except (TypeError, ValueError):
return default
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>
"""
def send_invite_email(to_email, invite_url, tenant_id, inviter):
from api.apps import app
with app.app_context():
msg = Message(subject="RAGFlow Invitation",
recipients=[to_email])
msg.html = render_template_string(
INVITE_EMAIL_TMPL,
email=to_email,
invite_url=invite_url,
tenant_id=tenant_id,
inviter=inviter,
)
smtp_mail_server.send(msg)

View File

@ -113,3 +113,14 @@ redis:
# switch: false
# component: false
# dataset: false
# smtp:
# mail_server: ""
# mail_port: 465
# mail_use_ssl: true
# mail_use_tls: false
# mail_username: ""
# mail_password: ""
# mail_default_sender:
# - "RAGFlow" # display name
# - "" # sender email address
# mail_frontend_url: "https://your-frontend.example.com"

View File

@ -90,9 +90,17 @@ class RAGFlowExcelParser:
return wb
def html(self, fnm, chunk_rows=256):
from html import escape
file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm
wb = RAGFlowExcelParser._load_excel_to_workbook(file_like_object)
tb_chunks = []
def _fmt(v):
if v is None:
return ""
return str(v).strip()
for sheetname in wb.sheetnames:
ws = wb[sheetname]
rows = list(ws.rows)
@ -101,7 +109,7 @@ class RAGFlowExcelParser:
tb_rows_0 = "<tr>"
for t in list(rows[0]):
tb_rows_0 += f"<th>{t.value}</th>"
tb_rows_0 += f"<th>{escape(_fmt(t.value))}</th>"
tb_rows_0 += "</tr>"
for chunk_i in range((len(rows) - 1) // chunk_rows + 1):
@ -109,7 +117,7 @@ class RAGFlowExcelParser:
tb += f"<table><caption>{sheetname}</caption>"
tb += tb_rows_0
for r in list(
rows[1 + chunk_i * chunk_rows: 1 + (chunk_i + 1) * chunk_rows]
rows[1 + chunk_i * chunk_rows: min(1 + (chunk_i + 1) * chunk_rows, len(rows))]
):
tb += "<tr>"
for i, c in enumerate(r):

View File

@ -5,7 +5,7 @@ slug: /http_api_reference
# HTTP API
A complete reference for RAGFlow's RESTful API. Before proceeding, please ensure you [have your RAGFlow API key ready for authentication](../guides/models/llm_api_key_setup.md).
A complete reference for RAGFlow's RESTful API. Before proceeding, please ensure you [have your RAGFlow API key ready for authentication](../develop/acquire_ragflow_api_key.md).
---
@ -191,13 +191,13 @@ curl --request POST \
##### Request Parameters
- `model` (*Body parameter*) `string`, *Required*
- `model` (*Body parameter*) `string`, *Required*
The model used to generate the response. The server will parse this automatically, so you can set it to any value for now.
- `messages` (*Body parameter*) `list[object]`, *Required*
- `messages` (*Body parameter*) `list[object]`, *Required*
A list of historical chat messages used to generate the response. This must contain at least one message with the `user` role.
- `stream` (*Body parameter*) `boolean`
- `stream` (*Body parameter*) `boolean`
Whether to receive the response as a stream. Set this to `false` explicitly if you prefer to receive the entire response in one go instead of as a stream.
#### Response
@ -2675,7 +2675,7 @@ curl --request POST \
- `agent_id`: (*Path parameter*)
The ID of the associated agent.
- `user_id`: (*Filter parameter*)
- `user_id`: (*Filter parameter*)
The optional user-defined ID for parsing docs (especially images) when creating a session while uploading files.
#### Response
@ -2755,7 +2755,7 @@ Success:
"mode": "conversational",
"outputs": {},
"prologue": "Hi! I'm your assistant. What can I do for you?",
"tips": "Please fill up the form"
"tips": "Please fill in the form"
}
},
"upstream": []
@ -2912,17 +2912,17 @@ Asks a specified agent a question to start an AI-powered conversation.
- Body:
- `"question"`: `string`
- `"stream"`: `boolean`
- `"session_id"`: `string`(optional)
- `"inputs"`: `object`(optional)
- `"user_id"`: `string`(optional)
- `"session_id"`: `string` (optional)
- `"inputs"`: `object` (optional)
- `"user_id"`: `string` (optional)
:::info IMPORTANT
You can include custom parameters in the request body, but first ensure they are defined in the [Begin](../guides/agent/agent_component_reference/begin.mdx) agent component.
You can include custom parameters in the request body, but first ensure they are defined in the [Begin](../guides/agent/agent_component_reference/begin.mdx) component.
:::
##### Request example
- If the **Begin** component does not take parameters.
- If the **Begin** component does not take parameters:
```bash
curl --request POST \
@ -2936,7 +2936,7 @@ curl --request POST \
}'
```
- If the **Begin** component takes parameters.
- If the **Begin** component takes parameters, include their values in the body of `"inputs"` as follows:
```bash
curl --request POST \

View File

@ -5,7 +5,7 @@ slug: /python_api_reference
# Python API
A complete reference for RAGFlow's Python APIs. Before proceeding, please ensure you [have your RAGFlow API key ready for authentication](../guides/models/llm_api_key_setup.md).
A complete reference for RAGFlow's Python APIs. Before proceeding, please ensure you [have your RAGFlow API key ready for authentication](../develop/acquire_ragflow_api_key.md).
:::tip NOTE
Run the following command to download the Python SDK:

View File

@ -22,6 +22,36 @@ The embedding models included in a full edition are:
These two embedding models are optimized specifically for English and Chinese, so performance may be compromised if you use them to embed documents in other languages.
:::
## v0.20.2 (Ongoing🔨)
Released on August ??, 2025.
### Improvements
- Revamps the user interface for the **Datasets**, **Chat**, and **Search** pages.
- Search: Supports creating search apps tailored to various business scenarios
- Chat: Supports comparing answer performance of up to three chat model settings on a single **Chat** page.
- Agent:
- Implements a toggle in the **Agent** component to enable or disable citation.
- Introduces a drag-and-drop method for creating components.
- Documentation: Corrects inaccuracies in the API reference.
### New Agent templates
- Report Agent: A template for generating summary reports in internal question-answering scenarios, supporting the display of tables and formulae. [#9427](https://github.com/infiniflow/ragflow/pull/9427)
### Fixed issues
- Predefined opening greeting in the **Agent** component was missing during conversations.
- An automatic line break issue in the prompt editor.
- A memory leak issue caused by PyPDF. [#9469](https://github.com/infiniflow/ragflow/pull/9469)
### API changes
#### Deprecated
[Create session with agent](./references/http_api_reference.md#create-session-with-agent)
## v0.20.1
Released on August 8, 2025.
@ -182,7 +212,7 @@ From this release onwards, if you still see RAGFlow's responses being cut short
- Unable to add models via Ollama/Xinference, an issue introduced in v0.17.1.
### Related APIs
### API changes
#### HTTP APIs
@ -243,7 +273,7 @@ The following is a screenshot of a conversation that integrates Deep Research:
![Image](https://github.com/user-attachments/assets/165b88ff-1f5d-4fb8-90e2-c836b25e32e9)
### Related APIs
### API changes
#### HTTP APIs
@ -318,7 +348,7 @@ This release fixes the following issues:
- Using the **Table** parsing method results in information loss.
- Miscellaneous API issues.
### Related APIs
### API changes
#### HTTP APIs
@ -354,7 +384,7 @@ Released on December 18, 2024.
- Upgrades the Document Layout Analysis model in DeepDoc.
- Significantly enhances the retrieval performance when using [Infinity](https://github.com/infiniflow/infinity) as document engine.
### Related APIs
### API changes
#### HTTP APIs
@ -411,7 +441,7 @@ This approach eliminates the need to manually update **service_config.yaml** aft
Ensure that you [upgrade **both** your code **and** Docker image to this release](https://ragflow.io/docs/dev/upgrade_ragflow#upgrade-ragflow-to-the-most-recent-officially-published-release) before trying this new approach.
:::
### Related APIs
### API changes
#### HTTP APIs
@ -570,7 +600,7 @@ While we also test RAGFlow on ARM64 platforms, we do not maintain RAGFlow Docker
If you are on an ARM platform, follow [this guide](./develop/build_docker_image.mdx) to build a RAGFlow Docker image.
:::
### Related APIs
### API changes
#### HTTP API
@ -591,7 +621,7 @@ Released on May 21, 2024.
- Supports monitoring of system components, including Elasticsearch, MySQL, Redis, and MinIO.
- Supports disabling **Layout Recognition** in the GENERAL chunking method to reduce file chunking time.
### Related APIs
### API changes
#### HTTP API

View File

@ -44,9 +44,21 @@ spec:
checksum/config-es: {{ include (print $.Template.BasePath "/elasticsearch-config.yaml") . | sha256sum }}
checksum/config-env: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.elasticsearch.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.elasticsearch.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
initContainers:
- name: fix-data-volume-permissions
image: alpine
image: {{ .Values.elasticsearch.initContainers.alpine.repository }}:{{ .Values.elasticsearch.initContainers.alpine.tag }}
{{- with .Values.elasticsearch.initContainers.alpine.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
command:
- sh
- -c
@ -55,14 +67,20 @@ spec:
- mountPath: /usr/share/elasticsearch/data
name: es-data
- name: sysctl
image: busybox
image: {{ .Values.elasticsearch.initContainers.busybox.repository }}:{{ .Values.elasticsearch.initContainers.busybox.tag }}
{{- with .Values.elasticsearch.initContainers.busybox.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
securityContext:
privileged: true
runAsUser: 0
command: ["sysctl", "-w", "vm.max_map_count=262144"]
containers:
- name: elasticsearch
image: elasticsearch:{{ .Values.env.STACK_VERSION }}
image: {{ .Values.elasticsearch.image.repository }}:{{ .Values.elasticsearch.image.tag }}
{{- with .Values.elasticsearch.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "ragflow.fullname" . }}-env-config

View File

@ -43,9 +43,21 @@ spec:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.infinity.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.infinity.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: infinity
image: {{ .Values.infinity.image.repository }}:{{ .Values.infinity.image.tag }}
{{- with .Values.infinity.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "ragflow.fullname" . }}-env-config

View File

@ -43,9 +43,21 @@ spec:
{{- include "ragflow.labels" . | nindent 8 }}
app.kubernetes.io/component: minio
spec:
{{- if or .Values.imagePullSecrets .Values.minio.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.minio.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: minio
image: {{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}
{{- with .Values.minio.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "ragflow.fullname" . }}-env-config

View File

@ -44,9 +44,21 @@ spec:
checksum/config-mysql: {{ include (print $.Template.BasePath "/mysql-config.yaml") . | sha256sum }}
checksum/config-env: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.mysql.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.mysql.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: mysql
image: {{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}
{{- with .Values.mysql.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "ragflow.fullname" . }}-env-config

View File

@ -44,9 +44,21 @@ spec:
checksum/config-opensearch: {{ include (print $.Template.BasePath "/opensearch-config.yaml") . | sha256sum }}
checksum/config-env: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.opensearch.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.opensearch.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
initContainers:
- name: fix-data-volume-permissions
image: alpine
image: {{ .Values.opensearch.initContainers.alpine.repository }}:{{ .Values.opensearch.initContainers.alpine.tag }}
{{- with .Values.opensearch.initContainers.alpine.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
command:
- sh
- -c
@ -55,7 +67,10 @@ spec:
- mountPath: /usr/share/opensearch/data
name: opensearch-data
- name: sysctl
image: busybox
image: {{ .Values.opensearch.initContainers.busybox.repository }}:{{ .Values.opensearch.initContainers.busybox.tag }}
{{- with .Values.opensearch.initContainers.busybox.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
securityContext:
privileged: true
runAsUser: 0
@ -63,6 +78,9 @@ spec:
containers:
- name: opensearch
image: {{ .Values.opensearch.image.repository }}:{{ .Values.opensearch.image.tag }}
{{- with .Values.opensearch.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "ragflow.fullname" . }}-env-config

View File

@ -25,9 +25,21 @@ spec:
checksum/config-env: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
checksum/config-ragflow: {{ include (print $.Template.BasePath "/ragflow_config.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.ragflow.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ragflow.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: ragflow
image: {{ .Values.env.RAGFLOW_IMAGE }}
image: {{ .Values.ragflow.image.repository }}:{{ .Values.ragflow.image.tag }}
{{- with .Values.ragflow.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
ports:
- containerPort: 80
name: http

View File

@ -40,10 +40,22 @@ spec:
annotations:
checksum/config-env: {{ include (print $.Template.BasePath "/env.yaml") . | sha256sum }}
spec:
{{- if or .Values.imagePullSecrets .Values.redis.image.pullSecrets }}
imagePullSecrets:
{{- with .Values.imagePullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.redis.image.pullSecrets }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
terminationGracePeriodSeconds: 60
containers:
- name: redis
image: {{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}
{{- with .Values.redis.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
command:
- "sh"
- "-c"

View File

@ -1,4 +1,8 @@
# Based on docker compose .env file
# Global image pull secrets configuration
imagePullSecrets: []
env:
# The type of doc engine to use.
# Available options:
@ -32,31 +36,6 @@ env:
# The password for Redis
REDIS_PASSWORD: infini_rag_flow_helm
# The RAGFlow Docker image to download.
# Defaults to the v0.20.1-slim edition, which is the RAGFlow Docker image without embedding models.
RAGFLOW_IMAGE: infiniflow/ragflow:v0.20.1-slim
#
# To download the RAGFlow Docker image with embedding models, uncomment the following line instead:
# RAGFLOW_IMAGE: infiniflow/ragflow:v0.20.1
#
# The Docker image of the v0.20.1 edition includes:
# - Built-in embedding models:
# - BAAI/bge-large-zh-v1.5
# - BAAI/bge-reranker-v2-m3
# - maidalun1020/bce-embedding-base_v1
# - maidalun1020/bce-reranker-base_v1
# - Embedding models that will be downloaded once you select them in the RAGFlow UI:
# - BAAI/bge-base-en-v1.5
# - BAAI/bge-large-en-v1.5
# - BAAI/bge-small-en-v1.5
# - BAAI/bge-small-zh-v1.5
# - jinaai/jina-embeddings-v2-base-en
# - jinaai/jina-embeddings-v2-small-en
# - nomic-ai/nomic-embed-text-v1.5
# - sentence-transformers/all-MiniLM-L6-v2
#
#
# The local time zone.
TIMEZONE: "Asia/Shanghai"
@ -75,7 +54,11 @@ env:
EMBEDDING_BATCH_SIZE: 16
ragflow:
image:
repository: infiniflow/ragflow
tag: v0.20.1-slim
pullPolicy: IfNotPresent
pullSecrets: []
# Optional service configuration overrides
# to be written to local.service_conf.yaml
# inside the RAGFlow container
@ -114,6 +97,8 @@ infinity:
image:
repository: infiniflow/infinity
tag: v0.6.0-dev5
pullPolicy: IfNotPresent
pullSecrets: []
storage:
className:
capacity: 5Gi
@ -124,6 +109,20 @@ infinity:
type: ClusterIP
elasticsearch:
image:
repository: elasticsearch
tag: "8.11.3"
pullPolicy: IfNotPresent
pullSecrets: []
initContainers:
alpine:
repository: alpine
tag: latest
pullPolicy: IfNotPresent
busybox:
repository: busybox
tag: latest
pullPolicy: IfNotPresent
storage:
className:
capacity: 20Gi
@ -140,6 +139,17 @@ opensearch:
image:
repository: opensearchproject/opensearch
tag: 2.19.1
pullPolicy: IfNotPresent
pullSecrets: []
initContainers:
alpine:
repository: alpine
tag: latest
pullPolicy: IfNotPresent
busybox:
repository: busybox
tag: latest
pullPolicy: IfNotPresent
storage:
className:
capacity: 20Gi
@ -156,6 +166,8 @@ minio:
image:
repository: quay.io/minio/minio
tag: RELEASE.2023-12-20T01-00-02Z
pullPolicy: IfNotPresent
pullSecrets: []
storage:
className:
capacity: 5Gi
@ -169,6 +181,8 @@ mysql:
image:
repository: mysql
tag: 8.0.39
pullPolicy: IfNotPresent
pullSecrets: []
storage:
className:
capacity: 5Gi
@ -182,6 +196,8 @@ redis:
image:
repository: valkey/valkey
tag: 8
pullPolicy: IfNotPresent
pullSecrets: []
storage:
className:
capacity: 5Gi

View File

@ -130,6 +130,7 @@ dependencies = [
"click>=8.1.8",
"python-calamine>=0.4.0",
"litellm>=1.74.15.post1",
"flask-mail>=0.10.0",
]
[project.optional-dependencies]

View File

@ -490,6 +490,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
sections = [(_, "") for _ in excel_parser.html(binary, 12) if _]
else:
sections = [(_, "") for _ in excel_parser(binary) if _]
parser_config["chunk_token_num"] = 12800
elif re.search(r"\.(txt|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|sql)$", filename, re.IGNORECASE):
callback(0.1, "Start to parse.")

26
uv.lock generated
View File

@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.10, <3.13"
resolution-markers = [
"python_full_version >= '3.12' and sys_platform == 'darwin'",
@ -1189,9 +1190,6 @@ name = "datrie"
version = "0.8.2"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9d/fe/db74bd405d515f06657f11ad529878fd389576dca4812bea6f98d9b31574/datrie-0.8.2.tar.gz", hash = "sha256:525b08f638d5cf6115df6ccd818e5a01298cd230b2dac91c8ff2e6499d18765d" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/44/02/53f0cf0bf0cd629ba6c2cc13f2f9db24323459e9c19463783d890a540a96/datrie-0.8.2-pp273-pypy_73-win32.whl", hash = "sha256:b07bd5fdfc3399a6dab86d6e35c72b1dbd598e80c97509c7c7518ab8774d3fda" },
]
[[package]]
name = "debugpy"
@ -1653,6 +1651,19 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d" },
]
[[package]]
name = "flask-mail"
version = "0.10.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "blinker" },
{ name = "flask" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ba/29/e92dc84c675d1e8d260d5768eb3fb65c70cbd33addecf424187587bee862/flask_mail-0.10.0.tar.gz", hash = "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/c0/a81083da779f482494d49195d8b6c9fde21072558253e4a9fb2ec969c3c1/flask_mail-0.10.0-py3-none-any.whl", hash = "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" },
]
[[package]]
name = "flask-session"
version = "0.8.0"
@ -4664,8 +4675,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886" },
{ url = "https://mirrors.aliyun.com/pypi/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/7c/f5b0556590e7b4e710509105e668adb55aa9470a9f0e4dea9c40a4a11ce1/pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56" },
{ url = "https://mirrors.aliyun.com/pypi/packages/33/38/dcc795578d610ea1aaffef4b148b8cafcfcf4d126b1e58231ddc4e475c70/pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379" },
{ url = "https://mirrors.aliyun.com/pypi/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630" },
@ -4689,8 +4698,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/48/7d/0f2b09490b98cc6a902ac15dda8760c568b9c18cfe70e0ef7a16de64d53a/pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/1c/375adb14b71ee1c8d8232904e928b3e7af5bbbca7c04e4bec94fe8e90c3d/pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/e8/1b92184ab7e5595bf38000587e6f8cf9556ebd1bf0a583619bee2057afbd/pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e7/c5/9140bb867141d948c8e242013ec8a8011172233c898dfdba0a2417c3169a/pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/6a/04acb4978ce08ab16890c70611ebc6efd251681341617bbb9e53356dee70/pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/eb/df/3f1ea084e43b91e6d2b6b3493cc948864c17ea5d93ff1261a03812fbfd1a/pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/f3/83ffbdfa0c8f9154bcd8866895f6cae5a3ec749da8b0840603cf936c4412/pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/9d/c113e640aaf02af5631ae2686b742aac5cd0e1402b9d6512b1c7ec5ef05d/pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781" },
@ -5293,6 +5300,7 @@ dependencies = [
{ name = "flask" },
{ name = "flask-cors" },
{ name = "flask-login" },
{ name = "flask-mail" },
{ name = "flask-session" },
{ name = "google-generativeai" },
{ name = "google-search-results" },
@ -5447,6 +5455,7 @@ requires-dist = [
{ name = "flask", specifier = "==3.0.3" },
{ name = "flask-cors", specifier = "==5.0.0" },
{ name = "flask-login", specifier = "==0.6.3" },
{ name = "flask-mail", specifier = ">=0.10.0" },
{ name = "flask-session", specifier = "==0.8.0" },
{ name = "google-generativeai", specifier = ">=0.8.1,<0.9.0" },
{ name = "google-search-results", specifier = "==2.4.2" },
@ -5492,7 +5501,7 @@ requires-dist = [
{ name = "pyicu", specifier = ">=2.13.1,<3.0.0" },
{ name = "pymysql", specifier = ">=1.1.1,<2.0.0" },
{ name = "pyodbc", specifier = ">=5.2.0,<6.0.0" },
{ name = "pypdf", specifier = "===6.0.0" },
{ name = "pypdf", specifier = "==6.0.0" },
{ name = "pypdf2", specifier = ">=3.0.1,<4.0.0" },
{ name = "python-calamine", specifier = ">=0.4.0" },
{ name = "python-dateutil", specifier = "==2.8.2" },
@ -5539,6 +5548,7 @@ requires-dist = [
{ name = "yfinance", specifier = "==0.2.65" },
{ name = "zhipuai", specifier = "==2.0.1" },
]
provides-extras = ["full"]
[package.metadata.requires-dev]
test = [

View File

@ -70,6 +70,10 @@ const KnowledgeBaseItem = ({
export default KnowledgeBaseItem;
function buildQueryVariableOptionsByShowVariable(showVariable?: boolean) {
return showVariable ? useBuildQueryVariableOptions : () => [];
}
export function KnowledgeBaseFormField({
showVariable = false,
}: {
@ -84,7 +88,7 @@ export function KnowledgeBaseFormField({
(x) => x.parser_id !== DocumentParserType.Tag,
);
const nextOptions = useBuildQueryVariableOptions();
const nextOptions = buildQueryVariableOptionsByShowVariable(showVariable)();
const knowledgeOptions = filteredKnowledgeList.map((x) => ({
label: x.name,

View File

@ -1,9 +1,8 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import {
useFetchDocumentInfosByIds,
@ -12,17 +11,13 @@ import {
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link';
import { Avatar, Flex, Space } from 'antd';
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
import { useTheme } from '../theme-provider';
import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less';
const { Text } = Typography;
interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {
item: IMessage;
reference: IReference;
@ -59,21 +54,11 @@ const MessageItem = ({
const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
const { data: documentThumbnails, setDocumentIds: setIds } =
useFetchDocumentThumbnailsByIds();
const { visible, hideModal, showModal } = useSetModalState();
const [clickedDocumentId, setClickedDocumentId] = useState('');
const referenceDocumentList = useMemo(() => {
return reference?.doc_aggs ?? [];
}, [reference?.doc_aggs]);
const handleUserDocumentClick = useCallback(
(id: string) => () => {
setClickedDocumentId(id);
showModal();
},
[showModal],
);
const handleRegenerateMessage = useCallback(() => {
regenerateMessage?.(item);
}, [regenerateMessage, item]);
@ -160,83 +145,18 @@ const MessageItem = ({
></MarkdownContent>
</div>
{isAssistant && referenceDocumentList.length > 0 && (
<List
bordered
dataSource={referenceDocumentList}
renderItem={(item) => {
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon
id={item.doc_id}
name={item.doc_name}
></FileIcon>
<NewDocumentLink
documentId={item.doc_id}
documentName={item.doc_name}
prefix="document"
link={item.url}
>
{item.doc_name}
</NewDocumentLink>
</Flex>
</List.Item>
);
}}
/>
<ReferenceDocumentList
list={referenceDocumentList}
></ReferenceDocumentList>
)}
{isUser && documentList.length > 0 && (
<List
bordered
dataSource={documentList}
renderItem={(item) => {
// TODO:
// const fileThumbnail =
// documentThumbnails[item.id] || documentThumbnails[item.id];
const fileExtension = getExtension(item.name);
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon id={item.id} name={item.name}></FileIcon>
{isImage(fileExtension) ? (
<NewDocumentLink
documentId={item.id}
documentName={item.name}
prefix="document"
>
{item.name}
</NewDocumentLink>
) : (
<Button
type={'text'}
onClick={handleUserDocumentClick(item.id)}
>
<Text
style={{ maxWidth: '40vw' }}
ellipsis={{ tooltip: item.name }}
>
{item.name}
</Text>
</Button>
)}
</Flex>
</List.Item>
);
}}
/>
<InnerUploadedMessageFiles
files={documentList}
></InnerUploadedMessageFiles>
)}
</Flex>
</div>
</section>
{visible && (
<IndentedTreeModal
visible={visible}
hideModal={hideModal}
documentId={clickedDocumentId}
></IndentedTreeModal>
)}
</div>
);
};

View File

@ -116,64 +116,6 @@ export const AssistantGroupButton = ({
)}
</>
);
return (
<>
<Radio.Group size="small">
<Radio.Button value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</Radio.Button>
{showLoudspeaker && (
<Radio.Button value="b" onClick={handleRead}>
<Tooltip title={t('chat.read')}>
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
</Tooltip>
<audio src="" ref={ref}></audio>
</Radio.Button>
)}
{showLikeButton && (
<>
<Radio.Button value="c" onClick={handleLike}>
<LikeOutlined />
</Radio.Button>
<Radio.Button value="d" onClick={showModal}>
<DislikeOutlined />
</Radio.Button>
</>
)}
{prompt && (
<Radio.Button value="e" onClick={showPromptModal}>
<PromptIcon style={{ fontSize: '16px' }} />
</Radio.Button>
)}
<Radio.Button
value="f"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleShowLogSheet();
}}
>
<NotebookText className="size-4" />
</Radio.Button>
</Radio.Group>
{visible && (
<FeedbackModal
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
)}
{promptVisible && (
<PromptModal
visible={promptVisible}
hideModal={hidePromptModal}
prompt={prompt}
></PromptModal>
)}
</>
);
};
interface UserGroupButtonProps extends Partial<IRemoveMessageById> {

View File

@ -1,6 +1,5 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
import classNames from 'classnames';
import {
@ -21,7 +20,6 @@ import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline';
import { IMessage } from '@/pages/chat/interface';
import { isEmpty } from 'lodash';
import { Atom, ChevronDown, ChevronUp } from 'lucide-react';
import IndentedTreeModal from '../indented-tree/modal';
import MarkdownContent from '../next-markdown-content';
import { RAGFlowAvatar } from '../ragflow-avatar';
import { useTheme } from '../theme-provider';
@ -79,8 +77,6 @@ function MessageItem({
const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant;
const isUser = item.role === MessageType.User;
const { visible, hideModal } = useSetModalState();
const [clickedDocumentId] = useState('');
const [showThinking, setShowThinking] = useState(false);
const { setLastSendLoadingFunc } = useContext(AgentChatContext);
@ -200,8 +196,6 @@ function MessageItem({
sendLoading={sendLoading}
></UserGroupButton>
)}
{/* <b>{isAssistant ? '' : nickname}</b> */}
</div>
</div>
@ -254,13 +248,6 @@ function MessageItem({
</section>
</div>
</section>
{visible && (
<IndentedTreeModal
visible={visible}
hideModal={hideModal}
documentId={clickedDocumentId}
></IndentedTreeModal>
)}
</div>
);
}

View File

@ -8,7 +8,7 @@ export function ReferenceDocumentList({ list }: { list: Docagg[] }) {
<section className="flex gap-3 flex-wrap">
{list.map((item) => (
<Card key={item.doc_id}>
<CardContent className="p-2">
<CardContent className="p-2 space-x-2">
<FileIcon id={item.doc_id} name={item.doc_name}></FileIcon>
<NewDocumentLink
documentId={item.doc_id}

View File

@ -1,34 +1,65 @@
import { IDocumentInfo } from '@/interfaces/database/document';
import { getExtension } from '@/utils/document-util';
import { formatBytes } from '@/utils/file-util';
import { memo } from 'react';
import FileIcon from '../file-icon';
import NewDocumentLink from '../new-document-link';
import SvgIcon from '../svg-icon';
interface IProps {
files?: File[];
files?: File[] | IDocumentInfo[];
}
type NameWidgetType = {
name: string;
size: number;
id?: string;
};
function NameWidget({ name, size, id }: NameWidgetType) {
return (
<div className="text-xs max-w-20">
{id ? (
<NewDocumentLink documentId={id} documentName={name} prefix="document">
{name}
</NewDocumentLink>
) : (
<div className="truncate">{name}</div>
)}
<p className="text-text-secondary pt-1">{formatBytes(size)}</p>
</div>
);
}
export function InnerUploadedMessageFiles({ files = [] }: IProps) {
return (
<section className="flex gap-2 pt-2">
{files?.map((file, idx) => (
<div key={idx} className="flex gap-1 border rounded-md p-1.5">
{file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
alt={file.name}
className="size-10 object-cover"
/>
) : (
<SvgIcon
name={`file-icon/${getExtension(file.name)}`}
width={24}
></SvgIcon>
)}
<div className="text-xs max-w-20">
<div className="truncate">{file.name}</div>
<p className="text-text-secondary pt-1">{formatBytes(file.size)}</p>
{files?.map((file, idx) => {
const name = file.name;
const isFile = file instanceof File;
return (
<div key={idx} className="flex gap-1 border rounded-md p-1.5">
{!isFile ? (
<FileIcon id={file.id} name={name}></FileIcon>
) : file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
alt={name}
className="size-10 object-cover"
/>
) : (
<SvgIcon
name={`file-icon/${getExtension(name)}`}
width={24}
></SvgIcon>
)}
<NameWidget
name={name}
size={file.size}
id={isFile ? undefined : file.id}
></NameWidget>
</div>
</div>
))}
);
})}
</section>
);
}

View File

@ -22,6 +22,7 @@ export const variableEnabledFieldMap = {
export enum SharedFrom {
Agent = 'agent',
Chat = 'chat',
Search = 'search',
}
export enum ChatSearchParams {

View File

@ -25,7 +25,7 @@ import { useDebounce } from 'ahooks';
import { get, set } from 'lodash';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useParams, useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
import {
useGetPaginationWithRouter,
@ -304,6 +304,9 @@ export const useSetAgent = (showMessage: boolean = true) => {
// Only one file can be uploaded at a time
export const useUploadCanvasFile = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const canvasId = id || shared_id;
const {
data,
isPending: loading,
@ -321,7 +324,7 @@ export const useUploadCanvasFile = () => {
}
const { data } = await agentService.uploadCanvasFile(
{ url: api.uploadAgentFile(id), data: nextBody },
{ url: api.uploadAgentFile(canvasId as string), data: nextBody },
true,
);
if (data?.code === 0) {

View File

@ -57,6 +57,18 @@ export interface IDialog {
similarity_threshold: number;
top_k: number;
top_n: number;
meta_data_filter: MetaDataFilter;
}
interface MetaDataFilter {
manual: Manual[];
method: string;
}
interface Manual {
key: string;
op: string;
value: string;
}
export interface IConversation {

View File

@ -1418,6 +1418,7 @@ This delimiter is used to split the input text into several text pieces echo of
},
search: {
createSearch: 'Create Search',
searchGreeting: 'How can I help you today ',
},
},
};

View File

@ -1185,9 +1185,13 @@ export default {
knowledge: '知識',
chat: '聊天',
},
},
modal: {
okText: '確認',
cancelText: '取消',
modal: {
okText: '確認',
cancelText: '取消',
},
search: {
createSearch: '新建查詢',
searchGreeting: '今天我能為你做些什麽?',
},
},
};

View File

@ -1316,12 +1316,13 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
},
},
},
},
modal: {
okText: '确认',
cancelText: '取消',
},
search: {
createSearch: '新建查询',
modal: {
okText: '确认',
cancelText: '取消',
},
search: {
createSearch: '新建查询',
searchGreeting: '今天我能为你做些什么?',
},
},
};

View File

@ -15,7 +15,7 @@ import { FormTooltip } from '@/components/ui/tooltip';
import { buildSelectOptions } from '@/utils/component-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plus } from 'lucide-react';
import { memo } from 'react';
import { memo, useEffect, useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
@ -70,6 +70,18 @@ function BeginForm({ node }: INextOperatorForm) {
name: 'enablePrologue',
});
const previousModeRef = useRef(mode);
useEffect(() => {
if (
previousModeRef.current === AgentDialogueMode.Task &&
mode === AgentDialogueMode.Conversational
) {
form.setValue('enablePrologue', true);
}
previousModeRef.current = mode;
}, [mode, form]);
const {
ok,
currentRecord,

View File

@ -85,7 +85,7 @@ const getUrlWithToken = (token: string, from: string = 'chat') => {
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};
const useFetchTokenListBeforeOtherStep = () => {
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();

View File

@ -2,6 +2,8 @@
import { FileUploader } from '@/components/file-uploader';
import { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SwitchFormField } from '@/components/switch-fom-field';
import { TavilyFormField } from '@/components/tavily-form-field';
import {
@ -14,11 +16,26 @@ import {
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { useFormContext } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';
import { DatasetMetadata } from '../../constants';
import { MetadataFilterConditions } from './metadata-filter-conditions';
export default function ChatBasicSetting() {
const { t } = useTranslate('chat');
const form = useFormContext();
const kbIds: string[] = useWatch({ control: form.control, name: 'kb_ids' });
const metadata = useWatch({
control: form.control,
name: 'meta_data_filter.method',
});
const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0;
const MetadataOptions = Object.values(DatasetMetadata).map((x) => {
return {
value: x,
label: t(`meta.${x}`),
};
});
return (
<div className="space-y-8">
@ -108,6 +125,18 @@ export default function ChatBasicSetting() {
></SwitchFormField>
<TavilyFormField></TavilyFormField>
<KnowledgeBaseFormField></KnowledgeBaseFormField>
{hasKnowledge && (
<RAGFlowFormItem
label={t('metadata')}
name={'meta_data_filter.method'}
tooltip={t('metadataTip')}
>
<SelectWithSearch options={MetadataOptions} />
</RAGFlowFormItem>
)}
{hasKnowledge && metadata === DatasetMetadata.Manual && (
<MetadataFilterConditions kbIds={kbIds}></MetadataFilterConditions>
)}
</div>
);
}

View File

@ -9,6 +9,7 @@ import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useParams } from 'umi';
import { z } from 'zod';
import { DatasetMetadata } from '../../constants';
import ChatBasicSetting from './chat-basic-settings';
import { ChatModelSettings } from './chat-model-settings';
import { ChatPromptEngine } from './chat-prompt-engine';
@ -38,6 +39,10 @@ export function ChatSettings({ switchSettingVisible }: ChatSettingsProps) {
top_n: 8,
vector_similarity_weight: 0.2,
top_k: 1024,
meta_data_filter: {
method: DatasetMetadata.Disabled,
manual: [],
},
},
});

View File

@ -0,0 +1,129 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
import { SwitchOperatorOptions } from '@/pages/agent/constant';
import { useBuildSwitchOperatorOptions } from '@/pages/agent/form/switch-form';
import { Plus, X } from 'lucide-react';
import { useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
export function MetadataFilterConditions({ kbIds }: { kbIds: string[] }) {
const { t } = useTranslation();
const form = useFormContext();
const name = 'meta_data_filter.manual';
const metadata = useFetchKnowledgeMetadata(kbIds);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const { fields, remove, append } = useFieldArray({
name,
control: form.control,
});
const add = useCallback(
(key: string) => () => {
append({
key,
value: '',
op: SwitchOperatorOptions[0].value,
});
},
[append],
);
return (
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>{t('chat.conditions')}</FormLabel>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant={'ghost'} type="button">
<Plus />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.keys(metadata.data).map((key, idx) => {
return (
<DropdownMenuItem key={idx} onClick={add(key)}>
{key}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;
return (
<div key={field.id} className="flex w-full items-center gap-2">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.op`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<SelectWithSearch
{...field}
options={switchOperatorOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input placeholder={t('common.pleaseInput')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
</div>
</section>
);
}

View File

@ -43,6 +43,18 @@ export function useChatSettingSchema() {
llm_id: z.string().optional(),
...vectorSimilarityWeightSchema,
...topnSchema,
meta_data_filter: z
.object({
method: z.string().optional(),
manual: z.array(
z.object({
key: z.string(),
op: z.string(),
value: z.string(),
}),
),
})
.optional(),
});
return formSchema;

View File

@ -27,6 +27,7 @@ export function SingleChatBox({ controller }: IProps) {
messageContainerRef,
sendLoading,
derivedMessages,
isUploading,
handleInputChange,
handlePressEnter,
regenerateMessage,
@ -91,6 +92,7 @@ export function SingleChatBox({ controller }: IProps) {
}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={isUploading}
/>
</section>
);

View File

@ -0,0 +1,7 @@
export const EmptyConversationId = 'empty';
export enum DatasetMetadata {
Disabled = 'disabled',
Automatic = 'automatic',
Manual = 'manual',
}

View File

@ -138,7 +138,8 @@ export const useSendMessage = (controller: AbortController) => {
const { conversationId, isNew } = useGetChatSearchParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { handleUploadFile, fileIds, clearFileIds } = useUploadFile();
const { handleUploadFile, fileIds, clearFileIds, isUploading } =
useUploadFile();
const { send, answer, done } = useSendMessageWithSse(
api.completeConversation,
@ -285,5 +286,6 @@ export const useSendMessage = (controller: AbortController) => {
removeMessageById,
stopOutputMessage,
handleUploadFile,
isUploading,
};
};

View File

@ -3,7 +3,7 @@ import { useUploadAndParseFile } from '@/hooks/use-chat-request';
import { useCallback, useState } from 'react';
export function useUploadFile() {
const { uploadAndParseFile } = useUploadAndParseFile();
const { uploadAndParseFile, loading } = useUploadAndParseFile();
const [fileIds, setFileIds] = useState<string[]>([]);
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =
@ -23,5 +23,5 @@ export function useUploadFile() {
setFileIds([]);
}, []);
return { handleUploadFile, clearFileIds, fileIds };
return { handleUploadFile, clearFileIds, fileIds, isUploading: loading };
}

View File

@ -12,7 +12,8 @@ import { useEffect, useState } from 'react';
interface IProps extends IModalProps<any> {
documentId: string;
chunk: IChunk | IReferenceChunk;
chunk: IChunk &
IReferenceChunk & { docnm_kwd: string; document_name: string };
}
function getFileExtensionRegex(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
@ -30,21 +31,22 @@ const PdfDrawer = ({
// const [loaded, setLoaded] = useState(false);
const url = getDocumentUrl();
console.log('chunk--->', chunk.docnm_kwd, url);
const [fileType, setFileType] = useState('');
useEffect(() => {
if (chunk.docnm_kwd) {
const type = getFileExtensionRegex(chunk.docnm_kwd);
if (chunk.docnm_kwd || chunk.document_name) {
const type = getFileExtensionRegex(
chunk.docnm_kwd || chunk.document_name,
);
setFileType(type);
}
}, [chunk.docnm_kwd]);
}, [chunk.docnm_kwd, chunk.document_name]);
return (
<Modal
title={
<div className="flex items-center gap-2">
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
<FileIcon name={chunk.docnm_kwd || chunk.document_name}></FileIcon>
{chunk.docnm_kwd || chunk.document_name}
</div>
}
onCancel={hideModal}

View File

@ -0,0 +1,140 @@
import HightLightMarkdown from '@/components/highlight-markdown';
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
LanguageAbbreviation,
LanguageAbbreviationMap,
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { useCallback, useMemo, useState } from 'react';
type IEmbedAppModalProps = {
open: any;
url: string;
token: string;
from: string;
beta: string;
setOpen: (e: any) => void;
tenantId: string;
};
const EmbedAppModal = (props: IEmbedAppModalProps) => {
const { t } = useTranslate('chat');
const { open, setOpen, token = '', from, beta = '', url, tenantId } = props;
const [hideAvatar, setHideAvatar] = useState(false);
const [locale, setLocale] = useState('');
const languageOptions = useMemo(() => {
return Object.values(LanguageAbbreviation).map((x) => ({
label: LanguageAbbreviationMap[x],
value: x,
}));
}, []);
const generateIframeSrc = useCallback(() => {
// const { visibleAvatar, locale } = values;
let src = `${location.origin}${url}?shared_id=${token}&from=${from}&auth=${beta}&tenantId=${tenantId}`;
if (hideAvatar) {
src += '&visible_avatar=1';
}
if (locale) {
src += `&locale=${locale}`;
}
return src;
}, [beta, from, token, hideAvatar, locale, url, tenantId]);
// ... existing code ...
const text = useMemo(() => {
const iframeSrc = generateIframeSrc();
return `\`\`\`html
<iframe
src="${iframeSrc}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0">
</iframe>
\`\`\``;
}, [generateIframeSrc]);
// ... existing code ...
return (
<Modal
title={t('embedIntoSite', { keyPrefix: 'common' })}
className="!bg-bg-base !text-text-disabled"
open={open}
onCancel={() => setOpen(false)}
showfooter={false}
footer={null}
>
<div className="w-full">
{/* Hide Avatar Toggle */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">
{t('avatarHidden')}
</label>
<div className="flex items-center">
<Switch
checked={hideAvatar}
onCheckedChange={(value) => {
setHideAvatar(value);
}}
/>
</div>
</div>
{/* Locale Select */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">Locale</label>
<RAGFlowSelect
placeholder="Select a locale"
value={locale}
onChange={(value) => setLocale(value)}
options={languageOptions}
></RAGFlowSelect>
</div>
{/* Embed Code */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">Embed code</label>
{/* <div className=" border rounded-lg"> */}
{/* <pre className="text-sm whitespace-pre-wrap">{text}</pre> */}
<HightLightMarkdown>{text}</HightLightMarkdown>
{/* </div> */}
</div>
{/* ID Field */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">ID</label>
<div className="flex items-center">
<input
type="text"
value={token}
readOnly
className="flex-1 px-4 py-2 border border-gray-700 rounded-lg bg-bg-base focus:outline-none"
/>
<button
type="button"
onClick={() => navigator.clipboard.writeText(token)}
className="ml-2 p-2 text-gray-400 hover:text-white transition-colors"
title="Copy ID"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h10a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
</div>
</div>
</Modal>
);
};
export default EmbedAppModal;

View File

@ -0,0 +1,488 @@
import message from '@/components/ui/message';
import { SharedFrom } from '@/constants/chat';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import {
useGetPaginationWithRouter,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import { useSetPaginationParams } from '@/hooks/route-hook';
import { useKnowledgeBaseId } from '@/hooks/use-knowledge-request';
import { ResponsePostType } from '@/interfaces/database/base';
import { IAnswer } from '@/interfaces/database/chat';
import { ITestingResult } from '@/interfaces/database/knowledge';
import { IAskRequestBody } from '@/interfaces/request/chat';
import chatService from '@/services/chat-service';
import kbService from '@/services/knowledge-service';
import searchService from '@/services/search-service';
import api from '@/utils/api';
import { useMutation } from '@tanstack/react-query';
import { has, isEmpty, trim } from 'lodash';
import {
ChangeEventHandler,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useSearchParams } from 'umi';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useShowMindMapDrawer } from '../search/hooks';
import { useClickDrawer } from './document-preview-modal/hooks';
export interface ISearchingProps {
searchText?: string;
data: ISearchAppDetailProps;
setIsSearching?: Dispatch<SetStateAction<boolean>>;
setSearchText?: Dispatch<SetStateAction<string>>;
}
export type ISearchReturnProps = ReturnType<typeof useSearching>;
export const useGetSharedSearchParams = () => {
const [searchParams] = useSearchParams();
const data_prefix = 'data_';
const data = Object.fromEntries(
searchParams
.entries()
.filter(([key]) => key.startsWith(data_prefix))
.map(([key, value]) => [key.replace(data_prefix, ''), value]),
);
return {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
tenantId: searchParams.get('tenantId'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
: true,
};
};
export const useSearchFetchMindMap = () => {
const [searchParams] = useSearchParams();
const sharedId = searchParams.get('shared_id');
const fetchMindMapFunc = sharedId
? searchService.mindmapShare
: chatService.getMindMap;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchMindMap'],
gcTime: 0,
mutationFn: async (params: IAskRequestBody) => {
try {
const ret = await fetchMindMapFunc(params);
return ret?.data?.data ?? {};
} catch (error: any) {
if (has(error, 'message')) {
message.error(error.message);
}
return [];
}
},
});
return { data, loading, fetchMindMap: mutateAsync };
};
export const useTestChunkRetrieval = (
tenantId?: string,
): ResponsePostType<ITestingResult> & {
testChunk: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? kbService.retrievalTestShare
: kbService.retrieval_test;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunk'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await retrievalTestFunc({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
page,
size: pageSize,
tenant_id: tenantId,
});
if (data.code === 0) {
const res = data.data;
return {
...res,
documents: res.doc_aggs,
};
}
return (
data?.data ?? {
chunks: [],
documents: [],
total: 0,
}
);
},
});
return {
data: data ?? { chunks: [], documents: [], total: 0 },
loading,
testChunk: mutateAsync,
};
};
export const useTestChunkAllRetrieval = (
tenantId?: string,
): ResponsePostType<ITestingResult> & {
testChunkAll: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? kbService.retrievalTestShare
: kbService.retrieval_test;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunkAll'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await retrievalTestFunc({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
doc_ids: [],
page,
size: pageSize,
tenant_id: tenantId,
});
if (data.code === 0) {
const res = data.data;
return {
...res,
documents: res.doc_aggs,
};
}
return (
data?.data ?? {
chunks: [],
documents: [],
total: 0,
}
);
},
});
return {
data: data ?? { chunks: [], documents: [], total: 0 },
loading,
testChunkAll: mutateAsync,
};
};
export const useTestRetrieval = (
kbIds: string[],
searchStr: string,
sendingLoading: boolean,
) => {
const { testChunk, loading } = useTestChunkRetrieval();
const { pagination } = useGetPaginationWithRouter();
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
const handleTestChunk = useCallback(() => {
const q = trim(searchStr);
if (sendingLoading || isEmpty(q)) return;
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: Array.isArray(selectedDocumentIds) ? selectedDocumentIds : [],
page: pagination.current,
size: pagination.pageSize,
});
}, [
sendingLoading,
searchStr,
kbIds,
testChunk,
selectedDocumentIds,
pagination,
]);
useEffect(() => {
handleTestChunk();
}, [handleTestChunk]);
return {
loading,
selectedDocumentIds,
setSelectedDocumentIds,
};
};
export const useFetchRelatedQuestions = (tenantId?: string) => {
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? searchService.getRelatedQuestionsShare
: chatService.getRelatedQuestions;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchRelatedQuestions'],
gcTime: 0,
mutationFn: async (question: string): Promise<string[]> => {
const { data } = await retrievalTestFunc({
question,
tenant_id: tenantId,
});
return data?.data ?? [];
},
});
return { data, loading, fetchRelatedQuestions: mutateAsync };
};
export const useSendQuestion = (kbIds: string[], tenantId?: string) => {
const { sharedId } = useGetSharedSearchParams();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
sharedId ? api.askShare : api.ask,
);
const { testChunk, loading } = useTestChunkRetrieval(tenantId);
const { testChunkAll } = useTestChunkAllRetrieval(tenantId);
const [sendingLoading, setSendingLoading] = useState(false);
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
const { fetchRelatedQuestions, data: relatedQuestions } =
useFetchRelatedQuestions(tenantId);
const [searchStr, setSearchStr] = useState<string>('');
const [isFirstRender, setIsFirstRender] = useState(true);
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
const { pagination, setPagination } = useGetPaginationWithRouter();
const sendQuestion = useCallback(
(question: string) => {
const q = trim(question);
if (isEmpty(q)) return;
setPagination({ page: 1 });
setIsFirstRender(false);
setCurrentAnswer({} as IAnswer);
setSendingLoading(true);
send({ kb_ids: kbIds, question: q, tenantId });
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
page: 1,
size: pagination.pageSize,
});
fetchRelatedQuestions(q);
},
[
send,
testChunk,
kbIds,
fetchRelatedQuestions,
setPagination,
pagination.pageSize,
tenantId,
],
);
const handleSearchStrChange: ChangeEventHandler<HTMLInputElement> =
useCallback((e) => {
setSearchStr(e.target.value);
}, []);
const handleClickRelatedQuestion = useCallback(
(question: string) => () => {
if (sendingLoading) return;
setSearchStr(question);
sendQuestion(question);
},
[sendQuestion, sendingLoading],
);
const handleTestChunk = useCallback(
(documentIds: string[], page: number = 1, size: number = 10) => {
const q = trim(searchStr);
if (sendingLoading || isEmpty(q)) return;
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: documentIds ?? selectedDocumentIds,
page,
size,
});
testChunkAll({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: [],
page,
size,
});
},
[
searchStr,
sendingLoading,
testChunk,
kbIds,
selectedDocumentIds,
testChunkAll,
],
);
useEffect(() => {
if (!isEmpty(answer)) {
setCurrentAnswer(answer);
}
}, [answer]);
useEffect(() => {
if (done) {
setSendingLoading(false);
}
}, [done]);
return {
sendQuestion,
handleSearchStrChange,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
loading,
sendingLoading,
answer: currentAnswer,
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
searchStr,
setSearchStr,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty: isEmpty(trim(searchStr)),
stopOutputMessage,
};
};
export const useSearching = ({
searchText,
data: searchData,
setSearchText,
}: ISearchingProps) => {
const { tenantId } = useGetSharedSearchParams();
const {
sendQuestion,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
} = useSendQuestion(searchData.search_config.kb_ids, tenantId as string);
const handleSearchStrChange = useCallback(
(value: string) => {
console.log('handleSearchStrChange', value);
setSearchStr(value);
},
[setSearchStr],
);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
if (searchText) {
setSearchStr(searchText);
sendQuestion(searchText);
setSearchText?.('');
}
}, [searchText, sendQuestion, setSearchStr, setSearchText]);
const {
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
} = useShowMindMapDrawer(searchData.search_config.kb_ids, searchStr);
const { chunks, total } = useSelectTestingResult();
const handleSearch = useCallback(
(value: string) => {
sendQuestion(value);
setSearchStr?.(value);
},
[setSearchStr, sendQuestion],
);
const { pagination, setPagination } = useGetPaginationWithRouter();
const onChange = (pageNumber: number, pageSize: number) => {
setPagination({ page: pageNumber, pageSize });
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
};
return {
sendQuestion,
handleClickRelatedQuestion,
handleSearchStrChange,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
visible,
hideModal,
documentId,
selectedChunk,
clickDocumentButton,
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
chunks,
total,
handleSearch,
pagination,
onChange,
};
};

View File

@ -8,13 +8,17 @@ import {
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { SharedFrom } from '@/constants/chat';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { Settings } from 'lucide-react';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { Send, Settings } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useFetchTokenListBeforeOtherStep } from '../agent/hooks/use-show-dialog';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../next-searches/hooks';
import EmbedAppModal from './embed-app-modal';
import './index.less';
import SearchHome from './search-home';
import { SearchSetting } from './search-setting';
@ -24,9 +28,15 @@ export default function SearchPage() {
const { navigateToSearchList } = useNavigatePage();
const [isSearching, setIsSearching] = useState(false);
const { data: SearchData } = useFetchSearchDetail();
const { beta, handleOperate } = useFetchTokenListBeforeOtherStep();
const [openSetting, setOpenSetting] = useState(false);
const [openEmbed, setOpenEmbed] = useState(false);
const [searchText, setSearchText] = useState('');
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
useEffect(() => {
handleOperate();
}, [handleOperate]);
useEffect(() => {
if (isSearching) {
setOpenSetting(false);
@ -81,8 +91,37 @@ export default function SearchPage() {
data={SearchData as ISearchAppDetailProps}
/>
)}
{
<EmbedAppModal
open={openEmbed}
setOpen={setOpenEmbed}
url="/next-search/share"
token={SearchData?.id as string}
from={SharedFrom.Search}
beta={beta}
tenantId={tenantId}
/>
}
{
// <EmbedDialog
// visible={openEmbed}
// hideModal={setOpenEmbed}
// token={SearchData?.id as string}
// from={SharedFrom.Search}
// beta={beta}
// isAgent={false}
// ></EmbedDialog>
}
</div>
<div className="absolute right-5 top-12 ">
<Button
className="bg-text-primary text-bg-base border-b-[#00BEB4] border-b-2"
onClick={() => setOpenEmbed(!openEmbed)}
>
<Send />
<div>Embed App</div>
</Button>
</div>
{!isSearching && (
<div className="absolute left-5 bottom-12 ">
<Button

View File

@ -13,7 +13,7 @@ const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
const { t } = useTranslation();
const percent = usePendingMindMap();
return (
<div className="w-[400px] h-[420px]">
<div className="w-full h-full">
<div className="flex w-full justify-between items-center mb-2">
<div className="text-text-primary font-medium text-base">
{t('chunk.mind')}
@ -32,11 +32,14 @@ const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
</div>
)}
{!loading && (
<div className="bg-bg-card rounded-lg p-4 w-[400px] h-[380px]">
<div className="bg-bg-card rounded-lg p-4 w-full h-full">
<IndentedTree
data={data}
show
style={{ width: '100%', height: '100%' }}
style={{
width: '100%',
height: '100%',
}}
></IndentedTree>
</div>
)}

View File

@ -1,5 +1,4 @@
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
@ -68,42 +67,6 @@ export default function SearchPage({
</div>
</div>
</div>
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">Related Search</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Related SearchRelated Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Related SearchRelated Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search
</Button>
</div>
</div>
</div>
</section>
);

View File

@ -12,7 +12,6 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Label } from '@/components/ui/label';
import {
MultiSelect,
MultiSelectOptionType,
@ -42,6 +41,7 @@ import {
import {
ISearchAppDetailProps,
IUpdateSearchProps,
IllmSettingProps,
useUpdateSearch,
} from '../next-searches/hooks';
import {
@ -55,14 +55,6 @@ interface SearchSettingProps {
className?: string;
data: ISearchAppDetailProps;
}
interface ISubmitLlmSettingProps {
llm_id: string;
parameter: string;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
const SearchSettingFormSchema = z
.object({
@ -120,16 +112,19 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]);
const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState('');
const descriptionDefaultValue = 'You are an intelligent assistant.';
const resetForm = useCallback(() => {
formMethods.reset({
search_id: data?.id,
name: data?.name || '',
avatar: data?.avatar || '',
description: data?.description || 'You are an intelligent assistant.',
description: data?.description || descriptionDefaultValue,
search_config: {
kb_ids: search_config?.kb_ids || [],
vector_similarity_weight: search_config?.vector_similarity_weight || 20,
vector_similarity_weight:
(search_config?.vector_similarity_weight
? 1 - search_config?.vector_similarity_weight
: 0.3) || 0.3,
web_search: search_config?.web_search || false,
doc_ids: [],
similarity_threshold: 0.0,
@ -198,8 +193,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
})();
}
}, [avatarFile]);
const { list: datasetListOrigin, loading: datasetLoading } =
useFetchKnowledgeList();
const { list: datasetListOrigin } = useFetchKnowledgeList();
useEffect(() => {
const datasetListMap = datasetListOrigin.map((item: IKnowledge) => {
@ -259,7 +253,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
) => {
try {
const { search_config, ...other_formdata } = formData;
const { llm_setting, ...other_config } = search_config;
const { llm_setting, vector_similarity_weight, ...other_config } =
search_config;
const llmSetting = {
llm_id: llm_setting.llm_id,
parameter: llm_setting.parameter,
@ -267,7 +262,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
top_p: llm_setting.top_p,
frequency_penalty: llm_setting.frequency_penalty,
presence_penalty: llm_setting.presence_penalty,
} as ISubmitLlmSettingProps;
} as IllmSettingProps;
if (!llm_setting.frequencyPenaltyEnabled) {
delete llmSetting.frequency_penalty;
}
@ -284,6 +280,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
...other_formdata,
search_config: {
...other_config,
vector_similarity_weight: 1 - vector_similarity_weight,
llm_setting: { ...llmSetting },
},
tenant_id: systemSetting.tenant_id,
@ -355,46 +352,54 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormControl>
<div className="relative group">
{!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
<div className="flex flex-col items-center">
<Upload />
<p>{t('common.upload')}</p>
<div className="relative group flex items-end gap-2">
<div>
{!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
<div className="flex flex-col items-center">
<Upload />
<p>{t('common.upload')}</p>
</div>
</div>
</div>
) : (
<div className="w-[64px] h-[64px] relative grid place-content-center">
<RAGFlowAvatar
avatar={avatarBase64Str}
name={data.name}
className="w-[64px] h-[64px] rounded-md block"
/>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil
size={20}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
) : (
<div className="w-[64px] h-[64px] relative grid place-content-center">
<RAGFlowAvatar
avatar={avatarBase64Str}
name={data.name}
className="w-[64px] h-[64px] rounded-md block"
/>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil
size={20}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
/>
</div>
</div>
</div>
)}
<input
placeholder=""
// {...field}
type="file"
title=""
accept="image/*"
className="absolute w-[64px] top-0 left-0 h-full opacity-0 cursor-pointer"
onChange={(ev) => {
const file = ev.target?.files?.[0];
if (
/\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '')
) {
setAvatarFile(file!);
}
ev.target.value = '';
}}
/>
)}
<input
placeholder=""
// {...field}
type="file"
title=""
accept="image/*"
className="absolute w-[64px] top-0 left-0 h-full opacity-0 cursor-pointer"
onChange={(ev) => {
const file = ev.target?.files?.[0];
if (
/\.(jpg|jpeg|png|webp|bmp)$/i.test(
file?.name ?? '',
)
) {
setAvatarFile(file!);
}
ev.target.value = '';
}}
/>
</div>
<div className="margin-1 text-muted-foreground">
{t('knowledgeConfiguration.photoTip')}
</div>
</div>
</FormControl>
<FormMessage />
@ -410,7 +415,20 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" {...field} />
<Input
placeholder="You are an intelligent assistant."
{...field}
onFocus={() => {
if (field.value === descriptionDefaultValue) {
field.onChange('');
}
}}
onBlur={() => {
if (field.value === '') {
field.onChange(descriptionDefaultValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -451,26 +469,58 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
control={formMethods.control}
name="search_config.vector_similarity_weight"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>Keyword
Similarity Weight
</FormLabel>
<FormControl>
<div className="flex justify-between items-center">
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
max={100}
step={1}
value={field.value as number}
onChange={(values) => field.onChange(values)}
{...field}
max={1}
min={0}
step={0.01}
></SingleFormSlider>
<Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20">
{field.value}
</Label>
</div>
</FormControl>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={1}
min={0}
step={0.01}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
// <FormItem className="flex flex-col">
// <FormLabel>
// <span className="text-destructive mr-1"> *</span>Keyword
// Similarity Weight
// </FormLabel>
// <FormControl>
// {/* <div className="flex justify-between items-center">
// <SingleFormSlider
// max={100}
// step={1}
// value={field.value as number}
// onChange={(values) => field.onChange(values)}
// ></SingleFormSlider>
// <Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20">
// {field.value}
// </Label>
// </div> */}
// </FormControl>
// <FormMessage />
// </FormItem>
)}
/>
@ -528,16 +578,15 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormControl>
<SingleFormSlider
{...field}
max={100}
max={2048}
min={0}
step={1}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={100}
max={2048}
min={0}
step={1}
{...field}

View File

@ -0,0 +1,321 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Skeleton } from '@/components/ui/skeleton';
import { Spin } from '@/components/ui/spin';
import { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import DOMPurify from 'dompurify';
import { TFunction } from 'i18next';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, Square, X } from 'lucide-react';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import PdfDrawer from './document-preview-modal';
import HightLightMarkdown from './highlight-markdown';
import { ISearchReturnProps } from './hooks';
import './index.less';
import MarkdownContent from './markdown-content';
import MindMapDrawer from './mindmap-drawer';
import RetrievalDocuments from './retrieval-documents';
export default function SearchingView({
setIsSearching,
searchData,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
searchStr,
stopOutputMessage,
visible,
hideModal,
documentId,
selectedChunk,
clickDocumentButton,
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
chunks,
total,
handleSearch,
pagination,
onChange,
t,
}: ISearchReturnProps & {
setIsSearching?: Dispatch<SetStateAction<boolean>>;
searchData: ISearchAppDetailProps;
t: TFunction<'translation', undefined>;
}) {
const { t: tt, i18n } = useTranslation();
useEffect(() => {
const changeLanguage = async () => {
await i18n.changeLanguage('zh');
};
changeLanguage();
}, [i18n]);
const [searchtext, setSearchtext] = useState<string>('');
useEffect(() => {
setSearchtext(searchStr);
}, [searchStr, setSearchtext]);
return (
<section
className={cn(
'relative w-full flex transition-all justify-start items-center',
)}
>
{/* search header */}
<div
className={cn(
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full',
)}
>
<h1
className={cn(
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching?.(false);
}}
>
RAGFlow
</h1>
<div
className={cn(
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
)}
>
<div className={cn('flex flex-col justify-start items-start w-full')}>
<div className="relative w-full text-primary">
<Input
placeholder={tt('search.searchGreeting')}
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
)}
value={searchtext}
onChange={(e) => {
setSearchtext(e.target.value);
}}
disabled={sendingLoading}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch(searchtext);
}
}}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X
className="text-text-secondary"
size={14}
onClick={() => {
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary">|</span>
<button
type="button"
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
onClick={() => {
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch(searchtext);
}
}}
>
{sendingLoading ? (
<Square size={22} className="m-auto" />
) : (
<Search size={22} className="m-auto" />
)}
</button>
</div>
</div>
</div>
{/* search body */}
<div
className="w-full mt-5 overflow-auto scrollbar-none "
style={{ height: 'calc(100vh - 250px)' }}
>
{searchData.search_config.summary && !isSearchStrEmpty && (
<>
<div className="flex justify-start items-start text-text-primary text-2xl">
AI Summary
</div>
{isEmpty(answer) && sendingLoading ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-2/3 bg-bg-card" />
</div>
) : (
answer.answer && (
<div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none">
<MarkdownContent
loading={sendingLoading}
content={answer.answer}
reference={answer.reference ?? ({} as IReference)}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
)
)}
<div className="w-full border-b border-border-default/80 my-6"></div>
</>
)}
{/* retrieval documents */}
{!isSearchStrEmpty && (
<>
<div className=" mt-3 w-44 ">
<RetrievalDocuments
selectedDocumentIds={selectedDocumentIds}
setSelectedDocumentIds={setSelectedDocumentIds}
onTesting={handleTestChunk}
></RetrievalDocuments>
</div>
<div className="w-full border-b border-border-default/80 my-6"></div>
</>
)}
<div className="mt-3 ">
<Spin spinning={loading}>
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<>
<div
key={chunk.chunk_id}
className="w-full flex flex-col"
>
<div className="w-full">
<ImageWithPopover
id={chunk.img_id}
></ImageWithPopover>
<Popover>
<PopoverTrigger asChild>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
`${chunk.highlight}...`,
),
}}
className="text-sm text-text-primary mb-1"
></div>
</PopoverTrigger>
<PopoverContent className="text-text-primary">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</PopoverContent>
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
>
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
</div>
{index < chunks.length - 1 && (
<div className="w-full border-b border-border-default/80 mt-6"></div>
)}
</>
);
})}
</>
)}
</Spin>
{relatedQuestions?.length > 0 && (
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">
Related Search
</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
{relatedQuestions?.map((x, idx) => (
<Button
key={idx}
variant="transparent"
className="bg-bg-card text-text-secondary"
onClick={handleClickRelatedQuestion(x)}
>
Related Search{x}
</Button>
))}
</div>
</div>
)}
</div>
</div>
{total > 0 && (
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
)}
</div>
{mindMapVisible && (
<div className="flex-1 h-[88dvh] z-30 ml-8 mt-5">
<MindMapDrawer
visible={mindMapVisible}
hideModal={hideMindMapModal}
data={mindMap}
loading={mindMapLoading}
></MindMapDrawer>
</div>
)}
</div>
{!mindMapVisible &&
!isFirstRender &&
!isSearchStrEmpty &&
!isEmpty(searchData.search_config.kb_ids) &&
searchData.search_config.query_mindmap && (
<Popover>
<PopoverTrigger asChild>
<div
className="rounded-lg h-16 w-16 p-0 absolute top-28 right-3 z-30 border cursor-pointer flex justify-center items-center bg-bg-card"
onClick={showMindMapModal}
>
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
<BrainCircuit size={36} />
</div>
</PopoverTrigger>
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
</Popover>
)}
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
</section>
);
}

View File

@ -1,332 +1,33 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Skeleton } from '@/components/ui/skeleton';
import { Spin } from '@/components/ui/spin';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import DOMPurify from 'dompurify';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, Square, Tag, X } from 'lucide-react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
} from 'react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useSendQuestion, useShowMindMapDrawer } from '../search/hooks';
import PdfDrawer from './document-preview-modal';
import HightLightMarkdown from './highlight-markdown';
import { useSearching } from './hooks';
import './index.less';
import styles from './index.less';
import MarkdownContent from './markdown-content';
import MindMapDrawer from './mindmap-drawer';
import RetrievalDocuments from './retrieval-documents';
import SearchingView from './search-view';
export default function SearchingPage({
searchText,
data: searchData,
setIsSearching,
setSearchText,
}: {
searchText: string;
setIsSearching: Dispatch<SetStateAction<boolean>>;
setSearchText: Dispatch<SetStateAction<string>>;
data: ISearchAppDetailProps;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const {
sendQuestion,
handleClickRelatedQuestion,
handleSearchStrChange,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
} = useSendQuestion(searchData.search_config.kb_ids);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
if (searchText) {
setSearchStr(searchText);
sendQuestion(searchText);
}
// regain focus
if (inputRef.current) {
inputRef.current.focus();
}
}, [searchText, sendQuestion, setSearchStr]);
const {
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
} = useShowMindMapDrawer(searchData.search_config.kb_ids, searchStr);
const { chunks, total } = useSelectTestingResult();
const handleSearch = useCallback(() => {
sendQuestion(searchStr);
}, [searchStr, sendQuestion]);
const { pagination, setPagination } = useGetPaginationWithRouter();
const onChange = (pageNumber: number, pageSize: number) => {
setPagination({ page: pageNumber, pageSize });
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
};
const searchingParam = useSearching({
searchText,
data: searchData,
setIsSearching,
setSearchText,
});
const { t } = useTranslation();
return (
<section
className={cn(
'relative w-full flex transition-all justify-start items-center',
)}
>
{/* search header */}
<div
className={cn(
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full',
)}
>
<h1
className={cn(
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching(false);
}}
>
RAGFlow
</h1>
<div
className={cn(
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
)}
>
<div className={cn('flex flex-col justify-start items-start w-full')}>
<div className="relative w-full text-primary">
<Input
ref={inputRef}
key="search-input"
placeholder="How can I help you today?"
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
)}
value={searchStr}
onChange={handleSearchStrChange}
disabled={sendingLoading}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X
className="text-text-secondary"
size={14}
onClick={() => {
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary">|</span>
<button
type="button"
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
onClick={() => {
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch();
}
}}
>
{sendingLoading ? (
<Square size={22} className="m-auto" />
) : (
<Search size={22} className="m-auto" />
)}
</button>
</div>
</div>
</div>
{/* search body */}
<div
className="w-full mt-5 overflow-auto scrollbar-none "
style={{ height: 'calc(100vh - 250px)' }}
>
{searchData.search_config.summary && (
<>
<div className="flex justify-start items-start text-text-primary text-2xl">
AI Summary
</div>
{isEmpty(answer) && sendingLoading ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-2/3 bg-bg-card" />
</div>
) : (
answer.answer && (
<div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none">
<MarkdownContent
loading={sendingLoading}
content={answer.answer}
reference={answer.reference ?? ({} as IReference)}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
)
)}
</>
)}
<div className="w-full border-b border-border-default/80 my-6"></div>
{/* retrieval documents */}
<div className=" mt-3 w-44 ">
<RetrievalDocuments
selectedDocumentIds={selectedDocumentIds}
setSelectedDocumentIds={setSelectedDocumentIds}
onTesting={handleTestChunk}
></RetrievalDocuments>
</div>
<div className="w-full border-b border-border-default/80 my-6"></div>
<div className="mt-3 ">
<Spin spinning={loading}>
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<>
<div
key={chunk.chunk_id}
className="w-full flex flex-col"
>
<div className="w-full">
<ImageWithPopover
id={chunk.img_id}
></ImageWithPopover>
<Popover>
<PopoverTrigger asChild>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
`${chunk.highlight}...`,
),
}}
className="text-sm text-text-primary mb-1"
></div>
</PopoverTrigger>
<PopoverContent className="text-text-primary">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</PopoverContent>
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
>
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
</div>
{index < chunks.length - 1 && (
<div className="w-full border-b border-border-default/80 mt-6"></div>
)}
</>
);
})}
</>
)}
</Spin>
{relatedQuestions?.length > 0 && (
<div title={t('chat.relatedQuestion')}>
<div className="flex gap-2">
{relatedQuestions?.map((x, idx) => (
<Tag
key={idx}
className={styles.tag}
onClick={handleClickRelatedQuestion(x)}
>
{x}
</Tag>
))}
</div>
</div>
)}
</div>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
</div>
</div>
{!mindMapVisible &&
!isFirstRender &&
!isSearchStrEmpty &&
!isEmpty(searchData.search_config.kb_ids) && (
<Popover>
<PopoverTrigger asChild>
<Button
className="rounded-lg h-8 w-8 p-0 absolute top-28 right-3 z-30"
variant={'transparent'}
onClick={showMindMapModal}
>
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
<BrainCircuit size={24} />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
</Popover>
)}
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
{mindMapVisible && (
<div className="absolute top-20 right-16 z-30">
<MindMapDrawer
visible={mindMapVisible}
hideModal={hideMindMapModal}
data={mindMap}
loading={mindMapLoading}
></MindMapDrawer>
</div>
)}
</section>
<SearchingView
{...searchingParam}
searchData={searchData}
setIsSearching={setIsSearching}
t={t}
/>
);
}

View File

@ -0,0 +1,35 @@
import i18n from '@/locales/config';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../../next-searches/hooks';
import { useGetSharedSearchParams, useSearching } from '../hooks';
import '../index.less';
import SearchingView from '../search-view';
export default function SearchingPage() {
const { tenantId, locale } = useGetSharedSearchParams();
const {
data: searchData = {
search_config: { kb_ids: [] },
} as unknown as ISearchAppDetailProps,
} = useFetchSearchDetail(tenantId as string);
const searchingParam = useSearching({
data: searchData,
});
const { t } = useTranslation();
// useEffect(() => {
// if (locale) {
// i18n.changeLanguage(locale);
// }
// }, [locale, i18n]);
useEffect(() => {
console.log('locale', locale, i18n.language);
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale]);
return <SearchingView {...searchingParam} searchData={searchData} t={t} />;
}

View File

@ -5,7 +5,7 @@ import searchService from '@/services/search-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useParams, useSearchParams } from 'umi';
interface CreateSearchProps {
name: string;
@ -156,13 +156,13 @@ export const useDeleteSearch = () => {
return { data, isError, deleteSearch };
};
interface IllmSettingProps {
export interface IllmSettingProps {
llm_id: string;
parameter: string;
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
interface IllmSettingEnableProps {
temperatureEnabled?: boolean;
@ -204,14 +204,29 @@ interface SearchDetailResponse {
message: string;
}
export const useFetchSearchDetail = () => {
export const useFetchSearchDetail = (tenantId?: string) => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const searchId = id || shared_id;
let param: { search_id: string | null; tenant_id?: string } = {
search_id: searchId,
};
if (shared_id) {
param = {
search_id: searchId,
tenant_id: tenantId,
};
}
const fetchSearchDetailFunc = shared_id
? searchService.getSearchDetailShare
: searchService.getSearchDetail;
const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({
queryKey: ['searchDetail', id],
queryKey: ['searchDetail', searchId],
enabled: !shared_id || !!tenantId,
queryFn: async () => {
const { data: response } = await searchService.getSearchDetail({
search_id: id,
});
const { data: response } = await fetchSearchDetailFunc(param);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to fetch search detail');
}

View File

@ -66,7 +66,7 @@ export default function SearchList() {
setSearchListParams({ ...searchParams, page, page_size: pageSize });
};
return (
<section>
<section className="w-full h-full flex flex-col">
<div className="px-8 pt-8">
<ListFilterBar
icon={
@ -89,18 +89,23 @@ export default function SearchList() {
</Button>
</ListFilterBar>
</div>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}
<div className="flex-1">
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}
</div>
</div>
{list?.data.total && (
<RAGFlowPagination
{...pick(searchParams, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
/>
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(searchParams, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
/>
</div>
)}
<Modal
open={openCreateModal}
onOpenChange={(open) => {

View File

@ -19,7 +19,7 @@ export function SearchCard({ data }: IProps) {
navigateToSearch(data?.id);
}}
>
<CardContent className="p-4 flex gap-2 items-start group">
<CardContent className="p-4 flex gap-2 items-start group h-full">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"
@ -27,7 +27,7 @@ export function SearchCard({ data }: IProps) {
name={data.name}
/>
</div>
<div className="flex flex-col gap-1 flex-1">
<div className="flex flex-col justify-between gap-1 flex-1 h-full">
<section className="flex justify-between">
<div className="text-[20px] font-bold w-80% leading-5">
{data.name}
@ -37,22 +37,13 @@ export function SearchCard({ data }: IProps) {
</SearchDropdown>
</section>
<div>{data.description}</div>
<section className="flex justify-between">
<section className="flex flex-col gap-1 mt-1">
<div>{data.description}</div>
<div>
Search app
<p className="text-sm opacity-80">
{formatDate(data.update_time)}
</p>
</div>
{/* <div className="space-x-2 invisible group-hover:visible">
<Button variant="icon" size="icon" onClick={navigateToSearch}>
<ChevronRight className="h-6 w-6" />
</Button>
<Button variant="icon" size="icon">
<Trash2 />
</Button>
</div> */}
</section>
</div>
</CardContent>

View File

@ -1,4 +1,4 @@
import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks';
import { useFetchRelatedQuestions } from '@/hooks/chat-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import {
useTestChunkAllRetrieval,
@ -18,17 +18,23 @@ import {
useRef,
useState,
} from 'react';
import {
useGetSharedSearchParams,
useSearchFetchMindMap,
} from '../next-search/hooks';
export const useSendQuestion = (kbIds: string[]) => {
export const useSendQuestion = (kbIds: string[], tenantId?: string) => {
const { sharedId } = useGetSharedSearchParams();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
api.ask,
sharedId ? api.askShare : api.ask,
);
const { testChunk, loading } = useTestChunkRetrieval();
const { testChunkAll } = useTestChunkAllRetrieval();
const { testChunk, loading } = useTestChunkRetrieval(tenantId);
const { testChunkAll } = useTestChunkAllRetrieval(tenantId);
const [sendingLoading, setSendingLoading] = useState(false);
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
const { fetchRelatedQuestions, data: relatedQuestions } =
useFetchRelatedQuestions();
useFetchRelatedQuestions(tenantId);
const [searchStr, setSearchStr] = useState<string>('');
const [isFirstRender, setIsFirstRender] = useState(true);
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
@ -43,7 +49,7 @@ export const useSendQuestion = (kbIds: string[]) => {
setIsFirstRender(false);
setCurrentAnswer({} as IAnswer);
setSendingLoading(true);
send({ kb_ids: kbIds, question: q });
send({ kb_ids: kbIds, question: q, tenantId });
testChunk({
kb_id: kbIds,
highlight: true,
@ -61,6 +67,7 @@ export const useSendQuestion = (kbIds: string[]) => {
fetchRelatedQuestions,
setPagination,
pagination.pageSize,
tenantId,
],
);
@ -218,7 +225,7 @@ export const useShowMindMapDrawer = (kbIds: string[], question: string) => {
fetchMindMap,
data: mindMap,
loading: mindMapLoading,
} = useFetchMindMap();
} = useSearchFetchMindMap();
const handleShowModal = useCallback(() => {
const searchParams = { question: trim(question), kb_ids: kbIds };

View File

@ -11,6 +11,7 @@ export enum Routes {
AgentList = '/agent-list',
Searches = '/next-searches',
Search = '/next-search',
SearchShare = '/next-search/share',
Chats = '/next-chats',
Chat = '/next-chat',
Files = '/files',
@ -234,6 +235,11 @@ const routes = [
layout: false,
component: `@/pages${Routes.Search}`,
},
{
path: `${Routes.SearchShare}`,
layout: false,
component: `@/pages${Routes.SearchShare}`,
},
{
path: Routes.Agents,
layout: false,

View File

@ -38,6 +38,7 @@ const {
listTagByKnowledgeIds,
setMeta,
getMeta,
retrievalTestShare,
} = api;
const methods = {
@ -164,6 +165,10 @@ const methods = {
url: getMeta,
method: 'get',
},
retrievalTestShare: {
url: retrievalTestShare,
method: 'post',
},
};
const kbService = registerServer<keyof typeof methods>(methods, request);

View File

@ -8,6 +8,10 @@ const {
deleteSearch,
getSearchDetail,
updateSearchSetting,
askShare,
mindmapShare,
getRelatedQuestionsShare,
getSearchDetailShare,
} = api;
const methods = {
createSearch: {
@ -27,6 +31,23 @@ const methods = {
url: updateSearchSetting,
method: 'post',
},
askShare: {
url: askShare,
method: 'post',
},
mindmapShare: {
url: mindmapShare,
method: 'post',
},
getRelatedQuestionsShare: {
url: getRelatedQuestionsShare,
method: 'post',
},
getSearchDetailShare: {
url: getSearchDetailShare,
method: 'get',
},
} as const;
const searchService = registerServer<keyof typeof methods>(methods, request);

View File

@ -181,5 +181,10 @@ export default {
getSearchList: `${api_host}/search/list`,
deleteSearch: `${api_host}/search/rm`,
getSearchDetail: `${api_host}/search/detail`,
getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`,
updateSearchSetting: `${api_host}/search/update`,
askShare: `${ExternalApi}${api_host}/searchbots/ask`,
mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`,
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`,
};