mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-06 02:25:05 +08:00
Compare commits
8 Commits
9a4cd81891
...
318cb7d792
| Author | SHA1 | Date | |
|---|---|---|---|
| 318cb7d792 | |||
| 4d1255b231 | |||
| b30f0be858 | |||
| a82e9b3d91 | |||
| 02a452993e | |||
| 307cdc62ea | |||
| 2d491188b8 | |||
| acc0f7396e |
@ -350,7 +350,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"label": "Tokenizer",
|
"label": "Tokenizer",
|
||||||
"name": "Tokenizer"
|
"name": "Indexer"
|
||||||
},
|
},
|
||||||
"dragging": false,
|
"dragging": false,
|
||||||
"id": "Tokenizer:EightRocketsAppear",
|
"id": "Tokenizer:EightRocketsAppear",
|
||||||
|
|||||||
@ -579,7 +579,7 @@ def run_graphrag():
|
|||||||
sample_document = documents[0]
|
sample_document = documents[0]
|
||||||
document_ids = [document["id"] for document in documents]
|
document_ids = [document["id"] for document in documents]
|
||||||
|
|
||||||
task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="graphrag", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
task_id = queue_raptor_o_graphrag_tasks(sample_doc_id=sample_document, ty="graphrag", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
||||||
|
|
||||||
if not KnowledgebaseService.update_by_id(kb.id, {"graphrag_task_id": task_id}):
|
if not KnowledgebaseService.update_by_id(kb.id, {"graphrag_task_id": task_id}):
|
||||||
logging.warning(f"Cannot save graphrag_task_id for kb {kb_id}")
|
logging.warning(f"Cannot save graphrag_task_id for kb {kb_id}")
|
||||||
@ -648,7 +648,7 @@ def run_raptor():
|
|||||||
sample_document = documents[0]
|
sample_document = documents[0]
|
||||||
document_ids = [document["id"] for document in documents]
|
document_ids = [document["id"] for document in documents]
|
||||||
|
|
||||||
task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="raptor", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
task_id = queue_raptor_o_graphrag_tasks(sample_doc_id=sample_document, ty="raptor", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
||||||
|
|
||||||
if not KnowledgebaseService.update_by_id(kb.id, {"raptor_task_id": task_id}):
|
if not KnowledgebaseService.update_by_id(kb.id, {"raptor_task_id": task_id}):
|
||||||
logging.warning(f"Cannot save raptor_task_id for kb {kb_id}")
|
logging.warning(f"Cannot save raptor_task_id for kb {kb_id}")
|
||||||
@ -717,7 +717,7 @@ def run_mindmap():
|
|||||||
sample_document = documents[0]
|
sample_document = documents[0]
|
||||||
document_ids = [document["id"] for document in documents]
|
document_ids = [document["id"] for document in documents]
|
||||||
|
|
||||||
task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="mindmap", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
task_id = queue_raptor_o_graphrag_tasks(sample_doc_id=sample_document, ty="mindmap", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
|
||||||
|
|
||||||
if not KnowledgebaseService.update_by_id(kb.id, {"mindmap_task_id": task_id}):
|
if not KnowledgebaseService.update_by_id(kb.id, {"mindmap_task_id": task_id}):
|
||||||
logging.warning(f"Cannot save mindmap_task_id for kb {kb_id}")
|
logging.warning(f"Cannot save mindmap_task_id for kb {kb_id}")
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import secrets
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import redirect, request, session, Response
|
from flask import redirect, request, session, make_response
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
@ -866,7 +866,9 @@ def forget_get_captcha():
|
|||||||
from captcha.image import ImageCaptcha
|
from captcha.image import ImageCaptcha
|
||||||
image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70])
|
image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70])
|
||||||
img_bytes = image.generate(captcha_text).read()
|
img_bytes = image.generate(captcha_text).read()
|
||||||
return Response(img_bytes, mimetype="image/png")
|
response = make_response(img_bytes)
|
||||||
|
response.headers.set("Content-Type", "image/JPEG")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/forget/otp", methods=["POST"]) # noqa: F821
|
@manager.route("/forget/otp", methods=["POST"]) # noqa: F821
|
||||||
|
|||||||
@ -671,9 +671,11 @@ class DocumentService(CommonService):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@DB.connection_context()
|
@DB.connection_context()
|
||||||
def _sync_progress(cls, docs:list[dict]):
|
def _sync_progress(cls, docs:list[dict]):
|
||||||
|
from api.db.services.task_service import TaskService
|
||||||
|
|
||||||
for d in docs:
|
for d in docs:
|
||||||
try:
|
try:
|
||||||
tsks = Task.query(doc_id=d["id"], order_by=Task.create_time)
|
tsks = TaskService.query(doc_id=d["id"], order_by=Task.create_time)
|
||||||
if not tsks:
|
if not tsks:
|
||||||
continue
|
continue
|
||||||
msg = []
|
msg = []
|
||||||
@ -791,21 +793,23 @@ class DocumentService(CommonService):
|
|||||||
"cancelled": int(cancelled),
|
"cancelled": int(cancelled),
|
||||||
}
|
}
|
||||||
|
|
||||||
def queue_raptor_o_graphrag_tasks(doc, ty, priority, fake_doc_id="", doc_ids=[]):
|
def queue_raptor_o_graphrag_tasks(sample_doc_id, ty, priority, fake_doc_id="", doc_ids=[]):
|
||||||
"""
|
"""
|
||||||
You can provide a fake_doc_id to bypass the restriction of tasks at the knowledgebase level.
|
You can provide a fake_doc_id to bypass the restriction of tasks at the knowledgebase level.
|
||||||
Optionally, specify a list of doc_ids to determine which documents participate in the task.
|
Optionally, specify a list of doc_ids to determine which documents participate in the task.
|
||||||
"""
|
"""
|
||||||
chunking_config = DocumentService.get_chunking_config(doc["id"])
|
assert ty in ["graphrag", "raptor", "mindmap"], "type should be graphrag, raptor or mindmap"
|
||||||
|
|
||||||
|
chunking_config = DocumentService.get_chunking_config(sample_doc_id["id"])
|
||||||
hasher = xxhash.xxh64()
|
hasher = xxhash.xxh64()
|
||||||
for field in sorted(chunking_config.keys()):
|
for field in sorted(chunking_config.keys()):
|
||||||
hasher.update(str(chunking_config[field]).encode("utf-8"))
|
hasher.update(str(chunking_config[field]).encode("utf-8"))
|
||||||
|
|
||||||
def new_task():
|
def new_task():
|
||||||
nonlocal doc
|
nonlocal sample_doc_id
|
||||||
return {
|
return {
|
||||||
"id": get_uuid(),
|
"id": get_uuid(),
|
||||||
"doc_id": fake_doc_id if fake_doc_id else doc["id"],
|
"doc_id": sample_doc_id["id"],
|
||||||
"from_page": 100000000,
|
"from_page": 100000000,
|
||||||
"to_page": 100000000,
|
"to_page": 100000000,
|
||||||
"task_type": ty,
|
"task_type": ty,
|
||||||
@ -820,9 +824,9 @@ def queue_raptor_o_graphrag_tasks(doc, ty, priority, fake_doc_id="", doc_ids=[])
|
|||||||
task["digest"] = hasher.hexdigest()
|
task["digest"] = hasher.hexdigest()
|
||||||
bulk_insert_into_db(Task, [task], True)
|
bulk_insert_into_db(Task, [task], True)
|
||||||
|
|
||||||
if ty in ["graphrag", "raptor", "mindmap"]:
|
task["doc_id"] = fake_doc_id
|
||||||
task["doc_ids"] = doc_ids
|
task["doc_ids"] = doc_ids
|
||||||
DocumentService.begin2parse(doc["id"])
|
DocumentService.begin2parse(sample_doc_id["id"])
|
||||||
assert REDIS_CONN.queue_product(get_svr_queue_name(priority), message=task), "Can't access Redis. Please check the Redis' status."
|
assert REDIS_CONN.queue_product(get_svr_queue_name(priority), message=task), "Can't access Redis. Please check the Redis' status."
|
||||||
return task["id"]
|
return task["id"]
|
||||||
|
|
||||||
|
|||||||
@ -210,19 +210,18 @@ class LLMBundle(LLM4Tenant):
|
|||||||
def _clean_param(chat_partial, **kwargs):
|
def _clean_param(chat_partial, **kwargs):
|
||||||
func = chat_partial.func
|
func = chat_partial.func
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
keyword_args = []
|
|
||||||
support_var_args = False
|
support_var_args = False
|
||||||
|
allowed_params = set()
|
||||||
|
|
||||||
for param in sig.parameters.values():
|
for param in sig.parameters.values():
|
||||||
if param.kind == inspect.Parameter.VAR_KEYWORD or param.kind == inspect.Parameter.VAR_POSITIONAL:
|
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||||
support_var_args = True
|
support_var_args = True
|
||||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
elif param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
|
||||||
keyword_args.append(param.name)
|
allowed_params.add(param.name)
|
||||||
|
if support_var_args:
|
||||||
use_kwargs = kwargs
|
return kwargs
|
||||||
if not support_var_args:
|
else:
|
||||||
use_kwargs = {k: v for k, v in kwargs.items() if k in keyword_args}
|
return {k: v for k, v in kwargs.items() if k in allowed_params}
|
||||||
return use_kwargs
|
|
||||||
|
|
||||||
def chat(self, system: str, history: list, gen_conf: dict = {}, **kwargs) -> str:
|
def chat(self, system: str, history: list, gen_conf: dict = {}, **kwargs) -> str:
|
||||||
if self.langfuse:
|
if self.langfuse:
|
||||||
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="chat", model=self.llm_name, input={"system": system, "history": history})
|
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="chat", model=self.llm_name, input={"system": system, "history": history})
|
||||||
|
|||||||
@ -105,16 +105,36 @@ class Extractor:
|
|||||||
|
|
||||||
async def extract_all(doc_id, chunks, max_concurrency=MAX_CONCURRENT_PROCESS_AND_EXTRACT_CHUNK):
|
async def extract_all(doc_id, chunks, max_concurrency=MAX_CONCURRENT_PROCESS_AND_EXTRACT_CHUNK):
|
||||||
out_results = []
|
out_results = []
|
||||||
|
error_count = 0
|
||||||
|
max_errors = 3
|
||||||
|
|
||||||
limiter = trio.Semaphore(max_concurrency)
|
limiter = trio.Semaphore(max_concurrency)
|
||||||
|
|
||||||
async def worker(chunk_key_dp: tuple[str, str], idx: int, total: int):
|
async def worker(chunk_key_dp: tuple[str, str], idx: int, total: int):
|
||||||
|
nonlocal error_count
|
||||||
async with limiter:
|
async with limiter:
|
||||||
await self._process_single_content(chunk_key_dp, idx, total, out_results)
|
try:
|
||||||
|
await self._process_single_content(chunk_key_dp, idx, total, out_results)
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
error_msg = f"Error processing chunk {idx+1}/{total}: {str(e)}"
|
||||||
|
logging.warning(error_msg)
|
||||||
|
if self.callback:
|
||||||
|
self.callback(msg=error_msg)
|
||||||
|
|
||||||
|
if error_count > max_errors:
|
||||||
|
raise Exception(f"Maximum error count ({max_errors}) reached. Last errors: {str(e)}")
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
for i, ck in enumerate(chunks):
|
for i, ck in enumerate(chunks):
|
||||||
nursery.start_soon(worker, (doc_id, ck), i, len(chunks))
|
nursery.start_soon(worker, (doc_id, ck), i, len(chunks))
|
||||||
|
|
||||||
|
if error_count > 0:
|
||||||
|
warning_msg = f"Completed with {error_count} errors (out of {len(chunks)} chunks processed)"
|
||||||
|
logging.warning(warning_msg)
|
||||||
|
if self.callback:
|
||||||
|
self.callback(msg=warning_msg)
|
||||||
|
|
||||||
return out_results
|
return out_results
|
||||||
|
|
||||||
out_results = await extract_all(doc_id, chunks, max_concurrency=MAX_CONCURRENT_PROCESS_AND_EXTRACT_CHUNK)
|
out_results = await extract_all(doc_id, chunks, max_concurrency=MAX_CONCURRENT_PROCESS_AND_EXTRACT_CHUNK)
|
||||||
@ -129,8 +149,8 @@ class Extractor:
|
|||||||
maybe_edges[tuple(sorted(k))].extend(v)
|
maybe_edges[tuple(sorted(k))].extend(v)
|
||||||
sum_token_count += token_count
|
sum_token_count += token_count
|
||||||
now = trio.current_time()
|
now = trio.current_time()
|
||||||
if callback:
|
if self.callback:
|
||||||
callback(msg=f"Entities and relationships extraction done, {len(maybe_nodes)} nodes, {len(maybe_edges)} edges, {sum_token_count} tokens, {now - start_ts:.2f}s.")
|
self.callback(msg=f"Entities and relationships extraction done, {len(maybe_nodes)} nodes, {len(maybe_edges)} edges, {sum_token_count} tokens, {now - start_ts:.2f}s.")
|
||||||
start_ts = now
|
start_ts = now
|
||||||
logging.info("Entities merging...")
|
logging.info("Entities merging...")
|
||||||
all_entities_data = []
|
all_entities_data = []
|
||||||
@ -138,8 +158,8 @@ class Extractor:
|
|||||||
for en_nm, ents in maybe_nodes.items():
|
for en_nm, ents in maybe_nodes.items():
|
||||||
nursery.start_soon(self._merge_nodes, en_nm, ents, all_entities_data)
|
nursery.start_soon(self._merge_nodes, en_nm, ents, all_entities_data)
|
||||||
now = trio.current_time()
|
now = trio.current_time()
|
||||||
if callback:
|
if self.callback:
|
||||||
callback(msg=f"Entities merging done, {now - start_ts:.2f}s.")
|
self.callback(msg=f"Entities merging done, {now - start_ts:.2f}s.")
|
||||||
|
|
||||||
start_ts = now
|
start_ts = now
|
||||||
logging.info("Relationships merging...")
|
logging.info("Relationships merging...")
|
||||||
@ -148,8 +168,8 @@ class Extractor:
|
|||||||
for (src, tgt), rels in maybe_edges.items():
|
for (src, tgt), rels in maybe_edges.items():
|
||||||
nursery.start_soon(self._merge_edges, src, tgt, rels, all_relationships_data)
|
nursery.start_soon(self._merge_edges, src, tgt, rels, all_relationships_data)
|
||||||
now = trio.current_time()
|
now = trio.current_time()
|
||||||
if callback:
|
if self.callback:
|
||||||
callback(msg=f"Relationships merging done, {now - start_ts:.2f}s.")
|
self.callback(msg=f"Relationships merging done, {now - start_ts:.2f}s.")
|
||||||
|
|
||||||
if not len(all_entities_data) and not len(all_relationships_data):
|
if not len(all_entities_data) and not len(all_relationships_data):
|
||||||
logging.warning("Didn't extract any entities and relationships, maybe your LLM is not working")
|
logging.warning("Didn't extract any entities and relationships, maybe your LLM is not working")
|
||||||
|
|||||||
@ -167,7 +167,7 @@ class Base(ABC):
|
|||||||
ans = response.choices[0].message.content.strip()
|
ans = response.choices[0].message.content.strip()
|
||||||
if response.choices[0].finish_reason == "length":
|
if response.choices[0].finish_reason == "length":
|
||||||
ans = self._length_stop(ans)
|
ans = self._length_stop(ans)
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def _chat_streamly(self, history, gen_conf, **kwargs):
|
def _chat_streamly(self, history, gen_conf, **kwargs):
|
||||||
logging.info("[HISTORY STREAMLY]" + json.dumps(history, ensure_ascii=False, indent=4))
|
logging.info("[HISTORY STREAMLY]" + json.dumps(history, ensure_ascii=False, indent=4))
|
||||||
@ -193,7 +193,7 @@ class Base(ABC):
|
|||||||
reasoning_start = False
|
reasoning_start = False
|
||||||
ans = resp.choices[0].delta.content
|
ans = resp.choices[0].delta.content
|
||||||
|
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
tol = num_tokens_from_string(resp.choices[0].delta.content)
|
tol = num_tokens_from_string(resp.choices[0].delta.content)
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ class Base(ABC):
|
|||||||
for _ in range(self.max_rounds + 1):
|
for _ in range(self.max_rounds + 1):
|
||||||
logging.info(f"{self.tools=}")
|
logging.info(f"{self.tools=}")
|
||||||
response = self.client.chat.completions.create(model=self.model_name, messages=history, tools=self.tools, tool_choice="auto", **gen_conf)
|
response = self.client.chat.completions.create(model=self.model_name, messages=history, tools=self.tools, tool_choice="auto", **gen_conf)
|
||||||
tk_count += self.total_token_count(response)
|
tk_count += total_token_count_from_response(response)
|
||||||
if any([not response.choices, not response.choices[0].message]):
|
if any([not response.choices, not response.choices[0].message]):
|
||||||
raise Exception(f"500 response structure error. Response: {response}")
|
raise Exception(f"500 response structure error. Response: {response}")
|
||||||
|
|
||||||
@ -401,7 +401,7 @@ class Base(ABC):
|
|||||||
answer += resp.choices[0].delta.content
|
answer += resp.choices[0].delta.content
|
||||||
yield resp.choices[0].delta.content
|
yield resp.choices[0].delta.content
|
||||||
|
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
||||||
else:
|
else:
|
||||||
@ -437,7 +437,7 @@ class Base(ABC):
|
|||||||
if not resp.choices[0].delta.content:
|
if not resp.choices[0].delta.content:
|
||||||
resp.choices[0].delta.content = ""
|
resp.choices[0].delta.content = ""
|
||||||
continue
|
continue
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
||||||
else:
|
else:
|
||||||
@ -472,9 +472,6 @@ class Base(ABC):
|
|||||||
|
|
||||||
yield total_tokens
|
yield total_tokens
|
||||||
|
|
||||||
def total_token_count(self, resp):
|
|
||||||
return total_token_count_from_response(resp)
|
|
||||||
|
|
||||||
def _calculate_dynamic_ctx(self, history):
|
def _calculate_dynamic_ctx(self, history):
|
||||||
"""Calculate dynamic context window size"""
|
"""Calculate dynamic context window size"""
|
||||||
|
|
||||||
@ -604,7 +601,7 @@ class BaiChuanChat(Base):
|
|||||||
ans += LENGTH_NOTIFICATION_CN
|
ans += LENGTH_NOTIFICATION_CN
|
||||||
else:
|
else:
|
||||||
ans += LENGTH_NOTIFICATION_EN
|
ans += LENGTH_NOTIFICATION_EN
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
||||||
if system and history and history[0].get("role") != "system":
|
if system and history and history[0].get("role") != "system":
|
||||||
@ -627,7 +624,7 @@ class BaiChuanChat(Base):
|
|||||||
if not resp.choices[0].delta.content:
|
if not resp.choices[0].delta.content:
|
||||||
resp.choices[0].delta.content = ""
|
resp.choices[0].delta.content = ""
|
||||||
ans = resp.choices[0].delta.content
|
ans = resp.choices[0].delta.content
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
total_tokens += num_tokens_from_string(resp.choices[0].delta.content)
|
||||||
else:
|
else:
|
||||||
@ -691,9 +688,9 @@ class ZhipuChat(Base):
|
|||||||
ans += LENGTH_NOTIFICATION_CN
|
ans += LENGTH_NOTIFICATION_CN
|
||||||
else:
|
else:
|
||||||
ans += LENGTH_NOTIFICATION_EN
|
ans += LENGTH_NOTIFICATION_EN
|
||||||
tk_count = self.total_token_count(resp)
|
tk_count = total_token_count_from_response(resp)
|
||||||
if resp.choices[0].finish_reason == "stop":
|
if resp.choices[0].finish_reason == "stop":
|
||||||
tk_count = self.total_token_count(resp)
|
tk_count = total_token_count_from_response(resp)
|
||||||
yield ans
|
yield ans
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield ans + "\n**ERROR**: " + str(e)
|
yield ans + "\n**ERROR**: " + str(e)
|
||||||
@ -812,7 +809,7 @@ class MiniMaxChat(Base):
|
|||||||
ans += LENGTH_NOTIFICATION_CN
|
ans += LENGTH_NOTIFICATION_CN
|
||||||
else:
|
else:
|
||||||
ans += LENGTH_NOTIFICATION_EN
|
ans += LENGTH_NOTIFICATION_EN
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf):
|
def chat_streamly(self, system, history, gen_conf):
|
||||||
if system and history and history[0].get("role") != "system":
|
if system and history and history[0].get("role") != "system":
|
||||||
@ -847,7 +844,7 @@ class MiniMaxChat(Base):
|
|||||||
if "choices" in resp and "delta" in resp["choices"][0]:
|
if "choices" in resp and "delta" in resp["choices"][0]:
|
||||||
text = resp["choices"][0]["delta"]["content"]
|
text = resp["choices"][0]["delta"]["content"]
|
||||||
ans = text
|
ans = text
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(text)
|
total_tokens += num_tokens_from_string(text)
|
||||||
else:
|
else:
|
||||||
@ -886,7 +883,7 @@ class MistralChat(Base):
|
|||||||
ans += LENGTH_NOTIFICATION_CN
|
ans += LENGTH_NOTIFICATION_CN
|
||||||
else:
|
else:
|
||||||
ans += LENGTH_NOTIFICATION_EN
|
ans += LENGTH_NOTIFICATION_EN
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
||||||
if system and history and history[0].get("role") != "system":
|
if system and history and history[0].get("role") != "system":
|
||||||
@ -1110,7 +1107,7 @@ class BaiduYiyanChat(Base):
|
|||||||
system = history[0]["content"] if history and history[0]["role"] == "system" else ""
|
system = history[0]["content"] if history and history[0]["role"] == "system" else ""
|
||||||
response = self.client.do(model=self.model_name, messages=[h for h in history if h["role"] != "system"], system=system, **gen_conf).body
|
response = self.client.do(model=self.model_name, messages=[h for h in history if h["role"] != "system"], system=system, **gen_conf).body
|
||||||
ans = response["result"]
|
ans = response["result"]
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
def chat_streamly(self, system, history, gen_conf={}, **kwargs):
|
||||||
gen_conf["penalty_score"] = ((gen_conf.get("presence_penalty", 0) + gen_conf.get("frequency_penalty", 0)) / 2) + 1
|
gen_conf["penalty_score"] = ((gen_conf.get("presence_penalty", 0) + gen_conf.get("frequency_penalty", 0)) / 2) + 1
|
||||||
@ -1124,7 +1121,7 @@ class BaiduYiyanChat(Base):
|
|||||||
for resp in response:
|
for resp in response:
|
||||||
resp = resp.body
|
resp = resp.body
|
||||||
ans = resp["result"]
|
ans = resp["result"]
|
||||||
total_tokens = self.total_token_count(resp)
|
total_tokens = total_token_count_from_response(resp)
|
||||||
|
|
||||||
yield ans
|
yield ans
|
||||||
|
|
||||||
@ -1478,7 +1475,7 @@ class LiteLLMBase(ABC):
|
|||||||
if response.choices[0].finish_reason == "length":
|
if response.choices[0].finish_reason == "length":
|
||||||
ans = self._length_stop(ans)
|
ans = self._length_stop(ans)
|
||||||
|
|
||||||
return ans, self.total_token_count(response)
|
return ans, total_token_count_from_response(response)
|
||||||
|
|
||||||
def _chat_streamly(self, history, gen_conf, **kwargs):
|
def _chat_streamly(self, history, gen_conf, **kwargs):
|
||||||
logging.info("[HISTORY STREAMLY]" + json.dumps(history, ensure_ascii=False, indent=4))
|
logging.info("[HISTORY STREAMLY]" + json.dumps(history, ensure_ascii=False, indent=4))
|
||||||
@ -1512,7 +1509,7 @@ class LiteLLMBase(ABC):
|
|||||||
reasoning_start = False
|
reasoning_start = False
|
||||||
ans = delta.content
|
ans = delta.content
|
||||||
|
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
tol = num_tokens_from_string(delta.content)
|
tol = num_tokens_from_string(delta.content)
|
||||||
|
|
||||||
@ -1665,7 +1662,7 @@ class LiteLLMBase(ABC):
|
|||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
tk_count += self.total_token_count(response)
|
tk_count += total_token_count_from_response(response)
|
||||||
|
|
||||||
if not hasattr(response, "choices") or not response.choices or not response.choices[0].message:
|
if not hasattr(response, "choices") or not response.choices or not response.choices[0].message:
|
||||||
raise Exception(f"500 response structure error. Response: {response}")
|
raise Exception(f"500 response structure error. Response: {response}")
|
||||||
@ -1797,7 +1794,7 @@ class LiteLLMBase(ABC):
|
|||||||
answer += delta.content
|
answer += delta.content
|
||||||
yield delta.content
|
yield delta.content
|
||||||
|
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(delta.content)
|
total_tokens += num_tokens_from_string(delta.content)
|
||||||
else:
|
else:
|
||||||
@ -1846,7 +1843,7 @@ class LiteLLMBase(ABC):
|
|||||||
delta = resp.choices[0].delta
|
delta = resp.choices[0].delta
|
||||||
if not hasattr(delta, "content") or delta.content is None:
|
if not hasattr(delta, "content") or delta.content is None:
|
||||||
continue
|
continue
|
||||||
tol = self.total_token_count(resp)
|
tol = total_token_count_from_response(resp)
|
||||||
if not tol:
|
if not tol:
|
||||||
total_tokens += num_tokens_from_string(delta.content)
|
total_tokens += num_tokens_from_string(delta.content)
|
||||||
else:
|
else:
|
||||||
@ -1880,17 +1877,6 @@ class LiteLLMBase(ABC):
|
|||||||
|
|
||||||
yield total_tokens
|
yield total_tokens
|
||||||
|
|
||||||
def total_token_count(self, resp):
|
|
||||||
try:
|
|
||||||
return resp.usage.total_tokens
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return resp["usage"]["total_tokens"]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _calculate_dynamic_ctx(self, history):
|
def _calculate_dynamic_ctx(self, history):
|
||||||
"""Calculate dynamic context window size"""
|
"""Calculate dynamic context window size"""
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ class Base(ABC):
|
|||||||
def describe_with_prompt(self, image, prompt=None):
|
def describe_with_prompt(self, image, prompt=None):
|
||||||
raise NotImplementedError("Please implement encode method!")
|
raise NotImplementedError("Please implement encode method!")
|
||||||
|
|
||||||
def _form_history(self, system, history, images=[]):
|
def _form_history(self, system, history, images=None):
|
||||||
hist = []
|
hist = []
|
||||||
if system:
|
if system:
|
||||||
hist.append({"role": "system", "content": system})
|
hist.append({"role": "system", "content": system})
|
||||||
@ -78,7 +78,7 @@ class Base(ABC):
|
|||||||
})
|
})
|
||||||
return pmpt
|
return pmpt
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[], **kwargs):
|
def chat(self, system, history, gen_conf, images=None, **kwargs):
|
||||||
try:
|
try:
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
model=self.model_name,
|
model=self.model_name,
|
||||||
@ -89,7 +89,7 @@ class Base(ABC):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "**ERROR**: " + str(e), 0
|
return "**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[], **kwargs):
|
def chat_streamly(self, system, history, gen_conf, images=None, **kwargs):
|
||||||
ans = ""
|
ans = ""
|
||||||
tk_count = 0
|
tk_count = 0
|
||||||
try:
|
try:
|
||||||
@ -228,7 +228,7 @@ class QWenCV(GptV4):
|
|||||||
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
super().__init__(key, model_name, lang=lang, base_url=base_url, **kwargs)
|
super().__init__(key, model_name, lang=lang, base_url=base_url, **kwargs)
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[], video_bytes=None, filename=""):
|
def chat(self, system, history, gen_conf, images=None, video_bytes=None, filename=""):
|
||||||
if video_bytes:
|
if video_bytes:
|
||||||
try:
|
try:
|
||||||
summary, summary_num_tokens = self._process_video(video_bytes, filename)
|
summary, summary_num_tokens = self._process_video(video_bytes, filename)
|
||||||
@ -506,7 +506,7 @@ class OllamaCV(Base):
|
|||||||
options["frequency_penalty"] = gen_conf["frequency_penalty"]
|
options["frequency_penalty"] = gen_conf["frequency_penalty"]
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def _form_history(self, system, history, images=[]):
|
def _form_history(self, system, history, images=None):
|
||||||
hist = deepcopy(history)
|
hist = deepcopy(history)
|
||||||
if system and hist[0]["role"] == "user":
|
if system and hist[0]["role"] == "user":
|
||||||
hist.insert(0, {"role": "system", "content": system})
|
hist.insert(0, {"role": "system", "content": system})
|
||||||
@ -547,7 +547,7 @@ class OllamaCV(Base):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "**ERROR**: " + str(e), 0
|
return "**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[]):
|
def chat(self, system, history, gen_conf, images=None):
|
||||||
try:
|
try:
|
||||||
response = self.client.chat(
|
response = self.client.chat(
|
||||||
model=self.model_name,
|
model=self.model_name,
|
||||||
@ -561,7 +561,7 @@ class OllamaCV(Base):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "**ERROR**: " + str(e), 0
|
return "**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[]):
|
def chat_streamly(self, system, history, gen_conf, images=None):
|
||||||
ans = ""
|
ans = ""
|
||||||
try:
|
try:
|
||||||
response = self.client.chat(
|
response = self.client.chat(
|
||||||
@ -596,7 +596,7 @@ class GeminiCV(Base):
|
|||||||
self.lang = lang
|
self.lang = lang
|
||||||
Base.__init__(self, **kwargs)
|
Base.__init__(self, **kwargs)
|
||||||
|
|
||||||
def _form_history(self, system, history, images=[]):
|
def _form_history(self, system, history, images=None):
|
||||||
hist = []
|
hist = []
|
||||||
if system:
|
if system:
|
||||||
hist.append({"role": "user", "parts": [system, history[0]["content"]]})
|
hist.append({"role": "user", "parts": [system, history[0]["content"]]})
|
||||||
@ -633,7 +633,7 @@ class GeminiCV(Base):
|
|||||||
return res.text, total_token_count_from_response(res)
|
return res.text, total_token_count_from_response(res)
|
||||||
|
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[], video_bytes=None, filename=""):
|
def chat(self, system, history, gen_conf, images=None, video_bytes=None, filename=""):
|
||||||
if video_bytes:
|
if video_bytes:
|
||||||
try:
|
try:
|
||||||
summary, summary_num_tokens = self._process_video(video_bytes, filename)
|
summary, summary_num_tokens = self._process_video(video_bytes, filename)
|
||||||
@ -651,7 +651,7 @@ class GeminiCV(Base):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "**ERROR**: " + str(e), 0
|
return "**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[]):
|
def chat_streamly(self, system, history, gen_conf, images=None):
|
||||||
ans = ""
|
ans = ""
|
||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
@ -782,7 +782,7 @@ class NvidiaCV(Base):
|
|||||||
total_token_count_from_response(response)
|
total_token_count_from_response(response)
|
||||||
)
|
)
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[], **kwargs):
|
def chat(self, system, history, gen_conf, images=None, **kwargs):
|
||||||
try:
|
try:
|
||||||
response = self._request(self._form_history(system, history, images), gen_conf)
|
response = self._request(self._form_history(system, history, images), gen_conf)
|
||||||
return (
|
return (
|
||||||
@ -792,7 +792,7 @@ class NvidiaCV(Base):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "**ERROR**: " + str(e), 0
|
return "**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[], **kwargs):
|
def chat_streamly(self, system, history, gen_conf, images=None, **kwargs):
|
||||||
total_tokens = 0
|
total_tokens = 0
|
||||||
try:
|
try:
|
||||||
response = self._request(self._form_history(system, history, images), gen_conf)
|
response = self._request(self._form_history(system, history, images), gen_conf)
|
||||||
@ -858,7 +858,7 @@ class AnthropicCV(Base):
|
|||||||
gen_conf["max_tokens"] = self.max_tokens
|
gen_conf["max_tokens"] = self.max_tokens
|
||||||
return gen_conf
|
return gen_conf
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[]):
|
def chat(self, system, history, gen_conf, images=None):
|
||||||
gen_conf = self._clean_conf(gen_conf)
|
gen_conf = self._clean_conf(gen_conf)
|
||||||
ans = ""
|
ans = ""
|
||||||
try:
|
try:
|
||||||
@ -879,7 +879,7 @@ class AnthropicCV(Base):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ans + "\n**ERROR**: " + str(e), 0
|
return ans + "\n**ERROR**: " + str(e), 0
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[]):
|
def chat_streamly(self, system, history, gen_conf, images=None):
|
||||||
gen_conf = self._clean_conf(gen_conf)
|
gen_conf = self._clean_conf(gen_conf)
|
||||||
total_tokens = 0
|
total_tokens = 0
|
||||||
try:
|
try:
|
||||||
@ -963,13 +963,13 @@ class GoogleCV(AnthropicCV, GeminiCV):
|
|||||||
else:
|
else:
|
||||||
return GeminiCV.describe_with_prompt(self, image, prompt)
|
return GeminiCV.describe_with_prompt(self, image, prompt)
|
||||||
|
|
||||||
def chat(self, system, history, gen_conf, images=[]):
|
def chat(self, system, history, gen_conf, images=None):
|
||||||
if "claude" in self.model_name:
|
if "claude" in self.model_name:
|
||||||
return AnthropicCV.chat(self, system, history, gen_conf, images)
|
return AnthropicCV.chat(self, system, history, gen_conf, images)
|
||||||
else:
|
else:
|
||||||
return GeminiCV.chat(self, system, history, gen_conf, images)
|
return GeminiCV.chat(self, system, history, gen_conf, images)
|
||||||
|
|
||||||
def chat_streamly(self, system, history, gen_conf, images=[]):
|
def chat_streamly(self, system, history, gen_conf, images=None):
|
||||||
if "claude" in self.model_name:
|
if "claude" in self.model_name:
|
||||||
for ans in AnthropicCV.chat_streamly(self, system, history, gen_conf, images):
|
for ans in AnthropicCV.chat_streamly(self, system, history, gen_conf, images):
|
||||||
yield ans
|
yield ans
|
||||||
|
|||||||
@ -228,9 +228,10 @@ async def collect():
|
|||||||
canceled = False
|
canceled = False
|
||||||
if msg.get("doc_id", "") in [GRAPH_RAPTOR_FAKE_DOC_ID, CANVAS_DEBUG_DOC_ID]:
|
if msg.get("doc_id", "") in [GRAPH_RAPTOR_FAKE_DOC_ID, CANVAS_DEBUG_DOC_ID]:
|
||||||
task = msg
|
task = msg
|
||||||
if task["task_type"] in ["graphrag", "raptor", "mindmap"] and msg.get("doc_ids", []):
|
if task["task_type"] in ["graphrag", "raptor", "mindmap"]:
|
||||||
task = TaskService.get_task(msg["id"], msg["doc_ids"])
|
task = TaskService.get_task(msg["id"], msg["doc_ids"])
|
||||||
task["doc_ids"] = msg["doc_ids"]
|
task["doc_id"] = msg["doc_id"]
|
||||||
|
task["doc_ids"] = msg.get("doc_ids", []) or []
|
||||||
else:
|
else:
|
||||||
task = TaskService.get_task(msg["id"])
|
task = TaskService.get_task(msg["id"])
|
||||||
|
|
||||||
@ -1052,12 +1053,12 @@ async def task_manager():
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
logging.info(r"""
|
logging.info(r"""
|
||||||
____ __ _
|
____ __ _
|
||||||
/ _/___ ____ ____ _____/ /_(_)___ ____ ________ ______ _____ _____
|
/ _/___ ____ ____ _____/ /_(_)___ ____ ________ ______ _____ _____
|
||||||
/ // __ \/ __ `/ _ \/ ___/ __/ / __ \/ __ \ / ___/ _ \/ ___/ | / / _ \/ ___/
|
/ // __ \/ __ `/ _ \/ ___/ __/ / __ \/ __ \ / ___/ _ \/ ___/ | / / _ \/ ___/
|
||||||
_/ // / / / /_/ / __(__ ) /_/ / /_/ / / / / (__ ) __/ / | |/ / __/ /
|
_/ // / / / /_/ / __(__ ) /_/ / /_/ / / / / (__ ) __/ / | |/ / __/ /
|
||||||
/___/_/ /_/\__, /\___/____/\__/_/\____/_/ /_/ /____/\___/_/ |___/\___/_/
|
/___/_/ /_/\__, /\___/____/\__/_/\____/_/ /_/ /____/\___/_/ |___/\___/_/
|
||||||
/____/
|
/____/
|
||||||
""")
|
""")
|
||||||
logging.info(f'RAGFlow version: {get_ragflow_version()}')
|
logging.info(f'RAGFlow version: {get_ragflow_version()}')
|
||||||
settings.init_settings()
|
settings.init_settings()
|
||||||
|
|||||||
@ -106,7 +106,7 @@ class RAGFlowOSS:
|
|||||||
|
|
||||||
@use_prefix_path
|
@use_prefix_path
|
||||||
@use_default_bucket
|
@use_default_bucket
|
||||||
def put(self, bucket, fnm, binary):
|
def put(self, bucket, fnm, binary, tenant_id=None):
|
||||||
logging.debug(f"bucket name {bucket}; filename :{fnm}:")
|
logging.debug(f"bucket name {bucket}; filename :{fnm}:")
|
||||||
for _ in range(1):
|
for _ in range(1):
|
||||||
try:
|
try:
|
||||||
@ -123,7 +123,7 @@ class RAGFlowOSS:
|
|||||||
|
|
||||||
@use_prefix_path
|
@use_prefix_path
|
||||||
@use_default_bucket
|
@use_default_bucket
|
||||||
def rm(self, bucket, fnm):
|
def rm(self, bucket, fnm, tenant_id=None):
|
||||||
try:
|
try:
|
||||||
self.conn.delete_object(Bucket=bucket, Key=fnm)
|
self.conn.delete_object(Bucket=bucket, Key=fnm)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -131,7 +131,7 @@ class RAGFlowOSS:
|
|||||||
|
|
||||||
@use_prefix_path
|
@use_prefix_path
|
||||||
@use_default_bucket
|
@use_default_bucket
|
||||||
def get(self, bucket, fnm):
|
def get(self, bucket, fnm, tenant_id=None):
|
||||||
for _ in range(1):
|
for _ in range(1):
|
||||||
try:
|
try:
|
||||||
r = self.conn.get_object(Bucket=bucket, Key=fnm)
|
r = self.conn.get_object(Bucket=bucket, Key=fnm)
|
||||||
@ -145,7 +145,7 @@ class RAGFlowOSS:
|
|||||||
|
|
||||||
@use_prefix_path
|
@use_prefix_path
|
||||||
@use_default_bucket
|
@use_default_bucket
|
||||||
def obj_exist(self, bucket, fnm):
|
def obj_exist(self, bucket, fnm, tenant_id=None):
|
||||||
try:
|
try:
|
||||||
if self.conn.head_object(Bucket=bucket, Key=fnm):
|
if self.conn.head_object(Bucket=bucket, Key=fnm):
|
||||||
return True
|
return True
|
||||||
@ -157,7 +157,7 @@ class RAGFlowOSS:
|
|||||||
|
|
||||||
@use_prefix_path
|
@use_prefix_path
|
||||||
@use_default_bucket
|
@use_default_bucket
|
||||||
def get_presigned_url(self, bucket, fnm, expires):
|
def get_presigned_url(self, bucket, fnm, expires, tenant_id=None):
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
try:
|
try:
|
||||||
r = self.conn.generate_presigned_url('get_object',
|
r = self.conn.generate_presigned_url('get_object',
|
||||||
|
|||||||
@ -51,8 +51,8 @@ export function Collapse({
|
|||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className={'w-full'}>
|
||||||
<section className="flex justify-between items-center pb-2">
|
<section className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<IconFontFill
|
<IconFontFill
|
||||||
name={`more`}
|
name={`more`}
|
||||||
@ -60,12 +60,18 @@ export function Collapse({
|
|||||||
'rotate-90': !currentOpen,
|
'rotate-90': !currentOpen,
|
||||||
})}
|
})}
|
||||||
></IconFontFill>
|
></IconFontFill>
|
||||||
{title}
|
<div
|
||||||
|
className={cn('text-text-secondary', {
|
||||||
|
'text-text-primary': open,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{rightContent}</div>
|
<div>{rightContent}</div>
|
||||||
</section>
|
</section>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>{children}</CollapsibleContent>
|
<CollapsibleContent className="pt-2">{children}</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,13 +56,13 @@ export function ConfirmDeleteDialog({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel onClick={onCancel}>
|
<AlertDialogCancel onClick={onCancel}>
|
||||||
{t('common.cancel')}
|
{t('common.no')}
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-state-error text-text-primary"
|
className="bg-state-error text-text-primary"
|
||||||
onClick={onOk}
|
onClick={onOk}
|
||||||
>
|
>
|
||||||
{t('common.ok')}
|
{t('common.yes')}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
48
web/src/components/theme-toggle.tsx
Normal file
48
web/src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { ThemeEnum } from '@/constants/common';
|
||||||
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
import { useIsDarkTheme, useTheme } from './theme-provider';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
|
const ThemeToggle: FC = () => {
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
const handleThemeChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
|
||||||
|
},
|
||||||
|
[setTheme],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThemeChange(!isDarkTheme)}
|
||||||
|
className="relative inline-flex h-6 w-14 items-center rounded-full transition-colors p-0.5 border-none focus:border-none bg-bg-card hover:bg-bg-card"
|
||||||
|
// aria-label={isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
>
|
||||||
|
<div className="inline-flex h-full w-full items-center">
|
||||||
|
<div
|
||||||
|
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
|
||||||
|
isDarkTheme
|
||||||
|
? ' text-text-disabled h-4 w-5'
|
||||||
|
: ' text-text-primary bg-bg-base h-full w-8 flex-1'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Sun />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
|
||||||
|
isDarkTheme
|
||||||
|
? ' text-text-primary bg-bg-base h-full w-8 flex-1'
|
||||||
|
: 'text-text-disabled h-4 w-5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Moon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
@ -73,7 +73,7 @@ const DialogFooter = ({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import * as React from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const labelVariants = cva(
|
const labelVariants = cva(
|
||||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-secondary',
|
||||||
);
|
);
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
const Label = React.forwardRef<
|
||||||
|
|||||||
@ -94,9 +94,9 @@ export const useShowDeleteConfirm = () => {
|
|||||||
title: title ?? t('common.deleteModalTitle'),
|
title: title ?? t('common.deleteModalTitle'),
|
||||||
icon: <ExclamationCircleFilled />,
|
icon: <ExclamationCircleFilled />,
|
||||||
content,
|
content,
|
||||||
okText: t('common.ok'),
|
okText: t('common.yes'),
|
||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: t('common.cancel'),
|
cancelText: t('common.no'),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
try {
|
try {
|
||||||
const ret = await onOk?.();
|
const ret = await onOk?.();
|
||||||
|
|||||||
@ -6,8 +6,9 @@ export default {
|
|||||||
selectAll: 'Select All',
|
selectAll: 'Select All',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteModalTitle: 'Are you sure to delete this item?',
|
deleteModalTitle: 'Are you sure to delete this item?',
|
||||||
ok: 'Yes',
|
ok: 'Ok',
|
||||||
cancel: 'No',
|
cancel: 'Cancel',
|
||||||
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
rename: 'Rename',
|
rename: 'Rename',
|
||||||
@ -1744,6 +1745,7 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
toolsAvailable: 'tools available',
|
toolsAvailable: 'tools available',
|
||||||
mcpServers: 'MCP Servers',
|
mcpServers: 'MCP Servers',
|
||||||
customizeTheListOfMcpServers: 'Customize the list of MCP servers',
|
customizeTheListOfMcpServers: 'Customize the list of MCP servers',
|
||||||
|
cachedTools: 'cached tools',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
searchApps: 'Search Apps',
|
searchApps: 'Search Apps',
|
||||||
@ -1790,6 +1792,8 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
result: 'Result',
|
result: 'Result',
|
||||||
parseSummary: 'Parse Summary',
|
parseSummary: 'Parse Summary',
|
||||||
parseSummaryTip: 'Parser:deepdoc',
|
parseSummaryTip: 'Parser:deepdoc',
|
||||||
|
parserMethod: 'Parser Method',
|
||||||
|
outputFormat: 'Output Format',
|
||||||
rerunFromCurrentStep: 'Rerun From Current Step',
|
rerunFromCurrentStep: 'Rerun From Current Step',
|
||||||
rerunFromCurrentStepTip: 'Changes detected. Click to re-run.',
|
rerunFromCurrentStepTip: 'Changes detected. Click to re-run.',
|
||||||
confirmRerun: 'Confirm Rerun Process',
|
confirmRerun: 'Confirm Rerun Process',
|
||||||
|
|||||||
@ -6,8 +6,10 @@ export default {
|
|||||||
selectAll: '全选',
|
selectAll: '全选',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteModalTitle: '确定删除吗?',
|
deleteModalTitle: '确定删除吗?',
|
||||||
ok: '是',
|
ok: '确认',
|
||||||
cancel: '否',
|
cancel: '取消',
|
||||||
|
yes: '是',
|
||||||
|
no: '否',
|
||||||
total: '总共',
|
total: '总共',
|
||||||
rename: '重命名',
|
rename: '重命名',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@ -1631,6 +1633,7 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
|||||||
toolsAvailable: '可用的工具',
|
toolsAvailable: '可用的工具',
|
||||||
mcpServers: 'MCP 服务器',
|
mcpServers: 'MCP 服务器',
|
||||||
customizeTheListOfMcpServers: '自定义 MCP 服务器列表',
|
customizeTheListOfMcpServers: '自定义 MCP 服务器列表',
|
||||||
|
cachedTools: '缓存工具',
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
searchApps: '搜索',
|
searchApps: '搜索',
|
||||||
@ -1677,6 +1680,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
|||||||
result: '结果',
|
result: '结果',
|
||||||
parseSummary: '解析摘要',
|
parseSummary: '解析摘要',
|
||||||
parseSummaryTip: '解析器: deepdoc',
|
parseSummaryTip: '解析器: deepdoc',
|
||||||
|
parserMethod: '解析方法',
|
||||||
|
outputFormat: '输出格式',
|
||||||
rerunFromCurrentStep: '从当前步骤重新运行',
|
rerunFromCurrentStep: '从当前步骤重新运行',
|
||||||
rerunFromCurrentStepTip: '已修改,点击重新运行。',
|
rerunFromCurrentStepTip: '已修改,点击重新运行。',
|
||||||
confirmRerun: '确认重新运行流程',
|
confirmRerun: '确认重新运行流程',
|
||||||
|
|||||||
@ -124,8 +124,8 @@ export const ParsingStatusCell = ({ record }: IProps) => {
|
|||||||
onConfirm={handleOperationIconClick(true)}
|
onConfirm={handleOperationIconClick(true)}
|
||||||
onCancel={handleOperationIconClick(false)}
|
onCancel={handleOperationIconClick(false)}
|
||||||
disabled={record.chunk_num === 0}
|
disabled={record.chunk_num === 0}
|
||||||
okText={t('common.ok')}
|
okText={t('common.yes')}
|
||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.no')}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.operationIcon)}
|
className={classNames(styles.operationIcon)}
|
||||||
|
|||||||
@ -345,10 +345,10 @@ export const useSummaryInfo = (
|
|||||||
const { output_format, parse_method } = setups;
|
const { output_format, parse_method } = setups;
|
||||||
const res = [];
|
const res = [];
|
||||||
if (parse_method) {
|
if (parse_method) {
|
||||||
res.push(`${t('dataflow.parserMethod')}: ${parse_method}`);
|
res.push(`${t('dataflowParser.parserMethod')}: ${parse_method}`);
|
||||||
}
|
}
|
||||||
if (output_format) {
|
if (output_format) {
|
||||||
res.push(`${t('dataflow.outputFormat')}: ${output_format}`);
|
res.push(`${t('dataflowParser.outputFormat')}: ${output_format}`);
|
||||||
}
|
}
|
||||||
return res.join(' ');
|
return res.join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,24 @@
|
|||||||
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
export function Title({ children }: PropsWithChildren) {
|
export function Title({ children }: PropsWithChildren) {
|
||||||
return <span className="font-bold text-xl">{children}</span>;
|
return <span className="font-bold text-xl">{children}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfileSettingWrapperCardProps = {
|
||||||
|
header: React.ReactNode;
|
||||||
|
} & PropsWithChildren;
|
||||||
|
|
||||||
|
export function ProfileSettingWrapperCard({
|
||||||
|
header,
|
||||||
|
children,
|
||||||
|
}: ProfileSettingWrapperCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full mb-5 border-border-button bg-transparent">
|
||||||
|
<CardHeader className="border-b border-border-button p-5">
|
||||||
|
{header}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-5">{children}</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Collapse } from '@/components/collapse';
|
import { Collapse } from '@/components/collapse';
|
||||||
import { Button, ButtonLoading } from '@/components/ui/button';
|
import { Button, ButtonLoading } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
@ -121,36 +123,44 @@ export function EditMcpDialog({
|
|||||||
form={form}
|
form={form}
|
||||||
setFieldChanged={setFieldChanged}
|
setFieldChanged={setFieldChanged}
|
||||||
></EditMcpForm>
|
></EditMcpForm>
|
||||||
<Collapse
|
<Card>
|
||||||
title={
|
<CardContent className="p-3">
|
||||||
<div>
|
<Collapse
|
||||||
{nextTools?.length || 0} {t('mcp.toolsAvailable')}
|
title={
|
||||||
</div>
|
<div>
|
||||||
}
|
{nextTools?.length || 0} {t('mcp.toolsAvailable')}
|
||||||
open={collapseOpen}
|
</div>
|
||||||
onOpenChange={setCollapseOpen}
|
}
|
||||||
rightContent={
|
open={collapseOpen}
|
||||||
<Button
|
onOpenChange={setCollapseOpen}
|
||||||
variant={'ghost'}
|
rightContent={
|
||||||
form={FormId}
|
<Button
|
||||||
type="submit"
|
variant={'transparent'}
|
||||||
onClick={handleTest}
|
form={FormId}
|
||||||
|
type="submit"
|
||||||
|
onClick={handleTest}
|
||||||
|
className="border-none p-0 hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={cn('text-text-secondary', {
|
||||||
|
'animate-spin': testLoading,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<div className="overflow-auto max-h-80 divide-y bg-bg-card rounded-md px-2.5">
|
||||||
className={cn('text-accent-primary', {
|
{nextTools?.map((x) => (
|
||||||
'animate-spin': testLoading,
|
<McpToolCard key={x.name} data={x}></McpToolCard>
|
||||||
})}
|
))}
|
||||||
/>
|
</div>
|
||||||
</Button>
|
</Collapse>
|
||||||
}
|
</CardContent>
|
||||||
>
|
</Card>
|
||||||
<div className="space-y-2.5 overflow-auto max-h-80">
|
|
||||||
{nextTools?.map((x) => (
|
|
||||||
<McpToolCard key={x.name} data={x}></McpToolCard>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">{t('common.cancel')}</Button>
|
||||||
|
</DialogClose>
|
||||||
<ButtonLoading
|
<ButtonLoading
|
||||||
type="submit"
|
type="submit"
|
||||||
form={FormId}
|
form={FormId}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export function EditMcpForm({
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('common.name')}</FormLabel>
|
<FormLabel required>{t('common.name')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.mcp.namePlaceholder')}
|
placeholder={t('common.mcp.namePlaceholder')}
|
||||||
@ -106,7 +106,7 @@ export function EditMcpForm({
|
|||||||
name="url"
|
name="url"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('mcp.url')}</FormLabel>
|
<FormLabel required>{t('mcp.url')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.mcp.urlPlaceholder')}
|
placeholder={t('common.mcp.urlPlaceholder')}
|
||||||
@ -127,7 +127,7 @@ export function EditMcpForm({
|
|||||||
name="server_type"
|
name="server_type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('mcp.serverType')}</FormLabel>
|
<FormLabel required>{t('mcp.serverType')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RAGFlowSelect
|
<RAGFlowSelect
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { BulkOperateBar } from '@/components/bulk-operate-bar';
|
import { BulkOperateBar } from '@/components/bulk-operate-bar';
|
||||||
import { CardContainer } from '@/components/card-container';
|
import { CardContainer } from '@/components/card-container';
|
||||||
|
import Spotlight from '@/components/spotlight';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { SearchInput } from '@/components/ui/input';
|
import { SearchInput } from '@/components/ui/input';
|
||||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||||
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { Import, Plus } from 'lucide-react';
|
import { Plus, SquareArrowOutDownLeft } from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ProfileSettingWrapperCard } from '../components';
|
||||||
import { EditMcpDialog } from './edit-mcp-dialog';
|
import { EditMcpDialog } from './edit-mcp-dialog';
|
||||||
import { ImportMcpDialog } from './import-mcp-dialog';
|
import { ImportMcpDialog } from './import-mcp-dialog';
|
||||||
import { McpCard } from './mcp-card';
|
import { McpCard } from './mcp-card';
|
||||||
@ -33,27 +35,33 @@ export default function McpServer() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="p-4 w-full">
|
<ProfileSettingWrapperCard
|
||||||
<div className="text-text-primary text-2xl">{t('mcp.mcpServers')}</div>
|
header={
|
||||||
<section className="flex items-center justify-between pb-5">
|
<>
|
||||||
<div className="text-text-secondary">
|
<div className="text-text-primary text-2xl font-semibold">
|
||||||
{t('mcp.customizeTheListOfMcpServers')}
|
{t('mcp.mcpServers')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-5">
|
<section className="flex items-center justify-between">
|
||||||
<SearchInput
|
<div className="text-text-secondary">
|
||||||
className="w-40"
|
{t('mcp.customizeTheListOfMcpServers')}
|
||||||
value={searchString}
|
</div>
|
||||||
onChange={handleInputChange}
|
<div className="flex gap-5">
|
||||||
></SearchInput>
|
<SearchInput
|
||||||
<Button variant={'secondary'} onClick={showImportModal}>
|
className="w-40"
|
||||||
<Import /> {t('mcp.import')}
|
value={searchString}
|
||||||
</Button>
|
onChange={handleInputChange}
|
||||||
<Button onClick={showEditModal('')}>
|
></SearchInput>
|
||||||
<Plus /> {t('mcp.addMCP')}
|
<Button onClick={showEditModal('')}>
|
||||||
</Button>
|
<Plus /> {t('mcp.addMCP')}
|
||||||
</div>
|
</Button>
|
||||||
</section>
|
<Button variant={'secondary'} onClick={showImportModal}>
|
||||||
|
<SquareArrowOutDownLeft /> {t('mcp.import')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{selectedList.length > 0 && (
|
{selectedList.length > 0 && (
|
||||||
<BulkOperateBar
|
<BulkOperateBar
|
||||||
list={list}
|
list={list}
|
||||||
@ -93,6 +101,7 @@ export default function McpServer() {
|
|||||||
onOk={onImportOk}
|
onOk={onImportOk}
|
||||||
></ImportMcpDialog>
|
></ImportMcpDialog>
|
||||||
)}
|
)}
|
||||||
</section>
|
<Spotlight className="z-0" opcity={0.7} coverage={70} />
|
||||||
|
</ProfileSettingWrapperCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { IMcpServer } from '@/interfaces/database/mcp';
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import { isPlainObject } from 'lodash';
|
import { isPlainObject } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { McpDropdown } from './mcp-dropdown';
|
import { McpDropdown } from './mcp-dropdown';
|
||||||
import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp';
|
import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp';
|
||||||
import { UseEditMcpReturnType } from './use-edit-mcp';
|
import { UseEditMcpReturnType } from './use-edit-mcp';
|
||||||
@ -20,6 +21,7 @@ export function McpCard({
|
|||||||
handleSelectChange,
|
handleSelectChange,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
}: DatasetCardProps) {
|
}: DatasetCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const toolLength = useMemo(() => {
|
const toolLength = useMemo(() => {
|
||||||
const tools = data.variables?.tools;
|
const tools = data.variables?.tools;
|
||||||
if (isPlainObject(tools)) {
|
if (isPlainObject(tools)) {
|
||||||
@ -36,7 +38,9 @@ export function McpCard({
|
|||||||
<Card key={data.id}>
|
<Card key={data.id}>
|
||||||
<CardContent className="p-2.5 pt-2 group">
|
<CardContent className="p-2.5 pt-2 group">
|
||||||
<section className="flex justify-between pb-2">
|
<section className="flex justify-between pb-2">
|
||||||
<h3 className="text-lg font-semibold truncate flex-1">{data.name}</h3>
|
<h3 className="text-base font-normal truncate flex-1 text-text-primary">
|
||||||
|
{data.name}
|
||||||
|
</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<McpDropdown mcpId={data.id} showEditModal={showEditModal}>
|
<McpDropdown mcpId={data.id} showEditModal={showEditModal}>
|
||||||
<MoreButton></MoreButton>
|
<MoreButton></MoreButton>
|
||||||
@ -50,14 +54,12 @@ export function McpCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end text-xs text-text-secondary">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="text-base font-semibold mb-3 line-clamp-1 text-text-secondary">
|
<div className="line-clamp-1 pb-1">
|
||||||
{toolLength} cached tools
|
{toolLength} {t('mcp.cachedTools')}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-text-secondary">
|
<p>{formatDate(data.update_date)}</p>
|
||||||
{formatDate(data.update_date)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { IMCPTool } from '@/interfaces/database/mcp';
|
import { IMCPTool } from '@/interfaces/database/mcp';
|
||||||
|
|
||||||
export type McpToolCardProps = {
|
export type McpToolCardProps = {
|
||||||
@ -7,13 +6,11 @@ export type McpToolCardProps = {
|
|||||||
|
|
||||||
export function McpToolCard({ data }: McpToolCardProps) {
|
export function McpToolCard({ data }: McpToolCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<section className="group py-2.5">
|
||||||
<CardContent className="p-2.5 pt-2 group">
|
<h3 className="text-sm font-semibold line-clamp-1 pb-2">{data.name}</h3>
|
||||||
<h3 className="text-sm font-semibold line-clamp-1 pb-2">{data.name}</h3>
|
<div className="text-xs font-normal text-text-secondary">
|
||||||
<div className="text-xs font-normal mb-3 text-text-secondary">
|
{data.description}
|
||||||
{data.description}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,7 @@ const ProfilePage: FC = () => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-bg-base text-text-secondary p-5">
|
<div className="h-full bg-bg-base text-text-secondary p-5">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
||||||
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useLogout } from '@/hooks/login-hooks';
|
import { useLogout } from '@/hooks/login-hooks';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'umi';
|
import { useNavigate } from 'umi';
|
||||||
|
|
||||||
export const useHandleMenuClick = () => {
|
export const useHandleMenuClick = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [active, setActive] = useState<Routes>();
|
||||||
const { logout } = useLogout();
|
const { logout } = useLogout();
|
||||||
|
|
||||||
const handleMenuClick = useCallback(
|
const handleMenuClick = useCallback(
|
||||||
@ -12,11 +13,12 @@ export const useHandleMenuClick = () => {
|
|||||||
if (key === Routes.Logout) {
|
if (key === Routes.Logout) {
|
||||||
logout();
|
logout();
|
||||||
} else {
|
} else {
|
||||||
|
setActive(key);
|
||||||
navigate(`${Routes.ProfileSetting}${key}`);
|
navigate(`${Routes.ProfileSetting}${key}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[logout, navigate],
|
[logout, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleMenuClick };
|
return { handleMenuClick, active };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
|
import ThemeToggle from '@/components/theme-toggle';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import { ThemeEnum } from '@/constants/common';
|
|
||||||
import { useLogout } from '@/hooks/login-hooks';
|
import { useLogout } from '@/hooks/login-hooks';
|
||||||
import { useSecondPathName } from '@/hooks/route-hook';
|
import { useSecondPathName } from '@/hooks/route-hook';
|
||||||
|
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import {
|
import {
|
||||||
@ -12,11 +11,9 @@ import {
|
|||||||
Banknote,
|
Banknote,
|
||||||
Box,
|
Box,
|
||||||
FileCog,
|
FileCog,
|
||||||
LayoutGrid,
|
|
||||||
LogOut,
|
|
||||||
User,
|
User,
|
||||||
|
Users,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useHandleMenuClick } from './hooks';
|
import { useHandleMenuClick } from './hooks';
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
@ -24,7 +21,7 @@ const menuItems = [
|
|||||||
section: 'Account & collaboration',
|
section: 'Account & collaboration',
|
||||||
items: [
|
items: [
|
||||||
{ icon: User, label: 'Profile', key: Routes.Profile },
|
{ icon: User, label: 'Profile', key: Routes.Profile },
|
||||||
{ icon: LayoutGrid, label: 'Team', key: Routes.Team },
|
{ icon: Users, label: 'Team', key: Routes.Team },
|
||||||
{ icon: Banknote, label: 'Plan', key: Routes.Plan },
|
{ icon: Banknote, label: 'Plan', key: Routes.Plan },
|
||||||
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
|
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
|
||||||
],
|
],
|
||||||
@ -53,66 +50,62 @@ const menuItems = [
|
|||||||
|
|
||||||
export function SideBar() {
|
export function SideBar() {
|
||||||
const pathName = useSecondPathName();
|
const pathName = useSecondPathName();
|
||||||
const { handleMenuClick } = useHandleMenuClick();
|
const { data: userInfo } = useFetchUserInfo();
|
||||||
const { setTheme } = useTheme();
|
const { handleMenuClick, active } = useHandleMenuClick();
|
||||||
const isDarkTheme = useIsDarkTheme();
|
|
||||||
|
|
||||||
const { logout } = useLogout();
|
const { logout } = useLogout();
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
|
||||||
(checked: boolean) => {
|
|
||||||
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
|
|
||||||
},
|
|
||||||
[setTheme],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-[303px] bg-background border-r flex flex-col">
|
<aside className="w-[303px] bg-bg-base flex flex-col">
|
||||||
|
<div className="px-6 flex gap-2 items-center">
|
||||||
|
<RAGFlowAvatar
|
||||||
|
avatar={userInfo?.avatar}
|
||||||
|
name={userInfo?.nickname}
|
||||||
|
isPerson
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-text-primary">{userInfo?.email}</p>
|
||||||
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{menuItems.map((section, idx) => (
|
{menuItems.map((section, idx) => (
|
||||||
<div key={idx}>
|
<div key={idx}>
|
||||||
<h2 className="p-6 text-sm font-semibold">{section.section}</h2>
|
{/* <h2 className="p-6 text-sm font-semibold">{section.section}</h2> */}
|
||||||
{section.items.map((item, itemIdx) => {
|
{section.items.map((item, itemIdx) => {
|
||||||
const active = pathName === item.key;
|
const hoverKey = pathName === item.key;
|
||||||
return (
|
return (
|
||||||
<Button
|
<div key={itemIdx} className="mx-6 my-5 ">
|
||||||
key={itemIdx}
|
<Button
|
||||||
variant={active ? 'secondary' : 'ghost'}
|
variant={hoverKey ? 'secondary' : 'ghost'}
|
||||||
className={cn('w-full justify-start gap-2.5 p-6 relative')}
|
className={cn('w-full justify-start gap-2.5 p-3 relative', {
|
||||||
onClick={handleMenuClick(item.key)}
|
'bg-bg-card text-text-primary': active === item.key,
|
||||||
>
|
'bg-bg-base text-text-secondary': active !== item.key,
|
||||||
<item.icon className="w-6 h-6" />
|
})}
|
||||||
<span>{item.label}</span>
|
onClick={handleMenuClick(item.key)}
|
||||||
{active && (
|
>
|
||||||
|
<item.icon className="w-6 h-6" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
{/* {active && (
|
||||||
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
|
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
|
||||||
)}
|
)} */}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 mt-auto border-t">
|
<div className="p-6 mt-auto ">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<div className="flex items-center gap-2 mb-6 justify-end">
|
||||||
<Switch
|
<ThemeToggle />
|
||||||
id="dark-mode"
|
|
||||||
onCheckedChange={handleThemeChange}
|
|
||||||
checked={isDarkTheme}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="dark-mode" className="text-sm">
|
|
||||||
Dark
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full gap-3"
|
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogOut className="w-6 h-6" />
|
Log Out
|
||||||
Logout
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Outlet } from 'umi';
|
import { Outlet } from 'umi';
|
||||||
import SideBar from './sidebar';
|
import { SideBar } from './sidebar';
|
||||||
|
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import {
|
import {
|
||||||
|
|||||||
151
web/src/pages/user-setting/profile/hooks/use-profile.ts
Normal file
151
web/src/pages/user-setting/profile/hooks/use-profile.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// src/hooks/useProfile.ts
|
||||||
|
import {
|
||||||
|
useFetchUserInfo,
|
||||||
|
useSaveSetting,
|
||||||
|
} from '@/hooks/use-user-setting-request';
|
||||||
|
import { rsaPsw } from '@/utils';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ProfileData {
|
||||||
|
userName: string;
|
||||||
|
timeZone: string;
|
||||||
|
currPasswd?: string;
|
||||||
|
newPasswd?: string;
|
||||||
|
avatar: string;
|
||||||
|
email: string;
|
||||||
|
confirmPasswd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditType = {
|
||||||
|
editName: 'editName',
|
||||||
|
editTimeZone: 'editTimeZone',
|
||||||
|
editPassword: 'editPassword',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IEditType = keyof typeof EditType;
|
||||||
|
|
||||||
|
export const modalTitle = {
|
||||||
|
[EditType.editName]: 'Edit Name',
|
||||||
|
[EditType.editTimeZone]: 'Edit Time Zone',
|
||||||
|
[EditType.editPassword]: 'Edit Password',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useProfile = () => {
|
||||||
|
const { data: userInfo } = useFetchUserInfo();
|
||||||
|
const [profile, setProfile] = useState<ProfileData>({
|
||||||
|
userName: '',
|
||||||
|
avatar: '',
|
||||||
|
timeZone: '',
|
||||||
|
email: '',
|
||||||
|
currPasswd: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [editType, setEditType] = useState<IEditType>(EditType.editName);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editForm, setEditForm] = useState<Partial<ProfileData>>({});
|
||||||
|
const {
|
||||||
|
saveSetting,
|
||||||
|
loading: submitLoading,
|
||||||
|
data: saveSettingData,
|
||||||
|
} = useSaveSetting();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// form.setValue('currPasswd', ''); // current password
|
||||||
|
const profile = {
|
||||||
|
userName: userInfo.nickname,
|
||||||
|
timeZone: userInfo.timezone,
|
||||||
|
avatar: userInfo.avatar || '',
|
||||||
|
email: userInfo.email,
|
||||||
|
currPasswd: userInfo.password,
|
||||||
|
};
|
||||||
|
setProfile(profile);
|
||||||
|
}, [userInfo, setProfile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (saveSettingData === 0) {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditForm({});
|
||||||
|
}
|
||||||
|
}, [saveSettingData]);
|
||||||
|
const onSubmit = (newProfile: ProfileData) => {
|
||||||
|
const payload: Partial<{
|
||||||
|
nickname: string;
|
||||||
|
password: string;
|
||||||
|
new_password: string;
|
||||||
|
avatar: string;
|
||||||
|
timezone: string;
|
||||||
|
}> = {
|
||||||
|
nickname: newProfile.userName,
|
||||||
|
avatar: newProfile.avatar,
|
||||||
|
timezone: newProfile.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
'currPasswd' in newProfile &&
|
||||||
|
'newPasswd' in newProfile &&
|
||||||
|
newProfile.currPasswd &&
|
||||||
|
newProfile.newPasswd
|
||||||
|
) {
|
||||||
|
payload.password = rsaPsw(newProfile.currPasswd!) as string;
|
||||||
|
payload.new_password = rsaPsw(newProfile.newPasswd!) as string;
|
||||||
|
}
|
||||||
|
console.log('payload', payload);
|
||||||
|
if (editType === EditType.editName && payload.nickname) {
|
||||||
|
saveSetting({ nickname: payload.nickname });
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
if (editType === EditType.editTimeZone && payload.timezone) {
|
||||||
|
saveSetting({ timezone: payload.timezone });
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
if (editType === EditType.editPassword && payload.password) {
|
||||||
|
saveSetting({
|
||||||
|
password: payload.password,
|
||||||
|
new_password: payload.new_password,
|
||||||
|
});
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
// saveSetting(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = useCallback(
|
||||||
|
(type: IEditType) => {
|
||||||
|
setEditForm(profile);
|
||||||
|
setEditType(type);
|
||||||
|
setIsEditing(true);
|
||||||
|
},
|
||||||
|
[profile],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditForm({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = (data: ProfileData) => {
|
||||||
|
console.log('handleSave', data);
|
||||||
|
const newProfile = { ...profile, ...data };
|
||||||
|
|
||||||
|
onSubmit(newProfile);
|
||||||
|
// setIsEditing(false);
|
||||||
|
// setEditForm({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarUpload = (avatar: string) => {
|
||||||
|
setProfile((prev) => ({ ...prev, avatar }));
|
||||||
|
saveSetting({ avatar });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
setProfile,
|
||||||
|
submitLoading: submitLoading,
|
||||||
|
isEditing,
|
||||||
|
editType,
|
||||||
|
editForm,
|
||||||
|
handleEditClick,
|
||||||
|
handleCancel,
|
||||||
|
handleSave,
|
||||||
|
handleAvatarUpload,
|
||||||
|
};
|
||||||
|
};
|
||||||
413
web/src/pages/user-setting/profile/index.tsx
Normal file
413
web/src/pages/user-setting/profile/index.tsx
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
// src/components/ProfilePage.tsx
|
||||||
|
import { AvatarUpload } from '@/components/avatar-upload';
|
||||||
|
import PasswordInput from '@/components/originui/password-input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { TimezoneList } from '@/pages/user-setting/constants';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { Loader2Icon, PenLine } from 'lucide-react';
|
||||||
|
import { FC, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
|
||||||
|
|
||||||
|
const baseSchema = z.object({
|
||||||
|
userName: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t('setting.usernameMessage') })
|
||||||
|
.trim(),
|
||||||
|
timeZone: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: t('setting.timezonePlaceholder') }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameSchema = baseSchema.extend({
|
||||||
|
currPasswd: z.string().optional(),
|
||||||
|
newPasswd: z.string().optional(),
|
||||||
|
confirmPasswd: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordSchema = baseSchema
|
||||||
|
.extend({
|
||||||
|
currPasswd: z
|
||||||
|
.string({
|
||||||
|
required_error: t('setting.currentPasswordMessage'),
|
||||||
|
})
|
||||||
|
.trim(),
|
||||||
|
newPasswd: z
|
||||||
|
.string({
|
||||||
|
required_error: t('setting.newPasswordMessage'),
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: t('setting.newPasswordDescription') }),
|
||||||
|
confirmPasswd: z
|
||||||
|
.string({
|
||||||
|
required_error: t('setting.confirmPasswordMessage'),
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: t('setting.newPasswordDescription') }),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (
|
||||||
|
data.newPasswd &&
|
||||||
|
data.confirmPasswd &&
|
||||||
|
data.newPasswd !== data.confirmPasswd
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ['confirmPasswd'],
|
||||||
|
message: t('setting.confirmPasswordNonMatchMessage'),
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const ProfilePage: FC = () => {
|
||||||
|
const { t } = useTranslate('setting');
|
||||||
|
|
||||||
|
const {
|
||||||
|
profile,
|
||||||
|
editType,
|
||||||
|
isEditing,
|
||||||
|
submitLoading,
|
||||||
|
editForm,
|
||||||
|
handleEditClick,
|
||||||
|
handleCancel,
|
||||||
|
handleSave,
|
||||||
|
handleAvatarUpload,
|
||||||
|
} = useProfile();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof baseSchema | typeof passwordSchema>>({
|
||||||
|
resolver: zodResolver(
|
||||||
|
editType === EditType.editPassword ? passwordSchema : nameSchema,
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
userName: '',
|
||||||
|
timeZone: '',
|
||||||
|
},
|
||||||
|
// shouldUnregister: true,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({ ...editForm, currPasswd: undefined });
|
||||||
|
}, [editForm, form]);
|
||||||
|
|
||||||
|
// const ModalContent: FC = () => {
|
||||||
|
// // let content = null;
|
||||||
|
// // if (editType === EditType.editName) {
|
||||||
|
// // content = editName();
|
||||||
|
// // }
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full bg-bg-base text-text-secondary p-5">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
||||||
|
<div className="text-sm text-text-secondary mb-6">
|
||||||
|
{t('profileDescription')}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="max-w-3xl space-y-11 w-3/4">
|
||||||
|
{/* Name */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium">
|
||||||
|
{t('username')}
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 flex items-center gap-4 min-w-60">
|
||||||
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
|
{profile.userName}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditClick(EditType.editName)}
|
||||||
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
|
>
|
||||||
|
<PenLine size={12} /> Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium">{t('avatar')}</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<AvatarUpload
|
||||||
|
value={profile.avatar}
|
||||||
|
onChange={handleAvatarUpload}
|
||||||
|
tips={'This will be displayed on your profile.'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time Zone */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium">
|
||||||
|
{t('timezone')}
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
|
{profile.timeZone}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditClick(EditType.editTimeZone)}
|
||||||
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
|
>
|
||||||
|
<PenLine size={12} /> Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Address */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium"> {t('email')}</label>
|
||||||
|
<div className="flex-1 flex flex-col items-start gap-2">
|
||||||
|
<div className="text-sm text-text-primary flex-1 rounded-md py-1.5 ">
|
||||||
|
{profile.email}
|
||||||
|
</div>
|
||||||
|
<span className="text-text-secondary text-xs">
|
||||||
|
{t('emailDescription')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium">
|
||||||
|
{t('password')}
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
|
{profile.currPasswd ? '********' : ''}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditClick(EditType.editPassword)}
|
||||||
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
|
>
|
||||||
|
<PenLine size={12} /> Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editType && (
|
||||||
|
<Modal
|
||||||
|
title={modalTitle[editType]}
|
||||||
|
open={isEditing}
|
||||||
|
showfooter={false}
|
||||||
|
titleClassName="text-base"
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="!w-[480px]"
|
||||||
|
>
|
||||||
|
{/* <ModalContent /> */}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit((data) => handleSave(data as any))}
|
||||||
|
className="flex flex-col mt-6 mb-8 ml-2 space-y-6 "
|
||||||
|
>
|
||||||
|
{editType === EditType.editName && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="userName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" items-center space-y-0 ">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
|
||||||
|
{t('username')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
{...field}
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<div className="w-1/4"></div>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editType === EditType.editTimeZone && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="timeZone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="items-center space-y-0">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
|
||||||
|
{t('timezone')}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl className="w-full bg-bg-input border-border-default">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a timeZone" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{TimezoneList.map((timeStr) => (
|
||||||
|
<SelectItem key={timeStr} value={timeStr}>
|
||||||
|
{timeStr}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<div className="w-1/4"></div>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editType === EditType.editPassword && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="currPasswd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="items-center space-y-0">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel
|
||||||
|
required
|
||||||
|
className="text-sm flex justify-between text-text-secondary whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t('currentPassword')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="newPasswd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" items-center space-y-0">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel
|
||||||
|
required
|
||||||
|
className="text-sm text-text-secondary whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t('newPassword')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
autoComplete="new-password"
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPasswd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" items-center space-y-0">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel
|
||||||
|
required
|
||||||
|
className="text-sm text-text-secondary whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t('confirmPassword')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onBlur={() => {
|
||||||
|
form.trigger('confirmPasswd');
|
||||||
|
}}
|
||||||
|
onChange={(ev) => {
|
||||||
|
form.setValue(
|
||||||
|
'confirmPasswd',
|
||||||
|
ev.target.value.trim(),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full text-right space-x-4 !mt-11">
|
||||||
|
<Button type="reset" variant="secondary" onClick={handleCancel}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={submitLoading}>
|
||||||
|
{submitLoading && <Loader2Icon className="animate-spin" />}
|
||||||
|
{t('save', { keyPrefix: 'common' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
24
web/src/pages/user-setting/sidebar/hooks.tsx
Normal file
24
web/src/pages/user-setting/sidebar/hooks.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useLogout } from '@/hooks/login-hooks';
|
||||||
|
import { Routes } from '@/routes';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate } from 'umi';
|
||||||
|
|
||||||
|
export const useHandleMenuClick = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [active, setActive] = useState<Routes>();
|
||||||
|
const { logout } = useLogout();
|
||||||
|
|
||||||
|
const handleMenuClick = useCallback(
|
||||||
|
(key: Routes) => () => {
|
||||||
|
if (key === Routes.Logout) {
|
||||||
|
logout();
|
||||||
|
} else {
|
||||||
|
setActive(key);
|
||||||
|
navigate(`${Routes.UserSetting}${key}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[logout, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleMenuClick, active };
|
||||||
|
};
|
||||||
@ -1,5 +0,0 @@
|
|||||||
.sideBarWrapper {
|
|
||||||
.version {
|
|
||||||
color: rgb(17, 206, 17);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +1,109 @@
|
|||||||
|
import { IconFontFill } from '@/components/icon-font';
|
||||||
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
|
import ThemeToggle from '@/components/theme-toggle';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Domain } from '@/constants/common';
|
import { Domain } from '@/constants/common';
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
|
||||||
import { useLogout } from '@/hooks/login-hooks';
|
import { useLogout } from '@/hooks/login-hooks';
|
||||||
import { useSecondPathName } from '@/hooks/route-hook';
|
import { useSecondPathName } from '@/hooks/route-hook';
|
||||||
import { useFetchSystemVersion } from '@/hooks/user-setting-hooks';
|
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import { Flex, Menu } from 'antd';
|
|
||||||
import React, { useEffect, useMemo } from 'react';
|
|
||||||
import { useNavigate } from 'umi';
|
|
||||||
import {
|
import {
|
||||||
UserSettingBaseKey,
|
useFetchSystemVersion,
|
||||||
UserSettingIconMap,
|
useFetchUserInfo,
|
||||||
UserSettingRouteKey,
|
} from '@/hooks/use-user-setting-request';
|
||||||
} from '../constants';
|
import { cn } from '@/lib/utils';
|
||||||
import styles from './index.less';
|
import { Routes } from '@/routes';
|
||||||
|
import { Banknote, Box, Cog, Unplug, User, Users } from 'lucide-react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useHandleMenuClick } from './hooks';
|
||||||
|
|
||||||
type MenuItem = Required<MenuProps>['items'][number];
|
const menuItems = [
|
||||||
|
{ icon: User, label: 'Profile', key: Routes.Profile },
|
||||||
|
{ icon: Users, label: 'Team', key: Routes.Team },
|
||||||
|
{ icon: Box, label: 'Model Providers', key: Routes.Model },
|
||||||
|
{ icon: Unplug, label: 'API', key: Routes.Api },
|
||||||
|
// {
|
||||||
|
// icon: MessageSquareQuote,
|
||||||
|
// label: 'Prompt Templates',
|
||||||
|
// key: Routes.Profile,
|
||||||
|
// },
|
||||||
|
// { icon: TextSearch, label: 'Retrieval Templates', key: Routes.Profile },
|
||||||
|
{ icon: Cog, label: 'System', key: Routes.System },
|
||||||
|
// { icon: Banknote, label: 'Plan', key: Routes.Plan },
|
||||||
|
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
|
||||||
|
];
|
||||||
|
|
||||||
const SideBar = () => {
|
export function SideBar() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const pathName = useSecondPathName();
|
const pathName = useSecondPathName();
|
||||||
const { logout } = useLogout();
|
const { data: userInfo } = useFetchUserInfo();
|
||||||
const { t } = useTranslate('setting');
|
const { handleMenuClick, active } = useHandleMenuClick();
|
||||||
const { version, fetchSystemVersion } = useFetchSystemVersion();
|
const { version, fetchSystemVersion } = useFetchSystemVersion();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.host !== Domain) {
|
if (location.host !== Domain) {
|
||||||
fetchSystemVersion();
|
fetchSystemVersion();
|
||||||
}
|
}
|
||||||
}, [fetchSystemVersion]);
|
}, [fetchSystemVersion]);
|
||||||
|
const { logout } = useLogout();
|
||||||
function getItem(
|
|
||||||
label: string,
|
|
||||||
key: React.Key,
|
|
||||||
icon?: React.ReactNode,
|
|
||||||
children?: MenuItem[],
|
|
||||||
type?: 'group',
|
|
||||||
): MenuItem {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
icon,
|
|
||||||
children,
|
|
||||||
label: (
|
|
||||||
<Flex justify={'space-between'}>
|
|
||||||
{t(label)}
|
|
||||||
<span className={styles.version}>
|
|
||||||
{label === 'system' && version}
|
|
||||||
</span>
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
type,
|
|
||||||
} as MenuItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: MenuItem[] = Object.values(UserSettingRouteKey).map((value) =>
|
|
||||||
getItem(value, value, UserSettingIconMap[value]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
|
||||||
if (key === UserSettingRouteKey.Logout) {
|
|
||||||
logout();
|
|
||||||
} else {
|
|
||||||
navigate(`/${UserSettingBaseKey}/${key}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedKeys = useMemo(() => {
|
|
||||||
return [pathName];
|
|
||||||
}, [pathName]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.sideBarWrapper}>
|
<aside className="w-[303px] bg-bg-base flex flex-col">
|
||||||
<Menu
|
<div className="px-6 flex gap-2 items-center">
|
||||||
selectedKeys={selectedKeys}
|
<RAGFlowAvatar
|
||||||
mode="inline"
|
avatar={userInfo?.avatar}
|
||||||
items={items}
|
name={userInfo?.nickname}
|
||||||
onClick={handleMenuClick}
|
isPerson
|
||||||
style={{ width: 312 }}
|
/>
|
||||||
/>
|
<p className="text-sm text-text-primary">{userInfo?.email}</p>
|
||||||
</section>
|
</div>
|
||||||
);
|
<div className="flex-1 overflow-auto">
|
||||||
};
|
{menuItems.map((item, idx) => {
|
||||||
|
const hoverKey = pathName === item.key;
|
||||||
|
return (
|
||||||
|
<div key={idx}>
|
||||||
|
<div key={idx} className="mx-6 my-5 ">
|
||||||
|
<Button
|
||||||
|
variant={hoverKey ? 'secondary' : 'ghost'}
|
||||||
|
className={cn('w-full justify-between gap-2.5 p-3 relative', {
|
||||||
|
'bg-bg-card text-text-primary': active === item.key,
|
||||||
|
'bg-bg-base text-text-secondary': active !== item.key,
|
||||||
|
})}
|
||||||
|
onClick={handleMenuClick(item.key)}
|
||||||
|
>
|
||||||
|
<section className="flex items-center gap-2.5">
|
||||||
|
{item.key === Routes.Mcp ? (
|
||||||
|
<IconFontFill name={'mcp'} className="size-4 w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="w-6 h-6" />
|
||||||
|
)}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</section>
|
||||||
|
{item.key === Routes.System && (
|
||||||
|
<div className="mr-2 px-2 bg-accent-primary-5 text-accent-primary rounded-md">
|
||||||
|
{version}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* {active && (
|
||||||
|
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
|
||||||
|
)} */}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
export default SideBar;
|
<div className="p-6 mt-auto ">
|
||||||
|
<div className="flex items-center gap-2 mb-6 justify-end">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -18,9 +18,11 @@ export enum Routes {
|
|||||||
Files = '/files',
|
Files = '/files',
|
||||||
ProfileSetting = '/profile-setting',
|
ProfileSetting = '/profile-setting',
|
||||||
Profile = '/profile',
|
Profile = '/profile',
|
||||||
|
Api = '/api',
|
||||||
Mcp = '/mcp',
|
Mcp = '/mcp',
|
||||||
Team = '/team',
|
Team = '/team',
|
||||||
Plan = '/plan',
|
Plan = '/plan',
|
||||||
|
System = '/system',
|
||||||
Model = '/model',
|
Model = '/model',
|
||||||
Prompt = '/prompt',
|
Prompt = '/prompt',
|
||||||
ProfileMcp = `${ProfileSetting}${Mcp}`,
|
ProfileMcp = `${ProfileSetting}${Mcp}`,
|
||||||
@ -362,7 +364,7 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/user-setting/profile',
|
path: '/user-setting/profile',
|
||||||
// component: '@/pages/user-setting/setting-profile',
|
// component: '@/pages/user-setting/setting-profile',
|
||||||
component: '@/pages/user-setting/setting-profile',
|
component: '@/pages/user-setting/profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user-setting/locale',
|
path: '/user-setting/locale',
|
||||||
@ -381,11 +383,11 @@ const routes = [
|
|||||||
component: '@/pages/user-setting/setting-team',
|
component: '@/pages/user-setting/setting-team',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user-setting/system',
|
path: `/user-setting${Routes.System}`,
|
||||||
component: '@/pages/user-setting/setting-system',
|
component: '@/pages/user-setting/setting-system',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user-setting/api',
|
path: `/user-setting${Routes.Api}`,
|
||||||
component: '@/pages/user-setting/setting-api',
|
component: '@/pages/user-setting/setting-api',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user