mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-04 03:25:30 +08:00
Compare commits
9 Commits
58836d84fe
...
ff4239c7cf
| Author | SHA1 | Date | |
|---|---|---|---|
| ff4239c7cf | |||
| cf5867b146 | |||
| 77481ab3ab | |||
| 9c53b3336a | |||
| 24481f0332 | |||
| 4e6b84bb41 | |||
| 65c3f0406c | |||
| 7fb8b30cc2 | |||
| acca3640f7 |
@ -53,12 +53,13 @@ class ExeSQLParam(ToolParamBase):
|
||||
self.max_records = 1024
|
||||
|
||||
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.username, "database username")
|
||||
self.check_empty(self.host, "IP Address")
|
||||
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")
|
||||
if self.database == "rag_flow":
|
||||
if self.host == "ragflow-mysql":
|
||||
@ -123,6 +124,45 @@ class ExeSQL(ToolBase, ABC):
|
||||
r'PWD=' + self._param.password
|
||||
)
|
||||
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':
|
||||
import ibm_db
|
||||
conn_str = (
|
||||
|
||||
@ -409,6 +409,49 @@ def test_db_connect():
|
||||
ibm_db.fetch_assoc(stmt)
|
||||
ibm_db.close(conn)
|
||||
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:
|
||||
return server_error_response("Unsupported database type.")
|
||||
if req["db_type"] != 'mssql':
|
||||
|
||||
@ -36,6 +36,7 @@ from api import settings
|
||||
from rag.nlp import search
|
||||
from api.constants import DATASET_NAME_LIMIT
|
||||
from rag.settings import PAGERANK_FLD
|
||||
from rag.utils.redis_conn import REDIS_CONN
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
|
||||
|
||||
@ -760,18 +761,25 @@ def delete_kb_task():
|
||||
match pipeline_task_type:
|
||||
case PipelineTaskType.GRAPH_RAG:
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
case _:
|
||||
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:
|
||||
return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}")
|
||||
|
||||
|
||||
@ -21,6 +21,10 @@ Ensure that your metadata is in JSON format; otherwise, your updates will not be
|
||||
|
||||

