Compare commits

...

9 Commits

Author SHA1 Message Date
ff4239c7cf Docs: Updated descriptions on metadata filtering (#10518)
### What problem does this PR solve?

### Type of change

- [x] Documentation Update
2025-10-13 17:33:04 +08:00
cf5867b146 Feat: Merge title splitter and token splitter into chunker category #9869 (#10517)
### What problem does this PR solve?

Feat: Merge title splitter and token splitter into chunker category
#9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-13 15:46:14 +08:00
77481ab3ab Fix: Optimized the login page and fixed some known issues. #9869 (#10514)
### What problem does this PR solve?

Fix: Optimized the login page and fixed some known issues. #9869

- Added the FlipCard3D component to implement a 3D flip effect on the
login/registration forms.
- Adjusted the Spotlight component to support custom positioning and
color configurations.
- Updated the route to point to the new login page /login-next.
- Added a cancel interface to the auto-generate function.
- Fixed scroll bar issues in PDF preview.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-13 15:31:36 +08:00
9c53b3336a Fix: The Context Generator(Transformer) node can only be followed by a Tokenizer(Indexer) and a Context Generator(Transformer). #9869 (#10515)
### What problem does this PR solve?

Fix: The Context Generator node can only be followed by a Tokenizer and
a Context Generator. #9869
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-13 14:37:30 +08:00
24481f0332 Fix: Update lm studio models support, refer to #8116 (#10509)
### What problem does this PR solve?

Fix: Update lm studio models support, refer to #8116

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Documentation Update
2025-10-13 13:58:08 +08:00
4e6b84bb41 Feat: add trino support (#10512)
### What problem does this PR solve?
issue:
[#10296](https://github.com/infiniflow/ragflow/issues/10296)
change:
- ExeSQL: support connecting to Trino.
- Validation: password can be empty only when db_type === "trino";
  all other database types keep the existing requirement (non-empty).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-13 13:57:40 +08:00
65c3f0406c Fix: maintain backward compatibility for KB tasks (#10508)
### What problem does this PR solve?

Maintain backward compatibility for KB tasks

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-13 11:53:48 +08:00
7fb8b30cc2 fix: decode before format to json (#10506)
### What problem does this PR solve?

Decode bytes before format to json.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-13 11:11:06 +08:00
acca3640f7 Feat: Modify the background color of the canvas #9869 (#10507)
### What problem does this PR solve?

Feat: Modify the background color of the canvas #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-13 11:10:54 +08:00
37 changed files with 650 additions and 324 deletions

View File

@ -53,12 +53,13 @@ class ExeSQLParam(ToolParamBase):
self.max_records = 1024 self.max_records = 1024
def check(self): def check(self):
self.check_valid_value(self.db_type, "Choose DB type", ['mysql', 'postgres', 'mariadb', 'mssql', 'IBM DB2']) self.check_valid_value(self.db_type, "Choose DB type", ['mysql', 'postgres', 'mariadb', 'mssql', 'IBM DB2', 'trino'])
self.check_empty(self.database, "Database name") self.check_empty(self.database, "Database name")
self.check_empty(self.username, "database username") self.check_empty(self.username, "database username")
self.check_empty(self.host, "IP Address") self.check_empty(self.host, "IP Address")
self.check_positive_integer(self.port, "IP Port") self.check_positive_integer(self.port, "IP Port")
self.check_empty(self.password, "Database password") if self.db_type != "trino":
self.check_empty(self.password, "Database password")
self.check_positive_integer(self.max_records, "Maximum number of records") self.check_positive_integer(self.max_records, "Maximum number of records")
if self.database == "rag_flow": if self.database == "rag_flow":
if self.host == "ragflow-mysql": if self.host == "ragflow-mysql":
@ -123,6 +124,45 @@ class ExeSQL(ToolBase, ABC):
r'PWD=' + self._param.password r'PWD=' + self._param.password
) )
db = pyodbc.connect(conn_str) db = pyodbc.connect(conn_str)
elif self._param.db_type == 'trino':
try:
import trino
from trino.auth import BasicAuthentication
except Exception:
raise Exception("Missing dependency 'trino'. Please install: pip install trino")
def _parse_catalog_schema(db: str):
if not db:
return None, None
if "." in db:
c, s = db.split(".", 1)
elif "/" in db:
c, s = db.split("/", 1)
else:
c, s = db, "default"
return c, s
catalog, schema = _parse_catalog_schema(self._param.database)
if not catalog:
raise Exception("For Trino, `database` must be 'catalog.schema' or at least 'catalog'.")
http_scheme = "https" if os.environ.get("TRINO_USE_TLS", "0") == "1" else "http"
auth = None
if http_scheme == "https" and self._param.password:
auth = BasicAuthentication(self._param.username, self._param.password)
try:
db = trino.dbapi.connect(
host=self._param.host,
port=int(self._param.port or 8080),
user=self._param.username or "ragflow",
catalog=catalog,
schema=schema or "default",
http_scheme=http_scheme,
auth=auth
)
except Exception as e:
raise Exception("Database Connection Failed! \n" + str(e))
elif self._param.db_type == 'IBM DB2': elif self._param.db_type == 'IBM DB2':
import ibm_db import ibm_db
conn_str = ( conn_str = (

View File

@ -409,6 +409,49 @@ def test_db_connect():
ibm_db.fetch_assoc(stmt) ibm_db.fetch_assoc(stmt)
ibm_db.close(conn) ibm_db.close(conn)
return get_json_result(data="Database Connection Successful!") return get_json_result(data="Database Connection Successful!")
elif req["db_type"] == 'trino':
def _parse_catalog_schema(db: str):
if not db:
return None, None
if "." in db:
c, s = db.split(".", 1)
elif "/" in db:
c, s = db.split("/", 1)
else:
c, s = db, "default"
return c, s
try:
import trino
import os
from trino.auth import BasicAuthentication
except Exception:
return server_error_response("Missing dependency 'trino'. Please install: pip install trino")
catalog, schema = _parse_catalog_schema(req["database"])
if not catalog:
return server_error_response("For Trino, 'database' must be 'catalog.schema' or at least 'catalog'.")
http_scheme = "https" if os.environ.get("TRINO_USE_TLS", "0") == "1" else "http"
auth = None
if http_scheme == "https" and req.get("password"):
auth = BasicAuthentication(req.get("username") or "ragflow", req["password"])
conn = trino.dbapi.connect(
host=req["host"],
port=int(req["port"] or 8080),
user=req["username"] or "ragflow",
catalog=catalog,
schema=schema or "default",
http_scheme=http_scheme,
auth=auth
)
cur = conn.cursor()
cur.execute("SELECT 1")
cur.fetchall()
cur.close()
conn.close()
return get_json_result(data="Database Connection Successful!")
else: else:
return server_error_response("Unsupported database type.") return server_error_response("Unsupported database type.")
if req["db_type"] != 'mssql': if req["db_type"] != 'mssql':

View File

@ -36,6 +36,7 @@ from api import settings
from rag.nlp import search from rag.nlp import search
from api.constants import DATASET_NAME_LIMIT from api.constants import DATASET_NAME_LIMIT
from rag.settings import PAGERANK_FLD from rag.settings import PAGERANK_FLD
from rag.utils.redis_conn import REDIS_CONN
from rag.utils.storage_factory import STORAGE_IMPL from rag.utils.storage_factory import STORAGE_IMPL
@ -760,18 +761,25 @@ def delete_kb_task():
match pipeline_task_type: match pipeline_task_type:
case PipelineTaskType.GRAPH_RAG: case PipelineTaskType.GRAPH_RAG:
settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), kb_id) settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), kb_id)
kb_task_id = "graphrag_task_id" kb_task_id_field = "graphrag_task_id"
task_id = kb.graphrag_task_id
kb_task_finish_at = "graphrag_task_finish_at" kb_task_finish_at = "graphrag_task_finish_at"
case PipelineTaskType.RAPTOR: case PipelineTaskType.RAPTOR:
kb_task_id = "raptor_task_id" kb_task_id_field = "raptor_task_id"
task_id = kb.raptor_task_id
kb_task_finish_at = "raptor_task_finish_at" kb_task_finish_at = "raptor_task_finish_at"
case PipelineTaskType.MINDMAP: case PipelineTaskType.MINDMAP:
kb_task_id = "mindmap_task_id" kb_task_id_field = "mindmap_task_id"
task_id = kb.mindmap_task_id
kb_task_finish_at = "mindmap_task_finish_at" kb_task_finish_at = "mindmap_task_finish_at"
case _: case _:
return get_error_data_result(message="Internal Error: Invalid task type") return get_error_data_result(message="Internal Error: Invalid task type")
ok = KnowledgebaseService.update_by_id(kb_id, {kb_task_id: "", kb_task_finish_at: None}) def cancel_task(task_id):
REDIS_CONN.set(f"{task_id}-cancel", "x")
cancel_task(task_id)
ok = KnowledgebaseService.update_by_id(kb_id, {kb_task_id_field: "", kb_task_finish_at: None})
if not ok: if not ok:
return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}") return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}")

