mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-04 03:25:30 +08:00
Compare commits
8 Commits
3d645ff31a
...
3ee9653170
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ee9653170 | |||
| 6d1078b538 | |||
| 6e862553cb | |||
| b1baa91ff0 | |||
| b55c3d07dc | |||
| 2b3318cd3d | |||
| 434b55be70 | |||
| 98b4c67292 |
327
agent/templates/knowledge_base_report.json
Normal file
327
agent/templates/knowledge_base_report.json
Normal 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=="
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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={})
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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. "
|
||||
|
||||
@ -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. "
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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]}"]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.',
|
||||
},
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -78,7 +78,6 @@ export function LlmSettingFieldItems({
|
||||
<FormLabel>{t('model')}</FormLabel>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
allowClear
|
||||
options={options || modelOptions}
|
||||
{...field}
|
||||
></SelectWithSearch>
|
||||
|
||||
@ -50,3 +50,4 @@ const Input = function ({
|
||||
};
|
||||
|
||||
export { Input };
|
||||
export default React.forwardRef(Input);
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
23
web/src/pages/next-chats/chat/llm-select-form.tsx
Normal file
23
web/src/pages/next-chats/chat/llm-select-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
14
web/src/pages/next-chats/chat/use-switch-debug-mode.ts
Normal file
14
web/src/pages/next-chats/chat/use-switch-debug-mode.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
29
web/src/pages/next-search/document-preview-modal/hooks.ts
Normal file
29
web/src/pages/next-search/document-preview-modal/hooks.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
65
web/src/pages/next-search/document-preview-modal/index.tsx
Normal file
65
web/src/pages/next-search/document-preview-modal/index.tsx
Normal 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;
|
||||
48
web/src/pages/next-search/highlight-markdown/index.tsx
Normal file
48
web/src/pages/next-search/highlight-markdown/index.tsx
Normal 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;
|
||||
@ -106,3 +106,11 @@
|
||||
.delay-700 {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
.highlightContent {
|
||||
.multipleLineEllipsis(2);
|
||||
em {
|
||||
color: red;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
295
web/src/pages/next-search/markdown-content/index.tsx
Normal file
295
web/src/pages/next-search/markdown-content/index.tsx
Normal 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;
|
||||
47
web/src/pages/next-search/mindmap-drawer.tsx
Normal file
47
web/src/pages/next-search/mindmap-drawer.tsx
Normal 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;
|
||||
11
web/src/pages/next-search/retrieval-documents/index.less
Normal file
11
web/src/pages/next-search/retrieval-documents/index.less
Normal file
@ -0,0 +1,11 @@
|
||||
.selectFilesCollapse {
|
||||
:global(.ant-collapse-header) {
|
||||
padding-left: 22px;
|
||||
}
|
||||
margin-bottom: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selectFilesTitle {
|
||||
padding-right: 10px;
|
||||
}
|
||||
237
web/src/pages/next-search/retrieval-documents/index.tsx
Normal file
237
web/src/pages/next-search/retrieval-documents/index.tsx
Normal 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;
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -135,6 +135,7 @@ export const useSendQuestion = (kbIds: string[]) => {
|
||||
answer: currentAnswer,
|
||||
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
|
||||
searchStr,
|
||||
setSearchStr,
|
||||
isFirstRender,
|
||||
selectedDocumentIds,
|
||||
isSearchStrEmpty: isEmpty(trim(searchStr)),
|
||||
|
||||
Reference in New Issue
Block a user