|
||||
|
||||
## Related APIs
|
||||
|
||||
[Retrieve chunks](../../references/http_api_reference.md#retrieve-chunks)
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### Can I set metadata for multiple documents at once?
|
||||
|
||||
@ -1823,7 +1823,21 @@ curl --request POST \
|
||||
{
|
||||
"question": "What is advantage of ragflow?",
|
||||
"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]`
|
||||
The languages that should be translated into, in order to achieve keywords retrievals in different languages.
|
||||
- `"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
|
||||
|
||||
Success:
|
||||
|
||||
@ -33,7 +33,7 @@ A complete list of models supported by RAGFlow, which will continue to expand.
|
||||
| Jina | | :heavy_check_mark: | :heavy_check_mark: | | | |
|
||||
| LeptonAI | :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: | | | | | |
|
||||
| Mistral | :heavy_check_mark: | :heavy_check_mark: | | | | |
|
||||
| ModelScope | :heavy_check_mark: | | | | | |
|
||||
|
||||
@ -411,7 +411,7 @@ class Parser(ProcessBase):
|
||||
dispositions = content_disposition.strip().split(";")
|
||||
if dispositions[0].lower() == "attachment":
|
||||
filename = part.get_filename()
|
||||
payload = part.get_payload(decode=True)
|
||||
payload = part.get_payload(decode=True).decode(part.get_content_charset())
|
||||
attachments.append({
|
||||
"filename": filename,
|
||||
"payload": payload,
|
||||
@ -448,7 +448,7 @@ class Parser(ProcessBase):
|
||||
for t in msg.attachments:
|
||||
attachments.append({
|
||||
"filename": t.name,
|
||||
"payload": t.data # binary
|
||||
"payload": t.data.decode("utf-8")
|
||||
})
|
||||
email_content["attachments"] = attachments
|
||||
|
||||
|
||||
@ -691,7 +691,7 @@ async def run_raptor_for_kb(row, kb_parser_config, chat_mdl, embd_mdl, vector_si
|
||||
raptor_config["threshold"],
|
||||
)
|
||||
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_id": fake_doc_id,
|
||||
"kb_id": [str(row["kb_id"])],
|
||||
@ -814,8 +814,22 @@ async def do_handle_task(task):
|
||||
|
||||
kb_parser_config = kb.parser_config
|
||||
if not kb_parser_config.get("raptor", {}).get("use_raptor", False):
|
||||
progress_callback(prog=-1.0, msg="Internal error: Invalid RAPTOR configuration")
|
||||
return
|
||||
kb_parser_config.update(
|
||||
{
|
||||
"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
|
||||
chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language)
|
||||
# run RAPTOR
|
||||
@ -838,8 +852,25 @@ async def do_handle_task(task):
|
||||
|
||||
kb_parser_config = kb.parser_config
|
||||
if not kb_parser_config.get("graphrag", {}).get("use_graphrag", False):
|
||||
progress_callback(prog=-1.0, msg="Internal error: Invalid GraphRAG configuration")
|
||||
return
|
||||
kb_parser_config.update(
|
||||
{
|
||||
"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", {})
|
||||
start_ts = timer()
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||
import { Background } from '@xyflow/react';
|
||||
|
||||
export function AgentBackground() {
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
return (
|
||||
<Background
|
||||
color={isDarkTheme ? 'rgba(255,255,255,0.15)' : '#A8A9B3'}
|
||||
bgColor={isDarkTheme ? 'rgba(11, 11, 12, 1)' : 'rgba(0, 0, 0, 0.05)'}
|
||||
color="var(--text-primary)"
|
||||
bgColor="rgb(var(--bg-canvas))"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ export function FileUploadDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||
import { parseColorToRGB } from '@/utils/common-util';
|
||||
import React from 'react';
|
||||
|
||||
interface SpotlightProps {
|
||||
className?: string;
|
||||
opcity?: number;
|
||||
coverage?: number;
|
||||
X?: string;
|
||||
Y?: string;
|
||||
color?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -16,9 +20,20 @@ const Spotlight: React.FC<SpotlightProps> = ({
|
||||
className,
|
||||
opcity = 0.5,
|
||||
coverage = 60,
|
||||
X = '50%',
|
||||
Y = '190%',
|
||||
color,
|
||||
}) => {
|
||||
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 (
|
||||
<div
|
||||
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
|
||||
@ -30,7 +45,7 @@ const Spotlight: React.FC<SpotlightProps> = ({
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
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',
|
||||
}}
|
||||
></div>
|
||||
|
||||
@ -57,6 +57,8 @@ export default {
|
||||
},
|
||||
},
|
||||
login: {
|
||||
loginTitle: 'Sign in to Your Account',
|
||||
signUpTitle: 'Create an Account',
|
||||
login: 'Sign in',
|
||||
signUp: 'Sign up',
|
||||
loginDescription: 'We’re so excited to see you again!',
|
||||
@ -72,7 +74,8 @@ export default {
|
||||
nicknamePlaceholder: 'Please input nickname',
|
||||
register: 'Create an account',
|
||||
continue: 'Continue',
|
||||
title: 'Start building your smart assistants.',
|
||||
title: 'A leading RAG engine for LLM context',
|
||||
start: "Let's get started",
|
||||
description:
|
||||
'Sign up for free to explore top RAG technology. Create knowledge bases and AIs to empower your business.',
|
||||
review: 'from 500+ reviews',
|
||||
@ -114,7 +117,7 @@ export default {
|
||||
generateRaptor:
|
||||
'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.',
|
||||
generate: 'Generate',
|
||||
raptor: 'Raptor',
|
||||
raptor: 'RAPTOR',
|
||||
processingType: 'Processing Type',
|
||||
dataPipeline: 'Ingestion pipeline',
|
||||
operations: 'Operations',
|
||||
@ -128,7 +131,7 @@ export default {
|
||||
fileName: 'File Name',
|
||||
datasetLogs: 'Dataset',
|
||||
fileLogs: 'File',
|
||||
overview: 'Overview',
|
||||
overview: 'Logs',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
completed: 'Completed',
|
||||
@ -270,7 +273,7 @@ export default {
|
||||
reRankModelWaring: 'Re-rank model is very time consuming.',
|
||||
},
|
||||
knowledgeConfiguration: {
|
||||
tocExtraction: 'toc toggle',
|
||||
tocExtraction: 'TOC Enhance',
|
||||
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.",
|
||||
deleteGenerateModalContent: `
|
||||
@ -1703,17 +1706,17 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
parser: 'Parser',
|
||||
parserDescription:
|
||||
'Extracts raw text and structure from files for downstream processing.',
|
||||
tokenizer: 'Tokenizer',
|
||||
tokenizerRequired: 'Please add the Tokenizer node first',
|
||||
tokenizer: 'Indexer',
|
||||
tokenizerRequired: 'Please add the Indexer node first',
|
||||
tokenizerDescription:
|
||||
'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:
|
||||
'Split text into chunks by token length with optional delimiters and overlap.',
|
||||
hierarchicalMergerDescription:
|
||||
'Split documents into sections by title hierarchy with regex rules for finer control.',
|
||||
hierarchicalMerger: 'Title Splitter',
|
||||
extractor: 'Context Generator',
|
||||
hierarchicalMerger: 'Title',
|
||||
extractor: 'Transformer',
|
||||
extractorDescription:
|
||||
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
|
||||
outputFormat: 'Output format',
|
||||
@ -1817,9 +1820,13 @@ Important structured information may include: names, dates, locations, events, k
|
||||
},
|
||||
datasetOverview: {
|
||||
downloadTip: 'Files being downloaded from data sources. ',
|
||||
processingTip: 'Files being processed by data flows.',
|
||||
processingTip: 'Files being processed by data pipelines.',
|
||||
totalFiles: 'Total Files',
|
||||
downloading: 'Downloading',
|
||||
downloadSuccessTip: 'Total successful downloads',
|
||||
downloadFailedTip: 'Total failed downloads',
|
||||
processingSuccessTip: 'Total successfully processed files',
|
||||
processingFailedTip: 'Total failed processes',
|
||||
processing: 'Processing',
|
||||
},
|
||||
},
|
||||
|
||||
@ -49,6 +49,8 @@ export default {
|
||||
promptPlaceholder: '请输入或使用 / 快速插入变量。',
|
||||
},
|
||||
login: {
|
||||
loginTitle: '登录账户',
|
||||
signUpTitle: '创建账户',
|
||||
login: '登录',
|
||||
signUp: '注册',
|
||||
loginDescription: '很高兴再次见到您!',
|
||||
@ -64,7 +66,8 @@ export default {
|
||||
nicknamePlaceholder: '请输入名称',
|
||||
register: '创建账户',
|
||||
continue: '继续',
|
||||
title: '开始构建您的智能助手',
|
||||
title: 'A leading RAG engine for LLM context',
|
||||
start: '立即开始',
|
||||
description:
|
||||
'免费注册以探索顶级 RAG 技术。 创建知识库和人工智能来增强您的业务',
|
||||
review: '来自 500 多条评论',
|
||||
@ -116,7 +119,7 @@ export default {
|
||||
fileName: '文件名',
|
||||
datasetLogs: '数据集',
|
||||
fileLogs: '文件',
|
||||
overview: '概览',
|
||||
overview: '日志',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
completed: '已完成',
|
||||
@ -255,7 +258,7 @@ export default {
|
||||
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
|
||||
},
|
||||
knowledgeConfiguration: {
|
||||
tocExtraction: '目录提取',
|
||||
tocExtraction: '目录增强',
|
||||
tocExtractionTip:
|
||||
'对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`目录增强`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。',
|
||||
deleteGenerateModalContent: `
|
||||
@ -1713,6 +1716,10 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
totalFiles: '文件总数',
|
||||
downloading: '正在下载',
|
||||
processing: '正在处理',
|
||||
downloadSuccessTip: '下载成功总数',
|
||||
downloadFailedTip: '下载失败总数',
|
||||
processingSuccessTip: '处理成功的文件总数',
|
||||
processingFailedTip: '处理失败的文件总数',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -232,7 +232,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.canvasWrapper}>
|
||||
<div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: 'absolute', top: 10, left: 0 }}
|
||||
|
||||
@ -47,13 +47,13 @@ function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
|
||||
<ResizeIcon />
|
||||
</NodeResizeControl>
|
||||
<section className="p-2 flex gap-2 bg-background-note items-center note-drag-handle rounded-t">
|
||||
<section className="p-2 flex gap-2 items-center note-drag-handle rounded-t">
|
||||
<NotebookPen className="size-4" />
|
||||
<Form {...nameForm}>
|
||||
<form className="flex-1">
|
||||
|
||||
@ -8,14 +8,27 @@ export const ExeSQLFormSchema = {
|
||||
username: z.string().min(1),
|
||||
host: z.string().min(1),
|
||||
port: z.number(),
|
||||
password: z.string().min(1),
|
||||
password: z.string().optional().or(z.literal('')),
|
||||
max_records: z.number(),
|
||||
};
|
||||
|
||||
export const FormSchema = z.object({
|
||||
sql: z.string().optional(),
|
||||
...ExeSQLFormSchema,
|
||||
});
|
||||
export const FormSchema = z
|
||||
.object({
|
||||
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() {
|
||||
const { testDbConnect, loading } = useTestDbConnect();
|
||||
|
||||
@ -2139,6 +2139,7 @@ export const ExeSQLOptions = [
|
||||
'mariadb',
|
||||
'mssql',
|
||||
'IBM DB2',
|
||||
'trino',
|
||||
].map((x) => ({
|
||||
label: upperFirst(x),
|
||||
value: x,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
// height: calc(100vh - 284px);
|
||||
height: calc(100vh - 170px);
|
||||
height: calc(100vh - 180px);
|
||||
position: relative;
|
||||
:global(.PdfHighlighter) {
|
||||
overflow-x: hidden;
|
||||
|
||||
@ -205,7 +205,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.canvasWrapper}>
|
||||
<div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: 'absolute', top: 10, left: 0 }}
|
||||
@ -292,6 +292,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
clearActiveDropdown();
|
||||
}}
|
||||
position={dropdownPosition}
|
||||
nodeId={connectionStartRef.current?.nodeId || ''}
|
||||
>
|
||||
<span></span>
|
||||
</NextStepDropdown>
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -19,12 +25,13 @@ import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Operator, SingleOperators } from '../../../constant';
|
||||
import { Operator } from '../../../constant';
|
||||
import { AgentInstanceContext, HandleContext } from '../../../context';
|
||||
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
|
||||
function useRestrictSingleOperatorOnCanvas() {
|
||||
const list: Operator[] = [];
|
||||
const { findNodeByName } = useGraphStore((state) => state);
|
||||
|
||||
SingleOperators.forEach((operator) => {
|
||||
if (!findNodeByName(operator)) {
|
||||
list.push(operator);
|
||||
}
|
||||
});
|
||||
const restrictSingleOperatorOnCanvas = useCallback(
|
||||
(singleOperators: Operator[]) => {
|
||||
const list: Operator[] = [];
|
||||
singleOperators.forEach((operator) => {
|
||||
if (!findNodeByName(operator)) {
|
||||
list.push(operator);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
},
|
||||
[findNodeByName],
|
||||
);
|
||||
|
||||
return list;
|
||||
return restrictSingleOperatorOnCanvas;
|
||||
}
|
||||
|
||||
function AccordionOperators({
|
||||
isCustomDropdown = false,
|
||||
mousePosition,
|
||||
nodeId,
|
||||
}: {
|
||||
isCustomDropdown?: boolean;
|
||||
mousePosition?: { x: number; y: number };
|
||||
nodeId?: string;
|
||||
}) {
|
||||
const singleOperators = useRestrictSingleOperatorOnCanvas();
|
||||
const restrictSingleOperatorOnCanvas = useRestrictSingleOperatorOnCanvas();
|
||||
const { getOperatorTypeFromId } = useGraphStore((state) => state);
|
||||
|
||||
const operators = useMemo(() => {
|
||||
const list = [...singleOperators];
|
||||
let list = [
|
||||
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
|
||||
];
|
||||
list.push(Operator.Extractor);
|
||||
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 (
|
||||
<OperatorItemList
|
||||
operators={operators}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
<>
|
||||
<OperatorItemList
|
||||
operators={operators}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
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({
|
||||
children,
|
||||
hideModal,
|
||||
position,
|
||||
onNodeCreated,
|
||||
}: PropsWithChildren &
|
||||
IModalProps<any> & {
|
||||
position?: { x: number; y: number };
|
||||
onNodeCreated?: (newNodeId: string) => void;
|
||||
}) {
|
||||
nodeId,
|
||||
}: NextStepDropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -200,6 +259,7 @@ export function InnerNextStepDropdown({
|
||||
<AccordionOperators
|
||||
isCustomDropdown={true}
|
||||
mousePosition={position}
|
||||
nodeId={nodeId}
|
||||
></AccordionOperators>
|
||||
</OnNodeCreatedContext.Provider>
|
||||
</HideModalContext.Provider>
|
||||
@ -224,7 +284,7 @@ export function InnerNextStepDropdown({
|
||||
>
|
||||
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
|
||||
<HideModalContext.Provider value={hideModal}>
|
||||
<AccordionOperators></AccordionOperators>
|
||||
<AccordionOperators nodeId={nodeId}></AccordionOperators>
|
||||
</HideModalContext.Provider>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -61,6 +61,7 @@ export function CommonHandle({
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
}}
|
||||
nodeId={nodeId}
|
||||
>
|
||||
<span></span>
|
||||
</NextStepDropdown>
|
||||
|
||||
@ -299,7 +299,9 @@ export const initialHierarchicalMergerValues = {
|
||||
export const initialExtractorValues = {
|
||||
...initialLlmBaseValues,
|
||||
field_name: ContextGeneratorFieldName.Summary,
|
||||
outputs: {},
|
||||
outputs: {
|
||||
chunks: { type: 'Array<Object>', value: [] },
|
||||
},
|
||||
};
|
||||
|
||||
export const CategorizeAnchorPointPositions = [
|
||||
|
||||
@ -19,7 +19,9 @@ import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { buildOutputList } from '../../utils/build-output-list';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
import { Output } from '../components/output';
|
||||
import { useSwitchPrompt } from './use-switch-prompt';
|
||||
|
||||
export const FormSchema = z.object({
|
||||
@ -31,6 +33,8 @@ export const FormSchema = z.object({
|
||||
|
||||
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const outputList = buildOutputList(initialExtractorValues.outputs);
|
||||
|
||||
const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialExtractorValues, node);
|
||||
const { t } = useTranslation();
|
||||
@ -85,6 +89,7 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
baseOptions={promptOptions}
|
||||
></PromptEditor>
|
||||
</RAGFlowFormItem>
|
||||
<Output list={outputList}></Output>
|
||||
</FormWrapper>
|
||||
{visible && (
|
||||
<ConfirmDeleteDialog
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
// height: calc(100vh - 284px);
|
||||
height: calc(100vh - 170px);
|
||||
height: calc(100vh - 180px);
|
||||
position: relative;
|
||||
:global(.PdfHighlighter) {
|
||||
overflow-x: hidden;
|
||||
|
||||
@ -4,7 +4,6 @@ import CSVFileViewer from './csv-preview';
|
||||
import { DocPreviewer } from './doc-preview';
|
||||
import { ExcelCsvPreviewer } from './excel-preview';
|
||||
import { ImagePreviewer } from './image-preview';
|
||||
import styles from './index.less';
|
||||
import PdfPreviewer, { IProps } from './pdf-preview';
|
||||
import { PptPreviewer } from './ppt-preview';
|
||||
import { TxtPreviewer } from './txt-preview';
|
||||
@ -24,7 +23,7 @@ const Preview = ({
|
||||
return (
|
||||
<>
|
||||
{fileType === 'pdf' && highlights && setWidthAndHeight && (
|
||||
<section className={styles.documentPreview}>
|
||||
<section>
|
||||
<PdfPreviewer
|
||||
highlights={highlights}
|
||||
setWidthAndHeight={setWidthAndHeight}
|
||||
|
||||
@ -24,6 +24,8 @@ interface StatCardProps {
|
||||
interface CardFooterProcessProps {
|
||||
success: number;
|
||||
failed: number;
|
||||
successTip?: string;
|
||||
failedTip?: string;
|
||||
}
|
||||
|
||||
const StatCard: FC<StatCardProps> = ({
|
||||
@ -56,7 +58,9 @@ const StatCard: FC<StatCardProps> = ({
|
||||
|
||||
const CardFooterProcess: FC<CardFooterProcessProps> = ({
|
||||
success = 0,
|
||||
successTip,
|
||||
failed = 0,
|
||||
failedTip,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
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 rounded-lg gap-1">
|
||||
<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')}
|
||||
{successTip && (
|
||||
<AntToolTip title={successTip} trigger="hover">
|
||||
<CircleQuestionMark size={12} />
|
||||
</AntToolTip>
|
||||
)}
|
||||
</div>
|
||||
</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 rounded-lg gap-1">
|
||||
<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')}
|
||||
{failedTip && (
|
||||
<AntToolTip title={failedTip} trigger="hover">
|
||||
<CircleQuestionMark size={12} />
|
||||
</AntToolTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{failed || 0}</div>
|
||||
@ -259,7 +273,9 @@ const FileLogsPage: FC = () => {
|
||||
>
|
||||
<CardFooterProcess
|
||||
success={topAllData.downloads.success}
|
||||
successTip={t('datasetOverview.downloadSuccessTip')}
|
||||
failed={topAllData.downloads.failed}
|
||||
failedTip={t('datasetOverview.downloadFailedTip')}
|
||||
/>
|
||||
</StatCard>
|
||||
<StatCard
|
||||
@ -276,7 +292,9 @@ const FileLogsPage: FC = () => {
|
||||
>
|
||||
<CardFooterProcess
|
||||
success={topAllData.processing.success}
|
||||
successTip={t('datasetOverview.processingSuccessTip')}
|
||||
failed={topAllData.processing.failed}
|
||||
failedTip={t('datasetOverview.processingFailedTip')}
|
||||
/>
|
||||
</StatCard>
|
||||
</div>
|
||||
|
||||
@ -144,7 +144,12 @@ export function ParseTypeItem({ line = 2 }: { line?: number }) {
|
||||
>
|
||||
<FormControl>
|
||||
<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={2}>{t('manualSetup')}</Radio>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@ import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'umi';
|
||||
import { ProcessingType } from '../../dataset-overview/dataset-common';
|
||||
import { GenerateType } from './generate';
|
||||
import { GenerateType, GenerateTypeMap } from './generate';
|
||||
export const generateStatus = {
|
||||
running: 'running',
|
||||
completed: 'completed',
|
||||
@ -103,9 +103,28 @@ export const useTraceGenerate = ({ open }: { open: boolean }) => {
|
||||
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 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { id } = useParams();
|
||||
const { handleUnbindTask } = useUnBindTask();
|
||||
const {
|
||||
data,
|
||||
isPending: loading,
|
||||
@ -143,8 +162,12 @@ export const useDatasetGenerate = () => {
|
||||
type: GenerateType;
|
||||
}) => {
|
||||
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({
|
||||
queryKey: [type],
|
||||
});
|
||||
@ -154,21 +177,3 @@ export const useDatasetGenerate = () => {
|
||||
});
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -10,7 +10,7 @@ import { cn, formatBytes } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { formatPureDate } from '@/utils/date';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { useHandleMenuClick } from './hooks';
|
||||
@ -40,7 +40,7 @@ export function SideBar({ refreshCount }: PropType) {
|
||||
key: Routes.DatasetTesting,
|
||||
},
|
||||
{
|
||||
icon: <DatabaseZap className="size-4" />,
|
||||
icon: <Logs className="size-4" />,
|
||||
label: t(`knowledgeDetails.overview`),
|
||||
key: Routes.DataSetOverview,
|
||||
},
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
@ -103,7 +102,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
form.setValue('pipeline_id', '');
|
||||
}
|
||||
}, [parseType, form]);
|
||||
const { navigateToAgents } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@ -157,7 +155,7 @@ export function DatasetCreatingDialog({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
39
web/src/pages/login-next/card.tsx
Normal file
39
web/src/pages/login-next/card.tsx
Normal 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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -25,11 +25,12 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { BgSvg } from './bg';
|
||||
import FlipCard3D from './card';
|
||||
import './index.less';
|
||||
import { SpotlightTopLeft, SpotlightTopRight } from './spotlight-top';
|
||||
|
||||
const Login = () => {
|
||||
const [title, setTitle] = useState('login');
|
||||
@ -40,6 +41,8 @@ const Login = () => {
|
||||
const { login: loginWithChannel, loading: loginWithChannelLoading } =
|
||||
useLoginWithChannel();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const [isLoginPage, setIsLoginPage] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const loading =
|
||||
signLoading ||
|
||||
registerLoading ||
|
||||
@ -60,10 +63,15 @@ const Login = () => {
|
||||
};
|
||||
|
||||
const changeTitle = () => {
|
||||
setIsLoginPage(title !== 'login');
|
||||
if (title === 'login' && !registerEnabled) {
|
||||
return;
|
||||
}
|
||||
setTitle((title) => (title === 'login' ? 'register' : 'login'));
|
||||
|
||||
setTimeout(() => {
|
||||
setTitle(title === 'login' ? 'register' : 'login');
|
||||
}, 200);
|
||||
// setTitle((title) => (title === 'login' ? 'register' : 'login'));
|
||||
};
|
||||
|
||||
const FormSchema = z
|
||||
@ -129,177 +137,214 @@ const Login = () => {
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
<BgSvg />
|
||||
<Spotlight opcity={0.6} coverage={60} />
|
||||
<SpotlightTopLeft opcity={0.2} coverage={20} />
|
||||
<SpotlightTopRight opcity={0.2} coverage={20} />
|
||||
<Spotlight opcity={0.4} coverage={60} color={'rgb(128, 255, 248)'} />
|
||||
<Spotlight
|
||||
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="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
|
||||
src={'/logo.svg'}
|
||||
alt="logo"
|
||||
className="size-10 mr-[12] cursor-pointer"
|
||||
className="size-8 mr-[12] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xl font-bold self-end">RAGFlow</span>
|
||||
<div className="text-xl font-bold self-center">RAGFlow</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-2">
|
||||
A Leading RAG engine with Agent for superior LLM context.
|
||||
</h1>
|
||||
<h1 className="text-2xl font-bold text-center mb-2">{t('title')}</h1>
|
||||
<div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 border border-accent-primary rounded-full hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
|
||||
Let's get started
|
||||
{t('start')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8">
|
||||
{/* Logo and Header */}
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login'
|
||||
? 'Sign in to Your Account'
|
||||
: 'Create an Account'}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-border-button">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
onSubmit={form.handleSubmit((data) => onCheck(data))}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('emailPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{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);
|
||||
}}
|
||||
<FlipCard3D isLoginPage={isLoginPage}>
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-8 pr-8 pb-2 border border-border-button ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-6 text-text-primary"
|
||||
onSubmit={form.handleSubmit((data) => onCheck(data))}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
<FormLabel>{t('rememberMe')}</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{title === 'register' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('nicknameLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('nicknamePlaceholder')}
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<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"
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete={
|
||||
title === 'login'
|
||||
? 'current-password'
|
||||
: 'new-password'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{title === '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
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
onClick={changeTitle}
|
||||
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
<div className="mt-6 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
{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>
|
||||
</div>
|
||||
</FlipCard3D>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -107,6 +107,11 @@ const OllamaModal = ({
|
||||
{ value: 'chat', label: 'chat' },
|
||||
{ value: 'rerank', label: 'rerank' },
|
||||
],
|
||||
[LLMFactory.LMStudio]: [
|
||||
{ value: 'chat', label: 'chat' },
|
||||
{ value: 'embedding', label: 'embedding' },
|
||||
{ value: 'image2text', label: 'image2text' },
|
||||
],
|
||||
[LLMFactory.Xinference]: [
|
||||
{ value: 'chat', label: 'chat' },
|
||||
{ value: 'embedding', label: 'embedding' },
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export enum Routes {
|
||||
Root = '/',
|
||||
Login = '/login',
|
||||
Login = '/login-next',
|
||||
Logout = '/logout',
|
||||
Home = '/home',
|
||||
Datasets = '/datasets',
|
||||
@ -52,7 +52,7 @@ export enum Routes {
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: '@/pages/login',
|
||||
component: '@/pages/login-next',
|
||||
layout: false,
|
||||
},
|
||||
{
|
||||
|
||||
@ -220,7 +220,7 @@ export function parseColorToRGB(color: string): [number, number, number] {
|
||||
|
||||
// Handling RGB colors (e.g., rgb(255, 87, 51))
|
||||
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) {
|
||||
return [
|
||||
parseInt(rgbMatch[1]),
|
||||
|
||||
Reference in New Issue
Block a user