View File

@ -21,6 +21,10 @@ Ensure that your metadata is in JSON format; otherwise, your updates will not be
![Input metadata](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/input_metadata.jpg) ![Input metadata](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/input_metadata.jpg)
## Related APIs
[Retrieve chunks](../../references/http_api_reference.md#retrieve-chunks)
## Frequently asked questions ## Frequently asked questions
### Can I set metadata for multiple documents at once? ### Can I set metadata for multiple documents at once?

View File

@ -1823,7 +1823,21 @@ curl --request POST \
{ {
"question": "What is advantage of ragflow?", "question": "What is advantage of ragflow?",
"dataset_ids": ["b2a62730759d11ef987d0242ac120004"], "dataset_ids": ["b2a62730759d11ef987d0242ac120004"],
"document_ids": ["77df9ef4759a11ef8bdd0242ac120004"] "document_ids": ["77df9ef4759a11ef8bdd0242ac120004"],
"metadata_condition": {
"conditions": [
{
"name": "author",
"comparison_operator": "=",
"value": "Toby"
},
{
"name": "url",
"comparison_operator": "not contains",
"value": "amd"
}
]
}
}' }'
``` ```
@ -1858,7 +1872,25 @@ curl --request POST \
- `"cross_languages"`: (*Body parameter*) `list[string]` - `"cross_languages"`: (*Body parameter*) `list[string]`
The languages that should be translated into, in order to achieve keywords retrievals in different languages. The languages that should be translated into, in order to achieve keywords retrievals in different languages.
- `"metadata_condition"`: (*Body parameter*), `object` - `"metadata_condition"`: (*Body parameter*), `object`
The metadata condition for filtering chunks. The metadata condition used for filtering chunks:
- `"conditions"`: (*Body parameter*), `array`
A list of metadata filter conditions.
- `"name"`: `string` - The metadata field name to filter by, e.g., `"author"`, `"company"`, `"url"`. Ensure this parameter before use. See [Set metadata](../guides/dataset/set_metadata.md) for details.
- `comparison_operator`: `string` - The comparison operator. Can be one of:
- `"contains"`
- `"not contains"`
- `"start with"`
- `"empty"`
- `"not empty"`
- `"="`
- `"≠"`
- `">"`
- `"<"`
- `"≥"`
- `"≤"`
- `"value"`: `string` - The value to compare.
#### Response #### Response
Success: Success:

View File

@ -33,7 +33,7 @@ A complete list of models supported by RAGFlow, which will continue to expand.
| Jina | | :heavy_check_mark: | :heavy_check_mark: | | | | | Jina | | :heavy_check_mark: | :heavy_check_mark: | | | |
| LeptonAI | :heavy_check_mark: | | | | | | | LeptonAI | :heavy_check_mark: | | | | | |
| LocalAI | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | | | | LocalAI | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | | |
| LM-Studio | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | LM-Studio | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | | |
| MiniMax | :heavy_check_mark: | | | | | | | MiniMax | :heavy_check_mark: | | | | | |
| Mistral | :heavy_check_mark: | :heavy_check_mark: | | | | | | Mistral | :heavy_check_mark: | :heavy_check_mark: | | | | |
| ModelScope | :heavy_check_mark: | | | | | | | ModelScope | :heavy_check_mark: | | | | | |

View File

@ -411,7 +411,7 @@ class Parser(ProcessBase):
dispositions = content_disposition.strip().split(";") dispositions = content_disposition.strip().split(";")
if dispositions[0].lower() == "attachment": if dispositions[0].lower() == "attachment":
filename = part.get_filename() filename = part.get_filename()
payload = part.get_payload(decode=True) payload = part.get_payload(decode=True).decode(part.get_content_charset())
attachments.append({ attachments.append({
"filename": filename, "filename": filename,
"payload": payload, "payload": payload,
@ -448,7 +448,7 @@ class Parser(ProcessBase):
for t in msg.attachments: for t in msg.attachments:
attachments.append({ attachments.append({
"filename": t.name, "filename": t.name,
"payload": t.data # binary "payload": t.data.decode("utf-8")
}) })
email_content["attachments"] = attachments email_content["attachments"] = attachments

View File

@ -691,7 +691,7 @@ async def run_raptor_for_kb(row, kb_parser_config, chat_mdl, embd_mdl, vector_si
raptor_config["threshold"], raptor_config["threshold"],
) )
original_length = len(chunks) original_length = len(chunks)
chunks = await raptor(chunks, row["kb_parser_config"]["raptor"]["random_seed"], callback) chunks = await raptor(chunks, kb_parser_config["raptor"]["random_seed"], callback)
doc = { doc = {
"doc_id": fake_doc_id, "doc_id": fake_doc_id,
"kb_id": [str(row["kb_id"])], "kb_id": [str(row["kb_id"])],
@ -814,8 +814,22 @@ async def do_handle_task(task):
kb_parser_config = kb.parser_config kb_parser_config = kb.parser_config
if not kb_parser_config.get("raptor", {}).get("use_raptor", False): if not kb_parser_config.get("raptor", {}).get("use_raptor", False):
progress_callback(prog=-1.0, msg="Internal error: Invalid RAPTOR configuration") kb_parser_config.update(
return {
"raptor": {
"use_raptor": True,
"prompt": "Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following:\n {cluster_content}\nThe above is the content you need to summarize.",
"max_token": 256,
"threshold": 0.1,
"max_cluster": 64,
"random_seed": 0,
},
}
)
if not KnowledgebaseService.update_by_id(kb.id, {"parser_config":kb_parser_config}):
progress_callback(prog=-1.0, msg="Internal error: Invalid RAPTOR configuration")
return
# bind LLM for raptor # bind LLM for raptor
chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language) chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language)
# run RAPTOR # run RAPTOR
@ -838,8 +852,25 @@ async def do_handle_task(task):
kb_parser_config = kb.parser_config kb_parser_config = kb.parser_config
if not kb_parser_config.get("graphrag", {}).get("use_graphrag", False): if not kb_parser_config.get("graphrag", {}).get("use_graphrag", False):
progress_callback(prog=-1.0, msg="Internal error: Invalid GraphRAG configuration") kb_parser_config.update(
return {
"graphrag": {
"use_graphrag": True,
"entity_types": [
"organization",
"person",
"geo",
"event",
"category",
],
"method": "light",
}
}
)
if not KnowledgebaseService.update_by_id(kb.id, {"parser_config":kb_parser_config}):
progress_callback(prog=-1.0, msg="Internal error: Invalid GraphRAG configuration")
return
graphrag_conf = kb_parser_config.get("graphrag", {}) graphrag_conf = kb_parser_config.get("graphrag", {})
start_ts = timer() start_ts = timer()

View File

@ -1,13 +1,11 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import { Background } from '@xyflow/react'; import { Background } from '@xyflow/react';
export function AgentBackground() { export function AgentBackground() {
const isDarkTheme = useIsDarkTheme();
return ( return (
<Background <Background
color={isDarkTheme ? 'rgba(255,255,255,0.15)' : '#A8A9B3'} color="var(--text-primary)"
bgColor={isDarkTheme ? 'rgba(11, 11, 12, 1)' : 'rgba(0, 0, 0, 0.05)'} bgColor="rgb(var(--bg-canvas))"
className="rounded-lg"
/> />
); );
} }

View File

@ -98,7 +98,7 @@ export function FileUploadDialog({
return ( return (
<Dialog open onOpenChange={hideModal}> <Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle> <DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@ -1,10 +1,14 @@
import { useIsDarkTheme } from '@/components/theme-provider'; import { useIsDarkTheme } from '@/components/theme-provider';
import { parseColorToRGB } from '@/utils/common-util';
import React from 'react'; import React from 'react';
interface SpotlightProps { interface SpotlightProps {
className?: string; className?: string;
opcity?: number; opcity?: number;
coverage?: number; coverage?: number;
X?: string;
Y?: string;
color?: string;
} }
/** /**
* *
@ -16,9 +20,20 @@ const Spotlight: React.FC<SpotlightProps> = ({
className, className,
opcity = 0.5, opcity = 0.5,
coverage = 60, coverage = 60,
X = '50%',
Y = '190%',
color,
}) => { }) => {
const isDark = useIsDarkTheme(); const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243'; let realColor: [number, number, number] | undefined = undefined;
if (color) {
realColor = parseColorToRGB(color);
}
const rgb = realColor
? realColor.join(',')
: isDark
? '255, 255, 255'
: '194, 221, 243';
return ( return (
<div <div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`} className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
@ -30,7 +45,7 @@ const Spotlight: React.FC<SpotlightProps> = ({
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
background: `radial-gradient(circle at 50% 190%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`, background: `radial-gradient(circle at ${X} ${Y}, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none', pointerEvents: 'none',
}} }}
></div> ></div>

View File

@ -57,6 +57,8 @@ export default {
}, },
}, },
login: { login: {
loginTitle: 'Sign in to Your Account',
signUpTitle: 'Create an Account',
login: 'Sign in', login: 'Sign in',
signUp: 'Sign up', signUp: 'Sign up',
loginDescription: 'Were so excited to see you again!', loginDescription: 'Were so excited to see you again!',
@ -72,7 +74,8 @@ export default {
nicknamePlaceholder: 'Please input nickname', nicknamePlaceholder: 'Please input nickname',
register: 'Create an account', register: 'Create an account',
continue: 'Continue', continue: 'Continue',
title: 'Start building your smart assistants.', title: 'A leading RAG engine for LLM context',
start: "Let's get started",
description: description:
'Sign up for free to explore top RAG technology. Create knowledge bases and AIs to empower your business.', 'Sign up for free to explore top RAG technology. Create knowledge bases and AIs to empower your business.',
review: 'from 500+ reviews', review: 'from 500+ reviews',
@ -114,7 +117,7 @@ export default {
generateRaptor: generateRaptor:
'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.', 'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.',
generate: 'Generate', generate: 'Generate',
raptor: 'Raptor', raptor: 'RAPTOR',
processingType: 'Processing Type', processingType: 'Processing Type',
dataPipeline: 'Ingestion pipeline', dataPipeline: 'Ingestion pipeline',
operations: 'Operations', operations: 'Operations',
@ -128,7 +131,7 @@ export default {
fileName: 'File Name', fileName: 'File Name',
datasetLogs: 'Dataset', datasetLogs: 'Dataset',
fileLogs: 'File', fileLogs: 'File',
overview: 'Overview', overview: 'Logs',
success: 'Success', success: 'Success',
failed: 'Failed', failed: 'Failed',
completed: 'Completed', completed: 'Completed',
@ -270,7 +273,7 @@ export default {
reRankModelWaring: 'Re-rank model is very time consuming.', reRankModelWaring: 'Re-rank model is very time consuming.',
}, },
knowledgeConfiguration: { knowledgeConfiguration: {
tocExtraction: 'toc toggle', tocExtraction: 'TOC Enhance',
tocExtractionTip: tocExtractionTip:
" For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.", " For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.",
deleteGenerateModalContent: ` deleteGenerateModalContent: `
@ -1703,17 +1706,17 @@ This delimiter is used to split the input text into several text pieces echo of
parser: 'Parser', parser: 'Parser',
parserDescription: parserDescription:
'Extracts raw text and structure from files for downstream processing.', 'Extracts raw text and structure from files for downstream processing.',
tokenizer: 'Tokenizer', tokenizer: 'Indexer',
tokenizerRequired: 'Please add the Tokenizer node first', tokenizerRequired: 'Please add the Indexer node first',
tokenizerDescription: tokenizerDescription:
'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.', 'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.',
splitter: 'Token Splitter', splitter: 'Token',
splitterDescription: splitterDescription:
'Split text into chunks by token length with optional delimiters and overlap.', 'Split text into chunks by token length with optional delimiters and overlap.',
hierarchicalMergerDescription: hierarchicalMergerDescription:
'Split documents into sections by title hierarchy with regex rules for finer control.', 'Split documents into sections by title hierarchy with regex rules for finer control.',
hierarchicalMerger: 'Title Splitter', hierarchicalMerger: 'Title',
extractor: 'Context Generator', extractor: 'Transformer',
extractorDescription: extractorDescription:
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.', 'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
outputFormat: 'Output format', outputFormat: 'Output format',
@ -1817,9 +1820,13 @@ Important structured information may include: names, dates, locations, events, k
}, },
datasetOverview: { datasetOverview: {
downloadTip: 'Files being downloaded from data sources. ', downloadTip: 'Files being downloaded from data sources. ',
processingTip: 'Files being processed by data flows.', processingTip: 'Files being processed by data pipelines.',
totalFiles: 'Total Files', totalFiles: 'Total Files',
downloading: 'Downloading', downloading: 'Downloading',
downloadSuccessTip: 'Total successful downloads',
downloadFailedTip: 'Total failed downloads',
processingSuccessTip: 'Total successfully processed files',
processingFailedTip: 'Total failed processes',
processing: 'Processing', processing: 'Processing',
}, },
}, },

View File

@ -49,6 +49,8 @@ export default {
promptPlaceholder: '请输入或使用 / 快速插入变量。', promptPlaceholder: '请输入或使用 / 快速插入变量。',
}, },
login: { login: {
loginTitle: '登录账户',
signUpTitle: '创建账户',
login: '登录', login: '登录',
signUp: '注册', signUp: '注册',
loginDescription: '很高兴再次见到您!', loginDescription: '很高兴再次见到您!',
@ -64,7 +66,8 @@ export default {
nicknamePlaceholder: '请输入名称', nicknamePlaceholder: '请输入名称',
register: '创建账户', register: '创建账户',
continue: '继续', continue: '继续',
title: '开始构建您的智能助手', title: 'A leading RAG engine for LLM context',
start: '立即开始',
description: description:
'免费注册以探索顶级 RAG 技术。 创建知识库和人工智能来增强您的业务', '免费注册以探索顶级 RAG 技术。 创建知识库和人工智能来增强您的业务',
review: '来自 500 多条评论', review: '来自 500 多条评论',
@ -116,7 +119,7 @@ export default {
fileName: '文件名', fileName: '文件名',
datasetLogs: '数据集', datasetLogs: '数据集',
fileLogs: '文件', fileLogs: '文件',
overview: '概览', overview: '日志',
success: '成功', success: '成功',
failed: '失败', failed: '失败',
completed: '已完成', completed: '已完成',
@ -255,7 +258,7 @@ export default {
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除', theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
}, },
knowledgeConfiguration: { knowledgeConfiguration: {
tocExtraction: '目录提取', tocExtraction: '目录增强',
tocExtractionTip: tocExtractionTip:
'对于已有的chunk生成层级结构的目录信息每个文件一个目录。在查询时激活`目录增强`后系统会用大模型去判断用户问题和哪些目录项相关从而找到相关的chunk。', '对于已有的chunk生成层级结构的目录信息每个文件一个目录。在查询时激活`目录增强`后系统会用大模型去判断用户问题和哪些目录项相关从而找到相关的chunk。',
deleteGenerateModalContent: ` deleteGenerateModalContent: `
@ -1713,6 +1716,10 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
totalFiles: '文件总数', totalFiles: '文件总数',
downloading: '正在下载', downloading: '正在下载',
processing: '正在处理', processing: '正在处理',
downloadSuccessTip: '下载成功总数',
downloadFailedTip: '下载失败总数',
processingSuccessTip: '处理成功的文件总数',
processingFailedTip: '处理失败的文件总数',
}, },
}, },
}; };

View File

@ -232,7 +232,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
]); ]);
return ( return (
<div className={styles.canvasWrapper}> <div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
style={{ position: 'absolute', top: 10, left: 0 }} style={{ position: 'absolute', top: 10, left: 0 }}

View File

@ -47,13 +47,13 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
return ( return (
<NodeWrapper <NodeWrapper
className="p-0 w-full h-full flex flex-col" className="p-0 w-full h-full flex flex-col bg-bg-component"
selected={selected} selected={selected}
> >
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}> <NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
<ResizeIcon /> <ResizeIcon />
</NodeResizeControl> </NodeResizeControl>
<section className="p-2 flex gap-2 bg-background-note items-center note-drag-handle rounded-t"> <section className="p-2 flex gap-2 items-center note-drag-handle rounded-t">
<NotebookPen className="size-4" /> <NotebookPen className="size-4" />
<Form {...nameForm}> <Form {...nameForm}>
<form className="flex-1"> <form className="flex-1">

View File

@ -8,14 +8,27 @@ export const ExeSQLFormSchema = {
username: z.string().min(1), username: z.string().min(1),
host: z.string().min(1), host: z.string().min(1),
port: z.number(), port: z.number(),
password: z.string().min(1), password: z.string().optional().or(z.literal('')),
max_records: z.number(), max_records: z.number(),
}; };
export const FormSchema = z.object({ export const FormSchema = z
sql: z.string().optional(), .object({
...ExeSQLFormSchema, sql: z.string().optional(),
}); ...ExeSQLFormSchema,
})
.superRefine((v, ctx) => {
if (
v.db_type !== 'trino' &&
!(v.password && v.password.trim().length > 0)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['password'],
message: 'String must contain at least 1 character(s)',
});
}
});
export function useSubmitForm() { export function useSubmitForm() {
const { testDbConnect, loading } = useTestDbConnect(); const { testDbConnect, loading } = useTestDbConnect();

View File

@ -2139,6 +2139,7 @@ export const ExeSQLOptions = [
'mariadb', 'mariadb',
'mssql', 'mssql',
'IBM DB2', 'IBM DB2',
'trino',
].map((x) => ({ ].map((x) => ({
label: upperFirst(x), label: upperFirst(x),
value: x, value: x,

View File

@ -1,7 +1,7 @@
.documentContainer { .documentContainer {
width: 100%; width: 100%;
// height: calc(100vh - 284px); // height: calc(100vh - 284px);
height: calc(100vh - 170px); height: calc(100vh - 180px);
position: relative; position: relative;
:global(.PdfHighlighter) { :global(.PdfHighlighter) {
overflow-x: hidden; overflow-x: hidden;

View File

@ -205,7 +205,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
}; };
return ( return (
<div className={styles.canvasWrapper}> <div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
style={{ position: 'absolute', top: 10, left: 0 }} style={{ position: 'absolute', top: 10, left: 0 }}
@ -292,6 +292,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
clearActiveDropdown(); clearActiveDropdown();
}} }}
position={dropdownPosition} position={dropdownPosition}
nodeId={connectionStartRef.current?.nodeId || ''}
> >
<span></span> <span></span>
</NextStepDropdown> </NextStepDropdown>

View File

@ -1,3 +1,9 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -19,12 +25,13 @@ import {
PropsWithChildren, PropsWithChildren,
createContext, createContext,
memo, memo,
useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { Operator, SingleOperators } from '../../../constant'; import { Operator } from '../../../constant';
import { AgentInstanceContext, HandleContext } from '../../../context'; import { AgentInstanceContext, HandleContext } from '../../../context';
import OperatorIcon from '../../../operator-icon'; import OperatorIcon from '../../../operator-icon';
@ -116,51 +123,103 @@ function OperatorItemList({
// Limit the number of operators of a certain type on the canvas to only one // Limit the number of operators of a certain type on the canvas to only one
function useRestrictSingleOperatorOnCanvas() { function useRestrictSingleOperatorOnCanvas() {
const list: Operator[] = [];
const { findNodeByName } = useGraphStore((state) => state); const { findNodeByName } = useGraphStore((state) => state);
SingleOperators.forEach((operator) => { const restrictSingleOperatorOnCanvas = useCallback(
if (!findNodeByName(operator)) { (singleOperators: Operator[]) => {
list.push(operator); const list: Operator[] = [];
} singleOperators.forEach((operator) => {
}); if (!findNodeByName(operator)) {
list.push(operator);
}
});
return list;
},
[findNodeByName],
);
return list; return restrictSingleOperatorOnCanvas;
} }
function AccordionOperators({ function AccordionOperators({
isCustomDropdown = false, isCustomDropdown = false,
mousePosition, mousePosition,
nodeId,
}: { }: {
isCustomDropdown?: boolean; isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number }; mousePosition?: { x: number; y: number };
nodeId?: string;
}) { }) {
const singleOperators = useRestrictSingleOperatorOnCanvas(); const restrictSingleOperatorOnCanvas = useRestrictSingleOperatorOnCanvas();
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const operators = useMemo(() => { const operators = useMemo(() => {
const list = [...singleOperators]; let list = [
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
];
list.push(Operator.Extractor); list.push(Operator.Extractor);
return list; return list;
}, [singleOperators]); }, [restrictSingleOperatorOnCanvas]);
const chunkerOperators = useMemo(() => {
return [
...restrictSingleOperatorOnCanvas([
Operator.Splitter,
Operator.HierarchicalMerger,
]),
];
}, [restrictSingleOperatorOnCanvas]);
const showChunker = useMemo(() => {
return (
getOperatorTypeFromId(nodeId) !== Operator.Extractor &&
chunkerOperators.length > 0
);
}, [chunkerOperators.length, getOperatorTypeFromId, nodeId]);
return ( return (
<OperatorItemList <>
operators={operators} <OperatorItemList
isCustomDropdown={isCustomDropdown} operators={operators}
mousePosition={mousePosition} isCustomDropdown={isCustomDropdown}
></OperatorItemList> mousePosition={mousePosition}
></OperatorItemList>
{showChunker && (
<Accordion
type="single"
collapsible
className="w-full px-4"
defaultValue="item-1"
>
<AccordionItem value="item-1">
<AccordionTrigger>Chunker</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={chunkerOperators}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</>
); );
} }
type NextStepDropdownProps = PropsWithChildren &
IModalProps<any> & {
position?: { x: number; y: number };
onNodeCreated?: (newNodeId: string) => void;
nodeId?: string;
};
export function InnerNextStepDropdown({ export function InnerNextStepDropdown({
children, children,
hideModal, hideModal,
position, position,
onNodeCreated, onNodeCreated,
}: PropsWithChildren & nodeId,
IModalProps<any> & { }: NextStepDropdownProps) {
position?: { x: number; y: number };
onNodeCreated?: (newNodeId: string) => void;
}) {
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -200,6 +259,7 @@ export function InnerNextStepDropdown({
<AccordionOperators <AccordionOperators
isCustomDropdown={true} isCustomDropdown={true}
mousePosition={position} mousePosition={position}
nodeId={nodeId}
></AccordionOperators> ></AccordionOperators>
</OnNodeCreatedContext.Provider> </OnNodeCreatedContext.Provider>
</HideModalContext.Provider> </HideModalContext.Provider>
@ -224,7 +284,7 @@ export function InnerNextStepDropdown({
> >
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel> <DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}> <HideModalContext.Provider value={hideModal}>
<AccordionOperators></AccordionOperators> <AccordionOperators nodeId={nodeId}></AccordionOperators>
</HideModalContext.Provider> </HideModalContext.Provider>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@ -61,6 +61,7 @@ export function CommonHandle({
hideModal(); hideModal();
clearActiveDropdown(); clearActiveDropdown();
}} }}
nodeId={nodeId}
> >
<span></span> <span></span>
</NextStepDropdown> </NextStepDropdown>

View File

@ -299,7 +299,9 @@ export const initialHierarchicalMergerValues = {
export const initialExtractorValues = { export const initialExtractorValues = {
...initialLlmBaseValues, ...initialLlmBaseValues,
field_name: ContextGeneratorFieldName.Summary, field_name: ContextGeneratorFieldName.Summary,
outputs: {}, outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
}; };
export const CategorizeAnchorPointPositions = [ export const CategorizeAnchorPointPositions = [

View File

@ -19,7 +19,9 @@ import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
import { useFormValues } from '../../hooks/use-form-values'; import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper'; import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { useSwitchPrompt } from './use-switch-prompt'; import { useSwitchPrompt } from './use-switch-prompt';
export const FormSchema = z.object({ export const FormSchema = z.object({
@ -31,6 +33,8 @@ export const FormSchema = z.object({
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>; export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialExtractorValues.outputs);
const ExtractorForm = ({ node }: INextOperatorForm) => { const ExtractorForm = ({ node }: INextOperatorForm) => {
const defaultValues = useFormValues(initialExtractorValues, node); const defaultValues = useFormValues(initialExtractorValues, node);
const { t } = useTranslation(); const { t } = useTranslation();
@ -85,6 +89,7 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
baseOptions={promptOptions} baseOptions={promptOptions}
></PromptEditor> ></PromptEditor>
</RAGFlowFormItem> </RAGFlowFormItem>
<Output list={outputList}></Output>
</FormWrapper> </FormWrapper>
{visible && ( {visible && (
<ConfirmDeleteDialog <ConfirmDeleteDialog

View File

@ -1,7 +1,7 @@
.documentContainer { .documentContainer {
width: 100%; width: 100%;
// height: calc(100vh - 284px); // height: calc(100vh - 284px);
height: calc(100vh - 170px); height: calc(100vh - 180px);
position: relative; position: relative;
:global(.PdfHighlighter) { :global(.PdfHighlighter) {
overflow-x: hidden; overflow-x: hidden;

View File

@ -4,7 +4,6 @@ import CSVFileViewer from './csv-preview';
import { DocPreviewer } from './doc-preview'; import { DocPreviewer } from './doc-preview';
import { ExcelCsvPreviewer } from './excel-preview'; import { ExcelCsvPreviewer } from './excel-preview';
import { ImagePreviewer } from './image-preview'; import { ImagePreviewer } from './image-preview';
import styles from './index.less';
import PdfPreviewer, { IProps } from './pdf-preview'; import PdfPreviewer, { IProps } from './pdf-preview';
import { PptPreviewer } from './ppt-preview'; import { PptPreviewer } from './ppt-preview';
import { TxtPreviewer } from './txt-preview'; import { TxtPreviewer } from './txt-preview';
@ -24,7 +23,7 @@ const Preview = ({
return ( return (
<> <>
{fileType === 'pdf' && highlights && setWidthAndHeight && ( {fileType === 'pdf' && highlights && setWidthAndHeight && (
<section className={styles.documentPreview}> <section>
<PdfPreviewer <PdfPreviewer
highlights={highlights} highlights={highlights}
setWidthAndHeight={setWidthAndHeight} setWidthAndHeight={setWidthAndHeight}

View File

@ -24,6 +24,8 @@ interface StatCardProps {
interface CardFooterProcessProps { interface CardFooterProcessProps {
success: number; success: number;
failed: number; failed: number;
successTip?: string;
failedTip?: string;
} }
const StatCard: FC<StatCardProps> = ({ const StatCard: FC<StatCardProps> = ({
@ -56,7 +58,9 @@ const StatCard: FC<StatCardProps> = ({
const CardFooterProcess: FC<CardFooterProcessProps> = ({ const CardFooterProcess: FC<CardFooterProcessProps> = ({
success = 0, success = 0,
successTip,
failed = 0, failed = 0,
failedTip,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -65,8 +69,13 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
<div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5"> <div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5">
<div className="flex items-center rounded-lg gap-1"> <div className="flex items-center rounded-lg gap-1">
<div className="w-2 h-2 rounded-full bg-state-success "></div> <div className="w-2 h-2 rounded-full bg-state-success "></div>
<div className="font-normal text-text-secondary text-xs"> <div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.success')} {t('knowledgeDetails.success')}
{successTip && (
<AntToolTip title={successTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div> </div>
</div> </div>
<div>{success || 0}</div> <div>{success || 0}</div>
@ -74,8 +83,13 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
<div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2"> <div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2">
<div className="flex items-center rounded-lg gap-1"> <div className="flex items-center rounded-lg gap-1">
<div className="w-2 h-2 rounded-full bg-state-error"></div> <div className="w-2 h-2 rounded-full bg-state-error"></div>
<div className="font-normal text-text-secondary text-xs"> <div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.failed')} {t('knowledgeDetails.failed')}
{failedTip && (
<AntToolTip title={failedTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div> </div>
</div> </div>
<div>{failed || 0}</div> <div>{failed || 0}</div>
@ -259,7 +273,9 @@ const FileLogsPage: FC = () => {
> >
<CardFooterProcess <CardFooterProcess
success={topAllData.downloads.success} success={topAllData.downloads.success}
successTip={t('datasetOverview.downloadSuccessTip')}
failed={topAllData.downloads.failed} failed={topAllData.downloads.failed}
failedTip={t('datasetOverview.downloadFailedTip')}
/> />
</StatCard> </StatCard>
<StatCard <StatCard
@ -276,7 +292,9 @@ const FileLogsPage: FC = () => {
> >
<CardFooterProcess <CardFooterProcess
success={topAllData.processing.success} success={topAllData.processing.success}
successTip={t('datasetOverview.processingSuccessTip')}
failed={topAllData.processing.failed} failed={topAllData.processing.failed}
failedTip={t('datasetOverview.processingFailedTip')}
/> />
</StatCard> </StatCard>
</div> </div>

View File

@ -144,7 +144,12 @@ export function ParseTypeItem({ line = 2 }: { line?: number }) {
> >
<FormControl> <FormControl>
<Radio.Group {...field}> <Radio.Group {...field}>
<div className="w-1/2 flex gap-2 justify-between text-muted-foreground"> <div
className={cn(
'flex gap-2 justify-between text-muted-foreground',
line === 1 ? 'w-1/2' : 'w-3/4',
)}
>
<Radio value={1}>{t('builtIn')}</Radio> <Radio value={1}>{t('builtIn')}</Radio>
<Radio value={2}>{t('manualSetup')}</Radio> <Radio value={2}>{t('manualSetup')}</Radio>
</div> </div>

View File

@ -6,7 +6,7 @@ import { t } from 'i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { ProcessingType } from '../../dataset-overview/dataset-common'; import { ProcessingType } from '../../dataset-overview/dataset-common';
import { GenerateType } from './generate'; import { GenerateType, GenerateTypeMap } from './generate';
export const generateStatus = { export const generateStatus = {
running: 'running', running: 'running',
completed: 'completed', completed: 'completed',
@ -103,9 +103,28 @@ export const useTraceGenerate = ({ open }: { open: boolean }) => {
raptorRunloading, raptorRunloading,
}; };
}; };
export const useUnBindTask = () => {
const { id } = useParams();
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({ type }: { type: ProcessingType }) => {
const { data } = await deletePipelineTask({ kb_id: id as string, type });
if (data.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return data;
},
});
return { handleUnbindTask };
};
export const useDatasetGenerate = () => { export const useDatasetGenerate = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { id } = useParams(); const { id } = useParams();
const { handleUnbindTask } = useUnBindTask();
const { const {
data, data,
isPending: loading, isPending: loading,
@ -143,8 +162,12 @@ export const useDatasetGenerate = () => {
type: GenerateType; type: GenerateType;
}) => { }) => {
const { data } = await agentService.cancelDataflow(task_id); const { data } = await agentService.cancelDataflow(task_id);
if (data.code === 0) {
message.success(t('message.operated')); const unbindData = await handleUnbindTask({
type: GenerateTypeMap[type as GenerateType],
});
if (data.code === 0 && unbindData.code === 0) {
// message.success(t('message.operated'));
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [type], queryKey: [type],
}); });
@ -154,21 +177,3 @@ export const useDatasetGenerate = () => {
}); });
return { runGenerate: mutateAsync, pauseGenerate, data, loading }; return { runGenerate: mutateAsync, pauseGenerate, data, loading };
}; };
export const useUnBindTask = () => {
const { id } = useParams();
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({ type }: { type: ProcessingType }) => {
const { data } = await deletePipelineTask({ kb_id: id as string, type });
if (data.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return data;
},
});
return { handleUnbindTask };
};

View File

@ -10,7 +10,7 @@ import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date'; import { formatPureDate } from '@/utils/date';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Banknote, DatabaseZap, FileSearch2, FolderOpen } from 'lucide-react'; import { Banknote, FileSearch2, FolderOpen, Logs } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks'; import { useHandleMenuClick } from './hooks';
@ -40,7 +40,7 @@ export function SideBar({ refreshCount }: PropType) {
key: Routes.DatasetTesting, key: Routes.DatasetTesting,
}, },
{ {
icon: <DatabaseZap className="size-4" />, icon: <Logs className="size-4" />,
label: t(`knowledgeDetails.overview`), label: t(`knowledgeDetails.overview`),
key: Routes.DataSetOverview, key: Routes.DataSetOverview,
}, },

View File

@ -17,7 +17,6 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { FormLayout } from '@/constants/form'; import { FormLayout } from '@/constants/form';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -103,7 +102,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
form.setValue('pipeline_id', ''); form.setValue('pipeline_id', '');
} }
}, [parseType, form]); }, [parseType, form]);
const { navigateToAgents } = useNavigatePage();
return ( return (
<Form {...form}> <Form {...form}>
@ -157,7 +155,7 @@ export function DatasetCreatingDialog({
return ( return (
<Dialog open onOpenChange={hideModal}> <Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px] focus-visible:!outline-none"> <DialogContent className="sm:max-w-[425px] focus-visible:!outline-none flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle> <DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import './index.less';
type IProps = {
children: React.ReactNode;
isLoginPage: boolean;
};
const FlipCard3D = (props: IProps) => {
const { children, isLoginPage } = props;
const [isFlipped, setIsFlipped] = useState(false);
useEffect(() => {
console.log('title', isLoginPage);
if (isLoginPage) {
setIsFlipped(false);
} else {
setIsFlipped(true);
}
}, [isLoginPage]);
return (
<div className="relative w-full h-full perspective-1000">
<div
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">
{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">
{children}
</div>
</div>
</div>
);
};
export default FlipCard3D;

View File

@ -40,3 +40,17 @@
} }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
.perspective-1000 {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
transition-duration: 0.4s;
}
.backface-hidden {
backface-visibility: hidden;
}
.rotate-y-180 {
transform: rotateY(180deg);
}

View File

@ -25,11 +25,12 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Eye, EyeOff } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { BgSvg } from './bg'; import { BgSvg } from './bg';
import FlipCard3D from './card';
import './index.less'; import './index.less';
import { SpotlightTopLeft, SpotlightTopRight } from './spotlight-top';
const Login = () => { const Login = () => {
const [title, setTitle] = useState('login'); const [title, setTitle] = useState('login');
@ -40,6 +41,8 @@ const Login = () => {
const { login: loginWithChannel, loading: loginWithChannelLoading } = const { login: loginWithChannel, loading: loginWithChannelLoading } =
useLoginWithChannel(); useLoginWithChannel();
const { t } = useTranslation('translation', { keyPrefix: 'login' }); const { t } = useTranslation('translation', { keyPrefix: 'login' });
const [isLoginPage, setIsLoginPage] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const loading = const loading =
signLoading || signLoading ||
registerLoading || registerLoading ||
@ -60,10 +63,15 @@ const Login = () => {
}; };
const changeTitle = () => { const changeTitle = () => {
setIsLoginPage(title !== 'login');
if (title === 'login' && !registerEnabled) { if (title === 'login' && !registerEnabled) {
return; return;
} }
setTitle((title) => (title === 'login' ? 'register' : 'login'));
setTimeout(() => {
setTitle(title === 'login' ? 'register' : 'login');
}, 200);
// setTitle((title) => (title === 'login' ? 'register' : 'login'));
}; };
const FormSchema = z const FormSchema = z
@ -129,177 +137,214 @@ const Login = () => {
return ( return (
<div className="min-h-screen relative overflow-hidden"> <div className="min-h-screen relative overflow-hidden">
<BgSvg /> <BgSvg />
<Spotlight opcity={0.6} coverage={60} /> <Spotlight opcity={0.4} coverage={60} color={'rgb(128, 255, 248)'} />
<SpotlightTopLeft opcity={0.2} coverage={20} /> <Spotlight
<SpotlightTopRight opcity={0.2} coverage={20} /> opcity={0.3}
coverage={12}
X={'10%'}
Y={'-10%'}
color={'rgb(128, 255, 248)'}
/>
<Spotlight
opcity={0.3}
coverage={12}
X={'90%'}
Y={'-10%'}
color={'rgb(128, 255, 248)'}
/>
{/* <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="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="flex items-center mb-4 w-full pl-10 pt-10 ">
<div className="w-10 h-10 rounded-lg border flex items-center justify-center mr-3"> <div className="w-12 h-12 p-2 rounded-lg border-2 border-border flex items-center justify-center mr-3">
<img <img
src={'/logo.svg'} src={'/logo.svg'}
alt="logo" alt="logo"
className="size-10 mr-[12] cursor-pointer" className="size-8 mr-[12] cursor-pointer"
/> />
</div> </div>
<span className="text-xl font-bold self-end">RAGFlow</span> <div className="text-xl font-bold self-center">RAGFlow</div>
</div> </div>
<h1 className="text-2xl font-bold text-center mb-2"> <h1 className="text-2xl font-bold text-center mb-2">{t('title')}</h1>
A Leading RAG engine with Agent for superior LLM context.
</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"> <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">
Let's get started {t('start')}
</div> </div>
</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"> <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 */} {/* Logo and Header */}
{/* Login Form */} {/* Login Form */}
<div className="text-center mb-8"> <FlipCard3D isLoginPage={isLoginPage}>
<h2 className="text-xl font-semibold text-text-primary"> <div className="flex flex-col items-center justify-center w-full">
{title === 'login' <div className="text-center mb-8">
? 'Sign in to Your Account' <h2 className="text-xl font-semibold text-text-primary">
: 'Create an Account'} {title === 'login' ? t('loginTitle') : t('signUpTitle')}
</h2> </h2>
</div> </div>
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-border-button"> <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 {...form}>
<form <form
className="space-y-6" className="flex flex-col gap-6 text-text-primary"
onSubmit={form.handleSubmit((data) => onCheck(data))} onSubmit={form.handleSubmit((data) => onCheck(data))}
> >
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel> <FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl> <FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} /> <Input
</FormControl> placeholder={t('emailPlaceholder')}
<FormMessage /> autoComplete="email"
</FormItem> {...field}
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('nicknamePlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{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>{t('rememberMe')}</FormLabel> </FormControl>
</div> <FormMessage />
</FormControl> </FormItem>
<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>
)}
/>
)} )}
/>
)} <FormField
<ButtonLoading control={form.control}
type="submit" name="password"
loading={loading} render={({ field }) => (
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full" <FormItem>
> <FormLabel required>{t('passwordLabel')}</FormLabel>
{title === 'login' ? t('login') : t('continue')} <FormControl>
</ButtonLoading> <div className="relative">
{title === 'login' && channels && channels.length > 0 && ( <Input
<div className="mt-3 border"> type={showPassword ? 'text' : 'password'}
{channels.map((item) => ( 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 === '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>{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"
>
{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 <Button
variant={'transparent'} variant={'transparent'}
key={item.channel} onClick={changeTitle}
onClick={() => handleLoginWithChannel(item.channel)} className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
style={{ marginTop: 10 }}
> >
<div className="flex items-center"> {t('signUp')}
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button> </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> </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"
>
{t('signUp')}
</Button>
</p>
</div> </div>
)} </div>
{title === 'register' && ( </FlipCard3D>
<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>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,70 +0,0 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import React from 'react';
interface SpotlightProps {
className?: string;
opcity?: number;
coverage?: number;
}
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopLeft: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 10% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopRight: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 90% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};

View File

@ -107,6 +107,11 @@ const OllamaModal = ({
{ value: 'chat', label: 'chat' }, { value: 'chat', label: 'chat' },
{ value: 'rerank', label: 'rerank' }, { value: 'rerank', label: 'rerank' },
], ],
[LLMFactory.LMStudio]: [
{ value: 'chat', label: 'chat' },
{ value: 'embedding', label: 'embedding' },
{ value: 'image2text', label: 'image2text' },
],
[LLMFactory.Xinference]: [ [LLMFactory.Xinference]: [
{ value: 'chat', label: 'chat' }, { value: 'chat', label: 'chat' },
{ value: 'embedding', label: 'embedding' }, { value: 'embedding', label: 'embedding' },

View File

@ -1,6 +1,6 @@
export enum Routes { export enum Routes {
Root = '/', Root = '/',
Login = '/login', Login = '/login-next',
Logout = '/logout', Logout = '/logout',
Home = '/home', Home = '/home',
Datasets = '/datasets', Datasets = '/datasets',
@ -52,7 +52,7 @@ export enum Routes {
const routes = [ const routes = [
{ {
path: '/login', path: '/login',
component: '@/pages/login', component: '@/pages/login-next',
layout: false, layout: false,
}, },
{ {

View File

@ -220,7 +220,7 @@ export function parseColorToRGB(color: string): [number, number, number] {
// Handling RGB colors (e.g., rgb(255, 87, 51)) // Handling RGB colors (e.g., rgb(255, 87, 51))
if (colorStr.startsWith('rgb')) { if (colorStr.startsWith('rgb')) {
const rgbMatch = colorStr.match(/rgb$$(\d+),\s*(\d+),\s*(\d+)$$/); const rgbMatch = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (rgbMatch) { if (rgbMatch) {
return [ return [
parseInt(rgbMatch[1]), parseInt(rgbMatch[1]),