mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-04 03:25:30 +08:00
Compare commits
14 Commits
aaae938f54
...
960f47c4d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 960f47c4d4 | |||
| 51139de178 | |||
| 1f5167f1ca | |||
| 578ea34b3e | |||
| 5fb3d2f55c | |||
| d99d1e3518 | |||
| 5b387b68ba | |||
| f92a45dcc4 | |||
| c4b8e4845c | |||
| 87659dcd3a | |||
| 6fd9508017 | |||
| 113851a692 | |||
| 66c69d10fe | |||
| 781d49cd0e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -149,6 +149,7 @@ out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
admin/release
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
@ -15,22 +15,48 @@ It consists of a server-side Service and a command-line client (CLI), both imple
|
||||
- **Admin Service**: A backend service that interfaces with the RAGFlow system to execute administrative operations and monitor its status.
|
||||
- **Admin CLI**: A command-line interface that allows users to connect to the Admin Service and issue commands for system management.
|
||||
|
||||
|
||||
|
||||
### Starting the Admin Service
|
||||
|
||||
1. Before start Admin Service, please make sure RAGFlow system is already started.
|
||||
#### Launching from source code
|
||||
|
||||
1. Before start Admin Service, please make sure RAGFlow system is already started.
|
||||
|
||||
2. Launch from source code:
|
||||
|
||||
```bash
|
||||
python admin/admin_server.py
|
||||
```
|
||||
The service will start and listen for incoming connections from the CLI on the configured port.
|
||||
|
||||
#### Using docker image
|
||||
|
||||
1. Before startup, please configure the `docker_compose.yml` file to enable admin server:
|
||||
|
||||
```bash
|
||||
command:
|
||||
- --enable-adminserver
|
||||
```
|
||||
|
||||
2. Start the containers, the service will start and listen for incoming connections from the CLI on the configured port.
|
||||
|
||||
|
||||
2. Run the service script:
|
||||
```bash
|
||||
python admin/admin_server.py
|
||||
```
|
||||
The service will start and listen for incoming connections from the CLI on the configured port.
|
||||
|
||||
### Using the Admin CLI
|
||||
|
||||
1. Ensure the Admin Service is running.
|
||||
2. Launch the CLI client:
|
||||
2. Install ragflow-cli.
|
||||
```bash
|
||||
python admin/admin_client.py -h 0.0.0.0 -p 9381
|
||||
pip install ragflow-cli
|
||||
```
|
||||
3. Launch the CLI client:
|
||||
```bash
|
||||
ragflow-cli -h 0.0.0.0 -p 9381
|
||||
```
|
||||
Enter superuser's password to login. Default password is `admin`.
|
||||
|
||||
|
||||
|
||||
## Supported Commands
|
||||
|
||||
@ -42,12 +68,7 @@ Commands are case-insensitive and must be terminated with a semicolon (`;`).
|
||||
- Lists all available services within the RAGFlow system.
|
||||
- `SHOW SERVICE <id>;`
|
||||
- Shows detailed status information for the service identified by `<id>`.
|
||||
- `STARTUP SERVICE <id>;`
|
||||
- Attempts to start the service identified by `<id>`.
|
||||
- `SHUTDOWN SERVICE <id>;`
|
||||
- Attempts to gracefully shut down the service identified by `<id>`.
|
||||
- `RESTART SERVICE <id>;`
|
||||
- Attempts to restart the service identified by `<id>`.
|
||||
|
||||
|
||||
### User Management Commands
|
||||
|
||||
@ -55,10 +76,17 @@ Commands are case-insensitive and must be terminated with a semicolon (`;`).
|
||||
- Lists all users known to the system.
|
||||
- `SHOW USER '<username>';`
|
||||
- Shows details and permissions for the specified user. The username must be enclosed in single or double quotes.
|
||||
|
||||
- `CREATE USER <username> <password>;`
|
||||
- Create user by username and password. The username and password must be enclosed in single or double quotes.
|
||||
|
||||
- `DROP USER '<username>';`
|
||||
- Removes the specified user from the system. Use with caution.
|
||||
- `ALTER USER PASSWORD '<username>' '<new_password>';`
|
||||
- Changes the password for the specified user.
|
||||
- `ALTER USER ACTIVE <username> <on/off>;`
|
||||
- Changes the user to active or inactive.
|
||||
|
||||
|
||||
### Data and Agent Commands
|
||||
|
||||
|
||||
@ -16,14 +16,14 @@
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from cmd import Cmd
|
||||
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
|
||||
from typing import Dict, List, Any
|
||||
from lark import Lark, Transformer, Tree
|
||||
from lark import Lark, Transformer, Tree, Token
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from api.common.base64 import encode_to_base64
|
||||
|
||||
GRAMMAR = r"""
|
||||
start: command
|
||||
@ -100,7 +100,6 @@ NUMBER: /[0-9]+/
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
|
||||
class AdminTransformer(Transformer):
|
||||
|
||||
def start(self, items):
|
||||
@ -183,7 +182,6 @@ class AdminTransformer(Transformer):
|
||||
def meta_args(self, items):
|
||||
return items
|
||||
|
||||
|
||||
def encrypt(input_string):
|
||||
pub = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----'
|
||||
pub_key = RSA.importKey(pub)
|
||||
@ -191,13 +189,50 @@ def encrypt(input_string):
|
||||
cipher_text = cipher.encrypt(base64.b64encode(input_string.encode('utf-8')))
|
||||
return base64.b64encode(cipher_text).decode("utf-8")
|
||||
|
||||
def encode_to_base64(input_string):
|
||||
base64_encoded = base64.b64encode(input_string.encode('utf-8'))
|
||||
return base64_encoded.decode('utf-8')
|
||||
|
||||
class AdminCommandParser:
|
||||
class AdminCLI(Cmd):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.parser = Lark(GRAMMAR, start='start', parser='lalr', transformer=AdminTransformer())
|
||||
self.command_history = []
|
||||
self.is_interactive = False
|
||||
self.admin_account = "admin@ragflow.io"
|
||||
self.admin_password: str = "admin"
|
||||
self.host: str = ""
|
||||
self.port: int = 0
|
||||
|
||||
def parse_command(self, command_str: str) -> Dict[str, Any]:
|
||||
intro = r"""Type "\h" for help."""
|
||||
prompt = "admin> "
|
||||
|
||||
def onecmd(self, command: str) -> bool:
|
||||
try:
|
||||
print(f"command: {command}")
|
||||
result = self.parse_command(command)
|
||||
self.execute_command(result)
|
||||
|
||||
if isinstance(result, Tree):
|
||||
return False
|
||||
|
||||
if result.get('type') == 'meta' and result.get('command') in ['q', 'quit', 'exit']:
|
||||
return True
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nUse '\\q' to quit")
|
||||
except EOFError:
|
||||
print("\nGoodbye!")
|
||||
return True
|
||||
return False
|
||||
|
||||
def emptyline(self) -> bool:
|
||||
return False
|
||||
|
||||
def default(self, line: str) -> bool:
|
||||
return self.onecmd(line)
|
||||
|
||||
def parse_command(self, command_str: str) -> dict[str, str] | Tree[Token]:
|
||||
if not command_str.strip():
|
||||
return {'type': 'empty'}
|
||||
|
||||
@ -209,16 +244,6 @@ class AdminCommandParser:
|
||||
except Exception as e:
|
||||
return {'type': 'error', 'message': f'Parse error: {str(e)}'}
|
||||
|
||||
|
||||
class AdminCLI:
|
||||
def __init__(self):
|
||||
self.parser = AdminCommandParser()
|
||||
self.is_interactive = False
|
||||
self.admin_account = "admin@ragflow.io"
|
||||
self.admin_password: str = "admin"
|
||||
self.host: str = ""
|
||||
self.port: int = 0
|
||||
|
||||
def verify_admin(self, args):
|
||||
|
||||
conn_info = self._parse_connection_args(args)
|
||||
@ -323,7 +348,7 @@ class AdminCLI:
|
||||
continue
|
||||
|
||||
print(f"command: {command}")
|
||||
result = self.parser.parse_command(command)
|
||||
result = self.parse_command(command)
|
||||
self.execute_command(result)
|
||||
|
||||
if isinstance(result, Tree):
|
||||
@ -610,10 +635,17 @@ def main():
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
|
||||
""")
|
||||
if cli.verify_admin(sys.argv):
|
||||
cli.run_interactive()
|
||||
cli.cmdloop()
|
||||
else:
|
||||
print(r"""
|
||||
____ ___ ______________ ___ __ _
|
||||
/ __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
|
||||
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / /| |/ __ / __ `__ \/ / __ \
|
||||
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
|
||||
""")
|
||||
if cli.verify_admin(sys.argv):
|
||||
cli.run_interactive()
|
||||
cli.cmdloop()
|
||||
# cli.run_single_command(sys.argv[1:])
|
||||
|
||||
|
||||
|
||||
47
admin/build_cli_release.sh
Executable file
47
admin/build_cli_release.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Start building..."
|
||||
echo "================================"
|
||||
|
||||
PROJECT_NAME="ragflow-cli"
|
||||
|
||||
RELEASE_DIR="release"
|
||||
BUILD_DIR="dist"
|
||||
SOURCE_DIR="src"
|
||||
PACKAGE_DIR="ragflow_cli"
|
||||
|
||||
echo "🧹 Clean old build folder..."
|
||||
rm -rf release/
|
||||
|
||||
echo "📁 Prepare source code..."
|
||||
mkdir release/$PROJECT_NAME/$SOURCE_DIR -p
|
||||
cp pyproject.toml release/$PROJECT_NAME/pyproject.toml
|
||||
cp README.md release/$PROJECT_NAME/README.md
|
||||
|
||||
mkdir release/$PROJECT_NAME/$SOURCE_DIR/$PACKAGE_DIR -p
|
||||
cp admin_client.py release/$PROJECT_NAME/$SOURCE_DIR/$PACKAGE_DIR/admin_client.py
|
||||
|
||||
if [ -d "release/$PROJECT_NAME/$SOURCE_DIR" ]; then
|
||||
echo "✅ source dir: release/$PROJECT_NAME/$SOURCE_DIR"
|
||||
else
|
||||
echo "❌ source dir not exist: release/$PROJECT_NAME/$SOURCE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔨 Make build file..."
|
||||
cd release/$PROJECT_NAME
|
||||
export PYTHONPATH=$(pwd)
|
||||
python -m build
|
||||
|
||||
echo "✅ check build result..."
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
echo "📦 Package generated:"
|
||||
ls -la $BUILD_DIR/
|
||||
else
|
||||
echo "❌ Build Failed: $BUILD_DIR not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 Build finished successfully!"
|
||||
24
admin/pyproject.toml
Normal file
24
admin/pyproject.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[project]
|
||||
name = "ragflow-cli"
|
||||
version = "0.21.0.dev2"
|
||||
description = "Admin Service's client of [RAGFlow](https://github.com/infiniflow/ragflow). The Admin Service provides user management and system monitoring. "
|
||||
authors = [{ name = "Lynn", email = "lynn_inf@hotmail.com" }]
|
||||
license = { text = "Apache License, Version 2.0" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
dependencies = [
|
||||
"requests>=2.30.0,<3.0.0",
|
||||
"beartype>=0.18.5,<0.19.0",
|
||||
"pycryptodomex>=3.10.0",
|
||||
"lark>=1.1.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"pytest>=8.3.5",
|
||||
"requests>=2.32.3",
|
||||
"requests-toolbelt>=1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ragflow-cli = "ragflow_cli.admin_client:main"
|
||||
@ -177,8 +177,17 @@ class ServiceMgr:
|
||||
def get_all_services():
|
||||
result = []
|
||||
configs = SERVICE_CONFIGS.configs
|
||||
for config in configs:
|
||||
result.append(config.to_dict())
|
||||
for service_id, config in enumerate(configs):
|
||||
config_dict = config.to_dict()
|
||||
try:
|
||||
service_detail = ServiceMgr.get_service_details(service_id)
|
||||
if service_detail['alive']:
|
||||
config_dict['status'] = 'Alive'
|
||||
else:
|
||||
config_dict['status'] = 'Timeout'
|
||||
except Exception:
|
||||
config_dict['status'] = 'Timeout'
|
||||
result.append(config_dict)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -568,7 +568,7 @@ def change_parser():
|
||||
|
||||
def reset_doc():
|
||||
nonlocal doc
|
||||
e = DocumentService.update_by_id(doc.id, {"parser_id": req["parser_id"], "progress": 0, "progress_msg": "", "run": TaskStatus.UNSTART.value})
|
||||
e = DocumentService.update_by_id(doc.id, {"pipeline_id": req["pipeline_id"], "parser_id": req["parser_id"], "progress": 0, "progress_msg": "", "run": TaskStatus.UNSTART.value})
|
||||
if not e:
|
||||
return get_data_error_result(message="Document not found!")
|
||||
if doc.token_num > 0:
|
||||
|
||||
@ -397,9 +397,10 @@ class KnowledgebaseService(CommonService):
|
||||
else:
|
||||
kbs = kbs.order_by(cls.model.getter_by(orderby).asc())
|
||||
|
||||
total = kbs.count()
|
||||
kbs = kbs.paginate(page_number, items_per_page)
|
||||
|
||||
return list(kbs.dicts()), kbs.count()
|
||||
return list(kbs.dicts()), total
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
|
||||
@ -151,10 +151,12 @@ def get_data_error_result(code=settings.RetCode.DATA_ERROR, message="Sorry! Data
|
||||
def server_error_response(e):
|
||||
logging.exception(e)
|
||||
try:
|
||||
if e.code == 401:
|
||||
return get_json_result(code=401, message=repr(e))
|
||||
except BaseException:
|
||||
pass
|
||||
msg = repr(e).lower()
|
||||
if getattr(e, "code", None) == 401 or ("unauthorized" in msg) or ("401" in msg):
|
||||
return get_json_result(code=settings.RetCode.UNAUTHORIZED, message=repr(e))
|
||||
except Exception as ex:
|
||||
logging.warning(f"error checking authorization: {ex}")
|
||||
|
||||
if len(e.args) > 1:
|
||||
try:
|
||||
serialized_data = serialize_for_json(e.args[1])
|
||||
|
||||
16
docs/faq.mdx
16
docs/faq.mdx
@ -40,19 +40,9 @@ Each RAGFlow release is available in two editions:
|
||||
RAGFlow offers two Docker image editions, `v0.20.5-slim` and `v0.20.5`:
|
||||
|
||||
- `infiniflow/ragflow:v0.20.5-slim` (default): The RAGFlow Docker image without embedding models.
|
||||
- `infiniflow/ragflow:v0.20.5`: The RAGFlow Docker image with embedding models including:
|
||||
- Built-in embedding models:
|
||||
- `BAAI/bge-large-zh-v1.5`
|
||||
- `maidalun1020/bce-embedding-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`
|
||||
- `infiniflow/ragflow:v0.20.5`: The RAGFlow Docker image with the following built-in embedding models:
|
||||
- `BAAI/bge-large-zh-v1.5`
|
||||
- `maidalun1020/bce-embedding-base_v1`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -12,33 +12,50 @@ The Admin CLI and Admin Service form a client-server architectural suite for RAG
|
||||
|
||||
|
||||
|
||||
## Starting the Admin Service
|
||||
### Starting the Admin Service
|
||||
|
||||
#### Launching from source code
|
||||
|
||||
1. Before start Admin Service, please make sure RAGFlow system is already started.
|
||||
2. Switch to ragflow/ directory and run the service script:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
python admin/admin_server.py
|
||||
```
|
||||
2. Launch from source code:
|
||||
|
||||
The service will start and listen for incoming connections from the CLI on the configured port. Default port is 9381.
|
||||
```bash
|
||||
python admin/admin_server.py
|
||||
```
|
||||
|
||||
The service will start and listen for incoming connections from the CLI on the configured port.
|
||||
|
||||
#### Using docker image
|
||||
|
||||
1. Before startup, please configure the `docker_compose.yml` file to enable admin server:
|
||||
|
||||
```bash
|
||||
command:
|
||||
- --enable-adminserver
|
||||
```
|
||||
|
||||
2. Start the containers, the service will start and listen for incoming connections from the CLI on the configured port.
|
||||
|
||||
|
||||
|
||||
## Using the Admin CLI
|
||||
### Using the Admin CLI
|
||||
|
||||
1. Ensure the Admin Service is running.
|
||||
2. Launch the CLI client:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
python admin/admin_client.py -h 0.0.0.0 -p 9381
|
||||
```
|
||||
2. Install ragflow-cli.
|
||||
|
||||
Enter superuser's password to login. Default password is `admin`.
|
||||
```bash
|
||||
pip install ragflow-cli
|
||||
```
|
||||
|
||||
3. Launch the CLI client:
|
||||
|
||||
```bash
|
||||
ragflow-cli -h 0.0.0.0 -p 9381
|
||||
```
|
||||
|
||||
Enter superuser's password to login. Default password is `admin`.
|
||||
|
||||
|
||||
|
||||
@ -121,16 +138,16 @@ Commands are case-insensitive and must be terminated with a semicolon(;).
|
||||
admin> list services;
|
||||
command: list services;
|
||||
Listing all services
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
|
||||
| extra | host | id | name | port | service_type |
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
|
||||
| {} | 0.0.0.0 | 0 | ragflow_0 | 9380 | ragflow_server |
|
||||
| {'meta_type': 'mysql', 'password': 'infini_rag_flow', 'username': 'root'} | localhost | 1 | mysql | 5455 | meta_data |
|
||||
| {'password': 'infini_rag_flow', 'store_type': 'minio', 'user': 'rag_flow'} | localhost | 2 | minio | 9000 | file_store |
|
||||
| {'password': 'infini_rag_flow', 'retrieval_type': 'elasticsearch', 'username': 'elastic'} | localhost | 3 | elasticsearch | 1200 | retrieval |
|
||||
| {'db_name': 'default_db', 'retrieval_type': 'infinity'} | localhost | 4 | infinity | 23817 | retrieval |
|
||||
| {'database': 1, 'mq_type': 'redis', 'password': 'infini_rag_flow'} | localhost | 5 | redis | 6379 | message_queue |
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+---------+
|
||||
| extra | host | id | name | port | service_type | status |
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+---------+
|
||||
| {} | 0.0.0.0 | 0 | ragflow_0 | 9380 | ragflow_server | Timeout |
|
||||
| {'meta_type': 'mysql', 'password': 'infini_rag_flow', 'username': 'root'} | localhost | 1 | mysql | 5455 | meta_data | Alive |
|
||||
| {'password': 'infini_rag_flow', 'store_type': 'minio', 'user': 'rag_flow'} | localhost | 2 | minio | 9000 | file_store | Alive |
|
||||
| {'password': 'infini_rag_flow', 'retrieval_type': 'elasticsearch', 'username': 'elastic'} | localhost | 3 | elasticsearch | 1200 | retrieval | Alive |
|
||||
| {'db_name': 'default_db', 'retrieval_type': 'infinity'} | localhost | 4 | infinity | 23817 | retrieval | Timeout |
|
||||
| {'database': 1, 'mq_type': 'redis', 'password': 'infini_rag_flow'} | localhost | 5 | redis | 6379 | message_queue | Alive |
|
||||
+-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+---------+
|
||||
|
||||
```
|
||||
|
||||
|
||||
@ -704,10 +704,9 @@ print("Async bulk parsing initiated.")
|
||||
DataSet.parse_documents(document_ids: list[str]) -> list[tuple[str, str, int, int]]
|
||||
```
|
||||
|
||||
Parses documents **synchronously** in the current dataset.
|
||||
This method wraps `async_parse_documents()` and automatically waits for all parsing tasks to complete.
|
||||
It returns detailed parsing results, including the status and statistics for each document.
|
||||
If interrupted by the user (e.g. `Ctrl+C`), all pending parsing jobs will be cancelled gracefully.
|
||||
*Asynchronously* parses documents in the current dataset.
|
||||
|
||||
This method encapsulates `async_parse_documents()`. It awaits the completion of all parsing tasks before returning detailed results, including the parsing status and statistics for each document. If a keyboard interruption occurs (e.g., `Ctrl+C`), all pending parsing tasks will be cancelled gracefully.
|
||||
|
||||
#### Parameters
|
||||
|
||||
@ -717,16 +716,17 @@ The IDs of the documents to parse.
|
||||
|
||||
#### Returns
|
||||
|
||||
A list of tuples with detailed parsing results:
|
||||
A list of tuples with detailed parsing results:
|
||||
|
||||
```python
|
||||
[
|
||||
(document_id: str, status: str, chunk_count: int, token_count: int),
|
||||
...
|
||||
]
|
||||
```
|
||||
- **status** — Final parsing state (`success`, `failed`, `cancelled`, etc.)
|
||||
- **chunk_count** — Number of content chunks created for the document.
|
||||
- **token_count** — Total number of tokens processed.
|
||||
- `status`: The final parsing state (e.g., `success`, `failed`, `cancelled`).
|
||||
- `chunk_count`: The number of content chunks created from the document.
|
||||
- `token_count`: The total number of tokens processed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -580,7 +580,7 @@ Released on September 30, 2024.
|
||||
|
||||
### Compatibility changes
|
||||
|
||||
From this release onwards, RAGFlow offers slim editions of its Docker images to improve the experience for users with limited Internet access. A slim edition of RAGFlow's Docker image does not include built-in BGE/BCE embedding models and has a size of about 1GB; a full edition of RAGFlow is approximately 9GB and includes both built-in embedding models and embedding models that will be downloaded once you select them in the RAGFlow UI.
|
||||
From this release onwards, RAGFlow offers slim editions of its Docker images to improve the experience for users with limited Internet access. A slim edition of RAGFlow's Docker image does not include built-in BGE/BCE embedding models and has a size of about 1GB; a full edition of RAGFlow is approximately 9GB and includes two built-in embedding models.
|
||||
|
||||
The default Docker image edition is `nightly-slim`. The following list clarifies the differences between various editions:
|
||||
|
||||
|
||||
@ -166,7 +166,7 @@ class HierarchicalMerger(ProcessBase):
|
||||
img = None
|
||||
for i in path:
|
||||
txt += lines[i] + "\n"
|
||||
concat_img(img, id2image(section_images[i], partial(STORAGE_IMPL.get)))
|
||||
concat_img(img, id2image(section_images[i], partial(STORAGE_IMPL.get, tenant_id=self._canvas._tenant_id)))
|
||||
cks.append(txt)
|
||||
images.append(img)
|
||||
|
||||
@ -180,7 +180,7 @@ class HierarchicalMerger(ProcessBase):
|
||||
]
|
||||
async with trio.open_nursery() as nursery:
|
||||
for d in cks:
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid())
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put, tenant_id=self._canvas._tenant_id), get_uuid())
|
||||
self.set_output("chunks", cks)
|
||||
|
||||
self.callback(1, "Done.")
|
||||
|
||||
@ -512,4 +512,4 @@ class Parser(ProcessBase):
|
||||
outs = self.output()
|
||||
async with trio.open_nursery() as nursery:
|
||||
for d in outs.get("json", []):
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid())
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put, tenant_id=self._canvas._tenant_id), get_uuid())
|
||||
|
||||
@ -87,7 +87,7 @@ class Splitter(ProcessBase):
|
||||
sections, section_images = [], []
|
||||
for o in from_upstream.json_result or []:
|
||||
sections.append((o.get("text", ""), o.get("position_tag", "")))
|
||||
section_images.append(id2image(o.get("img_id"), partial(STORAGE_IMPL.get)))
|
||||
section_images.append(id2image(o.get("img_id"), partial(STORAGE_IMPL.get, tenant_id=self._canvas._tenant_id)))
|
||||
|
||||
chunks, images = naive_merge_with_images(
|
||||
sections,
|
||||
@ -106,6 +106,6 @@ class Splitter(ProcessBase):
|
||||
]
|
||||
async with trio.open_nursery() as nursery:
|
||||
for d in cks:
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid())
|
||||
nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put, tenant_id=self._canvas._tenant_id), get_uuid())
|
||||
self.set_output("chunks", cks)
|
||||
self.callback(1, "Done.")
|
||||
|
||||
@ -680,8 +680,7 @@ async def gen_toc_from_text(txt_info: dict, chat_mdl, callback=None):
|
||||
chat_mdl,
|
||||
gen_conf={"temperature": 0.0, "top_p": 0.9}
|
||||
)
|
||||
print(ans, "::::::::::::::::::::::::::::::::::::", flush=True)
|
||||
txt_info["toc"] = ans if ans else []
|
||||
txt_info["toc"] = ans if ans and not isinstance(ans, str) else []
|
||||
if callback:
|
||||
callback(msg="")
|
||||
except Exception as e:
|
||||
@ -728,8 +727,6 @@ async def run_toc_from_text(chunks, chat_mdl, callback=None):
|
||||
|
||||
for chunk in chunks_res:
|
||||
titles.extend(chunk.get("toc", []))
|
||||
|
||||
print(titles, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
||||
|
||||
# Filter out entries with title == -1
|
||||
prune = len(titles) > 512
|
||||
@ -745,12 +742,16 @@ async def run_toc_from_text(chunks, chat_mdl, callback=None):
|
||||
filtered.append(x)
|
||||
|
||||
logging.info(f"\n\nFiltered TOC sections:\n{filtered}")
|
||||
if not filtered:
|
||||
return []
|
||||
|
||||
# Generate initial level (level/title)
|
||||
raw_structure = [x.get("title", "") for x in filtered]
|
||||
|
||||
# Assign hierarchy levels using LLM
|
||||
toc_with_levels = assign_toc_levels(raw_structure, chat_mdl, {"temperature": 0.0, "top_p": 0.9})
|
||||
if not toc_with_levels:
|
||||
return []
|
||||
|
||||
# Merge structure and content (by index)
|
||||
prune = len(toc_with_levels) > 512
|
||||
@ -779,7 +780,6 @@ def relevant_chunks_with_toc(query: str, toc:list[dict], chat_mdl, topn: int=6):
|
||||
chat_mdl,
|
||||
gen_conf={"temperature": 0.0, "top_p": 0.9}
|
||||
)
|
||||
print(ans, "::::::::::::::::::::::::::::::::::::", flush=True)
|
||||
id2score = {}
|
||||
for ti, sc in zip(toc, ans):
|
||||
if not isinstance(sc, dict) or sc.get("score", -1) < 1:
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import concurrent
|
||||
# from beartype import BeartypeConf
|
||||
# from beartype.claw import beartype_all # <-- you didn't sign up for this
|
||||
# beartype_all(conf=BeartypeConf(violation_type=UserWarning)) # <-- emit warnings from all code
|
||||
@ -317,7 +317,7 @@ async def build_chunks(task, progress_callback):
|
||||
d["img_id"] = ""
|
||||
docs.append(d)
|
||||
return
|
||||
await image2id(d, partial(STORAGE_IMPL.put), d["id"], task["kb_id"])
|
||||
await image2id(d, partial(STORAGE_IMPL.put, tenant_id=task["tenant_id"]), d["id"], task["kb_id"])
|
||||
docs.append(d)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
@ -370,38 +370,6 @@ async def build_chunks(task, progress_callback):
|
||||
nursery.start_soon(doc_question_proposal, chat_mdl, d, task["parser_config"]["auto_questions"])
|
||||
progress_callback(msg="Question generation {} chunks completed in {:.2f}s".format(len(docs), timer() - st))
|
||||
|
||||
if task["parser_id"].lower() == "naive" and task["parser_config"].get("toc_extraction", False):
|
||||
progress_callback(msg="Start to generate table of content ...")
|
||||
chat_mdl = LLMBundle(task["tenant_id"], LLMType.CHAT, llm_name=task["llm_id"], lang=task["language"])
|
||||
docs = sorted(docs, key=lambda d:(
|
||||
d.get("page_num_int", 0)[0] if isinstance(d.get("page_num_int", 0), list) else d.get("page_num_int", 0),
|
||||
d.get("top_int", 0)[0] if isinstance(d.get("top_int", 0), list) else d.get("top_int", 0)
|
||||
))
|
||||
toc: list[dict] = await run_toc_from_text([d["content_with_weight"] for d in docs], chat_mdl, progress_callback)
|
||||
logging.info("------------ T O C -------------\n"+json.dumps(toc, ensure_ascii=False, indent=' '))
|
||||
ii = 0
|
||||
while ii < len(toc):
|
||||
try:
|
||||
idx = int(toc[ii]["chunk_id"])
|
||||
del toc[ii]["chunk_id"]
|
||||
toc[ii]["ids"] = [docs[idx]["id"]]
|
||||
if ii == len(toc) -1:
|
||||
break
|
||||
for jj in range(idx+1, int(toc[ii+1]["chunk_id"])+1):
|
||||
toc[ii]["ids"].append(docs[jj]["id"])
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
ii += 1
|
||||
|
||||
if toc:
|
||||
d = copy.deepcopy(docs[-1])
|
||||
d["content_with_weight"] = json.dumps(toc, ensure_ascii=False)
|
||||
d["toc_kwd"] = "toc"
|
||||
d["available_int"] = 0
|
||||
d["page_num_int"] = 100000000
|
||||
d["id"] = xxhash.xxh64((d["content_with_weight"] + str(d["doc_id"])).encode("utf-8", "surrogatepass")).hexdigest()
|
||||
docs.append(d)
|
||||
|
||||
if task["kb_parser_config"].get("tag_kb_ids", []):
|
||||
progress_callback(msg="Start to tag for every chunk ...")
|
||||
kb_ids = task["kb_parser_config"]["tag_kb_ids"]
|
||||
@ -451,6 +419,39 @@ async def build_chunks(task, progress_callback):
|
||||
return docs
|
||||
|
||||
|
||||
def build_TOC(task, docs, progress_callback):
|
||||
progress_callback(msg="Start to generate table of content ...")
|
||||
chat_mdl = LLMBundle(task["tenant_id"], LLMType.CHAT, llm_name=task["llm_id"], lang=task["language"])
|
||||
docs = sorted(docs, key=lambda d:(
|
||||
d.get("page_num_int", 0)[0] if isinstance(d.get("page_num_int", 0), list) else d.get("page_num_int", 0),
|
||||
d.get("top_int", 0)[0] if isinstance(d.get("top_int", 0), list) else d.get("top_int", 0)
|
||||
))
|
||||
toc: list[dict] = trio.run(run_toc_from_text, [d["content_with_weight"] for d in docs], chat_mdl, progress_callback)
|
||||
logging.info("------------ T O C -------------\n"+json.dumps(toc, ensure_ascii=False, indent=' '))
|
||||
ii = 0
|
||||
while ii < len(toc):
|
||||
try:
|
||||
idx = int(toc[ii]["chunk_id"])
|
||||
del toc[ii]["chunk_id"]
|
||||
toc[ii]["ids"] = [docs[idx]["id"]]
|
||||
if ii == len(toc) -1:
|
||||
break
|
||||
for jj in range(idx+1, int(toc[ii+1]["chunk_id"])+1):
|
||||
toc[ii]["ids"].append(docs[jj]["id"])
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
ii += 1
|
||||
|
||||
if toc:
|
||||
d = copy.deepcopy(docs[-1])
|
||||
d["content_with_weight"] = json.dumps(toc, ensure_ascii=False)
|
||||
d["toc_kwd"] = "toc"
|
||||
d["available_int"] = 0
|
||||
d["page_num_int"] = 100000000
|
||||
d["id"] = xxhash.xxh64((d["content_with_weight"] + str(d["doc_id"])).encode("utf-8", "surrogatepass")).hexdigest()
|
||||
return d
|
||||
|
||||
|
||||
def init_kb(row, vector_size: int):
|
||||
idxnm = search.index_name(row["tenant_id"])
|
||||
return settings.docStoreConn.createIdx(idxnm, row.get("kb_id", ""), vector_size)
|
||||
@ -753,7 +754,7 @@ async def insert_es(task_id, task_tenant_id, task_dataset_id, chunks, progress_c
|
||||
return True
|
||||
|
||||
|
||||
@timeout(60*60*2, 1)
|
||||
@timeout(60*60*3, 1)
|
||||
async def do_handle_task(task):
|
||||
task_type = task.get("task_type", "")
|
||||
|
||||
@ -773,6 +774,8 @@ async def do_handle_task(task):
|
||||
task_document_name = task["name"]
|
||||
task_parser_config = task["parser_config"]
|
||||
task_start_ts = timer()
|
||||
toc_thread = None
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
# prepare the progress callback function
|
||||
progress_callback = partial(set_progress, task_id, task_from_page, task_to_page)
|
||||
@ -905,8 +908,6 @@ async def do_handle_task(task):
|
||||
if not chunks:
|
||||
progress_callback(1., msg=f"No chunk built from {task_document_name}")
|
||||
return
|
||||
# TODO: exception handler
|
||||
## set_progress(task["did"], -1, "ERROR: ")
|
||||
progress_callback(msg="Generate {} chunks".format(len(chunks)))
|
||||
start_ts = timer()
|
||||
try:
|
||||
@ -920,6 +921,8 @@ async def do_handle_task(task):
|
||||
progress_message = "Embedding chunks ({:.2f}s)".format(timer() - start_ts)
|
||||
logging.info(progress_message)
|
||||
progress_callback(msg=progress_message)
|
||||
if task["parser_id"].lower() == "naive" and task["parser_config"].get("toc_extraction", False):
|
||||
toc_thread = executor.submit(build_TOC,task, chunks, progress_callback)
|
||||
|
||||
chunk_count = len(set([chunk["id"] for chunk in chunks]))
|
||||
start_ts = timer()
|
||||
@ -934,8 +937,17 @@ async def do_handle_task(task):
|
||||
DocumentService.increment_chunk_num(task_doc_id, task_dataset_id, token_count, chunk_count, 0)
|
||||
|
||||
time_cost = timer() - start_ts
|
||||
progress_callback(msg="Indexing done ({:.2f}s).".format(time_cost))
|
||||
if toc_thread:
|
||||
d = toc_thread.result()
|
||||
if d:
|
||||
e = await insert_es(task_id, task_tenant_id, task_dataset_id, [d], progress_callback)
|
||||
if not e:
|
||||
return
|
||||
DocumentService.increment_chunk_num(task_doc_id, task_dataset_id, 0, 1, 0)
|
||||
|
||||
task_time_cost = timer() - task_start_ts
|
||||
progress_callback(prog=1.0, msg="Indexing done ({:.2f}s). Task done ({:.2f}s)".format(time_cost, task_time_cost))
|
||||
progress_callback(prog=1.0, msg="Task done ({:.2f}s)".format(task_time_cost))
|
||||
logging.info(
|
||||
"Chunk doc({}), page({}-{}), chunks({}), token({}), elapsed:{:.2f}".format(task_document_name, task_from_page,
|
||||
task_to_page, len(chunks),
|
||||
|
||||
@ -60,7 +60,7 @@ class RAGFlowMinio:
|
||||
)
|
||||
return r
|
||||
|
||||
def put(self, bucket, fnm, binary):
|
||||
def put(self, bucket, fnm, binary, tenant_id=None):
|
||||
for _ in range(3):
|
||||
try:
|
||||
if not self.conn.bucket_exists(bucket):
|
||||
@ -76,13 +76,13 @@ class RAGFlowMinio:
|
||||
self.__open__()
|
||||
time.sleep(1)
|
||||
|
||||
def rm(self, bucket, fnm):
|
||||
def rm(self, bucket, fnm, tenant_id=None):
|
||||
try:
|
||||
self.conn.remove_object(bucket, fnm)
|
||||
except Exception:
|
||||
logging.exception(f"Fail to remove {bucket}/{fnm}:")
|
||||
|
||||
def get(self, bucket, filename):
|
||||
def get(self, bucket, filename, tenant_id=None):
|
||||
for _ in range(1):
|
||||
try:
|
||||
r = self.conn.get_object(bucket, filename)
|
||||
@ -93,7 +93,7 @@ class RAGFlowMinio:
|
||||
time.sleep(1)
|
||||
return
|
||||
|
||||
def obj_exist(self, bucket, filename):
|
||||
def obj_exist(self, bucket, filename, tenant_id=None):
|
||||
try:
|
||||
if not self.conn.bucket_exists(bucket):
|
||||
return False
|
||||
@ -121,7 +121,7 @@ class RAGFlowMinio:
|
||||
logging.exception(f"bucket_exist {bucket} got exception")
|
||||
return False
|
||||
|
||||
def get_presigned_url(self, bucket, fnm, expires):
|
||||
def get_presigned_url(self, bucket, fnm, expires, tenant_id=None):
|
||||
for _ in range(10):
|
||||
try:
|
||||
return self.conn.get_presigned_url("GET", bucket, fnm, expires)
|
||||
|
||||
@ -104,7 +104,7 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
|
||||
<TooltipProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider
|
||||
defaultTheme={ThemeEnum.Light}
|
||||
defaultTheme={ThemeEnum.Dark}
|
||||
storageKey="ragflow-ui-theme"
|
||||
>
|
||||
<Root>{children}</Root>
|
||||
|
||||
@ -108,6 +108,7 @@ export function DataFlowSelect(props: IProps) {
|
||||
{...field}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
triggerClassName="!bg-bg-base"
|
||||
/>
|
||||
)}
|
||||
{isMult && (
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -36,6 +37,7 @@ export const DelimiterInput = forwardRef<HTMLInputElement, InputProps & IProps>(
|
||||
maxLength={maxLength}
|
||||
defaultValue={defaultValue}
|
||||
ref={ref}
|
||||
className={cn('bg-bg-base', props.className)}
|
||||
{...props}
|
||||
></Input>
|
||||
);
|
||||
|
||||
@ -54,7 +54,10 @@ function MarkdownContent({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content);
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['think', 'section'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
// let text = content;
|
||||
if (text === '') {
|
||||
text = t('chat.searching');
|
||||
|
||||
@ -21,7 +21,7 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = ThemeEnum.Light,
|
||||
defaultTheme = ThemeEnum.Dark,
|
||||
storageKey = 'vite-ui-theme',
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
|
||||
@ -31,7 +31,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-8 w-full rounded-md border border-input bg-bg-card px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-8 w-full rounded-md border border-input bg-bg-base px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@ -65,7 +65,11 @@ const ExpandedInput = ({
|
||||
{prefix}
|
||||
</span>
|
||||
<Input
|
||||
className={cn({ 'pr-8': !!suffix, 'pl-8': !!prefix }, className)}
|
||||
className={cn(
|
||||
{ 'pr-8': !!suffix, 'pl-8': !!prefix },
|
||||
'bg-bg-base',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
></Input>
|
||||
<span
|
||||
|
||||
@ -291,7 +291,7 @@ export const RAGFlowSelect = forwardRef<
|
||||
onReset={handleReset}
|
||||
allowClear={allowClear}
|
||||
ref={ref}
|
||||
className={triggerClassName}
|
||||
className={cn(triggerClassName, 'bg-bg-base')}
|
||||
>
|
||||
<SelectValue placeholder={placeholder}>{label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
@ -8,7 +8,7 @@ const Table = React.forwardRef<
|
||||
>(({ className, rootClassName, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full overflow-auto rounded-2xl bg-bg-card scrollbar-none',
|
||||
'relative w-full overflow-auto rounded-2xl bg-bg-card scrollbar-auto',
|
||||
rootClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-auto scrollbar-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-w-[20vw]',
|
||||
'z-50 overflow-auto scrollbar-auto rounded-md whitespace-pre-wrap border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-w-[30vw]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -41,9 +41,7 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
|
||||
>
|
||||
<Info className="size-3 ml-2" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@ -24,6 +24,12 @@ export const useNavigatePage = () => {
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
const navigateToDatasetOverview = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateToDataFile = useCallback(
|
||||
(id: string) => () => {
|
||||
@ -160,6 +166,7 @@ export const useNavigatePage = () => {
|
||||
return {
|
||||
navigateToDatasetList,
|
||||
navigateToDataset,
|
||||
navigateToDatasetOverview,
|
||||
navigateToHome,
|
||||
navigateToProfile,
|
||||
navigateToChatList,
|
||||
|
||||
@ -300,8 +300,8 @@ export default {
|
||||
dataFlowPlaceholder: 'Please select a pipeline.',
|
||||
buildItFromScratch: 'Build it from scratch',
|
||||
dataFlow: 'Pipeline',
|
||||
parseType: 'Parse Type',
|
||||
manualSetup: 'Manual Setup',
|
||||
parseType: 'Ingestion pipeline',
|
||||
manualSetup: 'Choose pipeline',
|
||||
builtIn: 'Built-in',
|
||||
titleDescription:
|
||||
'Update your knowledge base configuration here, particularly the chunking method.',
|
||||
@ -477,8 +477,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
useGraphRagTip:
|
||||
'Construct a knowledge graph over file chunks of the current knowledge base to enhance multi-hop question-answering involving nested logic. See https://ragflow.io/docs/dev/construct_knowledge_graph for details.',
|
||||
graphRagMethod: 'Method',
|
||||
graphRagMethodTip: `Light: (Default) Use prompts provided by github.com/HKUDS/LightRAG to extract entities and relationships. This option consumes fewer tokens, less memory, and fewer computational resources.</br>
|
||||
General: Use prompts provided by github.com/microsoft/graphrag to extract entities and relationships`,
|
||||
graphRagMethodTip: `
|
||||
Light: (Default) Use prompts provided by github.com/HKUDS/LightRAG to extract entities and relationships. This option consumes fewer tokens, less memory, and fewer computational resources.</br>
|
||||
General: Use prompts provided by github.com/microsoft/graphrag to extract entities and relationships`,
|
||||
resolution: 'Entity resolution',
|
||||
resolutionTip: `An entity deduplication switch. When enabled, the LLM will combine similar entities - e.g., '2025' and 'the year of 2025', or 'IT' and 'Information Technology' - to construct a more accurate graph`,
|
||||
community: 'Community reports',
|
||||
@ -1672,6 +1673,7 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
page: '{{page}} /Page',
|
||||
},
|
||||
dataflowParser: {
|
||||
result: 'Result',
|
||||
parseSummary: 'Parse Summary',
|
||||
parseSummaryTip: 'Parser:deepdoc',
|
||||
rerunFromCurrentStep: 'Rerun From Current Step',
|
||||
@ -1736,10 +1738,10 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
addParser: 'Add Parser',
|
||||
hierarchy: 'Hierarchy',
|
||||
regularExpressions: 'Regular Expressions',
|
||||
overlappedPercent: 'Overlapped percent',
|
||||
overlappedPercent: 'Overlapped percent (%)',
|
||||
searchMethod: 'Search method',
|
||||
searchMethodTip: `Defines how the content can be searched — by full-text, embedding, or both.
|
||||
The Tokenizer will store the content in the corresponding data structures for the selected methods.`,
|
||||
The Indexer will store the content in the corresponding data structures for the selected methods.`,
|
||||
begin: 'File',
|
||||
parserMethod: 'Parsing method',
|
||||
systemPrompt: 'System Prompt',
|
||||
@ -1748,11 +1750,11 @@ The Tokenizer will store the content in the corresponding data structures for th
|
||||
exportJson: 'Export JSON',
|
||||
viewResult: 'View result',
|
||||
running: 'Running',
|
||||
summary: 'Augmented Context',
|
||||
summary: 'Summary',
|
||||
keywords: 'Keywords',
|
||||
questions: 'Questions',
|
||||
metadata: 'Metadata',
|
||||
fieldName: 'Result Destination',
|
||||
fieldName: 'Result destination',
|
||||
prompts: {
|
||||
system: {
|
||||
keywords: `Role
|
||||
@ -1817,6 +1819,9 @@ Important structured information may include: names, dates, locations, events, k
|
||||
imageParseMethodOptions: {
|
||||
ocr: 'OCR',
|
||||
},
|
||||
note: 'Note',
|
||||
noteDescription: 'Note',
|
||||
notePlaceholder: 'Please enter a note',
|
||||
},
|
||||
datasetOverview: {
|
||||
downloadTip: 'Files being downloaded from data sources. ',
|
||||
|
||||
@ -268,25 +268,25 @@ export default {
|
||||
<br/>
|
||||
是否要继续?
|
||||
`,
|
||||
extractRaptor: '从文档中提取Raptor',
|
||||
extractRaptor: '从文档中提取RAPTOR',
|
||||
extractKnowledgeGraph: '从文档中提取知识图谱',
|
||||
filterPlaceholder: '请输入',
|
||||
fileFilterTip: '',
|
||||
fileFilter: '正则匹配表达式',
|
||||
setDefaultTip: '',
|
||||
setDefault: '设置默认',
|
||||
eidtLinkDataPipeline: '编辑数据流',
|
||||
eidtLinkDataPipeline: '编辑pipeline',
|
||||
linkPipelineSetTip: '管理与此数据集的数据管道链接',
|
||||
default: '默认',
|
||||
dataPipeline: '数据流',
|
||||
linkDataPipeline: '关联数据流',
|
||||
dataPipeline: 'pipeline',
|
||||
linkDataPipeline: '关联pipeline',
|
||||
enableAutoGenerate: '是否启用自动生成',
|
||||
teamPlaceholder: '请选择团队',
|
||||
dataFlowPlaceholder: '请选择数据流',
|
||||
dataFlowPlaceholder: '请选择pipeline',
|
||||
buildItFromScratch: '去Scratch构建',
|
||||
dataFlow: '数据流',
|
||||
parseType: '切片方法',
|
||||
manualSetup: '手动设置',
|
||||
dataFlow: 'pipeline',
|
||||
parseType: 'Ingestion pipeline',
|
||||
manualSetup: '选择pipeline',
|
||||
builtIn: '内置',
|
||||
titleDescription: '在这里更新您的知识库详细信息,尤其是切片方法。',
|
||||
name: '知识库名称',
|
||||
@ -1588,6 +1588,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
page: '{{page}}条/页',
|
||||
},
|
||||
dataflowParser: {
|
||||
result: '结果',
|
||||
parseSummary: '解析摘要',
|
||||
parseSummaryTip: '解析器: deepdoc',
|
||||
rerunFromCurrentStep: '从当前步骤重新运行',
|
||||
@ -1610,7 +1611,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
<p>要保留这些更改,请点击“重新运行”以重新运行当前阶段。</p> `,
|
||||
changeStepModalConfirmText: '继续切换',
|
||||
changeStepModalCancelText: '取消',
|
||||
unlinkPipelineModalTitle: '解绑数据流',
|
||||
unlinkPipelineModalTitle: '解绑pipeline',
|
||||
unlinkPipelineModalContent: `
|
||||
<p>一旦取消链接,该数据集将不再连接到当前数据管道。</p>
|
||||
<p>正在解析的文件将继续解析,直到完成。</p>
|
||||
@ -1641,7 +1642,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
addParser: '增加解析器',
|
||||
hierarchy: '层次结构',
|
||||
regularExpressions: '正则表达式',
|
||||
overlappedPercent: '重叠百分比',
|
||||
overlappedPercent: '重叠百分比(%)',
|
||||
searchMethod: '搜索方法',
|
||||
searchMethodTip: `决定该数据集启用的搜索方式,可选择全文、向量,或两者兼有。
|
||||
Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
@ -1709,6 +1710,9 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
cancel: '取消',
|
||||
filenameEmbeddingWeight: '文件名嵌入权重',
|
||||
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
|
||||
note: '注释',
|
||||
noteDescription: '注释',
|
||||
notePlaceholder: '请输入注释',
|
||||
},
|
||||
datasetOverview: {
|
||||
downloadTip: '正在从数据源下载文件。',
|
||||
|
||||
@ -18,7 +18,7 @@ const InnerNodeHeader = ({
|
||||
wrapperClassName,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<section className={cn(wrapperClassName, 'pb-4')}>
|
||||
<section className={cn(wrapperClassName, 'pb-2')}>
|
||||
<div className={cn(className, 'flex gap-2.5')}>
|
||||
<OperatorIcon name={label as Operator}></OperatorIcon>
|
||||
<span className="truncate text-center font-semibold text-sm">
|
||||
|
||||
@ -7,7 +7,7 @@ export function NodeWrapper({ children, className, selected }: IProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'bg-text-title-invert p-2.5 rounded-sm w-[200px] text-xs group',
|
||||
'bg-text-title-invert p-2.5 rounded-md w-[200px] text-xs group',
|
||||
{ 'border border-accent-primary': selected },
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -28,7 +28,18 @@ const NameFormSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
type NoteNodeProps = NodeProps<INoteNode> & {
|
||||
useWatchNoteFormChange?: typeof useWatchFormChange;
|
||||
useWatchNoteNameFormChange?: typeof useWatchNameFormChange;
|
||||
};
|
||||
|
||||
function NoteNode({
|
||||
data,
|
||||
id,
|
||||
selected,
|
||||
useWatchNoteFormChange,
|
||||
useWatchNoteNameFormChange,
|
||||
}: NoteNodeProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
@ -41,19 +52,19 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
defaultValues: { name: data.name },
|
||||
});
|
||||
|
||||
useWatchFormChange(id, form);
|
||||
(useWatchNoteFormChange || useWatchFormChange)(id, form);
|
||||
|
||||
useWatchNameFormChange(id, nameForm);
|
||||
(useWatchNoteNameFormChange || useWatchNameFormChange)(id, nameForm);
|
||||
|
||||
return (
|
||||
<NodeWrapper
|
||||
className="p-0 w-full h-full flex flex-col bg-bg-component"
|
||||
className="p-0 w-full h-full flex flex-col bg-bg-component border border-state-warning rounded-lg shadow-md pb-1"
|
||||
selected={selected}
|
||||
>
|
||||
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
|
||||
<ResizeIcon />
|
||||
</NodeResizeControl>
|
||||
<section className="p-2 flex gap-2 items-center note-drag-handle rounded-t">
|
||||
<section className="px-2 py-1 flex gap-2 items-center note-drag-handle rounded-t border-t-2 border-state-warning">
|
||||
<NotebookPen className="size-4" />
|
||||
<Form {...nameForm}>
|
||||
<form className="flex-1">
|
||||
@ -67,7 +78,7 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
{...field}
|
||||
type="text"
|
||||
className="bg-transparent border-none focus-visible:outline focus-visible:outline-text-sub-title"
|
||||
className="bg-transparent border-none focus-visible:outline focus-visible:outline-text-sub-title p-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -78,7 +89,7 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
</Form>
|
||||
</section>
|
||||
<Form {...form}>
|
||||
<form className="flex-1 p-1">
|
||||
<form className="flex-1 px-1 min-h-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="text"
|
||||
@ -87,7 +98,7 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
className="resize-none rounded-none p-1 h-full overflow-auto bg-transparent focus-visible:ring-0 border-none"
|
||||
className="resize-none rounded-none p-1 py-0 overflow-auto bg-transparent focus-visible:ring-0 border-none text-text-secondary focus-visible:ring-offset-0 !text-xs"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -6,7 +6,7 @@ export function ResizeIcon() {
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="rgba(76, 164, 231, 1)"
|
||||
stroke="var(--text-disabled)"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
||||
@ -48,7 +48,11 @@ const MarkdownContent = ({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content);
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['think', 'section'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
|
||||
// let text = content;
|
||||
if (text === '') {
|
||||
text = t('chat.searching');
|
||||
|
||||
@ -45,7 +45,6 @@ import { RagNode } from './node';
|
||||
import { BeginNode } from './node/begin-node';
|
||||
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||
import { ExtractorNode } from './node/extractor-node';
|
||||
import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
|
||||
import NoteNode from './node/note-node';
|
||||
import ParserNode from './node/parser-node';
|
||||
import { SplitterNode } from './node/splitter-node';
|
||||
@ -58,7 +57,6 @@ export const nodeTypes: NodeTypes = {
|
||||
parserNode: ParserNode,
|
||||
tokenizerNode: TokenizerNode,
|
||||
splitterNode: SplitterNode,
|
||||
hierarchicalMergerNode: HierarchicalMergerNode,
|
||||
contextNode: ExtractorNode,
|
||||
};
|
||||
|
||||
|
||||
@ -1,57 +1,12 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export function CardWithForm() {
|
||||
type LabelCardProps = {
|
||||
className?: string;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function LabelCard({ children, className }: LabelCardProps) {
|
||||
return (
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Create project</CardTitle>
|
||||
<CardDescription>Deploy your new project in one-click.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="grid w-full items-center gap-4">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="Name of your project" />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="framework">Framework</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="framework">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="next">Next.js</SelectItem>
|
||||
<SelectItem value="sveltekit">SvelteKit</SelectItem>
|
||||
<SelectItem value="astro">Astro</SelectItem>
|
||||
<SelectItem value="nuxt">Nuxt.js</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1 +1,18 @@
|
||||
export { RagNode as ExtractorNode } from './index';
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { IRagNode } from '@/interfaces/database/agent';
|
||||
import { NodeProps } from '@xyflow/react';
|
||||
import { get } from 'lodash';
|
||||
import { LabelCard } from './card';
|
||||
import { RagNode } from './index';
|
||||
|
||||
export function ExtractorNode({ ...props }: NodeProps<IRagNode>) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<RagNode {...props}>
|
||||
<LabelCard>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</LabelCard>
|
||||
</RagNode>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { RagNode as HierarchicalMergerNode } from './index';
|
||||
@ -1,6 +1,6 @@
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { PropsWithChildren, memo, useMemo } from 'react';
|
||||
import { NodeHandleId, SingleOperators } from '../../constant';
|
||||
import useGraphStore from '../../store';
|
||||
import { CommonHandle } from './handle';
|
||||
@ -9,12 +9,14 @@ import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
|
||||
function InnerRagNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
children,
|
||||
}: RagNodeProps) {
|
||||
const getOperatorTypeFromId = useGraphStore(
|
||||
(state) => state.getOperatorTypeFromId,
|
||||
);
|
||||
@ -45,6 +47,7 @@ function InnerRagNode({
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
{children}
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ interface IProps {
|
||||
gap?: number;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const InnerNodeHeader = ({
|
||||
@ -16,11 +17,12 @@ const InnerNodeHeader = ({
|
||||
name,
|
||||
className,
|
||||
wrapperClassName,
|
||||
icon,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<section className={cn(wrapperClassName, 'pb-4')}>
|
||||
<div className={cn(className, 'flex gap-2.5')}>
|
||||
<OperatorIcon name={label as Operator}></OperatorIcon>
|
||||
{icon || <OperatorIcon name={label as Operator}></OperatorIcon>}
|
||||
<span className="truncate text-center font-semibold text-sm">
|
||||
{name}
|
||||
</span>
|
||||
|
||||
@ -1,103 +1,16 @@
|
||||
import { NodeProps, NodeResizeControl } from '@xyflow/react';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { INoteNode } from '@/interfaces/database/flow';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { NotebookPen } from 'lucide-react';
|
||||
import BaseNoteNode from '@/pages/agent/canvas/node/note-node';
|
||||
import { NodeProps } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { NodeWrapper } from '../node-wrapper';
|
||||
import { ResizeIcon, controlStyle } from '../resize-icon';
|
||||
import { useWatchFormChange, useWatchNameFormChange } from './use-watch-change';
|
||||
|
||||
const FormSchema = z.object({
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
const NameFormSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: data.form,
|
||||
});
|
||||
|
||||
const nameForm = useForm<z.infer<typeof NameFormSchema>>({
|
||||
resolver: zodResolver(NameFormSchema),
|
||||
defaultValues: { name: data.name },
|
||||
});
|
||||
|
||||
useWatchFormChange(id, form);
|
||||
|
||||
useWatchNameFormChange(id, nameForm);
|
||||
|
||||
function NoteNode({ ...props }: NodeProps<INoteNode>) {
|
||||
return (
|
||||
<NodeWrapper
|
||||
className="p-0 w-full h-full flex flex-col"
|
||||
selected={selected}
|
||||
>
|
||||
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
|
||||
<ResizeIcon />
|
||||
</NodeResizeControl>
|
||||
<section className="p-2 flex gap-2 bg-background-note items-center note-drag-handle rounded-t">
|
||||
<NotebookPen className="size-4" />
|
||||
<Form {...nameForm}>
|
||||
<form className="flex-1">
|
||||
<FormField
|
||||
control={nameForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="h-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
{...field}
|
||||
type="text"
|
||||
className="bg-transparent border-none focus-visible:outline focus-visible:outline-text-sub-title"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</section>
|
||||
<Form {...form}>
|
||||
<form className="flex-1 p-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="text"
|
||||
render={({ field }) => (
|
||||
<FormItem className="h-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
className="resize-none rounded-none p-1 h-full overflow-auto bg-transparent focus-visible:ring-0 border-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</NodeWrapper>
|
||||
<BaseNoteNode
|
||||
{...props}
|
||||
useWatchNoteFormChange={useWatchFormChange}
|
||||
useWatchNoteNameFormChange={useWatchNameFormChange}
|
||||
></BaseNoteNode>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import useGraphStore from '@/pages/agent/store';
|
||||
import useGraphStore from '@/pages/data-flow/store';
|
||||
import { useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { BaseNode } from '@/interfaces/database/agent';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { ParserFormSchemaType } from '../../form/parser-form';
|
||||
import { LabelCard } from './card';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
@ -12,7 +15,8 @@ function ParserNode({
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
}: NodeProps<BaseNode<ParserFormSchemaType>>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
@ -33,6 +37,17 @@ function ParserNode({
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
<section className="space-y-2">
|
||||
{data.form?.setups.map((x, idx) => (
|
||||
<LabelCard
|
||||
key={idx}
|
||||
className="flex justify- flex-col text-text-primary gap-1"
|
||||
>
|
||||
<span className="text-text-secondary">Parser {idx + 1}</span>
|
||||
{t(`dataflow.fileFormatOptions.${x.fileFormat}`)}
|
||||
</LabelCard>
|
||||
))}
|
||||
</section>
|
||||
</NodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1 +1,52 @@
|
||||
export { RagNode as SplitterNode } from './index';
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import { NodeHandleId, Operator } from '../../constant';
|
||||
import OperatorIcon from '../../operator-icon';
|
||||
import { LabelCard } from './card';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
|
||||
function InnerSplitterNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: RagNodeProps) {
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label} showCopy={false}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={'Chunker'}
|
||||
label={data.label}
|
||||
icon={<OperatorIcon name={Operator.Splitter}></OperatorIcon>}
|
||||
></NodeHeader>
|
||||
<LabelCard>{data.name}</LabelCard>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const SplitterNode = memo(InnerSplitterNode);
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { BaseNode } from '@/interfaces/database/agent';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { TokenizerFormSchemaType } from '../../form/tokenizer-form';
|
||||
import { LabelCard } from './card';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
@ -13,7 +16,9 @@ function TokenizerNode({
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
}: NodeProps<BaseNode<TokenizerFormSchemaType>>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ToolBar
|
||||
selected={selected}
|
||||
@ -32,6 +37,16 @@ function TokenizerNode({
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
<LabelCard className="text-text-primary flex justify-between flex-col gap-1">
|
||||
<span className="text-text-secondary">
|
||||
{t('dataflow.searchMethod')}
|
||||
</span>
|
||||
<ul className="space-y-1">
|
||||
{data.form?.search_method.map((x) => (
|
||||
<li key={x}>{t(`dataflow.tokenizerSearchMethodOptions.${x}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</LabelCard>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
|
||||
@ -337,7 +337,7 @@ export const NodeMap = {
|
||||
[Operator.Parser]: 'parserNode',
|
||||
[Operator.Tokenizer]: 'tokenizerNode',
|
||||
[Operator.Splitter]: 'splitterNode',
|
||||
[Operator.HierarchicalMerger]: 'hierarchicalMergerNode',
|
||||
[Operator.HierarchicalMerger]: 'splitterNode',
|
||||
[Operator.Extractor]: 'contextNode',
|
||||
};
|
||||
|
||||
|
||||
@ -58,7 +58,13 @@ const FormSheet = ({
|
||||
<SheetTitle className="hidden"></SheetTitle>
|
||||
<section className="flex-col border-b py-2 px-5">
|
||||
<div className="flex items-center gap-2 pb-3">
|
||||
<OperatorIcon name={operatorName}></OperatorIcon>
|
||||
<OperatorIcon
|
||||
name={
|
||||
operatorName === Operator.HierarchicalMerger
|
||||
? Operator.Splitter
|
||||
: operatorName
|
||||
}
|
||||
></OperatorIcon>
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<label htmlFor="">{t('flow.title')}</label>
|
||||
{node?.id === BeginId ? (
|
||||
|
||||
@ -30,7 +30,6 @@ import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { buildOutputList } from '../../utils/build-output-list';
|
||||
import { Output } from '../components/output';
|
||||
import { OutputFormatFormField } from './common-form-fields';
|
||||
import { EmailFormFields } from './email-form-fields';
|
||||
import { ImageFormFields } from './image-form-fields';
|
||||
import { PdfFormFields } from './pdf-form-fields';
|
||||
@ -147,10 +146,10 @@ function ParserItem({
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
|
||||
<OutputFormatFormField
|
||||
{/* <OutputFormatFormField
|
||||
prefix={prefix}
|
||||
fileType={fileFormat as FileType}
|
||||
/>
|
||||
/> */}
|
||||
{index < fieldLength - 1 && <Separator />}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -26,7 +26,7 @@ export const FormSchema = z.object({
|
||||
value: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
overlapped_percent: z.number(), // 0.0 - 0.3
|
||||
overlapped_percent: z.number(), // 0.0 - 0.3 , 0% - 30%
|
||||
});
|
||||
|
||||
export type SplitterFormSchemaType = z.infer<typeof FormSchema>;
|
||||
@ -58,9 +58,8 @@ const SplitterForm = ({ node }: INextOperatorForm) => {
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name="overlapped_percent"
|
||||
max={0.3}
|
||||
max={30}
|
||||
min={0}
|
||||
step={0.01}
|
||||
label={t('dataflow.overlappedPercent')}
|
||||
></SliderInputFormField>
|
||||
<section>
|
||||
|
||||
@ -29,6 +29,8 @@ export const FormSchema = z.object({
|
||||
fields: z.string(),
|
||||
});
|
||||
|
||||
export type TokenizerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialTokenizerValues, node);
|
||||
@ -44,7 +46,7 @@ const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
'dataflow.tokenizerFieldsOptions',
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
const form = useForm<TokenizerFormSchemaType>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: 'onChange',
|
||||
|
||||
@ -131,6 +131,7 @@ function transformParserParams(params: ParserFormSchemaType) {
|
||||
function transformSplitterParams(params: SplitterFormSchemaType) {
|
||||
return {
|
||||
...params,
|
||||
overlapped_percent: Number(params.overlapped_percent) / 100,
|
||||
delimiters: transformObjectArrayToPureArray(params.delimiters, 'value'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ const FormatPreserveEditor = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{['text', 'html'].includes(initialValue.key) && (
|
||||
{['text', 'html', 'markdown'].includes(initialValue.key) && (
|
||||
<ObjectContainer
|
||||
isReadonly={isReadonly}
|
||||
className={className}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CheckedState } from '@radix-ui/react-checkbox';
|
||||
import { ChunkTextMode } from '../../constant';
|
||||
import { IChunk } from '../../interface';
|
||||
import { ComponentParams, IChunk } from '../../interface';
|
||||
import { parserKeyMap } from './json-parser';
|
||||
|
||||
export interface FormatPreserveEditorProps {
|
||||
@ -28,6 +28,7 @@ export type IJsonContainerProps = {
|
||||
value: {
|
||||
[key: string]: string;
|
||||
}[];
|
||||
params: ComponentParams;
|
||||
};
|
||||
isChunck?: boolean;
|
||||
handleCheck: (e: CheckedState, index: number) => void;
|
||||
@ -52,6 +53,7 @@ export type IObjContainerProps = {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
params: ComponentParams;
|
||||
};
|
||||
isChunck?: boolean;
|
||||
handleCheck: (e: CheckedState, index: number) => void;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { isArray } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { ChunkTextMode } from '../../constant';
|
||||
import styles from '../../index.less';
|
||||
import { useParserInit } from './hook';
|
||||
@ -33,7 +34,13 @@ export const ArrayContainer = (props: IJsonContainerProps) => {
|
||||
editDivRef,
|
||||
} = useParserInit({ initialValue });
|
||||
|
||||
const parserKey = parserKeyMap[content.key as keyof typeof parserKeyMap];
|
||||
const parserKey = useMemo(() => {
|
||||
const key =
|
||||
content.key === 'chunks' && content.params.field_name
|
||||
? content.params.field_name
|
||||
: parserKeyMap[content.key as keyof typeof parserKeyMap];
|
||||
return key;
|
||||
}, [content]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(e?: any, index?: number) => {
|
||||
@ -73,67 +80,68 @@ export const ArrayContainer = (props: IJsonContainerProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{content.value?.map((item, index) => {
|
||||
if (
|
||||
item[parserKeyMap[content.key as keyof typeof parserKeyMap]] === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section
|
||||
key={index}
|
||||
className={cn(
|
||||
isChunck
|
||||
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
|
||||
: '',
|
||||
activeEditIndex === index && isChunck ? 'bg-bg-title' : '',
|
||||
)}
|
||||
>
|
||||
{isChunck && !isReadonly && (
|
||||
<Checkbox
|
||||
onCheckedChange={(e) => {
|
||||
handleCheck(e, index);
|
||||
}}
|
||||
checked={selectedChunkIds?.some(
|
||||
(id) => id.toString() === index.toString(),
|
||||
)}
|
||||
></Checkbox>
|
||||
)}
|
||||
{activeEditIndex === index && (
|
||||
<div
|
||||
ref={editDivRef}
|
||||
contentEditable={!isReadonly}
|
||||
onBlur={handleSave}
|
||||
className={cn(
|
||||
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0',
|
||||
{isArray(content.value) &&
|
||||
content.value?.map((item, index) => {
|
||||
if (
|
||||
item[parserKeyMap[content.key as keyof typeof parserKeyMap]] === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section
|
||||
key={index}
|
||||
className={cn(
|
||||
isChunck
|
||||
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
|
||||
: '',
|
||||
activeEditIndex === index && isChunck ? 'bg-bg-title' : '',
|
||||
)}
|
||||
>
|
||||
{isChunck && !isReadonly && (
|
||||
<Checkbox
|
||||
onCheckedChange={(e) => {
|
||||
handleCheck(e, index);
|
||||
}}
|
||||
checked={selectedChunkIds?.some(
|
||||
(id) => id.toString() === index.toString(),
|
||||
)}
|
||||
></Checkbox>
|
||||
)}
|
||||
{activeEditIndex === index && (
|
||||
<div
|
||||
ref={editDivRef}
|
||||
contentEditable={!isReadonly}
|
||||
onBlur={handleSave}
|
||||
className={cn(
|
||||
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0',
|
||||
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
)}
|
||||
{activeEditIndex !== index && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-text-secondary overflow-auto scrollbar-auto w-full',
|
||||
{
|
||||
[styles.contentEllipsis]:
|
||||
textMode === ChunkTextMode.Ellipse,
|
||||
},
|
||||
)}
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
clickChunk(item);
|
||||
if (!isReadonly) {
|
||||
handleEdit(e, index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item[parserKeyMap[content.key]]}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
)}
|
||||
{activeEditIndex !== index && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-text-secondary overflow-auto scrollbar-auto w-full',
|
||||
{
|
||||
[styles.contentEllipsis]:
|
||||
textMode === ChunkTextMode.Ellipse,
|
||||
},
|
||||
)}
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
clickChunk(item);
|
||||
if (!isReadonly) {
|
||||
handleEdit(e, index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item[parserKey]}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -218,18 +218,11 @@ export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
|
||||
const nodes: Array<ITimelineNodeObj & { id: number | string }> = [];
|
||||
console.log('time-->', data);
|
||||
const times = data?.dsl?.components;
|
||||
const graphNodes = data?.dsl?.graph?.nodes;
|
||||
if (times) {
|
||||
const getNode = (
|
||||
key: string,
|
||||
index: number,
|
||||
type:
|
||||
| TimelineNodeType.begin
|
||||
| TimelineNodeType.parser
|
||||
| TimelineNodeType.tokenizer
|
||||
| TimelineNodeType.characterSplitter
|
||||
| TimelineNodeType.titleSplitter,
|
||||
) => {
|
||||
const getNode = (key: string, index: number, type: TimelineNodeType) => {
|
||||
const node = times[key].obj;
|
||||
const graphNode = graphNodes?.find((item) => item.id === key);
|
||||
const name = camelCase(
|
||||
node.component_name,
|
||||
) as keyof typeof TimelineNodeObj;
|
||||
@ -247,6 +240,7 @@ export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
|
||||
}
|
||||
const timeNode = {
|
||||
...TimelineNodeObj[name],
|
||||
title: graphNode?.data?.name,
|
||||
id: index,
|
||||
className: 'w-32',
|
||||
completed: false,
|
||||
@ -255,6 +249,13 @@ export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
|
||||
),
|
||||
type: tempType,
|
||||
detail: { value: times[key], key: key },
|
||||
} as ITimelineNodeObj & {
|
||||
id: number | string;
|
||||
className: string;
|
||||
completed: boolean;
|
||||
date: string;
|
||||
type: TimelineNodeType;
|
||||
detail: { value: IDslComponent; key: string };
|
||||
};
|
||||
console.log('timeNodetype-->', type);
|
||||
nodes.push(timeNode);
|
||||
@ -329,3 +330,30 @@ export function useFetchPipelineResult({
|
||||
|
||||
return { pipelineResult };
|
||||
}
|
||||
|
||||
export const useSummaryInfo = (
|
||||
data: IPipelineFileLogDetail,
|
||||
currentTimeNode: TimelineNode,
|
||||
) => {
|
||||
const summaryInfo = useMemo(() => {
|
||||
if (currentTimeNode.type === TimelineNodeType.parser) {
|
||||
const setups =
|
||||
currentTimeNode.detail?.value?.obj?.params?.setups?.[
|
||||
data.document_suffix
|
||||
];
|
||||
if (setups) {
|
||||
const { output_format, parse_method } = setups;
|
||||
const res = [];
|
||||
if (parse_method) {
|
||||
res.push(`${t('dataflow.parserMethod')}: ${parse_method}`);
|
||||
}
|
||||
if (output_format) {
|
||||
res.push(`${t('dataflow.outputFormat')}: ${output_format}`);
|
||||
}
|
||||
return res.join(' ');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}, [data, currentTimeNode]);
|
||||
return { summaryInfo };
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
useGetPipelineResultSearchParams,
|
||||
useHandleChunkCardClick,
|
||||
useRerunDataflow,
|
||||
useSummaryInfo,
|
||||
useTimelineDataFlow,
|
||||
} from './hooks';
|
||||
|
||||
@ -61,7 +62,7 @@ const Chunk = () => {
|
||||
);
|
||||
|
||||
const {
|
||||
navigateToDataset,
|
||||
navigateToDatasetOverview,
|
||||
navigateToDatasetList,
|
||||
navigateToAgents,
|
||||
navigateToDataflow,
|
||||
@ -150,7 +151,7 @@ const Chunk = () => {
|
||||
({} as TimelineNode)
|
||||
);
|
||||
}, [activeStepId, timelineNodes]);
|
||||
|
||||
const { summaryInfo } = useSummaryInfo(dataset, currentTimeNode);
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
@ -175,7 +176,7 @@ const Chunk = () => {
|
||||
<BreadcrumbLink
|
||||
onClick={() => {
|
||||
if (knowledgeId) {
|
||||
navigateToDataset(knowledgeId)();
|
||||
navigateToDatasetOverview(knowledgeId)();
|
||||
}
|
||||
if (agentId) {
|
||||
navigateToDataflow(agentId)();
|
||||
@ -220,7 +221,7 @@ const Chunk = () => {
|
||||
></DocumentPreview>
|
||||
</section>
|
||||
</div>
|
||||
<div className="h-dvh border-r -mt-3"></div>
|
||||
<div className="h-[calc(100vh-100px)] border-r -mt-3"></div>
|
||||
<div className="w-3/5 h-full">
|
||||
{/* {currentTimeNode?.type === TimelineNodeType.splitter && (
|
||||
<ChunkerContainer
|
||||
@ -246,6 +247,7 @@ const Chunk = () => {
|
||||
key: string;
|
||||
}
|
||||
}
|
||||
summaryInfo={summaryInfo}
|
||||
clickChunk={handleChunkCardClick}
|
||||
reRunFunc={handleReRunFunc}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PipelineResultSearchParams } from './constant';
|
||||
|
||||
interface ComponentParams {
|
||||
export interface ComponentParams {
|
||||
debug_inputs: Record<string, any>;
|
||||
delay_after_error: number;
|
||||
description: string;
|
||||
@ -8,6 +8,7 @@ interface ComponentParams {
|
||||
exception_goto: any;
|
||||
exception_method: any;
|
||||
inputs: Record<string, any>;
|
||||
field_name: string;
|
||||
max_retries: number;
|
||||
message_history_window_size: number;
|
||||
outputs: {
|
||||
@ -30,6 +31,66 @@ export interface IDslComponent {
|
||||
obj: ComponentObject;
|
||||
upstream: Array<string>;
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
label: string;
|
||||
name: string;
|
||||
form?: {
|
||||
outputs?: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
value: string | Array<Record<string, any>> | number;
|
||||
}
|
||||
>;
|
||||
setups?: Array<Record<string, any>>;
|
||||
chunk_token_size?: number;
|
||||
delimiters?: Array<{
|
||||
value: string;
|
||||
}>;
|
||||
overlapped_percent?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface EdgeData {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Measured {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
data: NodeData;
|
||||
dragging: boolean;
|
||||
id: string;
|
||||
measured: Measured;
|
||||
position: Position;
|
||||
selected: boolean;
|
||||
sourcePosition: string;
|
||||
targetPosition: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
data: EdgeData;
|
||||
id: string;
|
||||
source: string;
|
||||
sourceHandle: string;
|
||||
target: string;
|
||||
targetHandle: string;
|
||||
}
|
||||
interface GraphData {
|
||||
edges: Edge[];
|
||||
nodes: Node[];
|
||||
}
|
||||
|
||||
export interface IPipelineFileLogDetail {
|
||||
avatar: string;
|
||||
create_date: string;
|
||||
@ -42,6 +103,7 @@ export interface IPipelineFileLogDetail {
|
||||
components: {
|
||||
[key: string]: IDslComponent;
|
||||
};
|
||||
graph: GraphData;
|
||||
task_id: string;
|
||||
path: Array<string>;
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ interface IProps {
|
||||
data: { value: IDslComponent; key: string };
|
||||
reRunLoading: boolean;
|
||||
clickChunk: (chunk: IChunk) => void;
|
||||
summaryInfo: string;
|
||||
reRunFunc: (data: { value: IDslComponent; key: string }) => void;
|
||||
}
|
||||
const ParserContainer = (props: IProps) => {
|
||||
@ -31,6 +32,7 @@ const ParserContainer = (props: IProps) => {
|
||||
reRunLoading,
|
||||
clickChunk,
|
||||
isReadonly,
|
||||
summaryInfo,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
|
||||
@ -46,6 +48,7 @@ const ParserContainer = (props: IProps) => {
|
||||
key,
|
||||
type,
|
||||
value,
|
||||
params: data?.value?.obj?.params,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
@ -130,7 +133,7 @@ const ParserContainer = (props: IProps) => {
|
||||
const newText = [...initialText.value, { text: text || ' ' }];
|
||||
setInitialText({
|
||||
...initialText,
|
||||
value: newText,
|
||||
value: newText as any,
|
||||
});
|
||||
},
|
||||
[initialText],
|
||||
@ -156,15 +159,16 @@ const ParserContainer = (props: IProps) => {
|
||||
{t('dataflowParser.parseSummary')}
|
||||
</h2>
|
||||
<div className="text-[12px] text-text-secondary italic ">
|
||||
{t('dataflowParser.parseSummaryTip')}
|
||||
{/* {t('dataflowParser.parseSummaryTip')} */}
|
||||
{summaryInfo}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isChunck && (
|
||||
<div>
|
||||
<h2 className="text-[16px]">{t('chunk.chunkResult')}</h2>
|
||||
<h2 className="text-[16px]">{t('dataflowParser.result')}</h2>
|
||||
<div className="text-[12px] text-text-secondary italic">
|
||||
{t('chunk.chunkResultTip')}
|
||||
{/* {t('chunk.chunkResultTip')} */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -190,7 +194,7 @@ const ParserContainer = (props: IProps) => {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
' border rounded-lg p-[20px] box-border w-[calc(100%-20px)] overflow-auto scrollbar-none',
|
||||
' border rounded-lg p-[20px] box-border w-[calc(100%-20px)] overflow-auto scrollbar-auto',
|
||||
{
|
||||
'h-[calc(100vh-240px)]': isChunck,
|
||||
'h-[calc(100vh-180px)]': !isChunck,
|
||||
|
||||
@ -10,5 +10,5 @@ export enum ProcessingType {
|
||||
|
||||
export const ProcessingTypeMap = {
|
||||
[ProcessingType.knowledgeGraph]: 'Knowledge Graph',
|
||||
[ProcessingType.raptor]: 'Raptor',
|
||||
[ProcessingType.raptor]: 'RAPTOR',
|
||||
};
|
||||
|
||||
@ -109,7 +109,9 @@ export const getFileLogsTableColumns = (
|
||||
name={row.original.pipeline_title}
|
||||
className="size-4"
|
||||
/>
|
||||
{row.original.pipeline_title}
|
||||
{row.original.pipeline_title === 'naive'
|
||||
? 'general'
|
||||
: row.original.pipeline_title}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -396,7 +398,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
<TableBody className="relative min-w-[1280px] overflow-auto">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
|
||||
@ -100,6 +100,7 @@ export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) {
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit ? disabled : false}
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
triggerClassName="!bg-bg-base"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,6 @@ import { DelimiterFormField } from '@/components/delimiter-form-field';
|
||||
import { ExcelToHtmlFormField } from '@/components/excel-to-html-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import { MaxTokenNumberFormField } from '@/components/max-token-number-from-field';
|
||||
import { TagItems } from '../components/tag-item';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
@ -26,7 +25,7 @@ export function NaiveConfiguration() {
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
<ExcelToHtmlFormField></ExcelToHtmlFormField>
|
||||
<TagItems></TagItems>
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
</MainContainer>
|
||||
);
|
||||
|
||||
@ -20,15 +20,15 @@ export function ApplicationCard({
|
||||
}: ApplicationCardProps) {
|
||||
return (
|
||||
<Card className="w-[264px]" onClick={onClick}>
|
||||
<CardContent className="p-2.5 group flex justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<CardContent className="p-2.5 group flex justify-between w-full">
|
||||
<div className="flex items-center gap-2.5 w-full">
|
||||
<RAGFlowAvatar
|
||||
className="size-14 rounded-lg"
|
||||
avatar={app.avatar}
|
||||
name={app.title || 'CN'}
|
||||
></RAGFlowAvatar>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-normal line-clamp-1 mb-1">
|
||||
<h3 className="text-sm font-normal line-clamp-1 mb-1 text-ellipsis w-[180px] overflow-hidden">
|
||||
{app.title}
|
||||
</h3>
|
||||
<p className="text-xs font-normal text-text-secondary">
|
||||
|
||||
@ -134,7 +134,7 @@ export const BgSvg = () => {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`w-full -mt-40`}
|
||||
className={`w-full -mt-48`}
|
||||
style={{ height: aspectRatio['middle'] + 'px' }}
|
||||
>
|
||||
{def(
|
||||
@ -144,7 +144,7 @@ export const BgSvg = () => {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`w-full -mt-52`}
|
||||
className={`w-full -mt-72`}
|
||||
style={{ height: aspectRatio['bottom'] + 'px' }}
|
||||
>
|
||||
{def(
|
||||
|
||||
@ -23,12 +23,12 @@ const FlipCard3D = (props: IProps) => {
|
||||
className={`relative w-full h-full transition-transform transform-style-3d ${isFlipped ? 'rotate-y-180' : ''}`}
|
||||
>
|
||||
{/* Front Face */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-500 text-white rounded-xl backface-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center backface-hidden">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Back Face */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-green-500 text-white rounded-xl backface-hidden rotate-y-180">
|
||||
<div className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-180">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,6 +42,10 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
.perspective-1000 {
|
||||
perspective: 1000px;
|
||||
overflow: hidden;
|
||||
min-height: 680px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.transform-style-3d {
|
||||
transform-style: preserve-3d;
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -135,8 +136,7 @@ const Login = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
<BgSvg />
|
||||
<>
|
||||
<Spotlight opcity={0.4} coverage={60} color={'rgb(128, 255, 248)'} />
|
||||
<Spotlight
|
||||
opcity={0.3}
|
||||
@ -152,69 +152,56 @@ const Login = () => {
|
||||
Y={'-10%'}
|
||||
color={'rgb(128, 255, 248)'}
|
||||
/>
|
||||
<div className=" h-[inherit] relative overflow-auto">
|
||||
<BgSvg />
|
||||
|
||||
{/* <SpotlightTopRight opcity={0.7} coverage={10} /> */}
|
||||
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
|
||||
<div className="flex items-center mb-4 w-full pl-10 pt-10 ">
|
||||
<div className="w-12 h-12 p-2 rounded-lg border-2 border-border flex items-center justify-center mr-3">
|
||||
<img
|
||||
src={'/logo.svg'}
|
||||
alt="logo"
|
||||
className="size-8 mr-[12] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold self-center">RAGFlow</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-2">{t('title')}</h1>
|
||||
<div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 border border-accent-primary rounded-full hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
|
||||
{t('start')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8">
|
||||
{/* Logo and Header */}
|
||||
|
||||
{/* Login Form */}
|
||||
<FlipCard3D isLoginPage={isLoginPage}>
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
|
||||
</h2>
|
||||
{/* <SpotlightTopRight opcity={0.7} coverage={10} /> */}
|
||||
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
|
||||
<div className="flex items-center mb-4 w-full pl-10 pt-10 ">
|
||||
<div className="w-12 h-12 p-2 rounded-lg bg-bg-base border-2 border-border flex items-center justify-center mr-3">
|
||||
<img
|
||||
src={'/logo.svg'}
|
||||
alt="logo"
|
||||
className="size-8 mr-[12] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-8 pr-8 pb-2 border border-border-button ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-6 text-text-primary"
|
||||
onSubmit={form.handleSubmit((data) => onCheck(data))}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{title === 'register' && (
|
||||
<div className="text-xl font-bold self-center">RAGFlow</div>
|
||||
</div>
|
||||
<h1 className="text-[36px] font-medium text-center mb-2">
|
||||
{t('title')}
|
||||
</h1>
|
||||
{/* border border-accent-primary rounded-full */}
|
||||
{/* <div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
|
||||
{t('start')}
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-col items-center justify-center min-h-[1050px] px-4 sm:px-6 lg:px-8">
|
||||
{/* Logo and Header */}
|
||||
|
||||
{/* Login Form */}
|
||||
<FlipCard3D isLoginPage={isLoginPage}>
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-8 text-text-primary "
|
||||
onSubmit={form.handleSubmit((data) => onCheck(data))}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('nicknameLabel')}</FormLabel>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('nicknamePlaceholder')}
|
||||
autoComplete="username"
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -222,131 +209,157 @@ const Login = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete={
|
||||
title === 'login'
|
||||
? 'current-password'
|
||||
: 'new-password'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
{title === 'register' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('nicknameLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('nicknamePlaceholder')}
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{title === 'login' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete={
|
||||
title === 'login'
|
||||
? 'current-password'
|
||||
: 'new-password'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
<FormLabel>{t('rememberMe')}</FormLabel>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
loading={loading}
|
||||
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{title === 'login' && registerEnabled && (
|
||||
<div className="mt-6 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signInTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
|
||||
{title === 'login' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
<FormLabel
|
||||
className={cn(' hover:text-text-primary', {
|
||||
'text-text-disabled': !field.value,
|
||||
'text-text-primary': field.value,
|
||||
})}
|
||||
>
|
||||
{t('rememberMe')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
loading={loading}
|
||||
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
|
||||
>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
<div className="mt-6 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{title === 'login' && registerEnabled && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signInTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FlipCard3D>
|
||||
</FlipCard3D>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ import { useAddChatBox } from '../use-add-box';
|
||||
type MultipleChatBoxProps = {
|
||||
controller: AbortController;
|
||||
chatBoxIds: string[];
|
||||
stopOutputMessage(): void;
|
||||
} & Pick<
|
||||
ReturnType<typeof useAddChatBox>,
|
||||
'removeChatBox' | 'addChatBox' | 'chatBoxIds'
|
||||
@ -200,6 +201,7 @@ export function MultipleChatBox({
|
||||
chatBoxIds,
|
||||
removeChatBox,
|
||||
addChatBox,
|
||||
stopOutputMessage,
|
||||
}: MultipleChatBoxProps) {
|
||||
const {
|
||||
value,
|
||||
@ -207,7 +209,6 @@ export function MultipleChatBox({
|
||||
messageRecord,
|
||||
handleInputChange,
|
||||
handlePressEnter,
|
||||
stopOutputMessage,
|
||||
setFormRef,
|
||||
handleUploadFile,
|
||||
} = useSendMultipleChatMessage(controller, chatBoxIds);
|
||||
|
||||
@ -20,9 +20,10 @@ import { buildMessageItemReference } from '../../utils';
|
||||
|
||||
interface IProps {
|
||||
controller: AbortController;
|
||||
stopOutputMessage(): void;
|
||||
}
|
||||
|
||||
export function SingleChatBox({ controller }: IProps) {
|
||||
export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
|
||||
const {
|
||||
value,
|
||||
scrollRef,
|
||||
@ -34,7 +35,6 @@ export function SingleChatBox({ controller }: IProps) {
|
||||
handlePressEnter,
|
||||
regenerateMessage,
|
||||
removeMessageById,
|
||||
stopOutputMessage,
|
||||
handleUploadFile,
|
||||
removeFile,
|
||||
} = useSendMessage(controller);
|
||||
|
||||
@ -39,7 +39,7 @@ export default function Chat() {
|
||||
const { t } = useTranslation();
|
||||
const { data: conversation } = useFetchConversation();
|
||||
|
||||
const { handleConversationCardClick, controller } =
|
||||
const { handleConversationCardClick, controller, stopOutputMessage } =
|
||||
useHandleClickConversationCard();
|
||||
const { visible: settingVisible, switchVisible: switchSettingVisible } =
|
||||
useSetModalState(true);
|
||||
@ -74,6 +74,7 @@ export default function Chat() {
|
||||
controller={controller}
|
||||
removeChatBox={removeChatBox}
|
||||
addChatBox={addChatBox}
|
||||
stopOutputMessage={stopOutputMessage}
|
||||
></MultipleChatBox>
|
||||
</section>
|
||||
);
|
||||
@ -129,7 +130,10 @@ export default function Chat() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-0 min-h-0">
|
||||
<SingleChatBox controller={controller}></SingleChatBox>
|
||||
<SingleChatBox
|
||||
controller={controller}
|
||||
stopOutputMessage={stopOutputMessage}
|
||||
></SingleChatBox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{settingVisible && (
|
||||
|
||||
@ -5,16 +5,20 @@ export function useHandleClickConversationCard() {
|
||||
const [controller, setController] = useState(new AbortController());
|
||||
const { handleClickConversation } = useClickConversationCard();
|
||||
|
||||
const stopOutputMessage = useCallback(() => {
|
||||
setController((pre) => {
|
||||
pre.abort();
|
||||
return new AbortController();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConversationCardClick = useCallback(
|
||||
(conversationId: string, isNew: boolean) => {
|
||||
handleClickConversation(conversationId, isNew ? 'true' : '');
|
||||
setController((pre) => {
|
||||
pre.abort();
|
||||
return new AbortController();
|
||||
});
|
||||
stopOutputMessage();
|
||||
},
|
||||
[handleClickConversation],
|
||||
[handleClickConversation, stopOutputMessage],
|
||||
);
|
||||
|
||||
return { controller, handleConversationCardClick };
|
||||
return { controller, handleConversationCardClick, stopOutputMessage };
|
||||
}
|
||||
|
||||
@ -123,10 +123,6 @@ export const useSendMessage = (controller: AbortController) => {
|
||||
[getConversationIsNew, handleUploadFile, setConversation],
|
||||
);
|
||||
|
||||
const stopOutputMessage = useCallback(() => {
|
||||
controller.abort();
|
||||
}, [controller]);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async ({
|
||||
message,
|
||||
@ -249,7 +245,6 @@ export const useSendMessage = (controller: AbortController) => {
|
||||
messageContainerRef,
|
||||
derivedMessages,
|
||||
removeMessageById,
|
||||
stopOutputMessage,
|
||||
handleUploadFile: onUploadFile,
|
||||
isUploading,
|
||||
removeFile,
|
||||
|
||||
@ -35,10 +35,6 @@ export function useSendMultipleChatMessage(
|
||||
const { setFormRef, getLLMConfigById, isLLMConfigEmpty } =
|
||||
useBuildFormRefs(chatBoxIds);
|
||||
|
||||
const stopOutputMessage = useCallback(() => {
|
||||
controller.abort();
|
||||
}, [controller]);
|
||||
|
||||
const addNewestQuestion = useCallback(
|
||||
(message: Message, answer: string = '') => {
|
||||
setMessageRecord((pre) => {
|
||||
@ -236,7 +232,6 @@ export function useSendMultipleChatMessage(
|
||||
sendMessage,
|
||||
handleInputChange,
|
||||
handlePressEnter,
|
||||
stopOutputMessage,
|
||||
sendLoading: !allDone,
|
||||
setFormRef,
|
||||
handleUploadFile,
|
||||
|
||||
@ -64,7 +64,10 @@ const MarkdownContent = ({
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = DOMPurify.sanitize(content);
|
||||
let text = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['think', 'section'],
|
||||
ADD_ATTR: ['class'],
|
||||
});
|
||||
// let text = content;
|
||||
if (text === '') {
|
||||
text = t('chat.searching');
|
||||
|
||||
Reference in New Issue
Block a user