Compare commits

...

8 Commits

Author SHA1 Message Date
3ee9653170 Agent template: report agent using knowledge base (#9427)
### What problem does this PR solve?

Agent template: report agent using knowledge base
### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-14 12:17:57 +08:00
6d1078b538 fix 'KeyError: "There is no item named 'word/NULL' in the archive"' (#9455)
### What problem does this PR solve?

Issue referring to:
https://github.com/python-openxml/python-docx/issues/797
Fix referring to:
https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-14 12:14:03 +08:00
6e862553cb Docs: Deprecated 'Create session with agent' (#9464)
### What problem does this PR solve?


### Type of change

- [x] Documentation Update
2025-08-14 12:13:11 +08:00
b1baa91ff0 feat(next-search): Implements document preview functionality #3221 (#9465)
### What problem does this PR solve?

feat(next-search): Implements document preview functionality

- Adds a new document preview modal component
- Implements document preview page logic
- Adds document preview-related hooks
- Optimizes document preview rendering logic
### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-14 12:11:53 +08:00
b55c3d07dc Test: Update error message assertions for chunk update tests (#9468)
### What problem does this PR solve?

Modify test cases to accept additional error message format when
updating chunks.
fix actions:
https://github.com/infiniflow/ragflow/actions/runs/16942741621/job/48015850297

### Type of change

- [x] Update test cases
2025-08-14 12:11:20 +08:00
2b3318cd3d Fix: KB folder may not there while creating virtual file (#9431)
### What problem does this PR solve?

KB folder may not there while creating virtual file. #9423 

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-14 09:40:30 +08:00
434b55be70 Feat: Display a separate chat multi-model comparison page #3221 (#9461)
### What problem does this PR solve?
Feat: Display a separate chat multi-model comparison page #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-14 09:39:20 +08:00
98b4c67292 Trival. (#9460)
### What problem does this PR solve?


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-14 09:39:00 +08:00
57 changed files with 1896 additions and 207 deletions

View File

@ -0,0 +1,327 @@
{
"id": 20,
"title": "Report Agent Using Knowledge Base",
"description": "A report generation assistant using local knowledge base, with advanced capabilities in task planning, reasoning, and reflective analysis. Recommended for academic research paper Q&A",
"canvas_type": "Agent",
"dsl": {
"components": {
"Agent:NewPumasLick": {
"downstream": [
"Message:OrangeYearsShine"
],
"obj": {
"component_name": "Agent",
"params": {
"delay_after_error": 1,
"description": "",
"exception_comment": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": null,
"frequencyPenaltyEnabled": false,
"frequency_penalty": 0.5,
"llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
"maxTokensEnabled": true,
"max_retries": 3,
"max_rounds": 3,
"max_tokens": 128000,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"parameter": "Precise",
"presencePenaltyEnabled": false,
"presence_penalty": 0.5,
"prompts": [
{
"content": "# User Query\n {sys.query}",
"role": "user"
}
],
"sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
"temperature": "0.1",
"temperatureEnabled": true,
"tools": [
{
"component_name": "Retrieval",
"name": "Retrieval",
"params": {
"cross_languages": [],
"description": "",
"empty_response": "",
"kb_ids": [],
"keywords_similarity_weight": 0.7,
"outputs": {
"formalized_content": {
"type": "string",
"value": ""
}
},
"rerank_id": "",
"similarity_threshold": 0.2,
"top_k": 1024,
"top_n": 8,
"use_kg": false
}
}
],
"topPEnabled": false,
"top_p": 0.75,
"user_prompt": "",
"visual_files_var": ""
}
},
"upstream": [
"begin"
]
},
"Message:OrangeYearsShine": {
"downstream": [],
"obj": {
"component_name": "Message",
"params": {
"content": [
"{Agent:NewPumasLick@content}"
]
}
},
"upstream": [
"Agent:NewPumasLick"
]
},
"begin": {
"downstream": [
"Agent:NewPumasLick"
],
"obj": {
"component_name": "Begin",
"params": {
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
}
},
"upstream": []
}
},
"globals": {
"sys.conversation_turns": 0,
"sys.files": [],
"sys.query": "",
"sys.user_id": ""
},
"graph": {
"edges": [
{
"data": {
"isHovered": false
},
"id": "xy-edge__beginstart-Agent:NewPumasLickend",
"source": "begin",
"sourceHandle": "start",
"target": "Agent:NewPumasLick",
"targetHandle": "end"
},
{
"data": {
"isHovered": false
},
"id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
"markerEnd": "logo",
"source": "Agent:NewPumasLick",
"sourceHandle": "start",
"style": {
"stroke": "rgba(91, 93, 106, 1)",
"strokeWidth": 1
},
"target": "Message:OrangeYearsShine",
"targetHandle": "end",
"type": "buttonEdge",
"zIndex": 1001
},
{
"data": {
"isHovered": false
},
"id": "xy-edge__Agent:NewPumasLicktool-Tool:AllBirdsNailend",
"selected": false,
"source": "Agent:NewPumasLick",
"sourceHandle": "tool",
"target": "Tool:AllBirdsNail",
"targetHandle": "end"
}
],
"nodes": [
{
"data": {
"form": {
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
},
"label": "Begin",
"name": "begin"
},
"dragging": false,
"id": "begin",
"measured": {
"height": 48,
"width": 200
},
"position": {
"x": -9.569875358221438,
"y": 205.84018385864917
},
"selected": false,
"sourcePosition": "left",
"targetPosition": "right",
"type": "beginNode"
},
{
"data": {
"form": {
"content": [
"{Agent:NewPumasLick@content}"
]
},
"label": "Message",
"name": "Response"
},
"dragging": false,
"id": "Message:OrangeYearsShine",
"measured": {
"height": 56,
"width": 200
},
"position": {
"x": 734.4061285881053,
"y": 199.9706031723009
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "messageNode"
},
{
"data": {
"form": {
"delay_after_error": 1,
"description": "",
"exception_comment": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": null,
"frequencyPenaltyEnabled": false,
"frequency_penalty": 0.5,
"llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
"maxTokensEnabled": true,
"max_retries": 3,
"max_rounds": 3,
"max_tokens": 128000,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"parameter": "Precise",
"presencePenaltyEnabled": false,
"presence_penalty": 0.5,
"prompts": [
{
"content": "# User Query\n {sys.query}",
"role": "user"
}
],
"sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
"temperature": "0.1",
"temperatureEnabled": true,
"tools": [
{
"component_name": "Retrieval",
"name": "Retrieval",
"params": {
"cross_languages": [],
"description": "",
"empty_response": "",
"kb_ids": [],
"keywords_similarity_weight": 0.7,
"outputs": {
"formalized_content": {
"type": "string",
"value": ""
}
},
"rerank_id": "",
"similarity_threshold": 0.2,
"top_k": 1024,
"top_n": 8,
"use_kg": false
}
}
],
"topPEnabled": false,
"top_p": 0.75,
"user_prompt": "",
"visual_files_var": ""
},
"label": "Agent",
"name": "Knowledge Base Agent"
},
"dragging": false,
"id": "Agent:NewPumasLick",
"measured": {
"height": 84,
"width": 200
},
"position": {
"x": 347.00048227952215,
"y": 186.49109364794631
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "agentNode"
},
{
"data": {
"form": {
"description": "This is an agent for a specific task.",
"user_prompt": "This is the order you need to send to the agent."
},
"label": "Tool",
"name": "flow.tool_10"
},
"dragging": false,
"id": "Tool:AllBirdsNail",
"measured": {
"height": 48,
"width": 200
},
"position": {
"x": 220.24819746977118,
"y": 403.31576836482583
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "toolNode"
}
]
},
"history": [],
"memory": [],
"messages": [],
"path": [],
"retrieval": []
},
"avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAH0klEQVR4nO2ZC1BU1wGG/3uRp/IygG+DGK0GOjE1U6cxI4tT03Y0E+kENbaJbKpj60wzgNMwnTjuEtu0miGasY+0krI202kMVEnVxtoOLG00oVa0LajVBDcSEI0REFBgkZv/3GWXfdzdvctuHs7kmzmec9//d+45914XCXc4Xwjk1+59VJGGF7C5QAFSWBvgyWmWLl7IKiny6QNL173B5YjB84bOyrpKA4B1DLySdQpLKAiZGtZ7a/KMVoQJz6UfEZyhTWwaEBmssiLvCueu6BJg8EwFqGTTAC+uvNWC9w82sRWcux/JwaSHstjywcogRt4RG0KExwWG4QsVYCebKSwe3L5lR9OOWjyzfg2WL/0a1/jncO3b2FHxGnKeWYqo+Giu8UEMrWJKWBACPMY/DG+63txhvnKshUu+DF2/hayMDFRsL+VScDb++AVc6OjAuInxXPJl2tfnIikrzUyJMi7qQmLRhOEr2fOFbX/7P6STF7BqoWevfdij4NWGQfx+57OYO2sG1wSnsek8Nm15EU8sikF6ouelXz9ph7JwDqYt+5IIZaGEkauDIrH4wPBmhjexCSEws+VdVG1M4NIoj+2xYzBuJtavWcEl/VS8dggx/ZdQvcGzQwp+cxOXsu5RBQQMVkYJM4LA/Txh+ELFMWFVPARS5kFiabZdx8Olh7l17BzdvhzZmROhdJ3j6D/nIyBgOCMlLAgA9xmF4TMV4BSbrgnrLiBl5rOsRCRRbDUsBzQFiJjY91PCBj9w+yiP1lXWsTLAjc9YQGB9I8+Yx1oTiUWFvW9QgDo2PdASaDp/EQ8/sRnhcPTVcuTMncXwQQVESL9DidscaPW+QEtAICRu9PSxFTpJiePV8AI9AsTvXZBY/Pa+wJ9ApNApIILm8S5Y4QXXQwhYFH6csemDP4G3G5v579i5d04mknknQhDYS4HCrCVr/mC3D305KnbCEpvVIia5Onw6WaWw+KAl0Np+FUXbdiMcyoqfUoeRHoFrJ1uRtnBG1/9Mf/3LtElp+VwF2wcd7woJib1vUPwMH4GWQCQJJtBa/V9cPmFD8uQUpMdNGDhY8bNYrobh8acHu270/l0ImJWRt64Wn6WACN9z5gq2lXwPW8pfweT0icP/fH23vO9QLYq3/QKyLBmFQI3CUcT9NdESEEPItKsSN3r7MBaSJoxHWZERM6ZmMLy2gDP8/pd/og418dTL37hFSUpMUC5f+UiWZcnY9s5+ixCwUiCXx2iiJdDNx6f4pgkH8Q3lbxK7h8+enoHha1cRNdMp8axiHxo6+/5bVdk8DSROYIW1X7QEIom3wHD3gEf4vu1bVYEJZeWQ0zJQvmcfyiv2QZak6raG/QWfK4Ez9mTc5v8xPMJfuojoxXmIX/9DOMe+FCWbcHu4BJJ0YEwCx0824bFNW9HesB+CqYu+jepfPYcHF+aoPXS8sQl/+vU2bgmOU2C+qRc9/YrrPPbGBtzavd0nvCxLxui4pJrBm911PFwak4CYA80cj+JCAiGUzYkmxrSY4N2c3GLi6UEIFL/wRxxqkhmHnTEpDQcrfq6ea+hcE8bNy3GFzyq4H22HW1Kd4WMSkg1jmsSRpKj0Rzhy4gNUv/y8Gjrv8SJK3OWScA+fMn/ysVPPvTmeh6nh1TcxBUJ+jEaKYr7N36x7h+Edj0pB6+WrLokn87+BrTt/p4ZPzZ6MM7/8R2//h33vOcNzdwgBMwVMbGvySQmo4a0NqOZccU7YmGXLEfPQUlUid/XT6B8YdIU/99vjsPcOdEhDsfOd4QVCwKB8yp8SWuG1njbTl83DpMWz1PCKAswuWPDI0e8WebyAJBbxNdrF7cls+hBpAb3h3XtehL/3+4u7D35rQwpP4YFTwMJ91rHpQyQFQgmf9sAMNL9Ur4afv/FBjIuPVj+n4YVTwMD96tj0IVICoYYXv/q1VJ1Sl8UveQyaRwErvOB6B5SwKhqP00gI6A0vhsycJ7/KIzxhyHqGN0ADbnNAAYOicRfCFdAb/p50Gbfuc/wy5w1D5lOghk0fuG0USlgVr7sQjoDe8C8WxKGKPy2KjzlvAQb02/sCbh+FApngX1QUtyeSuwDi0hxFByV7L+LIf3r5kvpp4PBr07Hqvn71Y85bgOG6WS2ggA1+4D6eUKKQApVsqngI6KSkqh9HzsoM/3zg8Oz5VQ9E8wjf30YFDGdkeAsCwH18oYRZGXk7C4HuYxcwe6rjQsFovzaEvoFxqNkTOPzMjGikJso8wsF77XYkLx6dAwxWxvBmBIH7aUMJi8J3w0DnTVz7dyvX6KPzVBt+kL8cmzesRq9ps2Z48bRJmOIapS7E4zM2lXNt5CcU6ID7+ocSZkqY2NRN6ysnsHbJEpR8ZwV6t5Yg+iuLELf2KVd48VwXQf3BQGUMb4ZOuH9gKFEIYJfiNrEDcXZHHV4q3YRv5i7ikgM94RlETNgihrcgBHhccCiRCf7VhBK5rAPyr9I/Y/WKPEyfksH/9NjQ2dODhsYzwcLXsypkeBtCRGLRDUUMAMyKHxEx4dtrzyP97nQMygripiQiKi4aSbPvQmKW7+OXF69ntYvBa1iPCYklZEZECsGm4ja0Ops7EJsaj4SprlU+8IJiqIjAFga3Ikx4vvAYkTGALxyWFArlsnbBC9Sz6mI5zWKNRGh3JJY7mjte4GOz+r4tkRbxQQAAAABJRU5ErkJggg=="
}

View File

@ -206,7 +206,7 @@
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "Hi! I'm your SQL assistant, what can I do for you?"
"prologue": "Hi! I'm your SQL assistant. What can I do for you?"
}
},
"upstream": []
@ -319,7 +319,7 @@
"enablePrologue": true,
"inputs": {},
"mode": "conversational",
"prologue": "Hi! I'm your SQL assistant, what can I do for you?"
"prologue": "Hi! I'm your SQL assistant. What can I do for you?"
},
"label": "Begin",
"name": "begin"

View File

@ -99,7 +99,7 @@ def create(tenant_id):
Here is the knowledge base:
{knowledge}
The above is the knowledge base.""",
"prologue": "Hi! I'm your assistant, what can I do for you?",
"prologue": "Hi! I'm your assistant. What can I do for you?",
"parameters": [{"key": "knowledge", "optional": False}],
"empty_response": "Sorry! No relevant content was found in the knowledge base!",
"quote": True,

View File

@ -28,7 +28,8 @@ from api.apps.auth import get_auth_client
from api.db import FileType, UserTenantRole
from api.db.db_models import TenantLLM
from api.db.services.file_service import FileService
from api.db.services.llm_service import TenantLLMService, get_init_tenant_llm
from api.db.services.llm_service import get_init_tenant_llm
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.user_service import TenantService, UserService, UserTenantService
from api.utils import (
current_timestamp,

View File

@ -742,7 +742,7 @@ class Dialog(DataBaseModel):
prompt_type = CharField(max_length=16, null=False, default="simple", help_text="simple|advanced", index=True)
prompt_config = JSONField(
null=False,
default={"system": "", "prologue": "Hi! I'm your assistant, what can I do for you?", "parameters": [], "empty_response": "Sorry! No relevant content was found in the knowledge base!"},
default={"system": "", "prologue": "Hi! I'm your assistant. What can I do for you?", "parameters": [], "empty_response": "Sorry! No relevant content was found in the knowledge base!"},
)
meta_data_filter = JSONField(null=True, default={})

View File

@ -227,10 +227,13 @@ class FileService(CommonService):
# tenant_id: Tenant ID
# Returns:
# Knowledge base folder dictionary
for root in cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == cls.model.id)):
for folder in cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == root.id), (cls.model.name == KNOWLEDGEBASE_FOLDER_NAME)):
return folder.to_dict()
assert False, "Can't find the KB folder. Database init error."
root_folder = cls.get_root_folder(tenant_id)
root_id = root_folder["id"]
kb_folder = cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == root_id), (cls.model.name == KNOWLEDGEBASE_FOLDER_NAME)).first()
if not kb_folder:
kb_folder = cls.new_a_file_from_kb(tenant_id, KNOWLEDGEBASE_FOLDER_NAME, root_id)
return kb_folder
return kb_folder.to_dict()
@classmethod
@DB.connection_context()
@ -499,10 +502,9 @@ class FileService(CommonService):
@staticmethod
def get_blob(user_id, location):
bname = f"{user_id}-downloads"
return STORAGE_IMPL.get(bname, location)
return STORAGE_IMPL.get(bname, location)
@staticmethod
def put_blob(user_id, location, blob):
bname = f"{user_id}-downloads"
return STORAGE_IMPL.put(bname, location, blob)
return STORAGE_IMPL.put(bname, location, blob)

View File

@ -1894,7 +1894,7 @@ Success:
"prompt": {
"empty_response": "Sorry! No relevant content was found in the knowledge base!",
"keywords_similarity_weight": 0.3,
"opener": "Hi! I'm your assistant, what can I do for you?",
"opener": "Hi! I'm your assistant. What can I do for you?",
"prompt": "You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence \"The answer you are looking for is not found in the knowledge base!\" Answers need to consider chat history.\n ",
"rerank_model": "",
"similarity_threshold": 0.2,
@ -2139,7 +2139,7 @@ Success:
"prompt": {
"empty_response": "Sorry! No relevant content was found in the knowledge base!",
"keywords_similarity_weight": 0.3,
"opener": "Hi! I'm your assistant, what can I do for you?",
"opener": "Hi! I'm your assistant. What can I do for you?",
"prompt": "You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence \"The answer you are looking for is not found in the knowledge base!\" Answers need to consider chat history.\n",
"rerank_model": "",
"similarity_threshold": 0.2,
@ -2534,7 +2534,7 @@ data:{
"code": 0,
"message": "",
"data": {
"answer": "Hi! I'm your assistant, what can I do for you?",
"answer": "Hi! I'm your assistant. What can I do for you?",
"reference": {},
"audio_binary": null,
"id": null,
@ -2638,6 +2638,10 @@ Failure:
### Create session with agent
:::danger DEPRECATED
This method is deprecated and not recommended. You can still call it but be mindful that calling `Converse with agent` will automatically generate a session ID for the associated agent.
:::
**POST** `/api/v1/agents/{agent_id}/sessions`
Creates a session with an agent.
@ -2750,7 +2754,7 @@ Success:
"message_history_window_size": 22,
"mode": "conversational",
"outputs": {},
"prologue": "Hi! I'm your assistant, what can I do for you?",
"prologue": "Hi! I'm your assistant. What can I do for you?",
"tips": "Please fill up the form"
}
},
@ -2803,7 +2807,7 @@ Success:
}
},
"mode": "conversational",
"prologue": "Hi! I'm your assistant, what can I do for you?"
"prologue": "Hi! I'm your assistant. What can I do for you?"
},
"label": "Begin",
"name": "begin"
@ -2860,7 +2864,7 @@ Success:
"id": "0b02fe80780e11f084adcfdc3ed1d902",
"message": [
{
"content": "Hi! I'm your assistant, what can I do for you?",
"content": "Hi! I'm your assistant. What can I do for you?",
"role": "assistant"
}
],

View File

@ -22,6 +22,8 @@ from timeit import default_timer as timer
from docx import Document
from docx.image.exceptions import InvalidImageStreamError, UnexpectedEndOfFileError, UnrecognizedImageError
from docx.opc.pkgreader import _SerializedRelationships, _SerializedRelationship
from docx.opc.oxml import parse_xml
from markdown import markdown
from PIL import Image
from tika import parser
@ -47,8 +49,8 @@ class Docx(DocxParser):
if not embed:
return None
embed = embed[0]
related_part = document.part.related_parts[embed]
try:
related_part = document.part.related_parts[embed]
image_blob = related_part.image.blob
except UnrecognizedImageError:
logging.info("Unrecognized image format. Skipping image.")
@ -62,6 +64,9 @@ class Docx(DocxParser):
except UnicodeDecodeError:
logging.info("The recognized image stream appears to be corrupted. Skipping image.")
return None
except Exception:
logging.info("The recognized image stream appears to be corrupted. Skipping image.")
return None
try:
image = Image.open(BytesIO(image_blob)).convert('RGB')
return image
@ -360,6 +365,20 @@ class Markdown(MarkdownParser):
tbls.append(((None, markdown(table, extensions=['markdown.extensions.tables'])), ""))
return sections, tbls
def load_from_xml_v2(baseURI, rels_item_xml):
"""
Return |_SerializedRelationships| instance loaded with the
relationships contained in *rels_item_xml*. Returns an empty
collection if *rels_item_xml* is |None|.
"""
srels = _SerializedRelationships()
if rels_item_xml is not None:
rels_elm = parse_xml(rels_item_xml)
for rel_elm in rels_elm.Relationship_lst:
if rel_elm.target_ref in ('../NULL', 'NULL'):
continue
srels._srels.append(_SerializedRelationship(baseURI, rel_elm))
return srels
def chunk(filename, binary=None, from_page=0, to_page=100000,
lang="Chinese", callback=None, **kwargs):
@ -391,6 +410,8 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
except Exception:
vision_model = None
# fix "There is no item named 'word/NULL' in the archive", referring to https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246
_SerializedRelationships.load_from_xml = load_from_xml_v2
sections, tables = Docx()(filename, binary)
if vision_model:

View File

@ -47,7 +47,7 @@ class Chat(Base):
self.variables = [{"key": "knowledge", "optional": True}]
self.rerank_model = ""
self.empty_response = None
self.opener = "Hi! I'm your assistant, what can I do for you?"
self.opener = "Hi! I'm your assistant. What can I do for you?"
self.show_quote = True
self.prompt = (
"You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. "

View File

@ -143,7 +143,7 @@ class RAGFlow:
},
)
if prompt.opener is None:
prompt.opener = "Hi! I'm your assistant, what can I do for you?"
prompt.opener = "Hi! I'm your assistant. What can I do for you?"
if prompt.prompt is None:
prompt.prompt = (
"You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. "

View File

@ -221,7 +221,7 @@ class TestChatAssistantCreate:
assert res["data"]["prompt"]["variables"] == [{"key": "knowledge", "optional": False}]
assert res["data"]["prompt"]["rerank_model"] == ""
assert res["data"]["prompt"]["empty_response"] == "Sorry! No relevant content was found in the knowledge base!"
assert res["data"]["prompt"]["opener"] == "Hi! I'm your assistant, what can I do for you?"
assert res["data"]["prompt"]["opener"] == "Hi! I'm your assistant. What can I do for you?"
assert res["data"]["prompt"]["show_quote"] is True
assert (
res["data"]["prompt"]["prompt"]

View File

@ -218,7 +218,7 @@ class TestChatAssistantUpdate:
assert res["data"]["prompt"][0]["variables"] == [{"key": "knowledge", "optional": False}]
assert res["data"]["prompt"][0]["rerank_model"] == ""
assert res["data"]["prompt"][0]["empty_response"] == "Sorry! No relevant content was found in the knowledge base!"
assert res["data"]["prompt"][0]["opener"] == "Hi! I'm your assistant, what can I do for you?"
assert res["data"]["prompt"][0]["opener"] == "Hi! I'm your assistant. What can I do for you?"
assert res["data"]["prompt"][0]["show_quote"] is True
assert (
res["data"]["prompt"][0]["prompt"]

View File

@ -222,7 +222,7 @@ class TestChatAssistantCreate:
assert res["data"]["prompt"]["variables"] == [{"key": "knowledge", "optional": False}]
assert res["data"]["prompt"]["rerank_model"] == ""
assert res["data"]["prompt"]["empty_response"] == "Sorry! No relevant content was found in the knowledge base!"
assert res["data"]["prompt"]["opener"] == "Hi! I'm your assistant, what can I do for you?"
assert res["data"]["prompt"]["opener"] == "Hi! I'm your assistant. What can I do for you?"
assert res["data"]["prompt"]["show_quote"] is True
assert (
res["data"]["prompt"]["prompt"]

View File

@ -219,7 +219,7 @@ class TestChatAssistantUpdate:
assert res["data"]["prompt"][0]["variables"] == [{"key": "knowledge", "optional": False}]
assert res["data"]["prompt"][0]["rerank_model"] == ""
assert res["data"]["prompt"][0]["empty_response"] == "Sorry! No relevant content was found in the knowledge base!"
assert res["data"]["prompt"][0]["opener"] == "Hi! I'm your assistant, what can I do for you?"
assert res["data"]["prompt"][0]["opener"] == "Hi! I'm your assistant. What can I do for you?"
assert res["data"]["prompt"][0]["show_quote"] is True
assert (
res["data"]["prompt"][0]["prompt"]

View File

@ -245,4 +245,4 @@ class TestUpdatedChunk:
delete_documents(HttpApiAuth, dataset_id, {"ids": [document_id]})
res = update_chunk(HttpApiAuth, dataset_id, document_id, chunk_ids[0])
assert res["code"] == 102
assert res["message"] == f"You don't own the document {document_id}."
assert res["message"] in [f"You don't own the document {document_id}.", f"Can't find this chunk {chunk_ids[0]}"]

View File

@ -207,7 +207,7 @@ class TestChatAssistantCreate:
assert attrgetter("variables")(chat_assistant.prompt) == [{"key": "knowledge", "optional": False}]
assert attrgetter("rerank_model")(chat_assistant.prompt) == ""
assert attrgetter("empty_response")(chat_assistant.prompt) == "Sorry! No relevant content was found in the knowledge base!"
assert attrgetter("opener")(chat_assistant.prompt) == "Hi! I'm your assistant, what can I do for you?"
assert attrgetter("opener")(chat_assistant.prompt) == "Hi! I'm your assistant. What can I do for you?"
assert attrgetter("show_quote")(chat_assistant.prompt) is True
assert (
attrgetter("prompt")(chat_assistant.prompt)

View File

@ -200,7 +200,7 @@ class TestChatAssistantUpdate:
"variables": [{"key": "knowledge", "optional": False}],
"rerank_model": "",
"empty_response": "Sorry! No relevant content was found in the knowledge base!",
"opener": "Hi! I'm your assistant, what can I do for you?",
"opener": "Hi! I'm your assistant. What can I do for you?",
"show_quote": True,
"prompt": 'You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history.\n Here is the knowledge base:\n {knowledge}\n The above is the knowledge base.',
},

View File

@ -151,4 +151,4 @@ class TestUpdatedChunk:
with pytest.raises(Exception) as excinfo:
chunks[0].update({})
assert f"You don't own the document {chunks[0].document_id}" in str(excinfo.value), str(excinfo.value)
assert str(excinfo.value) in [f"You don't own the document {chunks[0].document_id}", f"Can't find this chunk {chunks[0].id}"], str(excinfo.value)

View File

@ -96,3 +96,22 @@ export function LargeModelFormField() {
</>
);
}
export function LargeModelFormFieldWithoutFilter() {
const form = useFormContext();
return (
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -78,7 +78,6 @@ export function LlmSettingFieldItems({
<FormLabel>{t('model')}</FormLabel>
<FormControl>
<SelectWithSearch
allowClear
options={options || modelOptions}
{...field}
></SelectWithSearch>

View File

@ -50,3 +50,4 @@ const Input = function ({
};
export { Input };
export default React.forwardRef(Input);

View File

@ -152,7 +152,7 @@ const Modal: ModalType = ({
onClick={() => maskClosable && onOpenChange?.(false)}
>
<DialogPrimitive.Content
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg transition-all focus-visible:!outline-none`}
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg border transition-all focus-visible:!outline-none`}
onClick={(e) => e.stopPropagation()}
>
{/* title */}

View File

@ -42,3 +42,92 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
</Tooltip>
);
};
export interface AntToolTipProps {
title: React.ReactNode;
children: React.ReactNode;
placement?: 'top' | 'bottom' | 'left' | 'right';
trigger?: 'hover' | 'click' | 'focus';
className?: string;
}
export const AntToolTip: React.FC<AntToolTipProps> = ({
title,
children,
placement = 'top',
trigger = 'hover',
className,
}) => {
const [visible, setVisible] = React.useState(false);
const showTooltip = () => {
if (trigger === 'hover' || trigger === 'focus') {
setVisible(true);
}
};
const hideTooltip = () => {
if (trigger === 'hover' || trigger === 'focus') {
setVisible(false);
}
};
const toggleTooltip = () => {
if (trigger === 'click') {
setVisible(!visible);
}
};
const getPlacementClasses = () => {
switch (placement) {
case 'top':
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
case 'bottom':
return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
case 'left':
return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
case 'right':
return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
default:
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
}
};
return (
<div className="inline-block relative">
<div
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
onClick={toggleTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
>
{children}
</div>
{visible && title && (
<div
className={cn(
'absolute z-50 px-2.5 py-1.5 text-xs text-white bg-gray-800 rounded-sm shadow-sm whitespace-nowrap',
getPlacementClasses(),
className,
)}
>
{title}
<div
className={cn(
'absolute w-2 h-2 bg-gray-800',
placement === 'top' &&
'bottom-[-4px] left-1/2 transform -translate-x-1/2 rotate-45',
placement === 'bottom' &&
'top-[-4px] left-1/2 transform -translate-x-1/2 rotate-45',
placement === 'left' &&
'right-[-4px] top-1/2 transform -translate-y-1/2 rotate-45',
placement === 'right' &&
'left-[-4px] top-1/2 transform -translate-y-1/2 rotate-45',
)}
/>
</div>
)}
</div>
);
};

View File

@ -445,7 +445,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
emptyResponseTip: `Set this as a response if no results are retrieved from the knowledge bases for your query, or leave this field blank to allow the LLM to improvise when nothing is found.`,
emptyResponseMessage: `Empty response will be triggered when nothing relevant is retrieved from knowledge bases. You must clear the 'Empty response' field if no knowledge base is selected.`,
setAnOpener: 'Opening greeting',
setAnOpenerInitial: `Hi! I'm your assistant, what can I do for you?`,
setAnOpenerInitial: `Hi! I'm your assistant. What can I do for you?`,
setAnOpenerTip: 'Set an opening greeting for users.',
knowledgeBases: 'Knowledge bases',
knowledgeBasesMessage: 'Please select',

View File

@ -262,7 +262,7 @@ export const initialRetrievalValues = {
export const initialBeginValues = {
mode: AgentDialogueMode.Conversational,
prologue: `Hi! I'm your assistant, what can I do for you?`,
prologue: `Hi! I'm your assistant. What can I do for you?`,
};
export const variableCheckBoxFieldMap = Object.keys(

View File

@ -3,7 +3,6 @@ import { Spin } from '@/components/ui/spin';
import request from '@/utils/request';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { useGetDocumentUrl } from './hooks';
interface CSVData {
rows: string[][];
@ -12,13 +11,14 @@ interface CSVData {
interface FileViewerProps {
className?: string;
url: string;
}
const CSVFileViewer: React.FC<FileViewerProps> = () => {
const CSVFileViewer: React.FC<FileViewerProps> = ({ url }) => {
const [data, setData] = useState<CSVData | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const containerRef = useRef<HTMLDivElement>(null);
const url = useGetDocumentUrl();
// const url = useGetDocumentUrl();
const parseCSV = (csvText: string): CSVData => {
console.log('Parsing CSV data:', csvText);
const lines = csvText.split('\n');

View File

@ -4,14 +4,17 @@ import request from '@/utils/request';
import classNames from 'classnames';
import mammoth from 'mammoth';
import { useEffect, useState } from 'react';
import { useGetDocumentUrl } from './hooks';
interface DocPreviewerProps {
className?: string;
url: string;
}
export const DocPreviewer: React.FC<DocPreviewerProps> = ({ className }) => {
const url = useGetDocumentUrl();
export const DocPreviewer: React.FC<DocPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const [htmlContent, setHtmlContent] = useState<string>('');
const [loading, setLoading] = useState(false);
const fetchDocument = async () => {

View File

@ -1,15 +1,16 @@
import { useFetchExcel } from '@/pages/document-viewer/hooks';
import classNames from 'classnames';
import { useGetDocumentUrl } from './hooks';
interface ExcelCsvPreviewerProps {
className?: string;
url: string;
}
export const ExcelCsvPreviewer: React.FC<ExcelCsvPreviewerProps> = ({
className,
url,
}) => {
const url = useGetDocumentUrl();
// const url = useGetDocumentUrl();
const { containerRef } = useFetchExcel(url);
return (

View File

@ -3,16 +3,17 @@ import { Spin } from '@/components/ui/spin';
import request from '@/utils/request';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useGetDocumentUrl } from './hooks';
interface ImagePreviewerProps {
className?: string;
url: string;
}
export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
className,
url,
}) => {
const url = useGetDocumentUrl();
// const url = useGetDocumentUrl();
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

View File

@ -12,12 +12,14 @@ import { TxtPreviewer } from './txt-preview';
type PreviewProps = {
fileType: string;
className?: string;
url: string;
};
const Preview = ({
fileType,
className,
highlights,
setWidthAndHeight,
url,
}: PreviewProps & Partial<IProps>) => {
return (
<>
@ -26,37 +28,38 @@ const Preview = ({
<PdfPreviewer
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
url={url}
></PdfPreviewer>
</section>
)}
{['doc', 'docx'].indexOf(fileType) > -1 && (
<section>
<DocPreviewer className={className} />
<DocPreviewer className={className} url={url} />
</section>
)}
{['txt', 'md'].indexOf(fileType) > -1 && (
<section>
<TxtPreviewer className={className} />
<TxtPreviewer className={className} url={url} />
</section>
)}
{['visual'].indexOf(fileType) > -1 && (
<section>
<ImagePreviewer className={className} />
<ImagePreviewer className={className} url={url} />
</section>
)}
{['pptx'].indexOf(fileType) > -1 && (
<section>
<PptPreviewer className={className} />
<PptPreviewer className={className} url={url} />
</section>
)}
{['xlsx'].indexOf(fileType) > -1 && (
<section>
<ExcelCsvPreviewer className={className} />
<ExcelCsvPreviewer className={className} url={url} />
</section>
)}
{['csv'].indexOf(fileType) > -1 && (
<section>
<CSVFileViewer className={className} />
<CSVFileViewer className={className} url={url} />
</section>
)}
</>

View File

@ -7,7 +7,6 @@ import {
PdfLoader,
Popup,
} from 'react-pdf-highlighter';
import { useGetDocumentUrl } from './hooks';
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
import { Spin } from '@/components/ui/spin';
@ -17,6 +16,7 @@ import styles from './index.less';
export interface IProps {
highlights: IHighlight[];
setWidthAndHeight: (width: number, height: number) => void;
url: string;
}
const HighlightPopup = ({
comment,
@ -30,8 +30,8 @@ const HighlightPopup = ({
) : null;
// TODO: merge with DocumentPreviewer
const PdfPreview = ({ highlights: state, setWidthAndHeight }: IProps) => {
const url = useGetDocumentUrl();
const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
// const url = useGetDocumentUrl();
const ref = useRef<(highlight: IHighlight) => void>(() => {});
const error = useCatchDocumentError(url);

View File

@ -3,13 +3,16 @@ import request from '@/utils/request';
import classNames from 'classnames';
import { init } from 'pptx-preview';
import { useEffect, useRef } from 'react';
import { useGetDocumentUrl } from './hooks';
interface PptPreviewerProps {
className?: string;
url: string;
}
export const PptPreviewer: React.FC<PptPreviewerProps> = ({ className }) => {
const url = useGetDocumentUrl();
export const PptPreviewer: React.FC<PptPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const wrapper = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const fetchDocument = async () => {

View File

@ -3,11 +3,10 @@ import { Spin } from '@/components/ui/spin';
import request from '@/utils/request';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useGetDocumentUrl } from './hooks';
type TxtPreviewerProps = { className?: string };
export const TxtPreviewer = ({ className }: TxtPreviewerProps) => {
const url = useGetDocumentUrl();
type TxtPreviewerProps = { className?: string; url: string };
export const TxtPreviewer = ({ className, url }: TxtPreviewerProps) => {
// const url = useGetDocumentUrl();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<string>('');
const fetchTxt = async () => {

View File

@ -40,6 +40,7 @@ import {
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { useGetDocumentUrl } from '../../../knowledge-chunk/components/document-preview/hooks';
import styles from './index.less';
const Chunk = () => {
@ -73,6 +74,7 @@ const Chunk = () => {
} = useUpdateChunk();
const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage();
const fileUrl = useGetDocumentUrl();
useEffect(() => {
setChunkList(data);
}, [data]);
@ -212,6 +214,7 @@ const Chunk = () => {
fileType={fileType}
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
url={fileUrl}
></DocumentPreview>
</section>
</div>

View File

@ -411,7 +411,7 @@ export const initialRetrievalValues = {
};
export const initialBeginValues = {
prologue: `Hi! I'm your assistant, what can I do for you?`,
prologue: `Hi! I'm your assistant. What can I do for you?`,
};
export const variableCheckBoxFieldMap = Object.keys(

View File

@ -2,6 +2,11 @@ import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/message-item';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { MessageType } from '@/constants/chat';
import {
useFetchConversation,
@ -10,7 +15,7 @@ import {
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { Trash2 } from 'lucide-react';
import { ListCheck, Plus, Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import {
useGetSendButtonDisabled,
@ -19,19 +24,30 @@ import {
import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation';
import { useSendMessage } from '../../hooks/use-send-chat-message';
import { buildMessageItemReference } from '../../utils';
import { LLMSelectForm } from '../llm-select-form';
import { useAddChatBox } from '../use-add-box';
type MultipleChatBoxProps = {
controller: AbortController;
chatBoxIds: string[];
} & Pick<ReturnType<typeof useAddChatBox>, 'removeChatBox'>;
type ChatCardProps = { id: string } & Pick<
MultipleChatBoxProps,
'controller' | 'removeChatBox'
} & Pick<
ReturnType<typeof useAddChatBox>,
'removeChatBox' | 'addChatBox' | 'chatBoxIds'
>;
function ChatCard({ controller, removeChatBox, id }: ChatCardProps) {
type ChatCardProps = { id: string; idx: number } & Pick<
MultipleChatBoxProps,
'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds'
>;
function ChatCard({
controller,
removeChatBox,
id,
idx,
addChatBox,
chatBoxIds,
}: ChatCardProps) {
const {
value,
// scrollRef,
@ -49,6 +65,8 @@ function ChatCard({ controller, removeChatBox, id }: ChatCardProps) {
const { data: currentDialog } = useFetchDialog();
const { data: conversation } = useFetchConversation();
const isLatestChat = idx === chatBoxIds.length - 1;
const handleRemoveChatBox = useCallback(() => {
removeChatBox(id);
}, [id, removeChatBox]);
@ -57,15 +75,31 @@ function ChatCard({ controller, removeChatBox, id }: ChatCardProps) {
<Card className="bg-transparent border flex-1">
<CardHeader className="border-b px-5 py-3">
<CardTitle className="flex justify-between items-center">
<div>
<span className="text-base">Card Title</span>
<Button variant={'ghost'} className="ml-2">
GPT-4
</Button>
<div className="flex items-center gap-3">
<span className="text-base">{idx + 1}</span>
<LLMSelectForm></LLMSelectForm>
</div>
<div className="space-x-2">
<Tooltip>
<TooltipTrigger>
<Button variant={'ghost'}>
<ListCheck />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Apply model configs</p>
</TooltipContent>
</Tooltip>
{!isLatestChat || chatBoxIds.length === 3 ? (
<Button variant={'ghost'} onClick={handleRemoveChatBox}>
<Trash2 />
</Button>
) : (
<Button variant={'ghost'} onClick={addChatBox}>
<Plus></Plus>
</Button>
)}
</div>
<Button variant={'ghost'} onClick={handleRemoveChatBox}>
<Trash2 />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
@ -111,6 +145,7 @@ export function MultipleChatBox({
controller,
chatBoxIds,
removeChatBox,
addChatBox,
}: MultipleChatBoxProps) {
const {
value,
@ -125,31 +160,37 @@ export function MultipleChatBox({
const { conversationId } = useGetChatSearchParams();
const disabled = useGetSendButtonDisabled();
const sendDisabled = useSendButtonDisabled(value);
return (
<section className="h-full flex flex-col">
<div className="flex gap-4 flex-1 px-5 pb-12">
{chatBoxIds.map((id) => (
<section className="h-full flex flex-col px-5">
<div className="flex gap-4 flex-1 px-5 pb-14">
{chatBoxIds.map((id, idx) => (
<ChatCard
key={id}
idx={idx}
controller={controller}
id={id}
chatBoxIds={chatBoxIds}
removeChatBox={removeChatBox}
addChatBox={addChatBox}
></ChatCard>
))}
</div>
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
/>
<div className="px-[20%]">
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
/>
</div>
</section>
);
}

View File

@ -11,21 +11,25 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchDialog } from '@/hooks/use-chat-request';
import { useFetchConversation, useFetchDialog } from '@/hooks/use-chat-request';
import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react';
import { ArrowUpRight, LogOut } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { ChatSettings } from './app-settings/chat-settings';
import { MultipleChatBox } from './chat-box/multiple-chat-box';
import { SingleChatBox } from './chat-box/single-chat-box';
import { LLMSelectForm } from './llm-select-form';
import { Sessions } from './sessions';
import { useAddChatBox } from './use-add-box';
import { useSwitchDebugMode } from './use-switch-debug-mode';
export default function Chat() {
const { navigateToChatList } = useNavigatePage();
const { data } = useFetchDialog();
const { t } = useTranslation();
const { data: conversation } = useFetchConversation();
const { handleConversationCardClick, controller } =
useHandleClickConversationCard();
const { visible: settingVisible, switchVisible: switchSettingVisible } =
@ -38,6 +42,29 @@ export default function Chat() {
hasThreeChatBox,
} = useAddChatBox();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
if (isDebugMode) {
return (
<section className="pt-14 h-[100vh] pb-24">
<div className="flex items-center justify-between px-10 pb-5">
<span className="text-2xl">
Multiple Models ({chatBoxIds.length}/3)
</span>
<Button variant={'ghost'} onClick={switchDebugMode}>
Exit <LogOut />
</Button>
</div>
<MultipleChatBox
chatBoxIds={chatBoxIds}
controller={controller}
removeChatBox={removeChatBox}
addChatBox={addChatBox}
></MultipleChatBox>
</section>
);
}
return (
<section className="h-full flex flex-col pr-5">
<PageHeader>
@ -57,6 +84,7 @@ export default function Chat() {
</PageHeader>
<div className="flex flex-1 min-h-0">
<Sessions
hasSingleChatBox={hasSingleChatBox}
handleConversationCardClick={handleConversationCardClick}
switchSettingVisible={switchSettingVisible}
></Sessions>
@ -67,32 +95,23 @@ export default function Chat() {
<CardHeader
className={cn('p-5', { 'border-b': hasSingleChatBox })}
>
<CardTitle className="flex justify-between items-center">
<div className="text-base">
Card Title
<Button variant={'ghost'} className="ml-2">
GPT-4
</Button>
<CardTitle className="flex justify-between items-center text-base">
<div className="flex gap-3 items-center">
{conversation.name}
<LLMSelectForm></LLMSelectForm>
</div>
<Button
variant={'ghost'}
onClick={addChatBox}
onClick={switchDebugMode}
disabled={hasThreeChatBox}
>
<Plus></Plus> Multiple Models
<ArrowUpRight /> Multiple Models
</Button>
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0">
{hasSingleChatBox ? (
<SingleChatBox controller={controller}></SingleChatBox>
) : (
<MultipleChatBox
chatBoxIds={chatBoxIds}
controller={controller}
removeChatBox={removeChatBox}
></MultipleChatBox>
)}
<SingleChatBox controller={controller}></SingleChatBox>
</CardContent>
</Card>
{settingVisible && (

View File

@ -0,0 +1,23 @@
import { LargeModelFormFieldWithoutFilter } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export function LLMSelectForm() {
const FormSchema = z.object(LlmSettingSchema);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
llm_id: '',
},
});
return (
<Form {...form}>
<LargeModelFormFieldWithoutFilter></LargeModelFormFieldWithoutFilter>
</Form>
);
}

View File

@ -17,8 +17,9 @@ import { useSelectDerivedConversationList } from '../hooks/use-select-conversati
type SessionProps = Pick<
ReturnType<typeof useHandleClickConversationCard>,
'handleConversationCardClick'
> & { switchSettingVisible(): void };
> & { switchSettingVisible(): void; hasSingleChatBox: boolean };
export function Sessions({
hasSingleChatBox,
handleConversationCardClick,
switchSettingVisible,
}: SessionProps) {
@ -91,7 +92,11 @@ export function Sessions({
))}
</div>
<div className="py-2">
<Button className="w-full" onClick={switchSettingVisible}>
<Button
className="w-full"
onClick={switchSettingVisible}
disabled={!hasSingleChatBox}
>
Chat Settings
</Button>
</div>

View File

@ -0,0 +1,14 @@
import { useCallback, useState } from 'react';
export function useSwitchDebugMode() {
const [isDebugMode, setIsDebugMode] = useState(false);
const switchDebugMode = useCallback(() => {
setIsDebugMode(!isDebugMode);
}, [isDebugMode]);
return {
isDebugMode,
switchDebugMode,
};
}

View File

@ -0,0 +1,29 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { IReferenceChunk } from '@/interfaces/database/chat';
import { useCallback, useState } from 'react';
export const useClickDrawer = () => {
const { visible, showModal, hideModal } = useSetModalState();
const [selectedChunk, setSelectedChunk] = useState<IReferenceChunk>(
{} as IReferenceChunk,
);
const [documentId, setDocumentId] = useState<string>('');
const clickDocumentButton = useCallback(
(documentId: string, chunk: IReferenceChunk) => {
showModal();
setSelectedChunk(chunk);
setDocumentId(documentId);
},
[showModal],
);
return {
clickDocumentButton,
visible,
showModal,
hideModal,
selectedChunk,
documentId,
};
};

View File

@ -0,0 +1,65 @@
import { FileIcon } from '@/components/icon-font';
import { Modal } from '@/components/ui/modal/modal';
import {
useGetChunkHighlights,
useGetDocumentUrl,
} from '@/hooks/document-hooks';
import { IModalProps } from '@/interfaces/common';
import { IReferenceChunk } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import DocumentPreview from '@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview';
import { useEffect, useState } from 'react';
interface IProps extends IModalProps<any> {
documentId: string;
chunk: IChunk | IReferenceChunk;
}
function getFileExtensionRegex(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
const PdfDrawer = ({
visible = false,
hideModal,
documentId,
chunk,
}: IProps) => {
const getDocumentUrl = useGetDocumentUrl(documentId);
const { highlights, setWidthAndHeight } = useGetChunkHighlights(chunk);
// const ref = useRef<(highlight: IHighlight) => void>(() => {});
// const [loaded, setLoaded] = useState(false);
const url = getDocumentUrl();
console.log('chunk--->', chunk.docnm_kwd, url);
const [fileType, setFileType] = useState('');
useEffect(() => {
if (chunk.docnm_kwd) {
const type = getFileExtensionRegex(chunk.docnm_kwd);
setFileType(type);
}
}, [chunk.docnm_kwd]);
return (
<Modal
title={
<div className="flex items-center gap-2">
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
}
onCancel={hideModal}
open={visible}
showfooter={false}
>
<DocumentPreview
className={'!h-[calc(100dvh-300px)] overflow-auto'}
fileType={fileType}
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
url={url}
></DocumentPreview>
</Modal>
);
};
export default PdfDrawer;

View File

@ -0,0 +1,48 @@
import Markdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { preprocessLaTeX } from '@/utils/chat';
const HightLightMarkdown = ({
children,
}: {
children: string | null | undefined;
}) => {
return (
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeRaw, rehypeKatex]}
className="text-text-primary text-sm"
components={
{
code(props: any) {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code
{...rest}
className={`${className} pt-1 px-2 pb-2 m-0 whitespace-break-spaces rounded text-text-primary text-sm`}
>
{children}
</code>
);
},
} as any
}
>
{children ? preprocessLaTeX(children) : children}
</Markdown>
);
};
export default HightLightMarkdown;

View File

@ -106,3 +106,11 @@
.delay-700 {
animation-delay: 0.7s;
}
.highlightContent {
.multipleLineEllipsis(2);
em {
color: red;
font-style: normal;
}
}

View File

@ -10,7 +10,7 @@ import {
import { Button } from '@/components/ui/button';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { Settings } from 'lucide-react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
@ -26,6 +26,13 @@ export default function SearchPage() {
const { data: SearchData } = useFetchSearchDetail();
const [openSetting, setOpenSetting] = useState(false);
const [searchText, setSearchText] = useState('');
useEffect(() => {
if (isSearching) {
setOpenSetting(false);
}
}, [isSearching]);
return (
<section>
<PageHeader>
@ -50,6 +57,8 @@ export default function SearchPage() {
<SearchHome
setIsSearching={setIsSearching}
isSearching={isSearching}
searchText={searchText}
setSearchText={setSearchText}
/>
</div>
)}
@ -57,33 +66,35 @@ export default function SearchPage() {
<div className="animate-fade-in-up">
<SearchingPage
setIsSearching={setIsSearching}
isSearching={isSearching}
searchText={searchText}
setSearchText={setSearchText}
data={SearchData as ISearchAppDetailProps}
/>
</div>
)}
</div>
{/* {openSetting && (
<div className=" w-[440px]"> */}
<SearchSetting
className="mt-20 mr-2"
open={openSetting}
setOpen={setOpenSetting}
data={SearchData as ISearchAppDetailProps}
/>
{/* </div>
)} */}
{openSetting && (
<SearchSetting
className="mt-20 mr-2"
open={openSetting}
setOpen={setOpenSetting}
data={SearchData as ISearchAppDetailProps}
/>
)}
</div>
<div className="absolute left-5 bottom-12 ">
<Button
variant="transparent"
className="bg-bg-card"
onClick={() => setOpenSetting(!openSetting)}
>
<Settings className="text-text-secondary" />
<div className="text-text-secondary">Search Settings</div>
</Button>
</div>
{!isSearching && (
<div className="absolute left-5 bottom-12 ">
<Button
variant="transparent"
className="bg-bg-card"
onClick={() => setOpenSetting(!openSetting)}
>
<Settings className="text-text-secondary" />
<div className="text-text-secondary">Search Settings</div>
</Button>
</div>
)}
</section>
);
}

View File

@ -0,0 +1,295 @@
import Image from '@/components/image';
import SvgIcon from '@/components/svg-icon';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import { getExtension } from '@/utils/document-util';
import { InfoCircleOutlined } from '@ant-design/icons';
import DOMPurify from 'dompurify';
import { useCallback, useEffect, useMemo } from 'react';
import Markdown from 'react-markdown';
import reactStringReplace from 'react-string-replace';
import SyntaxHighlighter from 'react-syntax-highlighter';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { visitParents } from 'unist-util-visit-parents';
import { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks';
import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import {
preprocessLaTeX,
replaceThinkToSection,
showImage,
} from '@/utils/chat';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { currentReg, replaceTextByOldReg } from '@/pages/next-chats/utils';
import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
const getChunkIndex = (match: string) => Number(match);
// Defining Tailwind CSS class name constants
const styles = {
referenceChunkImage: 'w-[10vw] object-contain',
referenceInnerChunkImage: 'block object-contain max-w-full max-h-[6vh]',
referenceImagePreview: 'max-w-[45vw] max-h-[45vh]',
chunkContentText: 'max-h-[45vh] overflow-y-auto',
documentLink: 'p-0',
referenceIcon: 'px-[6px]',
fileThumbnail: 'inline-block max-w-[40px]',
};
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
const MarkdownContent = ({
reference,
clickDocumentButton,
content,
}: {
content: string;
loading: boolean;
reference: IReference;
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
}) => {
const { t } = useTranslation();
const { setDocumentIds, data: fileThumbnails } =
useFetchDocumentThumbnailsByIds();
const contentWithCursor = useMemo(() => {
// let text = DOMPurify.sanitize(content);
let text = content;
if (text === '') {
text = t('chat.searching');
}
const nextText = replaceTextByOldReg(text);
return pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
}, [content, t]);
useEffect(() => {
const docAggs = reference?.doc_aggs;
setDocumentIds(Array.isArray(docAggs) ? docAggs.map((x) => x.doc_id) : []);
}, [reference, setDocumentIds]);
const handleDocumentButtonClick = useCallback(
(
documentId: string,
chunk: IReferenceChunk,
isPdf: boolean,
documentUrl?: string,
) =>
() => {
if (!isPdf) {
if (!documentUrl) {
return;
}
window.open(documentUrl, '_blank');
} else {
clickDocumentButton?.(documentId, chunk);
}
},
[clickDocumentButton],
);
const rehypeWrapReference = () => {
return function wrapTextTransform(tree: any) {
visitParents(tree, 'text', (node, ancestors) => {
const latestAncestor = ancestors.at(-1);
if (
latestAncestor.tagName !== 'custom-typography' &&
latestAncestor.tagName !== 'code'
) {
node.type = 'element';
node.tagName = 'custom-typography';
node.properties = {};
node.children = [{ type: 'text', value: node.value }];
}
});
};
};
const getReferenceInfo = useCallback(
(chunkIndex: number) => {
const chunks = reference?.chunks ?? [];
const chunkItem = chunks[chunkIndex];
const document = reference?.doc_aggs?.find(
(x) => x?.doc_id === chunkItem?.document_id,
);
const documentId = document?.doc_id;
const documentUrl = document?.url;
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
const imageId = chunkItem?.image_id;
return {
documentUrl,
fileThumbnail,
fileExtension,
imageId,
chunkItem,
documentId,
document,
};
},
[fileThumbnails, reference],
);
const getPopoverContent = useCallback(
(chunkIndex: number) => {
const {
documentUrl,
fileThumbnail,
fileExtension,
imageId,
chunkItem,
documentId,
document,
} = getReferenceInfo(chunkIndex);
return (
<div key={chunkItem?.id} className="flex gap-2">
{imageId && (
<Popover>
<PopoverTrigger>
<Image
id={imageId}
className={styles.referenceChunkImage}
></Image>
</PopoverTrigger>
<PopoverContent>
<Image
id={imageId}
className={styles.referenceImagePreview}
></Image>
</PopoverContent>
</Popover>
)}
<div className={'space-y-2 max-w-[40vw]'}>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(chunkItem?.content ?? ''),
}}
className={classNames(styles.chunkContentText)}
></div>
{documentId && (
<div className="flex gap-2">
{fileThumbnail ? (
<img
src={fileThumbnail}
alt=""
className={styles.fileThumbnail}
/>
) : (
<SvgIcon
name={`file-icon/${fileExtension}`}
width={24}
></SvgIcon>
)}
<Button
variant="link"
className={classNames(styles.documentLink, 'text-wrap')}
onClick={handleDocumentButtonClick(
documentId,
chunkItem,
fileExtension === 'pdf',
documentUrl,
)}
>
{document?.doc_name}
</Button>
</div>
)}
</div>
</div>
);
},
[getReferenceInfo, handleDocumentButtonClick],
);
const renderReference = useCallback(
(text: string) => {
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
const chunkIndex = getChunkIndex(match);
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
getReferenceInfo(chunkIndex);
const docType = chunkItem?.doc_type;
return showImage(docType) ? (
<Image
id={imageId}
className={styles.referenceInnerChunkImage}
onClick={
documentId
? handleDocumentButtonClick(
documentId,
chunkItem,
fileExtension === 'pdf',
documentUrl,
)
: () => {}
}
></Image>
) : (
<Popover>
<PopoverTrigger>
<InfoCircleOutlined className={styles.referenceIcon} />
</PopoverTrigger>
<PopoverContent>{getPopoverContent(chunkIndex)}</PopoverContent>
</Popover>
);
});
return replacedText;
},
[getPopoverContent, getReferenceInfo, handleDocumentButtonClick],
);
return (
<Markdown
rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]}
remarkPlugins={[remarkGfm, remarkMath]}
className="[&>section.think]:pl-[10px] [&>section.think]:text-[#8b8b8b] [&>section.think]:border-l-2 [&>section.think]:border-l-[#d5d3d3] [&>section.think]:mb-[10px] [&>section.think]:text-xs [&>blockquote]:pl-[10px] [&>blockquote]:border-l-4 [&>blockquote]:border-l-[#ccc] text-sm"
components={
{
'custom-typography': ({ children }: { children: string }) =>
renderReference(children),
code(props: any) {
const { children, className, ...rest } = props;
const restProps = omit(rest, 'node');
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
{...restProps}
PreTag="div"
language={match[1]}
wrapLongLines
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code
{...restProps}
className={classNames(className, 'text-wrap')}
>
{children}
</code>
);
},
} as any
}
>
{contentWithCursor}
</Markdown>
);
};
export default MarkdownContent;

View File

@ -0,0 +1,47 @@
import IndentedTree from '@/components/indented-tree/indented-tree';
import { Progress } from '@/components/ui/progress';
import { IModalProps } from '@/interfaces/common';
import { X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { usePendingMindMap } from '../search/hooks';
interface IProps extends IModalProps<any> {
data: any;
}
const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
const { t } = useTranslation();
const percent = usePendingMindMap();
return (
<div className="w-[400px] h-[420px]">
<div className="flex w-full justify-between items-center mb-2">
<div className="text-text-primary font-medium text-base">
{t('chunk.mind')}
</div>
<X
className="text-text-primary cursor-pointer"
size={16}
onClick={() => {
hideModal?.();
}}
/>
</div>
{loading && (
<div className="absolute top-48">
<Progress value={percent} className="h-1 flex-1 min-w-10" />
</div>
)}
{!loading && (
<div className="bg-bg-card rounded-lg p-4 w-[400px] h-[380px]">
<IndentedTree
data={data}
show
style={{ width: '100%', height: '100%' }}
></IndentedTree>
</div>
)}
</div>
);
};
export default MindMapDrawer;

View File

@ -0,0 +1,11 @@
.selectFilesCollapse {
:global(.ant-collapse-header) {
padding-left: 22px;
}
margin-bottom: 32px;
overflow-y: auto;
}
.selectFilesTitle {
padding-right: 10px;
}

View File

@ -0,0 +1,237 @@
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import { MultiSelectOptionType } from '@/components/ui/multi-select';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
useAllTestingResult,
useSelectTestingResult,
} from '@/hooks/knowledge-hooks';
import { cn } from '@/lib/utils';
import { Separator } from '@radix-ui/react-select';
import { CheckIcon, ChevronDown, Files, XIcon } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface IProps {
onTesting(documentIds: string[]): void;
setSelectedDocumentIds(documentIds: string[]): void;
selectedDocumentIds: string[];
}
const RetrievalDocuments = ({
onTesting,
selectedDocumentIds,
setSelectedDocumentIds,
}: IProps) => {
const { t } = useTranslation();
const { documents: documentsAll } = useAllTestingResult();
const { documents } = useSelectTestingResult();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const maxCount = 3;
const { documents: useDocuments } = {
documents:
documentsAll?.length > documents?.length ? documentsAll : documents,
};
const [selectedValues, setSelectedValues] =
useState<string[]>(selectedDocumentIds);
const multiOptions = useMemo(() => {
return useDocuments?.map((item) => {
return {
label: item.doc_name,
value: item.doc_id,
disabled: item.doc_name === 'Disabled User',
// suffix: (
// <div className="flex justify-between gap-3 ">
// <div>{item.count}</div>
// <div>
// <Eye />
// </div>
// </div>
// ),
};
});
}, [useDocuments]);
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const onValueChange = (value: string[]) => {
console.log(value);
onTesting(value);
setSelectedDocumentIds(value);
// handleDatasetSelectChange(value, field.onChange);
};
const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
};
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
setIsPopoverOpen(true);
} else if (event.key === 'Backspace' && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
onClick={handleTogglePopover}
className={cn(
'flex w-full p-1 rounded-md text-base text-text-primary border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
)}
>
<div className="flex justify-between items-center w-full">
<div className="flex flex-wrap items-center gap-2">
<Files />
<span>
{selectedDocumentIds?.length ?? 0}/{useDocuments?.length ?? 0}
</span>
Files
</div>
<div className="flex items-center justify-between">
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
</div>
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<CommandInput
placeholder="Search..."
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{!multiOptions.some((x) => 'options' in x) &&
(multiOptions as unknown as MultiSelectOptionType[]).map(
(option) => {
const isSelected = selectedValues.includes(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (option.disabled) return false;
toggleOption(option.value);
}}
className={cn('cursor-pointer', {
'cursor-not-allowed text-text-disabled':
option.disabled,
})}
>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary '
: 'opacity-50 [&_svg]:invisible',
{ 'text-primary-foreground': !option.disabled },
{ 'text-text-disabled': option.disabled },
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{option.icon && (
<option.icon
className={cn('mr-2 h-4 w-4 ', {
'text-text-disabled': option.disabled,
'text-muted-foreground': !option.disabled,
})}
/>
)}
<span
className={cn({
'text-text-disabled': option.disabled,
})}
>
{option.label}
</span>
{option.suffix && (
<span
className={cn({
'text-text-disabled': option.disabled,
})}
>
{option.suffix}
</span>
)}
</CommandItem>
);
},
)}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 justify-center cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export default RetrievalDocuments;

View File

@ -1,5 +1,6 @@
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
@ -9,10 +10,15 @@ import Spotlight from './spotlight';
export default function SearchPage({
isSearching,
setIsSearching,
searchText,
setSearchText,
}: {
isSearching: boolean;
setIsSearching: Dispatch<SetStateAction<boolean>>;
searchText: string;
setSearchText: Dispatch<SetStateAction<string>>;
}) {
const { data: userInfo } = useFetchUserInfo();
return (
<section className="relative w-full flex transition-all justify-center items-center mt-32">
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]">
@ -30,14 +36,25 @@ export default function SearchPage({
{!isSearching && (
<>
<p className="mb-4 transition-opacity">👋 Hi there</p>
<p className="mb-10 transition-opacity">Welcome back, KiKi</p>
<p className="mb-10 transition-opacity">
Welcome back, {userInfo?.nickname}
</p>
</>
)}
<div className="relative w-full ">
<Input
placeholder="How can I help you today?"
className="w-full rounded-full py-6 px-4 pr-10 text-white text-lg bg-background delay-700"
className="w-full rounded-full py-6 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
value={searchText}
onKeyUp={(e) => {
if (e.key === 'Enter') {
setIsSearching(!isSearching);
}
}}
onChange={(e) => {
setSearchText(e.target.value || '');
}}
/>
<button
type="button"

View File

@ -33,15 +33,16 @@ interface LlmSettingFieldItemsProps {
export const LlmSettingSchema = {
llm_id: z.string(),
parameter: z.string(),
temperature: z.coerce.number(),
top_p: z.string(),
top_p: z.coerce.number(),
presence_penalty: z.coerce.number(),
frequency_penalty: z.coerce.number(),
temperatureEnabled: z.boolean(),
topPEnabled: z.boolean(),
presencePenaltyEnabled: z.boolean(),
frequencyPenaltyEnabled: z.boolean(),
maxTokensEnabled: z.boolean(),
// maxTokensEnabled: z.boolean(),
};
export function LlmSettingFieldItems({
@ -58,7 +59,8 @@ export function LlmSettingFieldItems({
const handleChange = useCallback(
(parameter: string) => {
// const currentValues = { ...form.getValues() };
const currentValues = { ...form.getValues() };
console.log('currentValues', currentValues);
const values =
settledModelVariableMap[
parameter as keyof typeof settledModelVariableMap
@ -145,28 +147,28 @@ export function LlmSettingFieldItems({
/>
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
checkName={getFieldWithPrefix('temperatureEnabled')}
label="temperature"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
checkName={getFieldWithPrefix('topPEnabled')}
label="topP"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
checkName={getFieldWithPrefix('presencePenaltyEnabled')}
label="presencePenalty"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
checkName={getFieldWithPrefix('frequencyPenaltyEnabled')}
label="frequencyPenalty"
max={1}
step={0.01}

View File

@ -30,17 +30,24 @@ import { cn } from '@/lib/utils';
import { transformFile2Base64 } from '@/utils/file-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { PanelRightClose, Pencil, Upload } from 'lucide-react';
import { Pencil, Upload, X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { LlmModelType, ModelVariableType } from '../dataset/dataset/constant';
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '../dataset/dataset/constant';
import {
ISearchAppDetailProps,
IUpdateSearchProps,
useUpdateSearch,
} from '../next-searches/hooks';
import { LlmSettingFieldItems } from './search-setting-aisummery-config';
import {
LlmSettingFieldItems,
LlmSettingSchema,
} from './search-setting-aisummery-config';
interface SearchSettingProps {
open: boolean;
@ -48,6 +55,15 @@ interface SearchSettingProps {
className?: string;
data: ISearchAppDetailProps;
}
interface ISubmitLlmSettingProps {
llm_id: string;
parameter: string;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
const SearchSettingFormSchema = z
.object({
search_id: z.string().optional(),
@ -64,14 +80,7 @@ const SearchSettingFormSchema = z
use_rerank: z.boolean(),
top_k: z.number(),
summary: z.boolean(),
llm_setting: z.object({
llm_id: z.string(),
parameter: z.string(),
temperature: z.number(),
top_p: z.union([z.string(), z.number()]),
frequency_penalty: z.number(),
presence_penalty: z.number(),
}),
llm_setting: z.object(LlmSettingSchema),
related_search: z.boolean(),
query_mindmap: z.boolean(),
}),
@ -133,10 +142,26 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
llm_setting: {
llm_id: llm_setting?.llm_id || '',
parameter: llm_setting?.parameter || ModelVariableType.Improvise,
temperature: llm_setting?.temperature || 0.8,
top_p: llm_setting?.top_p || 0.9,
frequency_penalty: llm_setting?.frequency_penalty || 0.1,
presence_penalty: llm_setting?.presence_penalty || 0.1,
temperature:
llm_setting?.temperature ||
settledModelVariableMap[ModelVariableType.Improvise].temperature,
top_p:
llm_setting?.top_p ||
settledModelVariableMap[ModelVariableType.Improvise].top_p,
frequency_penalty:
llm_setting?.frequency_penalty ||
settledModelVariableMap[ModelVariableType.Improvise]
.frequency_penalty,
presence_penalty:
llm_setting?.presence_penalty ||
settledModelVariableMap[ModelVariableType.Improvise]
.presence_penalty,
temperatureEnabled: llm_setting?.temperature ? true : false,
topPEnabled: llm_setting?.top_p ? true : false,
presencePenaltyEnabled: llm_setting?.presence_penalty ? true : false,
frequencyPenaltyEnabled: llm_setting?.frequency_penalty
? true
: false,
},
chat_settingcross_languages: [],
highlight: false,
@ -193,7 +218,10 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
setDatasetList(datasetListMap);
}, [datasetListOrigin, datasetSelectEmbdId]);
const handleDatasetSelectChange = (value, onChange) => {
const handleDatasetSelectChange = (
value: string[],
onChange: (value: string[]) => void,
) => {
console.log(value);
if (value.length) {
const data = datasetListOrigin?.find((item) => item.id === value[0]);
@ -224,18 +252,44 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
name: 'search_config.summary',
});
const { updateSearch, isLoading: isUpdating } = useUpdateSearch();
const { updateSearch } = useUpdateSearch();
const { data: systemSetting } = useFetchTenantInfo();
const onSubmit = async (
formData: IUpdateSearchProps & { tenant_id: string },
) => {
try {
const { search_config, ...other_formdata } = formData;
const { llm_setting, ...other_config } = search_config;
const llmSetting = {
llm_id: llm_setting.llm_id,
parameter: llm_setting.parameter,
temperature: llm_setting.temperature,
top_p: llm_setting.top_p,
frequency_penalty: llm_setting.frequency_penalty,
presence_penalty: llm_setting.presence_penalty,
} as ISubmitLlmSettingProps;
if (!llm_setting.frequencyPenaltyEnabled) {
delete llmSetting.frequency_penalty;
}
if (!llm_setting.presencePenaltyEnabled) {
delete llmSetting.presence_penalty;
}
if (!llm_setting.temperatureEnabled) {
delete llmSetting.temperature;
}
if (!llm_setting.topPEnabled) {
delete llmSetting.top_p;
}
await updateSearch({
...formData,
...other_formdata,
search_config: {
...other_config,
llm_setting: { ...llmSetting },
},
tenant_id: systemSetting.tenant_id,
avatar: avatarBase64Str,
});
setOpen(false); // 关闭弹窗
setOpen(false);
} catch (error) {
console.error('Failed to update search:', error);
}
@ -256,10 +310,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<div className="flex justify-between items-center text-base mb-8">
<div className="text-text-primary">Search Settings</div>
<div onClick={() => setOpen(false)}>
<PanelRightClose
size={16}
className="text-text-primary cursor-pointer"
/>
<X size={16} className="text-text-primary cursor-pointer" />
</div>
</div>
<div
@ -271,7 +322,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
onSubmit={formMethods.handleSubmit(
(data) => {
console.log('Form submitted with data:', data);
onSubmit(data as IUpdateSearchProps);
onSubmit(data as unknown as IUpdateSearchProps);
},
(errors) => {
console.log('Validation errors:', errors);
@ -462,26 +513,37 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
</FormItem>
)}
/>
<FormField
control={formMethods.control}
name="search_config.top_k"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormItem>
<FormLabel>Top K</FormLabel>
<FormControl>
<div className="flex justify-between items-center">
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
{...field}
max={100}
min={0}
step={1}
value={field.value as number}
onChange={(values) => field.onChange(values)}
></SingleFormSlider>
<Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20">
{field.value}
</Label>
</div>
</FormControl>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={100}
min={0}
step={1}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}

View File

@ -1,22 +1,107 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Skeleton } from '@/components/ui/skeleton';
import { Spin } from '@/components/ui/spin';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import { Search, X } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import DOMPurify from 'dompurify';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, Square, Tag, X } from 'lucide-react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
} from 'react';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useSendQuestion, useShowMindMapDrawer } from '../search/hooks';
import PdfDrawer from './document-preview-modal';
import HightLightMarkdown from './highlight-markdown';
import './index.less';
import styles from './index.less';
import MarkdownContent from './markdown-content';
import MindMapDrawer from './mindmap-drawer';
import RetrievalDocuments from './retrieval-documents';
export default function SearchingPage({
isSearching,
searchText,
data: searchData,
setIsSearching,
}: {
isSearching: boolean;
searchText: string;
setIsSearching: Dispatch<SetStateAction<boolean>>;
setSearchText: Dispatch<SetStateAction<string>>;
data: ISearchAppDetailProps;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const {
sendQuestion,
handleClickRelatedQuestion,
handleSearchStrChange,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
} = useSendQuestion(searchData.search_config.kb_ids);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
if (searchText) {
setSearchStr(searchText);
sendQuestion(searchText);
}
// regain focus
if (inputRef.current) {
inputRef.current.focus();
}
}, [searchText, sendQuestion, setSearchStr]);
const {
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
} = useShowMindMapDrawer(searchData.search_config.kb_ids, searchStr);
const { chunks, total } = useSelectTestingResult();
const handleSearch = useCallback(() => {
sendQuestion(searchStr);
}, [searchStr, sendQuestion]);
const { pagination, setPagination } = useGetPaginationWithRouter();
const onChange = (pageNumber: number, pageSize: number) => {
setPagination({ page: pageNumber, pageSize });
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
};
return (
<section
className={cn(
'relative w-full flex transition-all justify-start items-center',
)}
>
{/* search header */}
<div
className={cn(
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full',
@ -24,41 +109,224 @@ export default function SearchingPage({
>
<h1
className={cn(
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text',
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching(false);
}}
>
RAGFlow
</h1>
<div
className={cn(
' rounded-lg text-primary text-xl sticky flex justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
)}
>
<div className={cn('flex flex-col justify-start items-start w-full')}>
<div className="relative w-full text-primary">
<Input
ref={inputRef}
key="search-input"
placeholder="How can I help you today?"
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
)}
value={searchStr}
onChange={handleSearchStrChange}
disabled={sendingLoading}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X />|
<X
className="text-text-secondary"
size={14}
onClick={() => {
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary">|</span>
<button
type="button"
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
onClick={() => {
setIsSearching(!isSearching);
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch();
}
}}
>
<Search size={22} className="m-auto" />
{sendingLoading ? (
<Square size={22} className="m-auto" />
) : (
<Search size={22} className="m-auto" />
)}
</button>
</div>
</div>
</div>
{/* search body */}
<div
className="w-full mt-5 overflow-auto scrollbar-none "
style={{ height: 'calc(100vh - 250px)' }}
>
{searchData.search_config.summary && (
<>
<div className="flex justify-start items-start text-text-primary text-2xl">
AI Summary
</div>
{isEmpty(answer) && sendingLoading ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-2/3 bg-bg-card" />
</div>
) : (
answer.answer && (
<div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none">
<MarkdownContent
loading={sendingLoading}
content={answer.answer}
reference={answer.reference ?? ({} as IReference)}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
)
)}
</>
)}
<div className="w-full border-b border-border-default/80 my-6"></div>
{/* retrieval documents */}
<div className=" mt-3 w-44 ">
<RetrievalDocuments
selectedDocumentIds={selectedDocumentIds}
setSelectedDocumentIds={setSelectedDocumentIds}
onTesting={handleTestChunk}
></RetrievalDocuments>
</div>
<div className="w-full border-b border-border-default/80 my-6"></div>
<div className="mt-3 ">
<Spin spinning={loading}>
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<>
<div
key={chunk.chunk_id}
className="w-full flex flex-col"
>
<div className="w-full">
<ImageWithPopover
id={chunk.img_id}
></ImageWithPopover>
<Popover>
<PopoverTrigger asChild>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
`${chunk.highlight}...`,
),
}}
className="text-sm text-text-primary mb-1"
></div>
</PopoverTrigger>
<PopoverContent className="text-text-primary">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</PopoverContent>
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
>
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
</div>
{index < chunks.length - 1 && (
<div className="w-full border-b border-border-default/80 mt-6"></div>
)}
</>
);
})}
</>
)}
</Spin>
{relatedQuestions?.length > 0 && (
<div title={t('chat.relatedQuestion')}>
<div className="flex gap-2">
{relatedQuestions?.map((x, idx) => (
<Tag
key={idx}
className={styles.tag}
onClick={handleClickRelatedQuestion(x)}
>
{x}
</Tag>
))}
</div>
</div>
)}
</div>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
</div>
</div>
{!mindMapVisible &&
!isFirstRender &&
!isSearchStrEmpty &&
!isEmpty(searchData.search_config.kb_ids) && (
<Popover>
<PopoverTrigger asChild>
<Button
className="rounded-lg h-8 w-8 p-0 absolute top-28 right-3 z-30"
variant={'transparent'}
onClick={showMindMapModal}
>
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
<BrainCircuit size={24} />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
</Popover>
)}
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
{mindMapVisible && (
<div className="absolute top-20 right-16 z-30">
<MindMapDrawer
visible={mindMapVisible}
hideModal={hideMindMapModal}
data={mindMap}
loading={mindMapLoading}
></MindMapDrawer>
</div>
)}
</section>
);
}

View File

@ -1,3 +1,4 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import React from 'react';
interface SpotlightProps {
@ -5,6 +6,8 @@ interface SpotlightProps {
}
const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
const isDark = useIsDarkTheme();
console.log('isDark', isDark);
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
@ -16,8 +19,9 @@ const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
<div
className="absolute inset-0"
style={{
background:
'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)',
background: isDark
? 'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)'
: 'radial-gradient(circle at 50% 190%, #E4F3FF 0%, #E4F3FF00 60%)',
pointerEvents: 'none',
}}
></div>

View File

@ -1,8 +1,8 @@
// src/pages/next-searches/hooks.ts
import message from '@/components/ui/message';
import searchService from '@/services/search-service';
import { useMutation, useQuery } from '@tanstack/react-query';
import { message } from 'antd';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
@ -23,7 +23,6 @@ export const useCreateSearch = () => {
const {
data,
isLoading,
isError,
mutateAsync: createSearchMutation,
} = useMutation<CreateSearchResponse, Error, CreateSearchProps>({
@ -50,7 +49,7 @@ export const useCreateSearch = () => {
[createSearchMutation],
);
return { data, isLoading, isError, createSearch };
return { data, isError, createSearch };
};
export interface SearchListParams {
@ -128,7 +127,6 @@ export const useDeleteSearch = () => {
const {
data,
isLoading,
isError,
mutateAsync: deleteSearchMutation,
} = useMutation<DeleteSearchResponse, Error, DeleteSearchProps>({
@ -155,7 +153,7 @@ export const useDeleteSearch = () => {
[deleteSearchMutation],
);
return { data, isLoading, isError, deleteSearch };
return { data, isError, deleteSearch };
};
interface IllmSettingProps {
@ -166,7 +164,12 @@ interface IllmSettingProps {
frequency_penalty: number;
presence_penalty: number;
}
interface IllmSettingEnableProps {
temperatureEnabled?: boolean;
topPEnabled?: boolean;
presencePenaltyEnabled?: boolean;
frequencyPenaltyEnabled?: boolean;
}
export interface ISearchAppDetailProps {
avatar: any;
created_by: string;
@ -184,7 +187,7 @@ export interface ISearchAppDetailProps {
rerank_id: string;
similarity_threshold: number;
summary: boolean;
llm_setting: IllmSettingProps;
llm_setting: IllmSettingProps & IllmSettingEnableProps;
top_k: number;
use_kg: boolean;
vector_similarity_weight: number;
@ -225,10 +228,9 @@ export type IUpdateSearchProps = Omit<ISearchAppDetailProps, 'id'> & {
export const useUpdateSearch = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isLoading,
isError,
mutateAsync: updateSearchMutation,
} = useMutation<any, Error, IUpdateSearchProps>({
@ -241,8 +243,11 @@ export const useUpdateSearch = () => {
}
return response.data;
},
onSuccess: () => {
onSuccess: (data, variables) => {
message.success(t('message.updated'));
queryClient.invalidateQueries({
queryKey: ['searchDetail', variables.search_id],
});
},
onError: (error) => {
message.error(t('message.error', { error: error.message }));
@ -256,5 +261,5 @@ export const useUpdateSearch = () => {
[updateSearchMutation],
);
return { data, isLoading, isError, updateSearch };
return { data, isError, updateSearch };
};

View File

@ -12,6 +12,7 @@ import {
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { pick } from 'lodash';
import { Plus, Search } from 'lucide-react';
@ -30,6 +31,7 @@ type SearchFormValues = z.infer<typeof searchFormSchema>;
export default function SearchList() {
// const { data } = useFetchFlowList();
const { t } = useTranslate('search');
const { navigateToSearch } = useNavigatePage();
const { isLoading, createSearch } = useCreateSearch();
const {
data: list,
@ -48,7 +50,10 @@ export default function SearchList() {
};
const onSubmit = async (values: SearchFormValues) => {
await createSearch({ name: values.name });
const res = await createSearch({ name: values.name });
if (res) {
navigateToSearch(res?.search_id);
}
if (!isLoading) {
setOpenCreateModal(false);
}
@ -88,16 +93,12 @@ export default function SearchList() {
{list?.data.search_apps.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}
{/* {data.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})} */}
</div>
{list?.data.total && (
<RAGFlowPagination
{...pick(searchParams, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
on
/>
)}
<Modal

View File

@ -135,6 +135,7 @@ export const useSendQuestion = (kbIds: string[]) => {
answer: currentAnswer,
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
searchStr,
setSearchStr,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty: isEmpty(trim(searchStr)),