Compare commits

..

10 Commits

Author SHA1 Message Date
f1c98aad6b Update version info (#564)
### What problem does this PR solve?

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

### Type of change

- [x] Documentation Update
- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2024-04-26 20:07:26 +08:00
ab06f502d7 fix bug of file management (#565)
### What problem does this PR solve?

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2024-04-26 19:59:21 +08:00
6329339a32 feat: add Tooltip to action icon of FileManager (#561)
### What problem does this PR solve?
#345
feat: add Tooltip to action icon of FileManager 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2024-04-26 18:55:37 +08:00
84b39c60f6 fix rename bug (#562)
### What problem does this PR solve?

fix rename file bugs
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2024-04-26 18:55:21 +08:00
eb62c669ae feat: translate FileManager #345 (#558)
### What problem does this PR solve?
#345
feat: translate FileManager
feat: batch delete files from the file table in the knowledge base

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2024-04-26 17:22:23 +08:00
f69ff39fa0 add file management feature (#560)
### What problem does this PR solve?

### Type of change

- [x] Documentation Update
2024-04-26 17:21:53 +08:00
b1cd203904 Update version to 0.3.2 (#550)
### What problem does this PR solve?

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

### Type of change

- [x] Documentation Update

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2024-04-26 09:58:35 +08:00
b75d75e995 fix youdao bug (#551)
### What problem does this PR solve?


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2024-04-26 09:58:22 +08:00
76c477f211 chore: disable Kibana volume storage in Docker Compose (#548)
### What problem does this PR solve?

Since Kibana service is not currently being used, the associated volume
'kibanadata' has been commented out in the Docker Compose file. This
change helps to prevent the allocation of unnecessary resources and
simplifies the configuration.

### Type of change

- [x] Refactoring
unused Kibana volume storage
2024-04-26 08:54:27 +08:00
1b01c4fe69 Updated badge link (#545)
### What problem does this PR solve?

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

### Type of change

- [x] Documentation Update
2024-04-25 19:34:21 +08:00
51 changed files with 1209 additions and 145 deletions

View File

@ -1,5 +1,5 @@
name: Bug Report
description: Create a bug issue for infinity
description: Create a bug issue for RAGFlow
title: "[Bug]: "
labels: [bug]
body:

View File

@ -1,7 +1,7 @@
---
name: Feature request
title: '[Feature Request]: '
about: Suggest an idea for Infinity
about: Suggest an idea for RAGFlow
labels: ''
---

View File

@ -1,5 +1,5 @@
name: Feature request
description: Propose a feature request for infinity.
description: Propose a feature request for RAGFlow.
title: "[Feature Request]: "
labels: [feature request]
body:

View File

@ -1,5 +1,5 @@
name: Question
description: Ask questions on infinity
description: Ask questions on RAGFlow
title: "[Question]: "
labels: [question]
body:

View File

@ -1,5 +1,5 @@
name: Subtask
description: "Propose a subtask for infinity"
description: "Propose a subtask for RAGFlow"
title: "[Subtask]: "
labels: [subtask]

View File

@ -11,14 +11,14 @@
</p>
<p align="center">
<a href="https://github.com/infiniflow/infinity/releases/latest">
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
</a>
<a href="https://demo.ragflow.io" target="_blank">
<img alt="Static Badge" src="https://img.shields.io/badge/RAGFLOW-LLM-white?&labelColor=dd0af7"></a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.3.1-brightgreen"
alt="docker pull infiniflow/ragflow:v0.3.1"></a>
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.4.0-brightgreen"
alt="docker pull infiniflow/ragflow:v0.4.0"></a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=d4eaf7&color=7d09f1" alt="license">
</a>
@ -58,6 +58,7 @@
## 📌 Latest Features
- 2024-04-26 Add file management.
- 2024-04-19 Support conversation API ([detail](./docs/conversation_api.md)).
- 2024-04-16 Add an embedding model 'bce-embedding-base_v1' from [BCEmbedding](https://github.com/netease-youdao/BCEmbedding).
- 2024-04-16 Add [FastEmbed](https://github.com/qdrant/fastembed), which is designed specifically for light and speedy embedding.
@ -179,7 +180,7 @@ To build the Docker images from source:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
$ cd ragflow/
$ docker build -t infiniflow/ragflow:v0.3.1 .
$ docker build -t infiniflow/ragflow:v0.4.0 .
$ cd ragflow/docker
$ chmod +x ./entrypoint.sh
$ docker compose up -d

View File

@ -11,14 +11,14 @@
</p>
<p align="center">
<a href="https://github.com/infiniflow/infinity/releases/latest">
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
</a>
<a href="https://demo.ragflow.io" target="_blank">
<img alt="Static Badge" src="https://img.shields.io/badge/RAGFLOW-LLM-white?&labelColor=dd0af7"></a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.3.1-brightgreen"
alt="docker pull infiniflow/ragflow:v0.3.1"></a>
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.4.0-brightgreen"
alt="docker pull infiniflow/ragflow:v0.4.0"></a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=d4eaf7&color=7d09f1" alt="license">
</a>
@ -58,6 +58,7 @@
## 📌 最新の機能
- 2024-04-26 「ファイル管理」機能を追加しました。
- 2024-04-19 会話 API をサポートします ([詳細](./docs/conversation_api.md))。
- 2024-04-16 [BCEmbedding](https://github.com/netease-youdao/BCEmbedding) から埋め込みモデル「bce-embedding-base_v1」を追加します。
- 2024-04-16 [FastEmbed](https://github.com/qdrant/fastembed) は、軽量かつ高速な埋め込み用に設計されています。
@ -179,7 +180,7 @@
```bash
$ git clone https://github.com/infiniflow/ragflow.git
$ cd ragflow/
$ docker build -t infiniflow/ragflow:v0.3.1 .
$ docker build -t infiniflow/ragflow:v0.4.0 .
$ cd ragflow/docker
$ chmod +x ./entrypoint.sh
$ docker compose up -d

View File

@ -11,14 +11,14 @@
</p>
<p align="center">
<a href="https://github.com/infiniflow/infinity/releases/latest">
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
</a>
<a href="https://demo.ragflow.io" target="_blank">
<img alt="Static Badge" src="https://img.shields.io/badge/RAGFLOW-LLM-white?&labelColor=dd0af7"></a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.3.1-brightgreen"
alt="docker pull infiniflow/ragflow:v0.3.1"></a>
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.4.0-brightgreen"
alt="docker pull infiniflow/ragflow:v0.4.0"></a>
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=d4eaf7&color=7d09f1" alt="license">
</a>
@ -58,6 +58,7 @@
## 📌 新增功能
- 2024-04-26 增添了'文件管理'功能.
- 2024-04-19 支持对话 API ([更多](./docs/conversation_api.md)).
- 2024-04-16 添加嵌入模型 [BCEmbedding](https://github.com/netease-youdao/BCEmbedding) 。
- 2024-04-16 添加 [FastEmbed](https://github.com/qdrant/fastembed) 专为轻型和高速嵌入而设计。
@ -179,7 +180,7 @@
```bash
$ git clone https://github.com/infiniflow/ragflow.git
$ cd ragflow/
$ docker build -t infiniflow/ragflow:v0.3.1 .
$ docker build -t infiniflow/ragflow:v0.4.0 .
$ cd ragflow/docker
$ chmod +x ./entrypoint.sh
$ docker compose up -d

View File

@ -23,6 +23,9 @@ import flask
from elasticsearch_dsl import Q
from flask import request
from flask_login import login_required, current_user
from api.db.services.file2document_service import File2DocumentService
from api.db.services.file_service import FileService
from rag.nlp import search
from rag.utils import ELASTICSEARCH
from api.db.services import duplicate_name
@ -68,7 +71,7 @@ def upload():
name=file.filename,
kb_id=kb.id)
filetype = filename_type(filename)
if not filetype:
if filetype == FileType.OTHER.value:
return get_data_error_result(
retmsg="This type of file has not been supported yet!")
@ -218,26 +221,37 @@ def change_status():
@validate_request("doc_id")
def rm():
req = request.json
try:
e, doc = DocumentService.get_by_id(req["doc_id"])
if not e:
return get_data_error_result(retmsg="Document not found!")
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
if not tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
doc_ids = req["doc_id"]
if isinstance(doc_ids, str): doc_ids = [doc_ids]
errors = ""
for doc_id in doc_ids:
try:
e, doc = DocumentService.get_by_id(doc_id)
DocumentService.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not DocumentService.delete(doc):
return get_data_error_result(
retmsg="Database error (Document removal)!")
if not e:
return get_data_error_result(retmsg="Document not found!")
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
MINIO.rm(doc.kb_id, doc.location)
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
DocumentService.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not DocumentService.delete(doc):
return get_data_error_result(
retmsg="Database error (Document removal)!")
informs = File2DocumentService.get_by_document_id(doc_id)
if not informs:
MINIO.rm(doc.kb_id, doc.location)
else:
File2DocumentService.delete_by_document_id(doc_id)
except Exception as e:
errors += str(e)
if errors: return server_error_response(e)
return get_json_result(data=True)
@manager.route('/run', methods=['POST'])
@ -289,6 +303,11 @@ def rename():
return get_data_error_result(
retmsg="Database error (Document rename)!")
informs = File2DocumentService.get_by_document_id(req["doc_id"])
if informs:
e, file = FileService.get_by_id(informs[0].file_id)
FileService.update_by_id(file.id, {"name": req["name"]})
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@ -302,7 +321,13 @@ def get(doc_id):
if not e:
return get_data_error_result(retmsg="Document not found!")
response = flask.make_response(MINIO.get(doc.kb_id, doc.location))
informs = File2DocumentService.get_by_document_id(doc_id)
if not informs:
response = flask.make_response(MINIO.get(doc.kb_id, doc.location))
else:
e, file = FileService.get_by_id(informs[0].file_id)
response = flask.make_response(MINIO.get(file.parent_id, doc.location))
ext = re.search(r"\.([^.]+)$", doc.name)
if ext:
if doc.type == FileType.VISUAL.value:

View File

@ -0,0 +1,137 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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
#
from elasticsearch_dsl import Q
from api.db.db_models import File2Document
from api.db.services.file2document_service import File2DocumentService
from api.db.services.file_service import FileService
from flask import request
from flask_login import login_required, current_user
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
from api.utils import get_uuid
from api.db import FileType
from api.db.services.document_service import DocumentService
from api.settings import RetCode
from api.utils.api_utils import get_json_result
from rag.nlp import search
from rag.utils import ELASTICSEARCH
@manager.route('/convert', methods=['POST'])
@login_required
@validate_request("file_ids", "kb_ids")
def convert():
req = request.json
kb_ids = req["kb_ids"]
file_ids = req["file_ids"]
file2documents = []
try:
for file_id in file_ids:
e, file = FileService.get_by_id(file_id)
file_ids_list = [file_id]
if file.type == FileType.FOLDER.value:
file_ids_list = FileService.get_all_innermost_file_ids(file_id, [])
for id in file_ids_list:
informs = File2DocumentService.get_by_file_id(id)
# delete
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if not e:
return get_data_error_result(retmsg="Document not found!")
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
DocumentService.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not DocumentService.delete(doc):
return get_data_error_result(
retmsg="Database error (Document removal)!")
File2DocumentService.delete_by_file_id(id)
# insert
for kb_id in kb_ids:
e, kb = KnowledgebaseService.get_by_id(kb_id)
if not e:
return get_data_error_result(
retmsg="Can't find this knowledgebase!")
e, file = FileService.get_by_id(id)
if not e:
return get_data_error_result(
retmsg="Can't find this file!")
doc = DocumentService.insert({
"id": get_uuid(),
"kb_id": kb.id,
"parser_id": kb.parser_id,
"parser_config": kb.parser_config,
"created_by": current_user.id,
"type": file.type,
"name": file.name,
"location": file.location,
"size": file.size
})
file2document = File2DocumentService.insert({
"id": get_uuid(),
"file_id": id,
"document_id": doc.id,
})
file2documents.append(file2document.to_json())
return get_json_result(data=file2documents)
except Exception as e:
return server_error_response(e)
@manager.route('/rm', methods=['POST'])
@login_required
@validate_request("file_ids")
def rm():
req = request.json
file_ids = req["file_ids"]
if not file_ids:
return get_json_result(
data=False, retmsg='Lack of "Files ID"', retcode=RetCode.ARGUMENT_ERROR)
try:
for file_id in file_ids:
informs = File2DocumentService.get_by_file_id(file_id)
if not informs:
return get_data_error_result(retmsg="Inform not found!")
for inform in informs:
if not inform:
return get_data_error_result(retmsg="Inform not found!")
File2DocumentService.delete_by_file_id(file_id)
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if not e:
return get_data_error_result(retmsg="Document not found!")
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
DocumentService.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not DocumentService.delete(doc):
return get_data_error_result(
retmsg="Database error (Document removal)!")
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)

347
api/apps/file_app.py Normal file
View File

@ -0,0 +1,347 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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 os
import pathlib
import re
import flask
from elasticsearch_dsl import Q
from flask import request
from flask_login import login_required, current_user
from api.db.services.document_service import DocumentService
from api.db.services.file2document_service import File2DocumentService
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
from api.utils import get_uuid
from api.db import FileType
from api.db.services import duplicate_name
from api.db.services.file_service import FileService
from api.settings import RetCode
from api.utils.api_utils import get_json_result
from api.utils.file_utils import filename_type
from rag.nlp import search
from rag.utils import ELASTICSEARCH
from rag.utils.minio_conn import MINIO
@manager.route('/upload', methods=['POST'])
@login_required
# @validate_request("parent_id")
def upload():
pf_id = request.form.get("parent_id")
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder.id
if 'file' not in request.files:
return get_json_result(
data=False, retmsg='No file part!', retcode=RetCode.ARGUMENT_ERROR)
file_objs = request.files.getlist('file')
for file_obj in file_objs:
if file_obj.filename == '':
return get_json_result(
data=False, retmsg='No file selected!', retcode=RetCode.ARGUMENT_ERROR)
file_res = []
try:
for file_obj in file_objs:
e, file = FileService.get_by_id(pf_id)
if not e:
return get_data_error_result(
retmsg="Can't find this folder!")
MAX_FILE_NUM_PER_USER = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
if MAX_FILE_NUM_PER_USER > 0 and DocumentService.get_doc_count(current_user.id) >= MAX_FILE_NUM_PER_USER:
return get_data_error_result(
retmsg="Exceed the maximum file number of a free user!")
# split file name path
if not file_obj.filename:
e, file = FileService.get_by_id(pf_id)
file_obj_names = [file.name, file_obj.filename]
else:
full_path = '/' + file_obj.filename
file_obj_names = full_path.split('/')
file_len = len(file_obj_names)
# get folder
file_id_list = FileService.get_id_list_by_id(pf_id, file_obj_names, 1, [pf_id])
len_id_list = len(file_id_list)
# create folder
if file_len != len_id_list:
e, file = FileService.get_by_id(file_id_list[len_id_list - 1])
if not e:
return get_data_error_result(retmsg="Folder not found!")
last_folder = FileService.create_folder(file, file_id_list[len_id_list - 1], file_obj_names,
len_id_list)
else:
e, file = FileService.get_by_id(file_id_list[len_id_list - 2])
if not e:
return get_data_error_result(retmsg="Folder not found!")
last_folder = FileService.create_folder(file, file_id_list[len_id_list - 2], file_obj_names,
len_id_list)
# file type
filetype = filename_type(file_obj_names[file_len - 1])
location = file_obj_names[file_len - 1]
while MINIO.obj_exist(last_folder.id, location):
location += "_"
blob = file_obj.read()
filename = duplicate_name(
FileService.query,
name=file_obj_names[file_len - 1],
parent_id=last_folder.id)
file = {
"id": get_uuid(),
"parent_id": last_folder.id,
"tenant_id": current_user.id,
"created_by": current_user.id,
"type": filetype,
"name": filename,
"location": location,
"size": len(blob),
}
file = FileService.insert(file)
MINIO.put(last_folder.id, location, blob)
file_res.append(file.to_json())
return get_json_result(data=file_res)
except Exception as e:
return server_error_response(e)
@manager.route('/create', methods=['POST'])
@login_required
@validate_request("name")
def create():
req = request.json
pf_id = request.json.get("parent_id")
input_file_type = request.json.get("type")
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder.id
try:
if not FileService.is_parent_folder_exist(pf_id):
return get_json_result(
data=False, retmsg="Parent Folder Doesn't Exist!", retcode=RetCode.OPERATING_ERROR)
if FileService.query(name=req["name"], parent_id=pf_id):
return get_data_error_result(
retmsg="Duplicated folder name in the same folder.")
if input_file_type == FileType.FOLDER.value:
file_type = FileType.FOLDER.value
else:
file_type = FileType.VIRTUAL.value
file = FileService.insert({
"id": get_uuid(),
"parent_id": pf_id,
"tenant_id": current_user.id,
"created_by": current_user.id,
"name": req["name"],
"location": "",
"size": 0,
"type": file_type
})
return get_json_result(data=file.to_json())
except Exception as e:
return server_error_response(e)
@manager.route('/list', methods=['GET'])
@login_required
def list():
pf_id = request.args.get("parent_id")
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 15))
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder.id
try:
e, file = FileService.get_by_id(pf_id)
if not e:
return get_data_error_result(retmsg="Folder not found!")
files, total = FileService.get_by_pf_id(
current_user.id, pf_id, page_number, items_per_page, orderby, desc, keywords)
parent_folder = FileService.get_parent_folder(pf_id)
if not FileService.get_parent_folder(pf_id):
return get_json_result(retmsg="File not found!")
return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/root_folder', methods=['GET'])
@login_required
def get_root_folder():
try:
root_folder = FileService.get_root_folder(current_user.id)
return get_json_result(data={"root_folder": root_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/parent_folder', methods=['GET'])
@login_required
def get_parent_folder():
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(retmsg="Folder not found!")
parent_folder = FileService.get_parent_folder(file_id)
return get_json_result(data={"parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/all_parent_folder', methods=['GET'])
@login_required
def get_all_parent_folders():
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(retmsg="Folder not found!")
parent_folders = FileService.get_all_parent_folders(file_id)
parent_folders_res = []
for parent_folder in parent_folders:
parent_folders_res.append(parent_folder.to_json())
return get_json_result(data={"parent_folders": parent_folders_res})
except Exception as e:
return server_error_response(e)
@manager.route('/rm', methods=['POST'])
@login_required
@validate_request("file_ids")
def rm():
req = request.json
file_ids = req["file_ids"]
try:
for file_id in file_ids:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(retmsg="File or Folder not found!")
if not file.tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
if file.type == FileType.FOLDER.value:
file_id_list = FileService.get_all_innermost_file_ids(file_id, [])
for inner_file_id in file_id_list:
e, file = FileService.get_by_id(inner_file_id)
if not e:
return get_data_error_result(retmsg="File not found!")
MINIO.rm(file.parent_id, file.location)
FileService.delete_folder_by_pf_id(current_user.id, file_id)
else:
if not FileService.delete(file):
return get_data_error_result(
retmsg="Database error (File removal)!")
# delete file2document
informs = File2DocumentService.get_by_file_id(file_id)
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if not e:
return get_data_error_result(retmsg="Document not found!")
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_data_error_result(retmsg="Tenant not found!")
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
DocumentService.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not DocumentService.delete(doc):
return get_data_error_result(
retmsg="Database error (Document removal)!")
File2DocumentService.delete_by_file_id(file_id)
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/rename', methods=['POST'])
@login_required
@validate_request("file_id", "name")
def rename():
req = request.json
try:
e, file = FileService.get_by_id(req["file_id"])
if not e:
return get_data_error_result(retmsg="File not found!")
if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
file.name.lower()).suffix:
return get_json_result(
data=False,
retmsg="The extension of file can't be changed",
retcode=RetCode.ARGUMENT_ERROR)
if FileService.query(name=req["name"], pf_id=file.parent_id):
return get_data_error_result(
retmsg="Duplicated file name in the same folder.")
if not FileService.update_by_id(
req["file_id"], {"name": req["name"]}):
return get_data_error_result(
retmsg="Database error (File rename)!")
informs = File2DocumentService.get_by_file_id(req["file_id"])
if informs:
if not DocumentService.update_by_id(
informs[0].document_id, {"name": req["name"]}):
return get_data_error_result(
retmsg="Database error (Document rename)!")
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/get/<file_id>', methods=['GET'])
# @login_required
def get(file_id):
try:
e, doc = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(retmsg="Document not found!")
response = flask.make_response(MINIO.get(doc.parent_id, doc.location))
ext = re.search(r"\.([^.]+)$", doc.name)
if ext:
if doc.type == FileType.VISUAL.value:
response.headers.set('Content-Type', 'image/%s' % ext.group(1))
else:
response.headers.set(
'Content-Type',
'application/%s' %
ext.group(1))
return response
except Exception as e:
return server_error_response(e)

View File

@ -111,7 +111,7 @@ def detail():
@login_required
def list():
page_number = request.args.get("page", 1)
items_per_page = request.args.get("page_size", 15)
items_per_page = request.args.get("page_size", 150)
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
try:

View File

@ -24,10 +24,11 @@ from api.db.db_models import TenantLLM
from api.db.services.llm_service import TenantLLMService, LLMService
from api.utils.api_utils import server_error_response, validate_request
from api.utils import get_uuid, get_format_time, decrypt, download_img, current_timestamp, datetime_format
from api.db import UserTenantRole, LLMType
from api.db import UserTenantRole, LLMType, FileType
from api.settings import RetCode, GITHUB_OAUTH, CHAT_MDL, EMBEDDING_MDL, ASR_MDL, IMAGE2TEXT_MDL, PARSERS, API_KEY, \
LLM_FACTORY, LLM_BASE_URL
from api.db.services.user_service import UserService, TenantService, UserTenantService
from api.db.services.file_service import FileService
from api.settings import stat_logger
from api.utils.api_utils import get_json_result, cors_reponse
@ -221,6 +222,17 @@ def user_register(user_id, user):
"invited_by": user_id,
"role": UserTenantRole.OWNER
}
file_id = get_uuid()
file = {
"id": file_id,
"parent_id": file_id,
"tenant_id": user_id,
"created_by": user_id,
"name": "/",
"type": FileType.FOLDER.value,
"size": 0,
"location": "",
}
tenant_llm = []
for llm in LLMService.query(fid=LLM_FACTORY):
tenant_llm.append({"tenant_id": user_id,
@ -236,6 +248,7 @@ def user_register(user_id, user):
TenantService.insert(**tenant)
UserTenantService.insert(**usr_tenant)
TenantLLMService.insert_many(tenant_llm)
FileService.insert(file)
return UserService.query(email=user["email"])

View File

@ -45,6 +45,8 @@ class FileType(StrEnum):
VISUAL = 'visual'
AURAL = 'aural'
VIRTUAL = 'virtual'
FOLDER = 'folder'
OTHER = "other"
class LLMType(StrEnum):

View File

@ -669,6 +669,61 @@ class Document(DataBaseModel):
db_table = "document"
class File(DataBaseModel):
id = CharField(
max_length=32,
primary_key=True,
)
parent_id = CharField(
max_length=32,
null=False,
help_text="parent folder id",
index=True)
tenant_id = CharField(
max_length=32,
null=False,
help_text="tenant id",
index=True)
created_by = CharField(
max_length=32,
null=False,
help_text="who created it")
name = CharField(
max_length=255,
null=False,
help_text="file name or folder name",
index=True)
location = CharField(
max_length=255,
null=True,
help_text="where dose it store")
size = IntegerField(default=0)
type = CharField(max_length=32, null=False, help_text="file extension")
class Meta:
db_table = "file"
class File2Document(DataBaseModel):
id = CharField(
max_length=32,
primary_key=True,
)
file_id = CharField(
max_length=32,
null=True,
help_text="file id",
index=True)
document_id = CharField(
max_length=32,
null=True,
help_text="document id",
index=True)
class Meta:
db_table = "file2document"
class Task(DataBaseModel):
id = CharField(max_length=32, primary_key=True)
doc_id = CharField(max_length=32, null=False, index=True)

View File

@ -347,8 +347,8 @@ def init_llm_factory():
LLMService.filter_delete([LLM.fid == "Local"])
LLMService.filter_delete([LLM.fid == "Moonshot", LLM.llm_name == "flag-embedding"])
TenantLLMService.filter_delete([TenantLLM.llm_factory == "Moonshot", TenantLLM.llm_name == "flag-embedding"])
LLMFactoriesService.filter_update([LLMFactoriesService.model.name == "QAnything"], {"name": "Youdao"})
LLMService.filter_update([LLMService.model.fid == "QAnything"], {"fid": "Youdao"})
LLMFactoriesService.filter_delete([LLMFactoriesService.model.name == "QAnything"])
LLMService.filter_delete([LLMService.model.fid == "QAnything"])
TenantLLMService.filter_update([TenantLLMService.model.llm_factory == "QAnything"], {"llm_factory": "Youdao"})
"""
drop table llm;

View File

@ -15,6 +15,11 @@
#
from peewee import Expression
from elasticsearch_dsl import Q
from rag.utils import ELASTICSEARCH
from rag.utils.minio_conn import MINIO
from rag.nlp import search
from api.db import FileType, TaskStatus
from api.db.db_models import DB, Knowledgebase, Tenant
from api.db.db_models import Document
@ -69,6 +74,20 @@ class DocumentService(CommonService):
raise RuntimeError("Database error (Knowledgebase)!")
return cls.delete_by_id(doc.id)
@classmethod
@DB.connection_context()
def remove_document(cls, doc, tenant_id):
ELASTICSEARCH.deleteByQuery(
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
cls.increment_chunk_num(
doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, 0)
if not cls.delete(doc):
raise RuntimeError("Database error (Document removal)!")
MINIO.rm(doc.kb_id, doc.location)
return cls.delete_by_id(doc.id)
@classmethod
@DB.connection_context()
def get_newly_uploaded(cls, tm, mod=0, comm=1, items_per_page=64):

View File

@ -0,0 +1,66 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
#
from datetime import datetime
from api.db.db_models import DB
from api.db.db_models import File, Document, File2Document
from api.db.services.common_service import CommonService
from api.utils import current_timestamp, datetime_format
class File2DocumentService(CommonService):
model = File2Document
@classmethod
@DB.connection_context()
def get_by_file_id(cls, file_id):
objs = cls.model.select().where(cls.model.file_id == file_id)
return objs
@classmethod
@DB.connection_context()
def get_by_document_id(cls, document_id):
objs = cls.model.select().where(cls.model.document_id == document_id)
return objs
@classmethod
@DB.connection_context()
def insert(cls, obj):
if not cls.save(**obj):
raise RuntimeError("Database error (File)!")
e, obj = cls.get_by_id(obj["id"])
if not e:
raise RuntimeError("Database error (File retrieval)!")
return obj
@classmethod
@DB.connection_context()
def delete_by_file_id(cls, file_id):
return cls.model.delete().where(cls.model.file_id == file_id).execute()
@classmethod
@DB.connection_context()
def delete_by_document_id(cls, doc_id):
return cls.model.delete().where(cls.model.document_id == doc_id).execute()
@classmethod
@DB.connection_context()
def update_by_file_id(cls, file_id, obj):
obj["update_time"] = current_timestamp()
obj["update_date"] = datetime_format(datetime.now())
num = cls.model.update(obj).where(cls.model.id == file_id).execute()
e, obj = cls.get_by_id(cls.model.id)
return obj

View File

@ -0,0 +1,243 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
#
from flask_login import current_user
from peewee import fn
from api.db import FileType
from api.db.db_models import DB, File2Document, Knowledgebase
from api.db.db_models import File, Document
from api.db.services.common_service import CommonService
from api.utils import get_uuid
from rag.utils import MINIO
class FileService(CommonService):
model = File
@classmethod
@DB.connection_context()
def get_by_pf_id(cls, tenant_id, pf_id, page_number, items_per_page,
orderby, desc, keywords):
if keywords:
files = cls.model.select().where(
(cls.model.tenant_id == tenant_id)
& (cls.model.parent_id == pf_id), (fn.LOWER(cls.model.name).like(f"%%{keywords.lower()}%%")))
else:
files = cls.model.select().where((cls.model.tenant_id == tenant_id)
& (cls.model.parent_id == pf_id))
count = files.count()
if desc:
files = files.order_by(cls.model.getter_by(orderby).desc())
else:
files = files.order_by(cls.model.getter_by(orderby).asc())
files = files.paginate(page_number, items_per_page)
res_files = list(files.dicts())
for file in res_files:
if file["type"] == FileType.FOLDER.value:
file["size"] = cls.get_folder_size(file["id"])
file['kbs_info'] = []
continue
kbs_info = cls.get_kb_id_by_file_id(file['id'])
file['kbs_info'] = kbs_info
return res_files, count
@classmethod
@DB.connection_context()
def get_kb_id_by_file_id(cls, file_id):
kbs = (cls.model.select(*[Knowledgebase.id, Knowledgebase.name])
.join(File2Document, on=(File2Document.file_id == file_id))
.join(Document, on=(File2Document.document_id == Document.id))
.join(Knowledgebase, on=(Knowledgebase.id == Document.kb_id))
.where(cls.model.id == file_id))
if not kbs: return []
kbs_info_list = []
for kb in list(kbs.dicts()):
kbs_info_list.append({"kb_id": kb['id'], "kb_name": kb['name']})
return kbs_info_list
@classmethod
@DB.connection_context()
def get_by_pf_id_name(cls, id, name):
file = cls.model.select().where((cls.model.parent_id == id) & (cls.model.name == name))
if file.count():
e, file = cls.get_by_id(file[0].id)
if not e:
raise RuntimeError("Database error (File retrieval)!")
return file
return None
@classmethod
@DB.connection_context()
def get_id_list_by_id(cls, id, name, count, res):
if count < len(name):
file = cls.get_by_pf_id_name(id, name[count])
if file:
res.append(file.id)
return cls.get_id_list_by_id(file.id, name, count + 1, res)
else:
return res
else:
return res
@classmethod
@DB.connection_context()
def get_all_innermost_file_ids(cls, folder_id, result_ids):
subfolders = cls.model.select().where(cls.model.parent_id == folder_id)
if subfolders.exists():
for subfolder in subfolders:
cls.get_all_innermost_file_ids(subfolder.id, result_ids)
else:
result_ids.append(folder_id)
return result_ids
@classmethod
@DB.connection_context()
def create_folder(cls, file, parent_id, name, count):
if count > len(name) - 2:
return file
else:
file = cls.insert({
"id": get_uuid(),
"parent_id": parent_id,
"tenant_id": current_user.id,
"created_by": current_user.id,
"name": name[count],
"location": "",
"size": 0,
"type": FileType.FOLDER.value
})
return cls.create_folder(file, file.id, name, count + 1)
@classmethod
@DB.connection_context()
def is_parent_folder_exist(cls, parent_id):
parent_files = cls.model.select().where(cls.model.id == parent_id)
if parent_files.count():
return True
cls.delete_folder_by_pf_id(parent_id)
return False
@classmethod
@DB.connection_context()
def get_root_folder(cls, tenant_id):
file = cls.model.select().where(cls.model.tenant_id == tenant_id and
cls.model.parent_id == cls.model.id)
if not file:
file_id = get_uuid()
file = {
"id": file_id,
"parent_id": file_id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"name": "/",
"type": FileType.FOLDER.value,
"size": 0,
"location": "",
}
cls.save(**file)
else:
file_id = file[0].id
e, file = cls.get_by_id(file_id)
if not e:
raise RuntimeError("Database error (File retrieval)!")
return file
@classmethod
@DB.connection_context()
def get_parent_folder(cls, file_id):
file = cls.model.select().where(cls.model.id == file_id)
if file.count():
e, file = cls.get_by_id(file[0].parent_id)
if not e:
raise RuntimeError("Database error (File retrieval)!")
else:
raise RuntimeError("Database error (File doesn't exist)!")
return file
@classmethod
@DB.connection_context()
def get_all_parent_folders(cls, start_id):
parent_folders = []
current_id = start_id
while current_id:
e, file = cls.get_by_id(current_id)
if file.parent_id != file.id and e:
parent_folders.append(file)
current_id = file.parent_id
else:
parent_folders.append(file)
break
return parent_folders
@classmethod
@DB.connection_context()
def insert(cls, file):
if not cls.save(**file):
raise RuntimeError("Database error (File)!")
e, file = cls.get_by_id(file["id"])
if not e:
raise RuntimeError("Database error (File retrieval)!")
return file
@classmethod
@DB.connection_context()
def delete(cls, file):
return cls.delete_by_id(file.id)
@classmethod
@DB.connection_context()
def delete_by_pf_id(cls, folder_id):
return cls.model.delete().where(cls.model.parent_id == folder_id).execute()
@classmethod
@DB.connection_context()
def delete_folder_by_pf_id(cls, user_id, folder_id):
try:
files = cls.model.select().where((cls.model.tenant_id == user_id)
& (cls.model.parent_id == folder_id))
for file in files:
cls.delete_folder_by_pf_id(user_id, file.id)
return cls.model.delete().where((cls.model.tenant_id == user_id)
& (cls.model.id == folder_id)).execute(),
except Exception as e:
print(e)
raise RuntimeError("Database error (File retrieval)!")
@classmethod
@DB.connection_context()
def get_file_count(cls, tenant_id):
files = cls.model.select(cls.model.id).where(cls.model.tenant_id == tenant_id)
return len(files)
@classmethod
@DB.connection_context()
def get_folder_size(cls, folder_id):
size = 0
def dfs(parent_id):
nonlocal size
for f in cls.model.select(*[cls.model.id, cls.model.size, cls.model.type]).where(
cls.model.parent_id == parent_id, cls.model.id != parent_id):
size += f.size
if f.type == FileType.FOLDER.value:
dfs(f.id)
dfs(folder_id)
return size

View File

@ -155,7 +155,9 @@ def filename_type(filename):
return FileType.AURAL.value
if re.match(r".*\.(jpg|jpeg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp|avif|apng|icon|ico|mpg|mpeg|avi|rm|rmvb|mov|wmv|asf|dat|asx|wvx|mpe|mpa|mp4)$", filename):
return FileType.VISUAL
return FileType.VISUAL.value
return FileType.OTHER.value
def thumbnail(filename, blob):

View File

@ -27,7 +27,7 @@ MINIO_PASSWORD=infini_rag_flow
SVR_HTTP_PORT=9380
RAGFLOW_VERSION=v0.3.1
RAGFLOW_VERSION=v0.4.0
TIMEZONE='Asia/Shanghai'

View File

@ -96,8 +96,8 @@ services:
volumes:
esdata01:
driver: local
kibanadata:
driver: local
# kibanadata:
# driver: local
mysql_data:
driver: local
minio_data:

View File

@ -55,7 +55,7 @@ This feature and the related APIs are still in development. Contributions are we
```
$ git clone https://github.com/infiniflow/ragflow.git
$ cd ragflow
$ docker build -t infiniflow/ragflow:v0.3.1 .
$ docker build -t infiniflow/ragflow:v0.4.0 .
$ cd ragflow/docker
$ chmod +x ./entrypoint.sh
$ docker compose up -d
@ -212,7 +212,7 @@ $ docker ps
*The system displays the following if all your RAGFlow components are running properly:*
```
5bc45806b680 infiniflow/ragflow:v0.3.1 "./entrypoint.sh" 11 hours ago Up 11 hours 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:9380->9380/tcp, :::9380->9380/tcp ragflow-server
5bc45806b680 infiniflow/ragflow:v0.4.0 "./entrypoint.sh" 11 hours ago Up 11 hours 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:9380->9380/tcp, :::9380->9380/tcp ragflow-server
91220e3285dd docker.elastic.co/elasticsearch/elasticsearch:8.11.3 "/bin/tini -- /usr/l…" 11 hours ago Up 11 hours (healthy) 9300/tcp, 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp ragflow-es-01
d8c86f06c56b mysql:5.7.18 "docker-entrypoint.s…" 7 days ago Up 16 seconds (healthy) 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp ragflow-mysql
cd29bcb254bc quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z "/usr/bin/docker-ent…" 2 weeks ago Up 11 hours 0.0.0.0:9001->9001/tcp, :::9001->9001/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp ragflow-minio

View File

@ -25,7 +25,7 @@ from deepdoc.parser import PdfParser, DocxParser, PlainParser
class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -58,7 +58,7 @@ class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -16,7 +16,7 @@ class Pdf(PdfParser):
to_page=100000, zoomin=3, callback=None):
from timeit import default_timer as timer
start = timer()
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -69,7 +69,7 @@ class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
start = timer()
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -21,7 +21,7 @@ from deepdoc.parser import PdfParser, ExcelParser, PlainParser
class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -28,7 +28,7 @@ class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(
filename if not binary else binary,
zoomin,

View File

@ -58,7 +58,7 @@ class Pdf(PdfParser):
def __call__(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None):
callback(msg="OCR is running...")
callback(msg="OCR is running...")
self.__images__(filename if not binary else binary,
zoomin, from_page, to_page, callback)
callback(0.8, "Page {}~{}: OCR finished".format(

View File

@ -17,12 +17,12 @@ class Dealer:
try:
self.dictionary = json.load(open(path, 'r'))
except Exception as e:
logging.warn("Miss synonym.json")
logging.warning("Missing synonym.json")
self.dictionary = {}
if not redis:
logging.warning(
"Realtime synonym is disabled, since no redis connection.")
"Real-time synonym is disabled, since no redis connection.")
if not len(self.dictionary.keys()):
logging.warning(f"Fail to load synonym")

View File

@ -27,7 +27,7 @@ export default defineConfig({
devtool: 'source-map',
proxy: {
'/v1': {
target: 'http://192.168.200.233:9380/',
target: 'http://123.60.95.134:9380/',
changeOrigin: true,
// pathRewrite: { '^/v1': '/v1' },
},

View File

@ -160,12 +160,12 @@ export const useRemoveDocument = () => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const removeDocument = useCallback(
(documentId: string) => {
(documentIds: string[]) => {
try {
return dispatch<any>({
type: 'kFModel/document_rm',
payload: {
doc_id: documentId,
doc_id: documentIds,
kb_id: knowledgeId,
},
});

View File

@ -14,5 +14,5 @@ export interface IModalProps<T> {
hideModal(): void;
visible: boolean;
loading?: boolean;
onOk?(payload?: T): Promise<void> | void;
onOk?(payload?: T): Promise<any> | void;
}

View File

@ -1,5 +1,5 @@
import { ReactComponent as StarIon } from '@/assets/svg/chat-star.svg';
// import { ReactComponent as FileIcon } from '@/assets/svg/file-management.svg';
import { ReactComponent as FileIcon } from '@/assets/svg/file-management.svg';
import { ReactComponent as KnowledgeBaseIcon } from '@/assets/svg/knowledge-base.svg';
import { ReactComponent as Logo } from '@/assets/svg/logo.svg';
import { useTranslate } from '@/hooks/commonHooks';
@ -25,7 +25,7 @@ const RagHeader = () => {
() => [
{ path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon },
{ path: '/chat', name: t('chat'), icon: StarIon },
// { path: '/file', name: 'File Management', icon: FileIcon },
{ path: '/file', name: t('fileManager'), icon: FileIcon },
],
[t],
);

View File

@ -22,6 +22,8 @@ export default {
languagePlaceholder: 'select your language',
copy: 'Copy',
copied: 'Copied',
comingSoon: 'Coming Soon',
download: 'Download',
},
login: {
login: 'Sign in',
@ -52,6 +54,7 @@ export default {
home: 'Home',
setting: '用户设置',
logout: '登出',
fileManager: 'File Management',
},
knowledgeList: {
welcome: 'Welcome back',
@ -459,6 +462,7 @@ export default {
renamed: 'Renamed',
operated: 'Operated',
updated: 'Updated',
uploaded: 'Uploaded',
200: 'The server successfully returns the requested data.',
201: 'Create or modify data successfully.',
202: 'A request has been queued in the background (asynchronous task).',
@ -480,6 +484,24 @@ export default {
networkAnomaly: 'network anomaly',
hint: 'hint',
},
fileManager: {
name: 'Name',
uploadDate: 'Upload Date',
knowledgeBase: 'Knowledge Base',
size: 'Size',
action: 'Action',
addToKnowledge: 'Add to Knowledge Base',
pleaseSelect: 'Please select',
newFolder: 'New Folder',
file: 'File',
uploadFile: 'Upload File',
directory: 'Directory',
uploadTitle: 'Click or drag file to this area to upload',
uploadDescription:
'Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.',
local: 'Local uploads',
s3: 'S3 uploads',
},
footer: {
profile: 'All rights reserved @ React',
},

View File

@ -22,6 +22,8 @@ export default {
languagePlaceholder: '請選擇語言',
copy: '複製',
copied: '複製成功',
comingSoon: '即將推出',
download: '下載',
},
login: {
login: '登入',
@ -52,6 +54,7 @@ export default {
home: '首頁',
setting: '用戶設置',
logout: '登出',
fileManager: '文件管理',
},
knowledgeList: {
welcome: '歡迎回來',
@ -218,7 +221,7 @@ export default {
您只需與<i>'ragflow'</i>交談即可列出所有符合資格的候選人。
</p>
`,
table: `支持<p><b>excel</b>和<b>csv/txt</b>格式文件。</p><p>以下是一些提示: <ul> <li>对于Csv或Txt文件列之间的分隔符为 <em><b>tab</b></em>。</li> <li>第一行必须是列标题。</li> <li>列标题必须是有意义的术语,以便我们的法学硕士能够理解。列举一些同义词时最好使用斜杠<i>'/'</i>来分隔,甚至更好使用方括号枚举值,例如 <i>“性別/性別(男性,女性)”</i>.<p>以下是标题的一些示例:<ol> <li>供应商/供货商<b>'tab'</b>顏色(黃色、紅色、棕色)<b>'tab'</b>性別(男、女)<b>'tab'</B>尺码m、l、xl、xxl</li> <li>姓名/名字<b>'tab'</b>電話/手機/微信<b>'tab'</b>最高学历高中职高硕士本科博士初中中技中专专科专升本mpambaemba</li> </ol> </p> </li> <li>表中的每一行都将被视为一个块。</li> </ul>`,
table: `支持<p><b>excel</b>和<b>csv/txt</b>格式文件。</p><p>以下是一些提示: <ul> <li>对于Csv或Txt文件列之间的分隔符为 <em><b>tab</b></em>。</li> <li>第一行必须是列标题。</li> <li>列标题必须是有意义的术语,以便我们的大語言模型能够理解。列举一些同义词时最好使用斜杠<i>'/'</i>来分隔,甚至更好使用方括号枚举值,例如 <i>“性別/性別(男性,女性)”</i>.<p>以下是标题的一些示例:<ol> <li>供应商/供货商<b>'tab'</b>顏色(黃色、紅色、棕色)<b>'tab'</b>性別(男、女)<b>'tab'</B>尺码m、l、xl、xxl</li> <li>姓名/名字<b>'tab'</b>電話/手機/微信<b>'tab'</b>最高学历高中职高硕士本科博士初中中技中专专科专升本mpambaemba</li> </ol> </p> </li> <li>表中的每一行都将被视为一个块。</li> </ul>`,
picture: `
<p>支持圖像文件。視頻即將推出。</p><p>
如果圖片中有文字,則應用 OCR 提取文字作為其文字描述。
@ -424,6 +427,7 @@ export default {
renamed: '重命名成功',
operated: '操作成功',
updated: '更新成功',
uploaded: '上傳成功',
200: '服務器成功返回請求的數據。',
201: '新建或修改數據成功。',
202: '一個請求已經進入後台排隊(異步任務)。',
@ -444,6 +448,23 @@ export default {
networkAnomaly: '網絡異常',
hint: '提示',
},
fileManager: {
name: '名稱',
uploadDate: '上傳日期',
knowledgeBase: '知識庫',
size: '大小',
action: '操作',
addToKnowledge: '添加到知識庫',
pleaseSelect: '請選擇',
newFolder: '新建文件夾',
uploadFile: '上傳文件',
uploadTitle: '點擊或拖拽文件至此區域即可上傳',
uploadDescription: '支持單次或批量上傳。嚴禁上傳公司數據或其他違禁文件。',
file: '文件',
directory: '文件夾',
local: '本地上傳',
s3: 'S3 上傳',
},
footer: {
profile: '“保留所有權利 @ react”',
},

View File

@ -22,6 +22,8 @@ export default {
languagePlaceholder: '请选择语言',
copy: '复制',
copied: '复制成功',
comingSoon: '即将推出',
download: '下载',
},
login: {
login: '登录',
@ -52,6 +54,7 @@ export default {
home: '首页',
setting: '用户设置',
logout: '登出',
fileManager: '文件管理',
},
knowledgeList: {
welcome: '欢迎回来',
@ -225,7 +228,7 @@ export default {
<ul>
<li>对于 csv 或 txt 文件,列之间的分隔符为 <em><b>TAB</b></em>。</li>
<li>第一行必须是列标题。</li>
<li>列标题必须是有意义的术语,以便我们的法学硕士能够理解。
<li>列标题必须是有意义的术语,以便我们的大语言模型能够理解。
列举一些同义词时最好使用斜杠<i>'/'</i>来分隔,甚至更好
使用方括号枚举值,例如 <i>'gender/sex(male,female)'</i>.<p>
以下是标题的一些示例:<ol>
@ -298,7 +301,7 @@ export default {
systemTip:
'当LLM回答问题时你需要LLM遵循的说明比如角色设计、答案长度和答案语言等。',
topN: 'Top N',
topNTip: `并非所有相似度得分高于“相似度阈值”的块都会被提供给法学硕士。 LLM 只能看到这些“Top N”块。`,
topNTip: `并非所有相似度得分高于“相似度阈值”的块都会被提供给大语言模型。 LLM 只能看到这些“Top N”块。`,
variable: '变量',
variableTip: `如果您使用对话 API变量可能会帮助您使用不同的策略与客户聊天。
这些变量用于填写提示中的“系统”部分以便给LLM一个提示。
@ -315,7 +318,7 @@ export default {
improvise: '即兴创作',
precise: '精确',
balance: '平衡',
freedomTip: `“精确”意味着法学硕士会保守并谨慎地回答你的问题。 “即兴发挥”意味着你希望法学硕士能够自由地畅所欲言。 “平衡”是谨慎与自由之间的平衡。`,
freedomTip: `“精确”意味着大语言模型会保守并谨慎地回答你的问题。 “即兴发挥”意味着你希望大语言模型能够自由地畅所欲言。 “平衡”是谨慎与自由之间的平衡。`,
temperature: '温度',
temperatureMessage: '温度是必填项',
temperatureTip:
@ -441,6 +444,7 @@ export default {
renamed: '重命名成功',
operated: '操作成功',
updated: '更新成功',
uploaded: '上传成功',
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
@ -461,6 +465,24 @@ export default {
networkAnomaly: '网络异常',
hint: '提示',
},
fileManager: {
name: '名称',
uploadDate: '上传日期',
knowledgeBase: '知识库',
size: '大小',
action: '操作',
addToKnowledge: '添加到知识库',
pleaseSelect: '请选择',
newFolder: '新建文件夹',
uploadFile: '上传文件',
uploadTitle: '点击或拖拽文件至此区域即可上传',
uploadDescription:
'支持单次或批量上传。 严禁上传公司数据或其他违禁文件。',
file: '文件',
directory: '文件夹',
local: '本地上传',
s3: 'S3 上传',
},
footer: {
profile: 'All rights reserved @ React',
},

View File

@ -80,9 +80,7 @@ const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => {
const handleDelete = useCallback(() => {
showDeleteConfirm({
onOk: () => {
selectedRowKeys.forEach((id) => {
removeDocument(id);
});
removeDocument(selectedRowKeys);
},
});
}, [removeDocument, showDeleteConfirm, selectedRowKeys]);

View File

@ -35,7 +35,7 @@ const ParsingActionCell = ({
const onRmDocument = () => {
if (!isRunning) {
showDeleteConfirm({ onOk: () => removeDocument(documentId) });
showDeleteConfirm({ onOk: () => removeDocument([documentId]) });
}
};

View File

@ -6,7 +6,7 @@ import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
ToolOutlined,
LinkOutlined,
} from '@ant-design/icons';
import { Button, Space, Tooltip } from 'antd';
import { useHandleDeleteFile } from '../hooks';
@ -30,7 +30,7 @@ const ActionCell = ({
}: IProps) => {
const documentId = record.id;
const beingUsed = false;
const { t } = useTranslate('knowledgeDetails');
const { t } = useTranslate('fileManager');
const { handleRemoveFile } = useHandleDeleteFile(
[documentId],
setSelectedRowKeys,
@ -38,7 +38,7 @@ const ActionCell = ({
const onDownloadDocument = () => {
downloadFile({
url: `${api_host}/document/get/${documentId}`,
url: `${api_host}/file/get/${documentId}`,
filename: record.name,
});
};
@ -58,13 +58,15 @@ const ActionCell = ({
return (
<Space size={0}>
<Button
type="text"
className={styles.iconButton}
onClick={onShowConnectToKnowledgeModal}
>
<ToolOutlined size={20} />
</Button>
<Tooltip title={t('addToKnowledge')}>
<Button
type="text"
className={styles.iconButton}
onClick={onShowConnectToKnowledgeModal}
>
<LinkOutlined size={20} />
</Button>
</Tooltip>
<Tooltip title={t('rename', { keyPrefix: 'common' })}>
<Button
@ -76,23 +78,27 @@ const ActionCell = ({
<EditOutlined size={20} />
</Button>
</Tooltip>
<Button
type="text"
disabled={beingUsed}
onClick={handleRemoveFile}
className={styles.iconButton}
>
<DeleteOutlined size={20} />
</Button>
{record.type !== 'folder' && (
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button
type="text"
disabled={beingUsed}
onClick={onDownloadDocument}
onClick={handleRemoveFile}
className={styles.iconButton}
>
<DownloadOutlined size={20} />
<DeleteOutlined size={20} />
</Button>
</Tooltip>
{record.type !== 'folder' && (
<Tooltip title={t('download', { keyPrefix: 'common' })}>
<Button
type="text"
disabled={beingUsed}
onClick={onDownloadDocument}
className={styles.iconButton}
>
<DownloadOutlined size={20} />
</Button>
</Tooltip>
)}
</Space>
);

View File

@ -1,3 +1,4 @@
import { useTranslate } from '@/hooks/commonHooks';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { IModalProps } from '@/interfaces/common';
import { Form, Modal, Select, SelectProps } from 'antd';
@ -8,9 +9,11 @@ const ConnectToKnowledgeModal = ({
hideModal,
onOk,
initialValue,
loading,
}: IModalProps<string[]> & { initialValue: string[] }) => {
const [form] = Form.useForm();
const { list, fetchList } = useFetchKnowledgeList();
const { t } = useTranslate('fileManager');
const options: SelectProps['options'] = list?.map((item) => ({
label: item.name,
@ -32,10 +35,11 @@ const ConnectToKnowledgeModal = ({
return (
<Modal
title="Add to Knowledge Base"
title={t('addToKnowledge')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Form form={form}>
<Form.Item name="knowledgeIds" noStyle>
@ -43,7 +47,7 @@ const ConnectToKnowledgeModal = ({
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="Please select"
placeholder={t('pleaseSelect')}
options={options}
/>
</Form.Item>

View File

@ -20,12 +20,12 @@ import {
import { useMemo } from 'react';
import {
useFetchDocumentListOnMount,
useHandleBreadcrumbClick,
useHandleDeleteFile,
useHandleSearchChange,
useSelectBreadcrumbItems,
} from './hooks';
import { Link } from 'umi';
import styles from './index.less';
interface IProps {
@ -35,20 +35,6 @@ interface IProps {
setSelectedRowKeys: (keys: string[]) => void;
}
const itemRender: BreadcrumbProps['itemRender'] = (
currentRoute,
params,
items,
) => {
const isLast = currentRoute?.path === items[items.length - 1]?.path;
return isLast ? (
<span>{currentRoute.title}</span>
) : (
<Link to={`${currentRoute.path}`}>{currentRoute.title}</Link>
);
};
const FileToolbar = ({
selectedRowKeys,
showFolderCreateModal,
@ -59,6 +45,26 @@ const FileToolbar = ({
useFetchDocumentListOnMount();
const { handleInputChange, searchString } = useHandleSearchChange();
const breadcrumbItems = useSelectBreadcrumbItems();
const { handleBreadcrumbClick } = useHandleBreadcrumbClick();
const itemRender: BreadcrumbProps['itemRender'] = (
currentRoute,
params,
items,
) => {
const isLast = currentRoute?.path === items[items.length - 1]?.path;
return isLast ? (
<span>{currentRoute.title}</span>
) : (
<span
className={styles.breadcrumbItemButton}
onClick={() => handleBreadcrumbClick(currentRoute.path)}
>
{currentRoute.title}
</span>
);
};
const actionItems: MenuProps['items'] = useMemo(() => {
return [
@ -70,7 +76,7 @@ const FileToolbar = ({
<Button type="link">
<Space>
<FileTextOutlined />
{t('localFiles')}
{t('uploadFile', { keyPrefix: 'fileManager' })}
</Space>
</Button>
</div>
@ -83,12 +89,13 @@ const FileToolbar = ({
label: (
<div>
<Button type="link">
<FolderOpenOutlined />
New Folder
<Space>
<FolderOpenOutlined />
{t('newFolder', { keyPrefix: 'fileManager' })}
</Space>
</Button>
</div>
),
// disabled: true,
},
];
}, [t, showFolderCreateModal, showFileUploadModal]);

View File

@ -0,0 +1,8 @@
.uploader {
:global {
.ant-upload-list {
max-height: 40vh;
overflow-y: auto;
}
}
}

View File

@ -1,3 +1,4 @@
import { useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { InboxOutlined } from '@ant-design/icons';
import {
@ -12,6 +13,8 @@ import {
} from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import styles from './index.less';
const { Dragger } = Upload;
const FileUpload = ({
@ -23,6 +26,7 @@ const FileUpload = ({
fileList: UploadFile[];
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
}) => {
const { t } = useTranslate('fileManager');
const props: UploadProps = {
multiple: true,
onRemove: (file) => {
@ -43,17 +47,12 @@ const FileUpload = ({
};
return (
<Dragger {...props}>
<Dragger {...props} className={styles.uploader}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">
Click or drag file to this area to upload
</p>
<p className="ant-upload-hint">
Support for a single or bulk upload. Strictly prohibited from uploading
company data or other banned files.
</p>
<p className="ant-upload-text">{t('uploadTitle')}</p>
<p className="ant-upload-hint">{t('uploadDescription')}</p>
</Dragger>
);
};
@ -64,18 +63,25 @@ const FileUploadModal = ({
loading,
onOk: onFileUploadOk,
}: IModalProps<UploadFile[]>) => {
const { t } = useTranslate('fileManager');
const [value, setValue] = useState<string | number>('local');
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);
const onOk = () => {
return onFileUploadOk?.([...fileList, ...directoryFileList]);
const onOk = async () => {
const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]);
console.info(ret);
if (ret !== undefined && ret === 0) {
setFileList([]);
setDirectoryFileList([]);
}
return ret;
};
const items: TabsProps['items'] = [
{
key: '1',
label: 'File',
label: t('file'),
children: (
<FileUpload
directory={false}
@ -86,7 +92,7 @@ const FileUploadModal = ({
},
{
key: '2',
label: 'Directory',
label: t('directory'),
children: (
<FileUpload
directory
@ -100,7 +106,7 @@ const FileUploadModal = ({
return (
<>
<Modal
title="File upload"
title={t('uploadFile')}
open={visible}
onOk={onOk}
onCancel={hideModal}
@ -109,8 +115,8 @@ const FileUploadModal = ({
<Flex gap={'large'} vertical>
<Segmented
options={[
{ label: 'Local uploads', value: 'local' },
{ label: 'S3 uploads', value: 's3' },
{ label: t('local'), value: 'local' },
{ label: t('s3'), value: 's3' },
]}
block
value={value}
@ -119,7 +125,7 @@ const FileUploadModal = ({
{value === 'local' ? (
<Tabs defaultActiveKey="1" items={items} />
) : (
'coming soon'
t('comingSoon', { keyPrefix: 'common' })
)}
</Flex>
</Modal>

View File

@ -35,7 +35,7 @@ const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => {
return (
<Modal
title={'New Folder'}
title={t('newFolder', { keyPrefix: 'fileManager' })}
open={visible}
onOk={handleOk}
onCancel={handleCancel}

View File

@ -244,14 +244,14 @@ export const useHandleUploadFile = () => {
const id = useGetFolderId();
const onFileUploadOk = useCallback(
async (fileList: UploadFile[]) => {
console.info('fileList', fileList);
async (fileList: UploadFile[]): Promise<number | undefined> => {
if (fileList.length > 0) {
const ret = await uploadFile(fileList, id);
const ret: number = await uploadFile(fileList, id);
console.info(ret);
if (ret === 0) {
hideFileUploadModal();
}
return ret;
}
},
[uploadFile, hideFileUploadModal, id],
@ -295,6 +295,7 @@ export const useHandleConnectToKnowledge = () => {
if (ret === 0) {
hideConnectToKnowledgeModal();
}
return ret;
},
[connectToKnowledge, hideConnectToKnowledgeModal, id, record.id],
);
@ -320,3 +321,20 @@ export const useHandleConnectToKnowledge = () => {
showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal,
};
};
export const useHandleBreadcrumbClick = () => {
const navigate = useNavigate();
const setPagination = useSetPagination('fileManager');
const handleBreadcrumbClick = useCallback(
(path?: string) => {
if (path) {
setPagination();
navigate(path);
}
},
[setPagination, navigate],
);
return { handleBreadcrumbClick };
};

View File

@ -20,3 +20,10 @@
.linkButton {
padding: 0;
}
.breadcrumbItemButton {
cursor: pointer;
color: #1677ff;
padding: 0;
height: auto;
}

View File

@ -1,7 +1,7 @@
import { useSelectFileList } from '@/hooks/fileManagerHooks';
import { IFile } from '@/interfaces/database/file-manager';
import { formatDate } from '@/utils/date';
import { Button, Flex, Table } from 'antd';
import { Button, Flex, Space, Table, Tag } from 'antd';
import { ColumnsType } from 'antd/es/table';
import ActionCell from './action-cell';
import FileToolbar from './file-toolbar';
@ -18,6 +18,8 @@ import {
import RenameModal from '@/components/rename-modal';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/commonHooks';
import { formatNumberWithThousandsSeparator } from '@/utils/commonUtil';
import { getExtension } from '@/utils/documentUtils';
import ConnectToKnowledgeModal from './connect-to-knowledge-modal';
import FileUploadModal from './file-upload-modal';
@ -25,6 +27,7 @@ import FolderCreateModal from './folder-create-modal';
import styles from './index.less';
const FileManager = () => {
const { t } = useTranslate('fileManager');
const fileList = useSelectFileList();
const { rowSelection, setSelectedRowKeys } = useGetRowSelection();
const loading = useSelectFileListLoading();
@ -57,12 +60,13 @@ const FileManager = () => {
showConnectToKnowledgeModal,
onConnectToKnowledgeOk,
initialValue,
connectToKnowledgeLoading,
} = useHandleConnectToKnowledge();
const { pagination } = useGetFilesPagination();
const columns: ColumnsType<IFile> = [
{
title: 'Name',
title: t('name'),
dataIndex: 'name',
key: 'name',
render(value, record) {
@ -88,7 +92,7 @@ const FileManager = () => {
},
},
{
title: 'Upload Date',
title: t('uploadDate'),
dataIndex: 'create_date',
key: 'create_date',
render(text) {
@ -96,22 +100,35 @@ const FileManager = () => {
},
},
{
title: 'Knowledge Base',
dataIndex: 'kbs_info',
key: 'kbs_info',
title: t('size'),
dataIndex: 'size',
key: 'size',
render(value) {
return Array.isArray(value)
? value?.map((x) => x.kb_name).join(',')
: '';
return (
formatNumberWithThousandsSeparator((value / 1024).toFixed(2)) + ' KB'
);
},
},
{
title: 'Location',
dataIndex: 'location',
key: 'location',
title: t('knowledgeBase'),
dataIndex: 'kbs_info',
key: 'kbs_info',
render(value) {
return Array.isArray(value) ? (
<Space wrap>
{value?.map((x) => (
<Tag color="blue" key={x.kb_id}>
{x.kb_name}
</Tag>
))}
</Space>
) : (
''
);
},
},
{
title: 'Action',
title: t('action'),
dataIndex: 'action',
key: 'action',
render: (text, record) => (
@ -168,6 +185,7 @@ const FileManager = () => {
visible={connectToKnowledgeVisible}
hideModal={hideConnectToKnowledgeModal}
onOk={onConnectToKnowledgeOk}
loading={connectToKnowledgeLoading}
></ConnectToKnowledgeModal>
</section>
);

View File

@ -1,7 +1,9 @@
import { paginationModel } from '@/base';
import { BaseState } from '@/interfaces/common';
import { IFile, IFolder } from '@/interfaces/database/file-manager';
import i18n from '@/locales/config';
import fileManagerService from '@/services/fileManagerService';
import { message } from 'antd';
import omit from 'lodash/omit';
import { DvaModel } from 'umi';
@ -33,6 +35,7 @@ const model: DvaModel<FileManagerModelState> = {
});
const { retcode } = data;
if (retcode === 0) {
message.success(i18n.t('message.deleted'));
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
@ -69,6 +72,7 @@ const model: DvaModel<FileManagerModelState> = {
omit(payload, ['parentId']),
);
if (data.retcode === 0) {
message.success(i18n.t('message.renamed'));
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
@ -89,6 +93,8 @@ const model: DvaModel<FileManagerModelState> = {
});
const { data } = yield call(fileManagerService.uploadFile, formData);
if (data.retcode === 0) {
message.success(i18n.t('message.uploaded'));
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
@ -99,6 +105,8 @@ const model: DvaModel<FileManagerModelState> = {
*createFolder({ payload = {} }, { call, put }) {
const { data } = yield call(fileManagerService.createFolder, payload);
if (data.retcode === 0) {
message.success(i18n.t('message.created'));
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
@ -125,6 +133,7 @@ const model: DvaModel<FileManagerModelState> = {
omit(payload, 'parentId'),
);
if (data.retcode === 0) {
message.success(i18n.t('message.operated'));
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },

View File

@ -27,3 +27,9 @@ export const getSearchValue = (key: string) => {
const params = new URL(document.location as any).searchParams;
return params.get(key);
};
// Formatize numbers, add thousands of separators
export const formatNumberWithThousandsSeparator = (numberStr: string) => {
const formattedNumber = numberStr.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return formattedNumber;
};