mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-02 16:45:08 +08:00
Compare commits
7 Commits
6cd1824a77
...
55c0468ac9
| Author | SHA1 | Date | |
|---|---|---|---|
| 55c0468ac9 | |||
| eeb36a5ce7 | |||
| aceca266ff | |||
| d82e502a71 | |||
| 0494b92371 | |||
| 8683a5b1b7 | |||
| 4cbe470089 |
@ -100,7 +100,7 @@ class FileService(CommonService):
|
||||
# Returns:
|
||||
# List of dictionaries containing dataset IDs and names
|
||||
kbs = (
|
||||
cls.model.select(*[Knowledgebase.id, Knowledgebase.name])
|
||||
cls.model.select(*[Knowledgebase.id, Knowledgebase.name, File2Document.document_id])
|
||||
.join(File2Document, on=(File2Document.file_id == file_id))
|
||||
.join(Document, on=(File2Document.document_id == Document.id))
|
||||
.join(Knowledgebase, on=(Knowledgebase.id == Document.kb_id))
|
||||
@ -110,7 +110,7 @@ class FileService(CommonService):
|
||||
return []
|
||||
kbs_info_list = []
|
||||
for kb in list(kbs.dicts()):
|
||||
kbs_info_list.append({"kb_id": kb["id"], "kb_name": kb["name"]})
|
||||
kbs_info_list.append({"kb_id": kb["id"], "kb_name": kb["name"], "document_id": kb["document_id"]})
|
||||
return kbs_info_list
|
||||
|
||||
@classmethod
|
||||
|
||||
37
docs/faq.mdx
37
docs/faq.mdx
@ -493,18 +493,35 @@ See [here](./guides/agent/best_practices/accelerate_agent_question_answering.md)
|
||||
|
||||
### How to use MinerU to parse PDF documents?
|
||||
|
||||
MinerU PDF document parsing is available starting from v0.22.0. RAGFlow works only as a remote client to MinerU (>= 2.6.3) and does not install or execute MinerU locally. To use this feature:
|
||||
From v0.22.0 onwards, RAGFlow includes MinerU (≥ 2.6.3) as an optional PDF parser of multiple backends. Please note that RAGFlow acts only as a *remote client* for MinerU, calling the MinerU API to parse PDFs and reading the returned files. To use this feature:
|
||||
|
||||
1. Prepare a reachable MinerU API service (for example, the FastAPI server provided by MinerU).
|
||||
2. Configure RAGFlow with remote MinerU settings (environment variables or UI model provider):
|
||||
- `MINERU_APISERVER`: MinerU API endpoint, for example `http://mineru-host:8886`.
|
||||
- `MINERU_BACKEND`: MinerU backend, defaults to `pipeline` (supports `vlm-http-client`, `vlm-transformers`, `vlm-vllm-engine`, `vlm-mlx-engine`, `vlm-vllm-async-engine`, `vlm-lmdeploy-engine`).
|
||||
- `MINERU_SERVER_URL`: (optional) For `vlm-http-client`, the downstream vLLM HTTP server, for example `http://vllm-host:30000`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) Local directory to store MinerU API outputs (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temp dir is used (`1` deletes temp outputs; set `0` to keep).
|
||||
3. In the web UI, navigate to the **Configuration** page of your dataset. Click **Built-in** in the **Ingestion pipeline** section, select a chunking method from the **Built-in** dropdown (which supports PDF parsing), and select **MinerU** in **PDF parser**.
|
||||
4. If you use a custom ingestion pipeline instead, provide the same MinerU settings and select **MinerU** in the **Parsing method** section of the **Parser** component.
|
||||
1. Prepare a reachable MinerU API service (FastAPI server).
|
||||
2. In the **.env** file or from the **Model providers** page in the UI, configure RAGFlow as a remote client to MinerU:
|
||||
- `MINERU_APISERVER`: The MinerU API endpoint (e.g., `http://mineru-host:8886`).
|
||||
- `MINERU_BACKEND`: The MinerU backend:
|
||||
- `"pipeline"` (default)
|
||||
- `"vlm-http-client"`
|
||||
- `"vlm-transformers"`
|
||||
- `"vlm-vllm-engine"`
|
||||
- `"vlm-mlx-engine"`
|
||||
- `"vlm-vllm-async-engine"`
|
||||
- `"vlm-lmdeploy-engine"`.
|
||||
- `MINERU_SERVER_URL`: (optional) The downstream vLLM HTTP server (e.g., `http://vllm-host:30000`). Applicable when `MINERU_BACKEND` is set to `"vlm-http-client"`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) The local directory for holding the outputs of the MinerU API service (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temporary directory is used:
|
||||
- `1`: Delete.
|
||||
- `0`: Retain.
|
||||
3. In the web UI, navigate to your dataset's **Configuration** page and find the **Ingestion pipeline** section:
|
||||
- If you decide to use a chunking method from the **Built-in** dropdown, ensure it supports PDF parsing, then select **MinerU** from the **PDF parser** dropdown.
|
||||
- If you use a custom ingestion pipeline instead, select **MinerU** in the **PDF parser** section of the **Parser** component.
|
||||
|
||||
:::note
|
||||
All MinerU environment variables are optional. When set, these values are used to auto-provision a MinerU OCR model for the tenant on first use. To avoid auto-provisioning, skip the environment variable settings and only configure MinerU from the **Model providers** page in the UI.
|
||||
:::
|
||||
|
||||
:::caution WARNING
|
||||
Third-party visual models are marked **Experimental**, because we have not fully tested these models for the aforementioned data extraction tasks.
|
||||
:::
|
||||
---
|
||||
|
||||
### How to configure MinerU-specific settings?
|
||||
|
||||
@ -24,7 +24,7 @@ We use gVisor to isolate code execution from the host system. Please follow [the
|
||||
RAGFlow Sandbox is a secure, pluggable code execution backend. It serves as the code executor for the **Code** component. Please follow the [instructions here](https://github.com/infiniflow/ragflow/tree/main/sandbox) to install RAGFlow Sandbox.
|
||||
|
||||
:::note Docker client version
|
||||
The executor manager image now bundles Docker CLI `29.1.0` (API 1.44+). Older images shipped Docker 24.x and will fail against newer Docker daemons with `client version 1.43 is too old`. Pull the latest `infiniflow/sandbox-executor-manager:latest` or rebuild `./sandbox/executor_manager` if you encounter this error.
|
||||
The executor manager image now bundles Docker CLI `29.1.0` (API 1.44+). Older images shipped Docker 24.x and will fail against newer Docker daemons with `client version 1.43 is too old`. Pull the latest `infiniflow/sandbox-executor-manager:latest` or rebuild it in `./sandbox/executor_manager` if you encounter this error.
|
||||
:::
|
||||
|
||||
:::tip NOTE
|
||||
@ -134,7 +134,7 @@ Your executor manager image includes Docker CLI 24.x (API 1.43), but the host Do
|
||||
|
||||
**Solution**
|
||||
|
||||
Pull the latest executor manager image or rebuild it locally to upgrade the built-in Docker client:
|
||||
Pull the latest executor manager image or rebuild it in `./sandbox/executor_manager` to upgrade the built-in Docker client:
|
||||
|
||||
```bash
|
||||
docker pull infiniflow/sandbox-executor-manager:latest
|
||||
|
||||
@ -40,21 +40,31 @@ The output of a PDF parser is `json`. In the PDF parser, you select the parsing
|
||||
- A third-party visual model from a specific model provider.
|
||||
|
||||
:::danger IMPORTANT
|
||||
MinerU PDF document parsing is available starting from v0.22.0. RAGFlow supports MinerU (>= 2.6.3) as an optional PDF parser with multiple backends. RAGFlow acts only as a **remote client** for MinerU, calling the MinerU API to parse documents, reading the returned output files, and ingesting the parsed content. To use this feature:
|
||||
Starting from v0.22.0, RAGFlow includes MinerU (≥ 2.6.3) as an optional PDF parser of multiple backends. Please note that RAGFlow acts only as a *remote client* for MinerU, calling the MinerU API to parse documents and reading the returned files. To use this feature:
|
||||
:::
|
||||
|
||||
1. Prepare a reachable MinerU API service (FastAPI server).
|
||||
2. Configure RAGFlow with the remote MinerU settings (env or UI model provider):
|
||||
- `MINERU_APISERVER`: MinerU API endpoint, for example `http://mineru-host:8886`.
|
||||
- `MINERU_BACKEND`: MinerU backend, defaults to `pipeline` (supports `vlm-http-client`, `vlm-transformers`, `vlm-vllm-engine`, `vlm-mlx-engine`, `vlm-vllm-async-engine`, `vlm-lmdeploy-engine`).
|
||||
- `MINERU_SERVER_URL`: (optional) For `vlm-http-client`, the downstream vLLM HTTP server, for example `http://vllm-host:30000`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) Local directory to store MinerU API outputs (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temp dir is used (`1` deletes temp outputs; set `0` to keep).
|
||||
3. In the web UI, navigate to the **Configuration** page of your dataset. Click **Built-in** in the **Ingestion pipeline** section, select a chunking method from the **Built-in** dropdown, which supports PDF parsing, and select **MinerU** in **PDF parser**.
|
||||
4. If you use a custom ingestion pipeline instead, provide the same MinerU settings and select **MinerU** in the **Parsing method** section of the **Parser** component.
|
||||
2. In the **.env** file or from the **Model providers** page in the UI, configure RAGFlow as a remote client to MinerU:
|
||||
- `MINERU_APISERVER`: The MinerU API endpoint (e.g., `http://mineru-host:8886`).
|
||||
- `MINERU_BACKEND`: The MinerU backend:
|
||||
- `"pipeline"` (default)
|
||||
- `"vlm-http-client"`
|
||||
- `"vlm-transformers"`
|
||||
- `"vlm-vllm-engine"`
|
||||
- `"vlm-mlx-engine"`
|
||||
- `"vlm-vllm-async-engine"`
|
||||
- `"vlm-lmdeploy-engine"`.
|
||||
- `MINERU_SERVER_URL`: (optional) The downstream vLLM HTTP server (e.g., `http://vllm-host:30000`). Applicable when `MINERU_BACKEND` is set to `"vlm-http-client"`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) The local directory for holding the outputs of the MinerU API service (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temporary directory is used:
|
||||
- `1`: Delete.
|
||||
- `0`: Retain.
|
||||
3. In the web UI, navigate to your dataset's **Configuration** page and find the **Ingestion pipeline** section:
|
||||
- If you decide to use a chunking method from the **Built-in** dropdown, ensure it supports PDF parsing, then select **MinerU** from the **PDF parser** dropdown.
|
||||
- If you use a custom ingestion pipeline instead, select **MinerU** in the **PDF parser** section of the **Parser** component.
|
||||
|
||||
:::note
|
||||
All MinerU environment variables are optional. If set, RAGFlow will auto-provision a MinerU OCR model for the tenant on first use with these values. To avoid auto-provisioning, configure MinerU solely through the UI and leave the env vars unset.
|
||||
All MinerU environment variables are optional. When set, these values are used to auto-provision a MinerU OCR model for the tenant on first use. To avoid auto-provisioning, skip the environment variable settings and only configure MinerU from the **Model providers** page in the UI.
|
||||
:::
|
||||
|
||||
:::caution WARNING
|
||||
|
||||
@ -29,7 +29,7 @@ The architecture consists of isolated Docker base images for each supported lang
|
||||
- (Optional) GNU Make for simplified command-line management.
|
||||
|
||||
:::tip NOTE
|
||||
The error message `client version 1.43 is too old. Minimum supported API version is 1.44` indicates that your executor manager image's built-in Docker CLI version is lower than `29.1.0` required by the Docker daemon in use. To solve this issue, pull the latest `infiniflow/sandbox-executor-manager:latest` from Docker Hub (or rebuild `./sandbox/executor_manager`).
|
||||
The error message `client version 1.43 is too old. Minimum supported API version is 1.44` indicates that your executor manager image's built-in Docker CLI version is lower than `29.1.0` required by the Docker daemon in use. To solve this issue, pull the latest `infiniflow/sandbox-executor-manager:latest` from Docker Hub or rebuild it in `./sandbox/executor_manager`.
|
||||
:::
|
||||
|
||||
## Build Docker base images
|
||||
|
||||
@ -45,7 +45,7 @@ Google Cloud external project.
|
||||
http://localhost:9380/v1/connector/google-drive/oauth/web/callback
|
||||
```
|
||||
|
||||
### If using Docker deployment:
|
||||
- If using Docker deployment:
|
||||
|
||||
**Authorized JavaScript origin:**
|
||||
```
|
||||
@ -53,15 +53,16 @@ http://localhost:80
|
||||
```
|
||||
|
||||

|
||||
### If running from source:
|
||||
|
||||
- If running from source:
|
||||
**Authorized JavaScript origin:**
|
||||
```
|
||||
http://localhost:9222
|
||||
```
|
||||
|
||||

|
||||
5. After saving, click **Download JSON**. This file will later be
|
||||
uploaded into RAGFlow.
|
||||
|
||||
5. After saving, click **Download JSON**. This file will later be uploaded into RAGFlow.
|
||||
|
||||

|
||||
|
||||
|
||||
@ -40,21 +40,31 @@ RAGFlow isn't one-size-fits-all. It is built for flexibility and supports deeper
|
||||
- A third-party visual model from a specific model provider.
|
||||
|
||||
:::danger IMPORTANT
|
||||
MinerU PDF document parsing is available starting from v0.22.0. RAGFlow supports MinerU (>= 2.6.3) as an optional PDF parser with multiple backends. RAGFlow acts only as a **remote client** for MinerU, calling the MinerU API to parse documents, reading the returned output files, and ingesting the parsed content. To use this feature:
|
||||
|
||||
1. Prepare a reachable MinerU API service (FastAPI server).
|
||||
2. Configure RAGFlow with the remote MinerU settings (env or UI model provider):
|
||||
- `MINERU_APISERVER`: MinerU API endpoint, for example `http://mineru-host:8886`.
|
||||
- `MINERU_BACKEND`: MinerU backend, defaults to `pipeline` (supports `vlm-http-client`, `vlm-transformers`, `vlm-vllm-engine`, `vlm-mlx-engine`, `vlm-vllm-async-engine`).
|
||||
- `MINERU_SERVER_URL`: (optional) For `vlm-http-client`, the downstream vLLM HTTP server, for example `http://vllm-host:30000`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) Local directory to store MinerU API outputs (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temp dir is used (`1` deletes temp outputs; set `0` to keep).
|
||||
3. In the web UI, navigate to the **Configuration** page of your dataset. Click **Built-in** in the **Ingestion pipeline** section, select a chunking method from the **Built-in** dropdown, which supports PDF parsing, and select **MinerU** in **PDF parser**.
|
||||
4. If you use a custom ingestion pipeline instead, provide the same MinerU settings and select **MinerU** in the **Parsing method** section of the **Parser** component.
|
||||
Starting from v0.22.0, RAGFlow includes MinerU (≥ 2.6.3) as an optional PDF parser of multiple backends. Please note that RAGFlow acts only as a *remote client* for MinerU, calling the MinerU API to parse documents and reading the returned files. To use this feature:
|
||||
:::
|
||||
|
||||
1. Prepare a reachable MinerU API service (FastAPI server).
|
||||
2. In the **.env** file or from the **Model providers** page in the UI, configure RAGFlow as a remote client to MinerU:
|
||||
- `MINERU_APISERVER`: The MinerU API endpoint (e.g., `http://mineru-host:8886`).
|
||||
- `MINERU_BACKEND`: The MinerU backend:
|
||||
- `"pipeline"` (default)
|
||||
- `"vlm-http-client"`
|
||||
- `"vlm-transformers"`
|
||||
- `"vlm-vllm-engine"`
|
||||
- `"vlm-mlx-engine"`
|
||||
- `"vlm-vllm-async-engine"`
|
||||
- `"vlm-lmdeploy-engine"`.
|
||||
- `MINERU_SERVER_URL`: (optional) The downstream vLLM HTTP server (e.g., `http://vllm-host:30000`). Applicable when `MINERU_BACKEND` is set to `"vlm-http-client"`.
|
||||
- `MINERU_OUTPUT_DIR`: (optional) The local directory for holding the outputs of the MinerU API service (zip/JSON) before ingestion.
|
||||
- `MINERU_DELETE_OUTPUT`: Whether to delete temporary output when a temporary directory is used:
|
||||
- `1`: Delete.
|
||||
- `0`: Retain.
|
||||
3. In the web UI, navigate to your dataset's **Configuration** page and find the **Ingestion pipeline** section:
|
||||
- If you decide to use a chunking method from the **Built-in** dropdown, ensure it supports PDF parsing, then select **MinerU** from the **PDF parser** dropdown.
|
||||
- If you use a custom ingestion pipeline instead, select **MinerU** in the **PDF parser** section of the **Parser** component.
|
||||
|
||||
:::note
|
||||
All MinerU environment variables are optional. When they are set, RAGFlow will auto-create a MinerU OCR model for a tenant on first use using these values. If you do not want this auto-provisioning, configure MinerU only through the UI and leave the env vars unset.
|
||||
All MinerU environment variables are optional. When set, these values are used to auto-provision a MinerU OCR model for the tenant on first use. To avoid auto-provisioning, skip the environment variable settings and only configure MinerU from the **Model providers** page in the UI.
|
||||
:::
|
||||
|
||||
:::caution WARNING
|
||||
|
||||
@ -77,6 +77,19 @@ A complete list of models supported by RAGFlow, which will continue to expand.
|
||||
If your model is not listed here but has APIs compatible with those of OpenAI, click **OpenAI-API-Compatible** on the **Model providers** page to configure your model.
|
||||
:::
|
||||
|
||||
## Example: AI Badgr (OpenAI-compatible)
|
||||
|
||||
You can use **AI Badgr** with RAGFlow via the existing OpenAI-API-Compatible provider.
|
||||
|
||||
To configure AI Badgr:
|
||||
|
||||
- **Provider**: `OpenAI-API-Compatible`
|
||||
- **Base URL**: `https://aibadgr.com/api/v1`
|
||||
- **API Key**: your AI Badgr API key (from the AI Badgr dashboard)
|
||||
- **Model**: any AI Badgr chat or embedding model ID, as exposed by AI Badgr's OpenAI-compatible APIs
|
||||
|
||||
AI Badgr implements OpenAI-compatible endpoints for `/v1/chat/completions`, `/v1/embeddings`, and `/v1/models`, so no additional code changes in RAGFlow are required.
|
||||
|
||||
:::note
|
||||
The list of supported models is extracted from [this source](https://github.com/infiniflow/ragflow/blob/main/rag/llm/__init__.py) and may not be the most current. For the latest supported model list, please refer to the Python file.
|
||||
:::
|
||||
|
||||
@ -36,7 +36,7 @@ A secure, pluggable code execution backend for RAGFlow and beyond.
|
||||
|
||||
> ⚠️ **New Docker CLI requirement**
|
||||
>
|
||||
> If you see `client version 1.43 is too old. Minimum supported API version is 1.44`, pull the latest `infiniflow/sandbox-executor-manager:latest` (rebuilt with Docker CLI `29.1.0`) or rebuild `./sandbox/executor_manager` locally. Older images shipped Docker 24.x, which cannot talk to newer Docker daemons.
|
||||
> If you see `client version 1.43 is too old. Minimum supported API version is 1.44`, pull the latest `infiniflow/sandbox-executor-manager:latest` (rebuilt with Docker CLI `29.1.0`) or rebuild it in `./sandbox/executor_manager`. Older images shipped Docker 24.x, which cannot talk to newer Docker daemons.
|
||||
|
||||
### 🐳 Build Docker Base Images
|
||||
|
||||
@ -304,7 +304,7 @@ Follow this checklist to troubleshoot:
|
||||
|
||||
**Fix:**
|
||||
|
||||
Pull the refreshed image that bundles Docker CLI `29.1.0`, or rebuild locally:
|
||||
Pull the refreshed image that bundles Docker CLI `29.1.0`, or rebuild it in `./sandbox/executor_manager`:
|
||||
|
||||
```bash
|
||||
docker pull infiniflow/sandbox-executor-manager:latest
|
||||
|
||||
29
web/package-lock.json
generated
29
web/package-lock.json
generated
@ -59,6 +59,7 @@
|
||||
"cmdk": "^1.0.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.1.6",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"human-id": "^4.1.1",
|
||||
"i18next": "^23.7.16",
|
||||
@ -17517,6 +17518,34 @@
|
||||
"resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||
},
|
||||
"node_modules/embla-carousel": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/embla-carousel-react": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
|
||||
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"embla-carousel": "8.6.0",
|
||||
"embla-carousel-reactive-utils": "8.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/embla-carousel-reactive-utils": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
|
||||
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"embla-carousel": "8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/emittery/-/emittery-0.13.1.tgz",
|
||||
|
||||
@ -72,6 +72,7 @@
|
||||
"cmdk": "^1.0.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.1.6",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"human-id": "^4.1.1",
|
||||
"i18next": "^23.7.16",
|
||||
|
||||
@ -25,3 +25,12 @@ const CopyToClipboard = ({ text }: Props) => {
|
||||
};
|
||||
|
||||
export default CopyToClipboard;
|
||||
|
||||
export function CopyToClipboardWithText({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="bg-bg-card p-1 rounded-md flex gap-2">
|
||||
<span className="flex-1 truncate">{text}</span>
|
||||
<CopyToClipboard text={text}></CopyToClipboard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -691,7 +691,7 @@ const DynamicForm = {
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
submit: form.handleSubmit,
|
||||
submit: form.handleSubmit(onSubmit),
|
||||
getValues: form.getValues,
|
||||
reset: (values?: T) => {
|
||||
if (values) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { X } from 'lucide-react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
@ -57,14 +57,15 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
|
||||
<HoverCard key={tag}>
|
||||
<HoverCardContent side="top">{tag}</HoverCardContent>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="w-fit flex items-center justify-center gap-2 border-dashed border px-2 py-1 rounded-sm bg-bg-card">
|
||||
<div className="w-fit flex items-center justify-center gap-2 border border-border-button px-2 py-1 rounded-sm bg-bg-card">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="max-w-80 overflow-hidden text-ellipsis">
|
||||
{tag}
|
||||
</div>
|
||||
{!disabled && (
|
||||
<X
|
||||
className="w-4 h-4 text-muted-foreground hover:text-primary"
|
||||
<Trash2
|
||||
size={14}
|
||||
className="text-text-secondary hover:text-state-error"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClose(tag);
|
||||
@ -80,10 +81,6 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
|
||||
|
||||
const tagChild = value?.map(forMap);
|
||||
|
||||
const tagPlusStyle: React.CSSProperties = {
|
||||
borderStyle: 'dashed',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inputVisible && (
|
||||
@ -102,15 +99,14 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 py-1">
|
||||
<div className="flex gap-2 py-1 flex-wrap">
|
||||
{Array.isArray(tagChild) && tagChild.length > 0 && <>{tagChild}</>}
|
||||
{!inputVisible && !disabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-fit flex items-center justify-center gap-2 bg-bg-card border-dashed border"
|
||||
className="w-fit flex items-center justify-center gap-2 bg-bg-card border-border-button border"
|
||||
onClick={showInput}
|
||||
disabled={disabled}
|
||||
style={tagPlusStyle}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
|
||||
139
web/src/components/markdown-content/image-carousel.tsx
Normal file
139
web/src/components/markdown-content/image-carousel.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import Image from '@/components/image';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from '@/components/ui/carousel';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
interface ImageCarouselProps {
|
||||
group: Array<{
|
||||
id: string;
|
||||
fullMatch: string;
|
||||
start: number;
|
||||
}>;
|
||||
reference: IReference;
|
||||
fileThumbnails: Record<string, string>;
|
||||
onImageClick: (
|
||||
documentId: string,
|
||||
chunk: IReferenceChunk,
|
||||
isPdf: boolean,
|
||||
documentUrl?: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface ReferenceInfo {
|
||||
documentUrl?: string;
|
||||
fileThumbnail?: string;
|
||||
fileExtension?: string;
|
||||
imageId?: string;
|
||||
chunkItem?: IReferenceChunk;
|
||||
documentId?: string;
|
||||
document?: any;
|
||||
}
|
||||
|
||||
const getReferenceInfo = (
|
||||
chunkIndex: number,
|
||||
reference: IReference,
|
||||
fileThumbnails: Record<string, string>,
|
||||
): ReferenceInfo => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to render image carousel for a group of consecutive image references
|
||||
*/
|
||||
export const ImageCarousel = ({
|
||||
group,
|
||||
reference,
|
||||
fileThumbnails,
|
||||
onImageClick,
|
||||
}: ImageCarouselProps) => {
|
||||
const getChunkIndex = (match: string) => Number(match);
|
||||
|
||||
const handleImageClick = useCallback(
|
||||
(
|
||||
imageId: string,
|
||||
chunkItem: IReferenceChunk,
|
||||
documentId: string,
|
||||
fileExtension: string,
|
||||
documentUrl?: string,
|
||||
) =>
|
||||
() =>
|
||||
onImageClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
),
|
||||
[onImageClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
className="w-44"
|
||||
opts={{
|
||||
align: 'start',
|
||||
skipSnaps: false,
|
||||
}}
|
||||
>
|
||||
<CarouselContent>
|
||||
{group.map((ref) => {
|
||||
const chunkIndex = getChunkIndex(ref.id);
|
||||
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
|
||||
getReferenceInfo(chunkIndex, reference, fileThumbnails);
|
||||
|
||||
return (
|
||||
<CarouselItem key={ref.id}>
|
||||
<section>
|
||||
<Image
|
||||
id={imageId!}
|
||||
className="object-contain max-h-36"
|
||||
onClick={
|
||||
documentId && chunkItem
|
||||
? handleImageClick(
|
||||
imageId!,
|
||||
chunkItem,
|
||||
documentId,
|
||||
fileExtension!,
|
||||
documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
}
|
||||
/>
|
||||
<span className="text-accent-primary">{imageId}</span>
|
||||
</section>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="h-8 w-8" />
|
||||
<CarouselNext className="h-8 w-8" />
|
||||
</Carousel>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageCarousel;
|
||||
@ -25,7 +25,7 @@
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 6vh;
|
||||
max-height: 10vh;
|
||||
}
|
||||
|
||||
.referenceImagePreview {
|
||||
|
||||
@ -5,7 +5,6 @@ import { getExtension } from '@/utils/document-util';
|
||||
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';
|
||||
@ -19,7 +18,6 @@ import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for
|
||||
|
||||
import { useFetchDocumentThumbnailsByIds } from '@/hooks/use-document-request';
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
@ -35,9 +33,15 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '../ui/hover-card';
|
||||
import { ImageCarousel } from './image-carousel';
|
||||
import styles from './index.less';
|
||||
import {
|
||||
groupConsecutiveReferences,
|
||||
shouldShowCarousel,
|
||||
} from './reference-utils';
|
||||
|
||||
const getChunkIndex = (match: string) => Number(match);
|
||||
|
||||
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||
const MarkdownContent = ({
|
||||
reference,
|
||||
@ -208,51 +212,88 @@ const MarkdownContent = ({
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
const groups = groupConsecutiveReferences(text);
|
||||
const elements = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
|
||||
getReferenceInfo(chunkIndex);
|
||||
groups.forEach((group, groupIndex) => {
|
||||
if (group[0].start > lastIndex) {
|
||||
elements.push(text.substring(lastIndex, group[0].start));
|
||||
}
|
||||
|
||||
const docType = chunkItem?.doc_type;
|
||||
if (shouldShowCarousel(group, reference)) {
|
||||
elements.push(
|
||||
<ImageCarousel
|
||||
key={`carousel-${groupIndex}`}
|
||||
group={group}
|
||||
reference={reference}
|
||||
fileThumbnails={fileThumbnails}
|
||||
onImageClick={handleDocumentButtonClick}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
group.forEach((ref) => {
|
||||
const chunkIndex = getChunkIndex(ref.id);
|
||||
const {
|
||||
documentUrl,
|
||||
fileExtension,
|
||||
imageId,
|
||||
chunkItem,
|
||||
documentId,
|
||||
} = getReferenceInfo(chunkIndex);
|
||||
const docType = chunkItem?.doc_type;
|
||||
|
||||
return showImage(docType) ? (
|
||||
<section>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
}
|
||||
></Image>
|
||||
<span className="text-accent-primary"> {imageId}</span>
|
||||
</section>
|
||||
) : (
|
||||
<HoverCard key={i}>
|
||||
<HoverCardTrigger>
|
||||
<CircleAlert className="size-4 inline-block" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-w-3xl">
|
||||
{getPopoverContent(chunkIndex)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
if (showImage(docType)) {
|
||||
elements.push(
|
||||
<section key={ref.id}>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
}
|
||||
/>
|
||||
<span className="text-accent-primary"> {imageId}</span>
|
||||
</section>,
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<HoverCard key={ref.id}>
|
||||
<HoverCardTrigger>
|
||||
<CircleAlert className="size-4 inline-block" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-w-3xl">
|
||||
{getPopoverContent(chunkIndex)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = group[group.length - 1].end;
|
||||
});
|
||||
|
||||
// replacedText = reactStringReplace(replacedText, curReg, (match, i) => (
|
||||
// <span className={styles.cursor} key={i}></span>
|
||||
// ));
|
||||
if (lastIndex < text.length) {
|
||||
elements.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
return replacedText;
|
||||
return elements;
|
||||
},
|
||||
[getPopoverContent, getReferenceInfo, handleDocumentButtonClick],
|
||||
[
|
||||
getPopoverContent,
|
||||
getReferenceInfo,
|
||||
handleDocumentButtonClick,
|
||||
reference,
|
||||
fileThumbnails,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
67
web/src/components/markdown-content/reference-utils.ts
Normal file
67
web/src/components/markdown-content/reference-utils.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { IReference } from '@/interfaces/database/chat';
|
||||
import { currentReg, showImage } from '@/utils/chat';
|
||||
|
||||
export interface ReferenceMatch {
|
||||
id: string;
|
||||
fullMatch: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export type ReferenceGroup = ReferenceMatch[];
|
||||
|
||||
export const findAllReferenceMatches = (text: string): ReferenceMatch[] => {
|
||||
const matches: ReferenceMatch[] = [];
|
||||
let match;
|
||||
while ((match = currentReg.exec(text)) !== null) {
|
||||
matches.push({
|
||||
id: match[1],
|
||||
fullMatch: match[0],
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
});
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to group consecutive references
|
||||
*/
|
||||
export const groupConsecutiveReferences = (text: string): ReferenceGroup[] => {
|
||||
const matches = findAllReferenceMatches(text);
|
||||
// Construct a two-dimensional array to distinguish whether images are continuous.
|
||||
const groups: ReferenceGroup[] = [];
|
||||
|
||||
if (matches.length === 0) return groups;
|
||||
|
||||
let currentGroup: ReferenceGroup = [matches[0]];
|
||||
// A group with only one element contains non-contiguous images,
|
||||
// while a group with multiple elements contains contiguous images.
|
||||
for (let i = 1; i < matches.length; i++) {
|
||||
// If the end of the previous element equals the start of the current element,
|
||||
// it means that they are consecutive images.
|
||||
if (matches[i].start === currentGroup[currentGroup.length - 1].end) {
|
||||
currentGroup.push(matches[i]);
|
||||
} else {
|
||||
// Save current group and start a new one
|
||||
groups.push(currentGroup);
|
||||
currentGroup = [matches[i]];
|
||||
}
|
||||
}
|
||||
groups.push(currentGroup);
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
export const shouldShowCarousel = (
|
||||
group: ReferenceGroup,
|
||||
reference: IReference,
|
||||
): boolean => {
|
||||
if (group.length < 2) return false; // Need at least 2 images for carousel
|
||||
|
||||
return group.every((ref) => {
|
||||
const chunkIndex = Number(ref.id);
|
||||
const chunk = reference.chunks[chunkIndex];
|
||||
return chunk && showImage(chunk.doc_type);
|
||||
});
|
||||
};
|
||||
@ -7,6 +7,7 @@ interface NumberInputProps {
|
||||
onChange?: (value: number) => void;
|
||||
height?: number | string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const NumberInput: React.FC<NumberInputProps> = ({
|
||||
@ -15,6 +16,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
onChange,
|
||||
height,
|
||||
min = 0,
|
||||
max = Infinity,
|
||||
}) => {
|
||||
const [value, setValue] = useState<number>(() => {
|
||||
return initialValue ?? 0;
|
||||
@ -34,6 +36,9 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
if (value > max - 1) {
|
||||
return;
|
||||
}
|
||||
setValue(value + 1);
|
||||
onChange?.(value + 1);
|
||||
};
|
||||
@ -41,6 +46,9 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = Number(e.target.value);
|
||||
if (!isNaN(newValue)) {
|
||||
if (newValue > max) {
|
||||
return;
|
||||
}
|
||||
setValue(newValue);
|
||||
onChange?.(newValue);
|
||||
}
|
||||
|
||||
@ -7,7 +7,11 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ReactNode, cloneElement, isValidElement } from 'react';
|
||||
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
ControllerRenderProps,
|
||||
UseControllerProps,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
|
||||
type RAGFlowFormItemProps = {
|
||||
name: string;
|
||||
@ -18,7 +22,7 @@ type RAGFlowFormItemProps = {
|
||||
required?: boolean;
|
||||
labelClassName?: string;
|
||||
className?: string;
|
||||
};
|
||||
} & Pick<UseControllerProps<any>, 'rules'>;
|
||||
|
||||
export function RAGFlowFormItem({
|
||||
name,
|
||||
@ -29,11 +33,13 @@ export function RAGFlowFormItem({
|
||||
required = false,
|
||||
labelClassName,
|
||||
className,
|
||||
rules,
|
||||
}: RAGFlowFormItemProps) {
|
||||
const form = useFormContext();
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
rules={rules}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
|
||||
241
web/src/components/ui/carousel.tsx
Normal file
241
web/src/components/ui/carousel.tsx
Normal file
@ -0,0 +1,241 @@
|
||||
'use client';
|
||||
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -left-12 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -right-12 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
type CarouselApi,
|
||||
};
|
||||
@ -196,7 +196,7 @@ export enum SwitchLogicOperator {
|
||||
Or = 'or',
|
||||
}
|
||||
|
||||
export const WebhookAlgorithmList = [
|
||||
export const WebhookJWTAlgorithmList = [
|
||||
'hs256',
|
||||
'hs384',
|
||||
'hs512',
|
||||
|
||||
@ -9,8 +9,12 @@ import {
|
||||
IFlowTemplate,
|
||||
IPipeLineListRequest,
|
||||
ITraceData,
|
||||
IWebhookTrace,
|
||||
} from '@/interfaces/database/agent';
|
||||
import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
|
||||
import {
|
||||
IAgentWebhookTraceRequest,
|
||||
IDebugSingleRequestBody,
|
||||
} from '@/interfaces/request/agent';
|
||||
import i18n from '@/locales/config';
|
||||
import { BeginId } from '@/pages/agent/constant';
|
||||
import { IInputs } from '@/pages/agent/interface';
|
||||
@ -19,6 +23,7 @@ import agentService, {
|
||||
fetchAgentLogsByCanvasId,
|
||||
fetchPipeLineList,
|
||||
fetchTrace,
|
||||
fetchWebhookTrace,
|
||||
} from '@/services/agent-service';
|
||||
import api from '@/utils/api';
|
||||
import { buildMessageListWithUuid } from '@/utils/chat';
|
||||
@ -55,6 +60,7 @@ export const enum AgentApiAction {
|
||||
FetchPrompt = 'fetchPrompt',
|
||||
CancelDataflow = 'cancelDataflow',
|
||||
CancelCanvas = 'cancelCanvas',
|
||||
FetchWebhookTrace = 'fetchWebhookTrace',
|
||||
}
|
||||
|
||||
export const EmptyDsl = {
|
||||
@ -786,3 +792,70 @@ export const useFetchFlowSSE = (): {
|
||||
|
||||
return { data, loading, refetch };
|
||||
};
|
||||
|
||||
export const useFetchWebhookTrace = (autoStart: boolean = true) => {
|
||||
const { id } = useParams();
|
||||
const [currentWebhookId, setCurrentWebhookId] = useState<string>('');
|
||||
const [currentNextSinceTs, setCurrentNextSinceTs] = useState<number>(0);
|
||||
const [shouldPoll, setShouldPoll] = useState(autoStart);
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useQuery<IWebhookTrace>({
|
||||
queryKey: [AgentApiAction.FetchWebhookTrace, id],
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchIntervalInBackground: false,
|
||||
gcTime: 0,
|
||||
enabled: !!id && shouldPoll,
|
||||
queryFn: async () => {
|
||||
if (!id) return {};
|
||||
|
||||
const payload: IAgentWebhookTraceRequest =
|
||||
{} as IAgentWebhookTraceRequest;
|
||||
|
||||
if (currentNextSinceTs) {
|
||||
payload['since_ts'] = currentNextSinceTs;
|
||||
}
|
||||
|
||||
if (currentWebhookId) {
|
||||
payload['webhook_id'] = currentWebhookId;
|
||||
}
|
||||
|
||||
const { data } = await fetchWebhookTrace(id, payload);
|
||||
|
||||
const result = data.data ?? {};
|
||||
|
||||
if (result.webhook_id && result.webhook_id !== currentWebhookId) {
|
||||
setCurrentWebhookId(result.webhook_id);
|
||||
}
|
||||
|
||||
if (
|
||||
currentNextSinceTs === 0 &&
|
||||
result.next_since_ts &&
|
||||
result.next_since_ts !== currentNextSinceTs
|
||||
) {
|
||||
setCurrentNextSinceTs(result.next_since_ts);
|
||||
}
|
||||
|
||||
if (result.finished) {
|
||||
setShouldPoll(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
refetchInterval: shouldPoll ? 3000 : false,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
refetch,
|
||||
isPolling: shouldPoll,
|
||||
currentWebhookId,
|
||||
currentNextSinceTs,
|
||||
};
|
||||
};
|
||||
|
||||
@ -291,3 +291,10 @@ export interface GlobalVariableType {
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IWebhookTrace {
|
||||
webhook_id: null;
|
||||
events: any[];
|
||||
next_since_ts: number;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
@ -2,3 +2,8 @@ export interface IDebugSingleRequestBody {
|
||||
component_id: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IAgentWebhookTraceRequest {
|
||||
since_ts: number; // From the first request for return
|
||||
webhook_id: string; // Each external request generates a unique webhook ID.
|
||||
}
|
||||
|
||||
@ -175,6 +175,21 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
|
||||
parserRequired: 'Chunk method is required',
|
||||
},
|
||||
knowledgeDetails: {
|
||||
metadata: {
|
||||
changesAffectNewParses: 'Changes affect new parses only.',
|
||||
editMetadataForDataset: 'View and edit metadata for ',
|
||||
restrictDefinedValues: 'Restrict to defined values',
|
||||
metadataGenerationSettings: 'Metadata generation settings',
|
||||
manageMetadataForDataset: 'Manage metadata for this dataset',
|
||||
manageMetadata: 'Manage metadata',
|
||||
metadata: 'Metadata',
|
||||
values: 'Values',
|
||||
action: 'Action',
|
||||
field: 'Field',
|
||||
description: 'Description',
|
||||
fieldName: 'Field name',
|
||||
editMetadata: 'Edit metadata',
|
||||
},
|
||||
localUpload: 'Local upload',
|
||||
fileSize: 'File size',
|
||||
fileType: 'File type',
|
||||
@ -348,6 +363,8 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
|
||||
reRankModelWaring: 'Re-rank model is very time consuming.',
|
||||
},
|
||||
knowledgeConfiguration: {
|
||||
settings: 'Settings',
|
||||
autoMetadata: 'Auto metadata',
|
||||
mineruOptions: 'MinerU Options',
|
||||
mineruParseMethod: 'Parse Method',
|
||||
mineruParseMethodTip:
|
||||
|
||||
@ -267,6 +267,8 @@ export default {
|
||||
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
|
||||
},
|
||||
knowledgeConfiguration: {
|
||||
settings: '设置',
|
||||
autoMetadata: '自动元数据',
|
||||
mineruOptions: 'MinerU 选项',
|
||||
mineruParseMethod: '解析方法',
|
||||
mineruParseMethodTip:
|
||||
|
||||
@ -1050,12 +1050,18 @@ export enum WebhookSecurityAuthType {
|
||||
Token = 'token',
|
||||
Basic = 'basic',
|
||||
Jwt = 'jwt',
|
||||
Hmac = 'hmac',
|
||||
}
|
||||
|
||||
export const RateLimitPerList = ['minute', 'hour', 'day'];
|
||||
export enum WebhookRateLimitPer {
|
||||
Second = 'second',
|
||||
Minute = 'minute',
|
||||
Hour = 'hour',
|
||||
Day = 'day',
|
||||
}
|
||||
|
||||
export const WebhookMaxBodySize = ['10MB', '50MB', '100MB', '1000MB'];
|
||||
export const RateLimitPerList = Object.values(WebhookRateLimitPer);
|
||||
|
||||
export const WebhookMaxBodySize = ['1MB', '5MB', '10MB'];
|
||||
|
||||
export enum WebhookRequestParameters {
|
||||
File = VariableType.File,
|
||||
|
||||
@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||
const form = useForm({
|
||||
defaultValues: values,
|
||||
resolver: zodResolver(BeginFormSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookJWTAlgorithmList } from '@/constants/agent';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BeginFormSchema = z.object({
|
||||
@ -30,7 +30,14 @@ export const BeginFormSchema = z.object({
|
||||
max_body_size: z.string(),
|
||||
jwt: z
|
||||
.object({
|
||||
algorithm: z.string().default(WebhookAlgorithmList[0]).optional(),
|
||||
algorithm: z.string().default(WebhookJWTAlgorithmList[0]).optional(),
|
||||
required_claims: z.array(z.object({ value: z.string() })),
|
||||
})
|
||||
.optional(),
|
||||
hmac: z
|
||||
.object({
|
||||
header: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
@ -2,11 +2,11 @@ import { useCallback } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
AgentDialogueMode,
|
||||
RateLimitPerList,
|
||||
WebhookContentType,
|
||||
WebhookExecutionMode,
|
||||
WebhookMaxBodySize,
|
||||
WebhookMethod,
|
||||
WebhookRateLimitPer,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../constant';
|
||||
|
||||
@ -14,7 +14,7 @@ const initialFormValuesMap = {
|
||||
methods: [WebhookMethod.Get],
|
||||
schema: {},
|
||||
'security.auth_type': WebhookSecurityAuthType.Basic,
|
||||
'security.rate_limit.per': RateLimitPerList[0],
|
||||
'security.rate_limit.per': WebhookRateLimitPer.Second,
|
||||
'security.rate_limit.limit': 10,
|
||||
'security.max_body_size': WebhookMaxBodySize[0],
|
||||
'response.status': 200,
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookJWTAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookSecurityAuthType } from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||
|
||||
const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
|
||||
|
||||
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);
|
||||
const AlgorithmOptions = buildOptions(WebhookJWTAlgorithmList);
|
||||
|
||||
export function Auth() {
|
||||
const { t } = useTranslation();
|
||||
@ -88,38 +87,10 @@ export function Auth() {
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
<DynamicStringForm
|
||||
name="security.jwt.required_claims"
|
||||
label={t('flow.webhook.requiredClaims')}
|
||||
>
|
||||
<SelectWithSearch options={RequiredClaimsOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderHmacAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.header"
|
||||
label={t('flow.webhook.header')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.secret"
|
||||
label={t('flow.webhook.secret')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.algorithm"
|
||||
label={t('flow.webhook.algorithm')}
|
||||
>
|
||||
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
></DynamicStringForm>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
@ -129,11 +100,14 @@ export function Auth() {
|
||||
[WebhookSecurityAuthType.Token]: renderTokenAuth,
|
||||
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
|
||||
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
|
||||
[WebhookSecurityAuthType.Hmac]: renderHmacAuth,
|
||||
[WebhookSecurityAuthType.None]: () => null,
|
||||
};
|
||||
|
||||
return AuthMap[
|
||||
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
|
||||
]();
|
||||
return (
|
||||
<div key={`auth-${authType}`} className="space-y-5">
|
||||
{AuthMap[
|
||||
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
|
||||
]()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,15 +6,10 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { omit } from 'lodash';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
TypesWithArray,
|
||||
WebhookContentType,
|
||||
WebhookRequestParameters,
|
||||
} from '../../../constant';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { TypesWithArray, WebhookRequestParameters } from '../../../constant';
|
||||
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
@ -28,16 +23,9 @@ type SelectKeysProps = {
|
||||
requiredField?: string;
|
||||
nodeId?: string;
|
||||
isObject?: boolean;
|
||||
operatorList: WebhookRequestParameters[];
|
||||
};
|
||||
|
||||
function buildParametersOptions(isObject: boolean) {
|
||||
const list = isObject
|
||||
? WebhookRequestParameters
|
||||
: omit(WebhookRequestParameters, ['File']);
|
||||
|
||||
return buildOptions(list);
|
||||
}
|
||||
|
||||
export function DynamicRequest({
|
||||
name,
|
||||
label,
|
||||
@ -45,15 +33,9 @@ export function DynamicRequest({
|
||||
keyField = 'key',
|
||||
operatorField = 'type',
|
||||
requiredField = 'required',
|
||||
isObject = false,
|
||||
operatorList,
|
||||
}: SelectKeysProps) {
|
||||
const form = useFormContext();
|
||||
const contentType = useWatch({
|
||||
name: 'content_types',
|
||||
control: form.control,
|
||||
});
|
||||
const isFormDataContentType =
|
||||
contentType === WebhookContentType.MultipartFormData;
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
@ -94,9 +76,7 @@ export function DynamicRequest({
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
options={buildParametersOptions(
|
||||
isObject && isFormDataContentType,
|
||||
)}
|
||||
options={buildOptions(operatorList)}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { CopyToClipboardWithText } from '@/components/copy-to-clipboard';
|
||||
import NumberInput from '@/components/originui/number-input';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import {
|
||||
RateLimitPerList,
|
||||
WebhookMaxBodySize,
|
||||
WebhookMethod,
|
||||
WebhookRateLimitPer,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../../constant';
|
||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||
@ -21,18 +24,32 @@ import { WebhookResponse } from './response';
|
||||
|
||||
const RateLimitPerOptions = buildOptions(RateLimitPerList);
|
||||
|
||||
const RequestLimitMap = {
|
||||
[WebhookRateLimitPer.Second]: 100,
|
||||
[WebhookRateLimitPer.Minute]: 1000,
|
||||
[WebhookRateLimitPer.Hour]: 10000,
|
||||
[WebhookRateLimitPer.Day]: 100000,
|
||||
};
|
||||
|
||||
export function WebHook() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const form = useFormContext();
|
||||
|
||||
const rateLimitPer = useWatch({
|
||||
name: 'security.rate_limit.per',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const getLimitRateLimitPerMax = useCallback((rateLimitPer: string) => {
|
||||
return RequestLimitMap[rateLimitPer as keyof typeof RequestLimitMap] ?? 100;
|
||||
}, []);
|
||||
|
||||
const text = `${location.protocol}//${location.host}/api/v1/webhook/${id}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-bg-card p-1 rounded-md flex gap-2">
|
||||
<span className="flex-1 truncate">{text}</span>
|
||||
<CopyToClipboard text={text}></CopyToClipboard>
|
||||
</div>
|
||||
<CopyToClipboardWithText text={text}></CopyToClipboardWithText>
|
||||
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
|
||||
{(field) => (
|
||||
<MultiSelect
|
||||
@ -61,13 +78,28 @@ export function WebHook() {
|
||||
name="security.rate_limit.limit"
|
||||
label={t('flow.webhook.limit')}
|
||||
>
|
||||
<Input type="number"></Input>
|
||||
<NumberInput
|
||||
max={getLimitRateLimitPerMax(rateLimitPer)}
|
||||
className="w-full"
|
||||
></NumberInput>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.rate_limit.per"
|
||||
label={t('flow.webhook.per')}
|
||||
>
|
||||
<SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
options={RateLimitPerOptions}
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
form.setValue(
|
||||
'security.rate_limit.limit',
|
||||
getLimitRateLimitPerMax(val),
|
||||
);
|
||||
}}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.max_body_size"
|
||||
|
||||
@ -1,13 +1,40 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { WebhookContentType } from '@/pages/agent/constant';
|
||||
import {
|
||||
WebhookContentType,
|
||||
WebhookRequestParameters,
|
||||
} from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicRequest } from './dynamic-request';
|
||||
|
||||
export function WebhookRequestSchema() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const contentType = useWatch({
|
||||
name: 'content_types',
|
||||
control: form.control,
|
||||
});
|
||||
const isFormDataContentType =
|
||||
contentType === WebhookContentType.MultipartFormData;
|
||||
|
||||
const bodyOperatorList = useMemo(() => {
|
||||
return isFormDataContentType
|
||||
? [
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
WebhookRequestParameters.File,
|
||||
]
|
||||
: [
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
];
|
||||
}, [isFormDataContentType]);
|
||||
|
||||
return (
|
||||
<Collapse title={<div>{t('flow.webhook.schema')}</div>}>
|
||||
@ -23,14 +50,20 @@ export function WebhookRequestSchema() {
|
||||
<DynamicRequest
|
||||
name="schema.query"
|
||||
label={t('flow.webhook.queryParameters')}
|
||||
operatorList={[
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
]}
|
||||
></DynamicRequest>
|
||||
<DynamicRequest
|
||||
name="schema.headers"
|
||||
label={t('flow.webhook.headerParameters')}
|
||||
operatorList={[WebhookRequestParameters.String]}
|
||||
></DynamicRequest>
|
||||
<DynamicRequest
|
||||
name="schema.body"
|
||||
isObject
|
||||
operatorList={bodyOperatorList}
|
||||
label={t('flow.webhook.requestBodyParameters')}
|
||||
></DynamicRequest>
|
||||
</section>
|
||||
|
||||
@ -49,7 +49,7 @@ export function WebhookResponse() {
|
||||
name="response.body_template"
|
||||
label={t('flow.webhook.bodyTemplate')}
|
||||
>
|
||||
<Textarea></Textarea>
|
||||
<Textarea className="overflow-auto"></Textarea>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
21
web/src/pages/agent/form/components/json-viewer.tsx
Normal file
21
web/src/pages/agent/form/components/json-viewer.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import JsonView from 'react18-json-view';
|
||||
|
||||
export function JsonViewer({
|
||||
data,
|
||||
title,
|
||||
}: {
|
||||
data: Record<string, any>;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<div>{title}</div>
|
||||
<JsonView
|
||||
src={data}
|
||||
displaySize
|
||||
collapseStringsAfterLength={100000000000}
|
||||
className="w-full h-[200px] break-words overflow-auto scrollbar-auto p-2 bg-muted"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
10
web/src/pages/agent/hooks/use-is-webhook.ts
Normal file
10
web/src/pages/agent/hooks/use-is-webhook.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { AgentDialogueMode, BeginId } from '../constant';
|
||||
import useGraphStore from '../store';
|
||||
|
||||
export function useIsWebhookMode() {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const beginNode = getNode(BeginId);
|
||||
|
||||
return beginNode?.data.form?.mode === AgentDialogueMode.Webhook;
|
||||
}
|
||||
@ -46,6 +46,7 @@ import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
|
||||
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
|
||||
import { useIsPipeline } from './hooks/use-is-pipeline';
|
||||
import { useIsWebhookMode } from './hooks/use-is-webhook';
|
||||
import { useRunDataflow } from './hooks/use-run-dataflow';
|
||||
import {
|
||||
useSaveGraph,
|
||||
@ -58,6 +59,7 @@ import { SettingDialog } from './setting-dialog';
|
||||
import useGraphStore from './store';
|
||||
import { useAgentHistoryManager } from './use-agent-history-manager';
|
||||
import { VersionDialog } from './version-dialog';
|
||||
import WebhookSheet from './webhook-sheet';
|
||||
|
||||
function AgentDropdownMenuItem({
|
||||
children,
|
||||
@ -110,6 +112,7 @@ export default function Agent() {
|
||||
useShowEmbedModal();
|
||||
const { navigateToAgentLogs } = useNavigatePage();
|
||||
const time = useWatchAgentChange(chatDrawerVisible);
|
||||
const isWebhookMode = useIsWebhookMode();
|
||||
|
||||
// pipeline
|
||||
|
||||
@ -119,6 +122,12 @@ export default function Agent() {
|
||||
showModal: showPipelineRunSheet,
|
||||
} = useSetModalState();
|
||||
|
||||
const {
|
||||
visible: webhookTestSheetVisible,
|
||||
hideModal: hideWebhookTestSheet,
|
||||
showModal: showWebhookTestSheet,
|
||||
} = useSetModalState();
|
||||
|
||||
const {
|
||||
visible: pipelineLogSheetVisible,
|
||||
showModal: showPipelineLogSheet,
|
||||
@ -172,12 +181,22 @@ export default function Agent() {
|
||||
});
|
||||
|
||||
const handleButtonRunClick = useCallback(() => {
|
||||
if (isPipeline) {
|
||||
if (isWebhookMode) {
|
||||
saveGraph();
|
||||
showWebhookTestSheet();
|
||||
} else if (isPipeline) {
|
||||
handleRunPipeline();
|
||||
} else {
|
||||
handleRunAgent();
|
||||
}
|
||||
}, [handleRunAgent, handleRunPipeline, isPipeline]);
|
||||
}, [
|
||||
handleRunAgent,
|
||||
handleRunPipeline,
|
||||
isPipeline,
|
||||
isWebhookMode,
|
||||
saveGraph,
|
||||
showWebhookTestSheet,
|
||||
]);
|
||||
|
||||
const {
|
||||
run: runPipeline,
|
||||
@ -320,6 +339,9 @@ export default function Agent() {
|
||||
hideModal={hideGlobalParamSheet}
|
||||
></GlobalParamSheet>
|
||||
)}
|
||||
{webhookTestSheetVisible && (
|
||||
<WebhookSheet hideModal={hideWebhookTestSheet}></WebhookSheet>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,243 +0,0 @@
|
||||
export const list = [
|
||||
{
|
||||
event: 'node_finished',
|
||||
message_id: 'dce6c0c8466611f08e04047c16ec874f',
|
||||
created_at: 1749606805,
|
||||
task_id: 'db68eb0645ab11f0bbdc047c16ec874f',
|
||||
data: {
|
||||
inputs: {},
|
||||
outputs: {
|
||||
_elapsed_time: 0.000010083022061735392,
|
||||
},
|
||||
component_id: 'begin',
|
||||
error: null,
|
||||
elapsed_time: 0.000010083022061735392,
|
||||
created_at: 1749606805,
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'node_finished',
|
||||
message_id: 'dce6c0c8466611f08e04047c16ec874f',
|
||||
created_at: 1749606805,
|
||||
task_id: 'db68eb0645ab11f0bbdc047c16ec874f',
|
||||
data: {
|
||||
inputs: {
|
||||
query: '算法',
|
||||
},
|
||||
outputs: {
|
||||
formalized_content:
|
||||
'\nDocument: Mallat算法频3333率混叠原因及其改进模型.pdf \nRelevant fragments as following:\n---\nID: Retrieval:ClearHornetsClap_0\nFig. 6 The improved decomposition model of Ma llat algorithm\n图6 改进的Mallat分解算法模型\ncaj\nG\n→cdj+1\n↑2\nT\ncdj+1\n---\nID: Retrieval:ClearHornetsClap_1\n2 Mallat算法Mallat算法是StéPhanMallat将计算机视觉领域内的多分辨分析思想引入到小波分析中,推导出的小波分析快速算法但在具体使用时,Mallat算法是利用与尺度函数和小波函数相对应的小波低通滤波器H, h和小波高通滤波器G, g对信号进行低通和高通滤波来具体实现的为了叙述方便,在此把尺度函数称为低频子带,小波函数称为次高频子带Mallat算法如下\n---\nID: Retrieval:ClearHornetsClap_2\n4 改进算法模型经过对Mallat算法的深入研究可知,Mallat算法实现过程会不可避免地产生频率混叠现象可是,为什么现在小波分析的Mallat算法还能得到广泛的应用呢?这就是Mallat算法的精妙之处由小波分解算法和重构算法可知,小波分解过程是隔点采样的过程,重构过程是隔点插零的过程,实际上这两个过程都产生频率混叠,但是它们产生混叠的方向正好相反也就是说分解过程产生的混叠又在重构过程中得到了纠正[13 ]限于篇幅隔点插零产生频率混叠现象本文不做详细的讨论不过,这也给如何解决Mallat分解算法产生频率混叠现象提供了一个思路:在利用Mallat算法对信号分解,得到各尺度上小波系数后,再按照重构的方法重构至所需的小波空间系数cdj ,利用该重构系数cdj代替相应尺度上得到的小波系数cdj 来分析该尺度上的信号,这样就能较好地解决频率混叠所带来的影响,以达到预期的目的本文称这种算法为子带信号重构算法经改进的算法模型如图6所示\n---\nID: Retrieval:ClearHornetsClap_3\n关键词:小波分析;Mallat算法;频率混叠中图分类号: TN911. 6 文献标识码: A 文章编号: 10030972 (2007)04051104Reason andM eliorationModel of Generating Frequency A liasing of Ma llat A lgor ithmGUO Chaofeng, L IM e ilian(College of Computer Science &Technology, XuchangUniversity, Xuchang 461000, China)Abstract:Because of the design ofMallatA lgorithm, the phenomenon of frequency aliasing exists in the signal decomposition process. Based on research and analysis ofMallat algorithm, the reasons thatMallat algorithm generates frequency aliasing were found out, and an improved modle that can elim inate efficiently frequency aliasingwas given.\n---\nID: Retrieval:ClearHornetsClap_4\n·应用技术研究·Mallat算率混叠原因及其改进模型郭超峰,李梅莲(许昌学院计算机科学与技术学院,河南许昌461000)摘 要:Mallat算法由于自身设计的原因,在信号分解过程中,存在频率混叠现象在利用小波分析进行信号提取时,这种现象是一个不容忽视的问题. 通过分析Mallat算法,找出了造成Mallat算法产生频率混叠的原因,给出了一个能有效消除频率混叠的改进算法模型\n---\nID: Retrieval:ClearHornetsClap_5\nF ig. 4 Two 1-D miensiona l pagoda decomposition process ofMa llat a lgor ithm重构算法:2fsHz, J表示分解的深度A j [f (t)]=2{∑k h (t -2k)A j+1 [f (t)]+其中: j、J意义与式(2)相同, j =J -1, J -2, …, 0; h, g为小波重构; A j、D j 意义与式(2)相同Mallat二维塔式小波变换的重构过程如图5所示\n---\nID: Retrieval:ClearHornetsClap_6\n1 Mallat算法的频率混叠现象对最高频率为的带限信号进行离散化抽样,如果抽样周期比较大,或者说抽样频率比较小,那么抽样将会导致频率相邻的2个被延拓的频谱发生叠加而互相产生影响,这种现象称为混叠[78 ]下面是一个利用Mallat算法进行信号分解的例子[912 ]\n---\nID: Retrieval:ClearHornetsClap_7\n∑k g (t -2k)D j+1 [f (t)]}, \n',
|
||||
_references: {
|
||||
total: 30,
|
||||
chunks: [
|
||||
{
|
||||
chunk_id: '64fe175ac75330dd',
|
||||
content_ltks:
|
||||
'fig 6 the improv decomposit model of ma llat algorithm 图 6 改进 的 mallat 分解 算法 模型 caj g cdj 12 t cdj 1',
|
||||
content_with_weight:
|
||||
'Fig. 6 The improved decomposition model of Ma llat algorithm\n图6 改进的Mallat分解算法模型\ncaj\nG\n→cdj+1\n↑2\nT\ncdj+1',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-64fe175ac75330dd',
|
||||
similarity: 0.8437627020510018,
|
||||
vector_similarity: 0.47920900683667284,
|
||||
term_similarity: 1,
|
||||
positions: [[3, 303, 500, 513, 545]],
|
||||
doc_type_kwd: 'image',
|
||||
},
|
||||
{
|
||||
chunk_id: 'dad90c5ea1b0945b',
|
||||
content_ltks:
|
||||
'2 mallat 算法 mallat 算法 是 st é phanmallat 将 计算机 视觉 领域 内 的 多 分辨 分析 思想 引入 到 小波 分析 中 推导 出 的 小波 分析 快速 算法 但 在 具体 使用 时 mallat 算法 是 利用 与 尺度 函数 和 小波 函数 相对 应 的 小波 低通滤波器 h h 和 小波 高通 滤波器 g g 对 信号 进行 低 通和 高 通滤波 来 具体 实现 的 为了 叙述 方便 在 此 把 尺度 函数 称为 低频 子 带 小 波函数 称为 次 高频 子 带 mallat 算法 如下',
|
||||
content_with_weight:
|
||||
'2 Mallat算法Mallat算法是StéPhanMallat将计算机视觉领域内的多分辨分析思想引入到小波分析中,推导出的小波分析快速算法但在具体使用时,Mallat算法是利用与尺度函数和小波函数相对应的小波低通滤波器H, h和小波高通滤波器G, g对信号进行低通和高通滤波来具体实现的为了叙述方便,在此把尺度函数称为低频子带,小波函数称为次高频子带Mallat算法如下',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-dad90c5ea1b0945b',
|
||||
similarity: 0.8354827046851212,
|
||||
vector_similarity: 0.4516090156170707,
|
||||
term_similarity: 1,
|
||||
positions: [[2, 38, 267, 673, 770]],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: '28df4d0c894e3201',
|
||||
content_ltks:
|
||||
'4 改进 算法 模型 经过 对 mallat 算法 的 深入研究 可知 mallat 算法 实现 过程 会 不可避免 地 产生 频率 混 叠 现象 可是 为什么 现在 小波 分析 的 mallat 算法 还 能 得到 广泛 的 应用 呢 这 就是 mallat 算法 的 精妙 之处 由 小波 分解 算法 和 重构 算法 可知 小波 分解 过程 是 隔 点 采样 的 过程 重构 过程 是 隔 点 插 零 的 过程 实际上 这 两个 过程 都 产生 频率 混 叠 但是 它们 产生 混 叠 的 方向 正好 相反 也就是说 分解 过程 产生 的 混 叠 又 在 重构 过程 中 得到 了 纠正 13 限于 篇幅 隔 点 插 零 产生 频率 混 叠 现象 本文 不 做 详细 的 讨论 不过 这 也 给 如何 解决 mallat 分解 算法 产生 频率 混 叠 现象 提供 了 一个 思路 在 利用 mallat 算法 对 信号 分解 得到 各 尺度 上 小波 系数 后 再 按照 重构 的 方法 重构 至 所需 的 小波 空间 系数 cdj 利用 该 重构 系数 cdj 代替 相应 尺度 上 得到 的 小波 系数 cdj 来 分析 该 尺度 上 的 信号 这样 就 能 较 好地解决 频率 混 叠 所 带来 的 影响 以 达到 预期 的 目的 本文 称 这种 算法 为 子 带 信号 重构 算法 经 改进 的 算法 模型 如 图 6 所示',
|
||||
content_with_weight:
|
||||
'4 改进算法模型经过对Mallat算法的深入研究可知,Mallat算法实现过程会不可避免地产生频率混叠现象可是,为什么现在小波分析的Mallat算法还能得到广泛的应用呢?这就是Mallat算法的精妙之处由小波分解算法和重构算法可知,小波分解过程是隔点采样的过程,重构过程是隔点插零的过程,实际上这两个过程都产生频率混叠,但是它们产生混叠的方向正好相反也就是说分解过程产生的混叠又在重构过程中得到了纠正[13 ]限于篇幅隔点插零产生频率混叠现象本文不做详细的讨论不过,这也给如何解决Mallat分解算法产生频率混叠现象提供了一个思路:在利用Mallat算法对信号分解,得到各尺度上小波系数后,再按照重构的方法重构至所需的小波空间系数cdj ,利用该重构系数cdj代替相应尺度上得到的小波系数cdj 来分析该尺度上的信号,这样就能较好地解决频率混叠所带来的影响,以达到预期的目的本文称这种算法为子带信号重构算法经改进的算法模型如图6所示',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-28df4d0c894e3201',
|
||||
similarity: 0.8322061155631969,
|
||||
vector_similarity: 0.4406870518773232,
|
||||
term_similarity: 1,
|
||||
positions: [
|
||||
[3, 285, 518, 257, 272],
|
||||
[3, 282, 515, 280, 504],
|
||||
],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: 'e79b07acbec9eb61',
|
||||
content_ltks:
|
||||
'关键词 小波 分析 mallat 算法 频率 混 叠 中图 分类号 tn911 6 文献 标识码 a 文章 编号 10030972 2007 04051104reason andm eliorationmodel of gener frequenc a lias of ma llat a lgor ithmguo chaofeng l im e ilian colleg of comput scienc technolog xuchangunivers xuchang 461000 china abstract becaus of the design ofmallata lgorithm the phenomenon of frequenc alias exist in the signal decomposit process base on research and analysi ofmallat algorithm the reason thatmallat algorithm gener frequenc alias were found out and an improv modl that can elim inat effici frequenc aliasingwa given',
|
||||
content_with_weight:
|
||||
'关键词:小波分析;Mallat算法;频率混叠中图分类号: TN911. 6 文献标识码: A 文章编号: 10030972 (2007)04051104Reason andM eliorationModel of Generating Frequency A liasing of Ma llat A lgor ithmGUO Chaofeng, L IM e ilian(College of Computer Science &Technology, XuchangUniversity, Xuchang 461000, China)Abstract:Because of the design ofMallatA lgorithm, the phenomenon of frequency aliasing exists in the signal decomposition process. Based on research and analysis ofMallat algorithm, the reasons thatMallat algorithm generates frequency aliasing were found out, and an improved modle that can elim inate efficiently frequency aliasingwas given.',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-e79b07acbec9eb61',
|
||||
similarity: 0.8294912664806687,
|
||||
vector_similarity: 0.43163755493556244,
|
||||
term_similarity: 1,
|
||||
positions: [
|
||||
[1, 81, 528, 240, 251],
|
||||
[1, 81, 528, 255, 266],
|
||||
[1, 54, 501, 287, 302],
|
||||
[1, 211, 657, 305, 317],
|
||||
[1, 113, 560, 319, 331],
|
||||
[1, 65, 512, 334, 395],
|
||||
],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: '138908de860b111c',
|
||||
content_ltks:
|
||||
'应用 技术 研究 mallat 算 率 混 叠 原因 及其 改进 模型 郭 超 峰 李 梅 莲 许昌 学院 计算机科学 与 技术 学院 河南 许昌 461000 摘 要 mallat 算法 由于 自身 设计 的 原因 在 信号 分解 过程 中 存在 频率 混 叠 现象 在 利用 小波 分析 进行 信号 提取 时 这种 现象 是 一个 不容忽视 的 问题 通过 分析 mallat 算法 找出 了 造成 mallat 算法 产生 频率 混 叠 的 原因 给出 了 一个 能 有效 消除 频率 混 叠 的 改进 算法 模型',
|
||||
content_with_weight:
|
||||
'·应用技术研究·Mallat算率混叠原因及其改进模型郭超峰,李梅莲(许昌学院计算机科学与技术学院,河南许昌461000)摘 要:Mallat算法由于自身设计的原因,在信号分解过程中,存在频率混叠现象在利用小波分析进行信号提取时,这种现象是一个不容忽视的问题. 通过分析Mallat算法,找出了造成Mallat算法产生频率混叠的原因,给出了一个能有效消除频率混叠的改进算法模型',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-138908de860b111c',
|
||||
similarity: 0.827624678600734,
|
||||
vector_similarity: 0.4254155953357798,
|
||||
term_similarity: 1,
|
||||
positions: [
|
||||
[1, 47, 471, 80, 92],
|
||||
[1, 75, 500, 112, 138],
|
||||
[1, 224, 649, 154, 168],
|
||||
[1, 172, 596, 179, 190],
|
||||
[1, 63, 487, 196, 237],
|
||||
],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: '77951868ce3d1994',
|
||||
content_ltks:
|
||||
'f ig 4 two 1 d miensiona l pagoda decomposit process ofma llat a lgor ithm 重构 算法 2fshz j 表示 分解 的 深度 a j f t2 k h t 2k a j 1 f t 其中 j j 意义 与 式 2 相同 j j 1 j 20 h g 为 小波 重构 a j d j 意义 与 式 2 相同 mallat 二维 塔式 小波 变换 的 重构 过程 如 图 5 所示',
|
||||
content_with_weight:
|
||||
'F ig. 4 Two 1-D miensiona l pagoda decomposition process ofMa llat a lgor ithm重构算法:2fsHz, J表示分解的深度A j [f (t)]=2{∑k h (t -2k)A j+1 [f (t)]+其中: j、J意义与式(2)相同, j =J -1, J -2, …, 0; h, g为小波重构; A j、D j 意义与式(2)相同Mallat二维塔式小波变换的重构过程如图5所示',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-77951868ce3d1994',
|
||||
similarity: 0.8263513588843328,
|
||||
vector_similarity: 0.42117119628110916,
|
||||
term_similarity: 1,
|
||||
positions: [
|
||||
[2, 284, 514, 754, 765],
|
||||
[3, 285, 515, 79, 91],
|
||||
[3, 57, 287, 95, 107],
|
||||
[3, 38, 268, 126, 167],
|
||||
],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: 'b8076c8ba1598567',
|
||||
content_ltks:
|
||||
'1 mallat 算法 的 频率 混 叠 现象 对 最高 频率 为 的 带 限 信号 进行 离散 化 抽样 如果 抽样 周期 比较 大 或者说 抽样 频率 比较 小 那么 抽样 将 会 导致 频率 相邻 的 2 个 被 延拓 的 频谱 发生 叠加 而 互相 产生 影响 这种 现象 称为 混 叠 78 下面 是 一个 利用 mallat 算法 进行 信号 分解 的 例子 912',
|
||||
content_with_weight:
|
||||
'1 Mallat算法的频率混叠现象对最高频率为的带限信号进行离散化抽样,如果抽样周期比较大,或者说抽样频率比较小,那么抽样将会导致频率相邻的2个被延拓的频谱发生叠加而互相产生影响,这种现象称为混叠[78 ]下面是一个利用Mallat算法进行信号分解的例子[912 ]',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-b8076c8ba1598567',
|
||||
similarity: 0.8260278445075276,
|
||||
vector_similarity: 0.4200928150250923,
|
||||
term_similarity: 1,
|
||||
positions: [
|
||||
[1, 287, 516, 430, 441],
|
||||
[1, 284, 513, 448, 520],
|
||||
],
|
||||
doc_type_kwd: '',
|
||||
},
|
||||
{
|
||||
chunk_id: '537d27bca0af2c0e',
|
||||
content_ltks: 'k g t 2k d j 1 f t',
|
||||
content_with_weight: '∑k g (t -2k)D j+1 [f (t)]}, ',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
kb_id: 'fd05dba641bf11f0a713047c16ec874f',
|
||||
important_kwd: [],
|
||||
image_id: 'fd05dba641bf11f0a713047c16ec874f-537d27bca0af2c0e',
|
||||
similarity: 0.8255620344768997,
|
||||
vector_similarity: 0.4185401149229989,
|
||||
term_similarity: 1,
|
||||
positions: [[3, 93, 187, 112, 124]],
|
||||
doc_type_kwd: 'image',
|
||||
},
|
||||
],
|
||||
doc_aggs: [
|
||||
{
|
||||
doc_name: 'Mallat算法频3333率混叠原因及其改进模型.pdf',
|
||||
doc_id: 'bf60855c41d911f09504047c16ec874f',
|
||||
count: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
component_id: 'Retrieval:ClearHornetsClap',
|
||||
error: null,
|
||||
elapsed_time: null,
|
||||
created_at: 1749606806,
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'node_finished',
|
||||
message_id: 'dce6c0c8466611f08e04047c16ec874f',
|
||||
created_at: 1749606805,
|
||||
task_id: 'db68eb0645ab11f0bbdc047c16ec874f',
|
||||
data: {
|
||||
inputs: {},
|
||||
outputs: {
|
||||
content: null,
|
||||
structured_output: null,
|
||||
_elapsed_time: 0.009871692978776991,
|
||||
},
|
||||
component_id: 'Agent:EvilBobcatsWish',
|
||||
error: null,
|
||||
elapsed_time: 0.009871692978776991,
|
||||
created_at: 1749606806,
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'node_finished',
|
||||
message_id: 'dce6c0c8466611f08e04047c16ec874f',
|
||||
created_at: 1749606805,
|
||||
task_id: 'db68eb0645ab11f0bbdc047c16ec874f',
|
||||
data: {
|
||||
inputs: {},
|
||||
outputs: {
|
||||
content:
|
||||
'您好,根据您提供的知识库内容,以下是关于Mallat算法的一些信息:\n\n1. **Mallat算法的定义**:\n Mallat算法是由Stéphane Mallat将计算机视觉领域的多分辨率分析思想引入到小波分析中推导出的小波分析快速算法。该算法利用与尺度函数和小波函数相对应的小波低通滤波器(记为H, h)和小波高通滤波器(记为G, g)对信号进行低通和高通滤波来实现。\n\n2. **频率混叠现象**:\n 在具体使用时,Mallat算法实现过程中不可避免地会产生频率混叠现象。这种现象是由于小波分解过程是隔点采样的过程,而重构过程是隔点插零的过程,这两个过程都可能产生频率混叠。\n\n3. **改进模型**:\n 通过对Mallat算法的深入研究,找到了造成频率混叠的原因,并提出了一个能有效消除频率混叠的改进模型。这种改进的模型被称为子带信号重构算法。具体来说,在利用Mallat算法对信号分解得到各尺度上的小波系数后,再按照重构的方法重构至所需的小波空间系数cdj,并用该重构系数代替相应尺度上得到的小波系数来分析该尺度上的信号,以较好地解决频率混叠所带来的影响。\n\n4. **应用技术研究**:\n Mallat算法由于自身设计的原因,在信号分解过程中存在频率混叠现象。通过分析Mallat算法找出了造成这种现象的原因,并给出了一个能有效消除这种现象的改进模型。\n\n这些信息提供了对Mallat算法及其相关问题和解决方案的基本理解。如果您有更具体的问题或需要进一步的信息,请随时告知!',
|
||||
_elapsed_time: 0.0001981810200959444,
|
||||
},
|
||||
component_id: 'Message:PurpleWordsBuy',
|
||||
error: null,
|
||||
elapsed_time: 0.0001981810200959444,
|
||||
created_at: 1749606814,
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -14,12 +14,9 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Operator } from '../constant';
|
||||
import { JsonViewer } from '../form/components/json-viewer';
|
||||
import OperatorIcon, { SVGIconMap } from '../operator-icon';
|
||||
import {
|
||||
JsonViewer,
|
||||
toLowerCaseStringAndDeleteChar,
|
||||
typeMap,
|
||||
} from './workflow-timeline';
|
||||
import { toLowerCaseStringAndDeleteChar, typeMap } from './workflow-timeline';
|
||||
type IToolIcon =
|
||||
| Operator.ArXiv
|
||||
| Operator.GitHub
|
||||
|
||||
@ -24,8 +24,8 @@ import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { get, isEmpty, isEqual, uniqWith } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import JsonView from 'react18-json-view';
|
||||
import { Operator } from '../constant';
|
||||
import { JsonViewer } from '../form/components/json-viewer';
|
||||
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
||||
import OperatorIcon from '../operator-icon';
|
||||
import ToolTimelineItem from './tool-timeline-item';
|
||||
@ -37,25 +37,7 @@ type LogFlowTimelineProps = Pick<
|
||||
sendLoading: boolean;
|
||||
isShare?: boolean;
|
||||
};
|
||||
export function JsonViewer({
|
||||
data,
|
||||
title,
|
||||
}: {
|
||||
data: Record<string, any>;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<div>{title}</div>
|
||||
<JsonView
|
||||
src={data}
|
||||
displaySize
|
||||
collapseStringsAfterLength={100000000000}
|
||||
className="w-full h-[200px] break-words overflow-auto scrollbar-auto p-2 bg-muted"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const typeMap = {
|
||||
begin: t('flow.logTimeline.begin'),
|
||||
agent: t('flow.logTimeline.agent'),
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
NodeHandleId,
|
||||
Operator,
|
||||
TypesWithArray,
|
||||
WebhookSecurityAuthType,
|
||||
} from './constant';
|
||||
import { BeginFormSchemaType } from './form/begin-form/schema';
|
||||
import { DataOperationsFormSchemaType } from './form/data-operations-form';
|
||||
@ -353,13 +354,20 @@ function transformRequestSchemaToJsonschema(
|
||||
|
||||
function transformBeginParams(params: BeginFormSchemaType) {
|
||||
if (params.mode === AgentDialogueMode.Webhook) {
|
||||
const nextSecurity: Record<string, any> = {
|
||||
...params.security,
|
||||
ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),
|
||||
};
|
||||
if (params.security?.auth_type === WebhookSecurityAuthType.Jwt) {
|
||||
nextSecurity.jwt = {
|
||||
...nextSecurity.jwt,
|
||||
required_claims: nextSecurity.jwt?.required_claims.map((x) => x.value),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
schema: transformRequestSchemaToJsonschema(params.schema),
|
||||
security: {
|
||||
...params.security,
|
||||
ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),
|
||||
},
|
||||
security: nextSecurity,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
119
web/src/pages/agent/webhook-sheet/index.tsx
Normal file
119
web/src/pages/agent/webhook-sheet/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { CopyToClipboardWithText } from '@/components/copy-to-clipboard';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useFetchWebhookTrace } from '@/hooks/use-agent-request';
|
||||
import { MessageEventType } from '@/hooks/use-send-message';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import { BeginId } from '../constant';
|
||||
import { JsonViewer } from '../form/components/json-viewer';
|
||||
import { WorkFlowTimeline } from './timeline';
|
||||
|
||||
type RunSheetProps = IModalProps<any>;
|
||||
|
||||
enum WebhookTraceTabType {
|
||||
Detail = 'detail',
|
||||
Tracing = 'tracing',
|
||||
}
|
||||
|
||||
const WebhookSheet = ({ hideModal }: RunSheetProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const text = `${location.protocol}//${location.host}/api/v1/webhook_test/${id}`;
|
||||
|
||||
const { data } = useFetchWebhookTrace(true);
|
||||
|
||||
const firstInput = data?.events.find(
|
||||
(event) =>
|
||||
event.event === MessageEventType.NodeFinished &&
|
||||
event.data.component_id === BeginId,
|
||||
)?.data.inputs;
|
||||
|
||||
const latestOutput = data?.events?.findLast(
|
||||
(event) =>
|
||||
event.event === MessageEventType.NodeFinished &&
|
||||
event.data.component_id !== BeginId,
|
||||
)?.data.outputs;
|
||||
|
||||
const statusInfo = useMemo(() => {
|
||||
if (data?.finished === false) {
|
||||
return { status: 'running' };
|
||||
}
|
||||
|
||||
let errorItem = data?.events.find(
|
||||
(x) => x.event === 'error' || x.data?.error,
|
||||
);
|
||||
if (errorItem) {
|
||||
return {
|
||||
status: 'fail',
|
||||
message: errorItem.data?.error || errorItem.message,
|
||||
};
|
||||
}
|
||||
return { status: 'success' };
|
||||
}, [data?.events, data?.finished]);
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={hideModal} open modal={false}>
|
||||
<SheetContent className={cn('top-20 p-2 space-y-5 flex flex-col pb-20')}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('flow.testRun')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Webhook URL:</div>
|
||||
<CopyToClipboardWithText text={text}></CopyToClipboardWithText>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div
|
||||
className={cn({
|
||||
'text-state-error': statusInfo.status === 'fail',
|
||||
'text-state-success': statusInfo.status === 'success',
|
||||
})}
|
||||
>
|
||||
{upperFirst(statusInfo.status)}
|
||||
</div>
|
||||
<div>{statusInfo?.message}</div>
|
||||
</section>
|
||||
|
||||
<Tabs
|
||||
defaultValue={WebhookTraceTabType.Detail}
|
||||
className="flex-1 min-h-0 flex flex-col"
|
||||
>
|
||||
<TabsList className="w-fit">
|
||||
<TabsTrigger value={WebhookTraceTabType.Detail}>Detail</TabsTrigger>
|
||||
<TabsTrigger value={WebhookTraceTabType.Tracing}>
|
||||
Tracing
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={WebhookTraceTabType.Detail}>
|
||||
<JsonViewer data={firstInput || {}} title={'Input'}></JsonViewer>
|
||||
<JsonViewer data={latestOutput || {}} title={'Output'}></JsonViewer>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value={WebhookTraceTabType.Tracing}
|
||||
className="overflow-auto flex-1"
|
||||
>
|
||||
<WorkFlowTimeline
|
||||
currentEventListWithoutMessage={data?.events || []}
|
||||
canvasId={id}
|
||||
currentMessageId=""
|
||||
sendLoading={false}
|
||||
></WorkFlowTimeline>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookSheet;
|
||||
337
web/src/pages/agent/webhook-sheet/timeline.tsx
Normal file
337
web/src/pages/agent/webhook-sheet/timeline.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
import HighLightMarkdown from '@/components/highlight-markdown';
|
||||
import {
|
||||
Timeline,
|
||||
TimelineContent,
|
||||
TimelineHeader,
|
||||
TimelineIndicator,
|
||||
TimelineItem,
|
||||
TimelineSeparator,
|
||||
} from '@/components/originui/timeline';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
|
||||
import {
|
||||
INodeData,
|
||||
INodeEvent,
|
||||
MessageEventType,
|
||||
} from '@/hooks/use-send-message';
|
||||
import { ITraceData } from '@/interfaces/database/agent';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { get, isEmpty, isEqual, uniqWith } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Operator } from '../constant';
|
||||
import { JsonViewer } from '../form/components/json-viewer';
|
||||
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
||||
import ToolTimelineItem from '../log-sheet/tool-timeline-item';
|
||||
import OperatorIcon from '../operator-icon';
|
||||
type LogFlowTimelineProps = Pick<
|
||||
ReturnType<typeof useCacheChatLog>,
|
||||
'currentEventListWithoutMessage' | 'currentMessageId'
|
||||
> & {
|
||||
canvasId?: string;
|
||||
sendLoading: boolean;
|
||||
isShare?: boolean;
|
||||
};
|
||||
|
||||
export const typeMap = {
|
||||
begin: t('flow.logTimeline.begin'),
|
||||
agent: t('flow.logTimeline.agent'),
|
||||
retrieval: t('flow.logTimeline.retrieval'),
|
||||
message: t('flow.logTimeline.message'),
|
||||
awaitResponse: t('flow.logTimeline.awaitResponse'),
|
||||
switch: t('flow.logTimeline.switch'),
|
||||
iteration: t('flow.logTimeline.iteration'),
|
||||
categorize: t('flow.logTimeline.categorize'),
|
||||
code: t('flow.logTimeline.code'),
|
||||
textProcessing: t('flow.logTimeline.textProcessing'),
|
||||
tavilySearch: t('flow.logTimeline.tavilySearch'),
|
||||
tavilyExtract: t('flow.logTimeline.tavilyExtract'),
|
||||
exeSQL: t('flow.logTimeline.exeSQL'),
|
||||
google: t('flow.logTimeline.google'),
|
||||
duckDuckGo: t('flow.logTimeline.google'),
|
||||
wikipedia: t('flow.logTimeline.wikipedia'),
|
||||
googleScholar: t('flow.logTimeline.googleScholar'),
|
||||
arXiv: t('flow.logTimeline.googleScholar'),
|
||||
pubMed: t('flow.logTimeline.googleScholar'),
|
||||
gitHub: t('flow.logTimeline.gitHub'),
|
||||
email: t('flow.logTimeline.email'),
|
||||
httpRequest: t('flow.logTimeline.httpRequest'),
|
||||
wenCai: t('flow.logTimeline.wenCai'),
|
||||
yahooFinance: t('flow.logTimeline.yahooFinance'),
|
||||
userFillUp: t('flow.logTimeline.userFillUp'),
|
||||
};
|
||||
export const toLowerCaseStringAndDeleteChar = (
|
||||
str: string,
|
||||
char: string = '_',
|
||||
) => str.toLowerCase().replace(/ /g, '').replaceAll(char, '');
|
||||
|
||||
// Convert all keys in typeMap to lowercase and output the new typeMap
|
||||
export const typeMapLowerCase = Object.fromEntries(
|
||||
Object.entries(typeMap).map(([key, value]) => [
|
||||
toLowerCaseStringAndDeleteChar(key),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
|
||||
function getInputsOrOutputs(
|
||||
nodeEventList: INodeData[],
|
||||
field: 'inputs' | 'outputs',
|
||||
) {
|
||||
const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {}));
|
||||
|
||||
if (inputsOrOutputs.length < 2) {
|
||||
return inputsOrOutputs[0] || {};
|
||||
}
|
||||
|
||||
return uniqWith(inputsOrOutputs, isEqual); // TODO: Violence should not be used to
|
||||
}
|
||||
export const WorkFlowTimeline = ({
|
||||
currentEventListWithoutMessage,
|
||||
currentMessageId,
|
||||
canvasId,
|
||||
sendLoading,
|
||||
isShare,
|
||||
}: LogFlowTimelineProps) => {
|
||||
// const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const {
|
||||
data: traceData,
|
||||
setMessageId,
|
||||
setISStopFetchTrace,
|
||||
} = useFetchMessageTrace(canvasId);
|
||||
|
||||
useEffect(() => {
|
||||
setMessageId(currentMessageId);
|
||||
}, [currentMessageId, setMessageId]);
|
||||
const getNodeName = (nodeId: string) => {
|
||||
if ('begin' === nodeId) return t('flow.begin');
|
||||
return nodeId;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setISStopFetchTrace(!sendLoading);
|
||||
}, [sendLoading, setISStopFetchTrace]);
|
||||
|
||||
const startedNodeList = useMemo(() => {
|
||||
const finish = currentEventListWithoutMessage?.some(
|
||||
(item) => item.event === MessageEventType.WorkflowFinished,
|
||||
);
|
||||
setISStopFetchTrace(finish || !sendLoading);
|
||||
const duplicateList = currentEventListWithoutMessage?.filter(
|
||||
(x) => x.event === MessageEventType.NodeStarted,
|
||||
) as INodeEvent[];
|
||||
|
||||
// Remove duplicate nodes
|
||||
return duplicateList?.reduce<Array<INodeEvent>>((pre, cur) => {
|
||||
if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
|
||||
pre.push(cur);
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
}, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]);
|
||||
|
||||
const getElapsedTime = (nodeId: string) => {
|
||||
if (nodeId === 'begin') {
|
||||
return '';
|
||||
}
|
||||
const data = currentEventListWithoutMessage?.find((x) => {
|
||||
return (
|
||||
x.data?.component_id === nodeId &&
|
||||
x.event === MessageEventType.NodeFinished
|
||||
);
|
||||
});
|
||||
if (!data || data?.data.elapsed_time < 0.000001) {
|
||||
return '';
|
||||
}
|
||||
return data?.data.elapsed_time || '';
|
||||
};
|
||||
|
||||
const hasTrace = useCallback(
|
||||
(componentId: string) => {
|
||||
if (Array.isArray(traceData)) {
|
||||
return traceData?.some((x) => x.component_id === componentId);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[traceData],
|
||||
);
|
||||
|
||||
const filterTrace = useCallback(
|
||||
(componentId: string) => {
|
||||
const trace = traceData
|
||||
?.filter((x) => x.component_id === componentId)
|
||||
.reduce<ITraceData['trace']>((pre, cur) => {
|
||||
pre.push(...cur.trace);
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
return Array.isArray(trace) ? trace : [{}];
|
||||
},
|
||||
[traceData],
|
||||
);
|
||||
|
||||
const filterFinishedNodeList = useCallback(
|
||||
(componentId: string) => {
|
||||
const nodeEventList = currentEventListWithoutMessage
|
||||
.filter(
|
||||
(x) =>
|
||||
x.event === MessageEventType.NodeFinished &&
|
||||
(x.data as INodeData)?.component_id === componentId,
|
||||
)
|
||||
.map((x) => x.data);
|
||||
|
||||
return nodeEventList;
|
||||
},
|
||||
[currentEventListWithoutMessage],
|
||||
);
|
||||
|
||||
return (
|
||||
<Timeline>
|
||||
{startedNodeList?.map((x, idx) => {
|
||||
const nodeDataList = filterFinishedNodeList(x.data.component_id);
|
||||
const finishNodeIds = nodeDataList.map(
|
||||
(x: INodeData) => x.component_id,
|
||||
);
|
||||
const inputs = getInputsOrOutputs(nodeDataList, 'inputs');
|
||||
const outputs = getInputsOrOutputs(nodeDataList, 'outputs');
|
||||
const nodeLabel = x.data.component_type;
|
||||
return (
|
||||
<>
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
step={idx}
|
||||
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator
|
||||
className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6 bg-accent-primary"
|
||||
style={{
|
||||
background:
|
||||
x.data.component_type === 'Agent'
|
||||
? 'repeating-linear-gradient( to bottom, rgba(76, 164, 231, 1), rgba(76, 164, 231, 1) 5px, transparent 5px, transparent 10px'
|
||||
: '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<TimelineIndicator
|
||||
className={cn(
|
||||
' group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 p-1 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
|
||||
{
|
||||
'border border-blue-500': finishNodeIds.includes(
|
||||
x.data.component_id,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className='relative after:content-[""] after:absolute after:inset-0 after:z-10 after:bg-transparent after:transition-all after:duration-300'>
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center ">
|
||||
<div
|
||||
className={cn('rounded-full w-6 h-6', {
|
||||
' border-muted-foreground border-2 border-t-transparent animate-spin ':
|
||||
!finishNodeIds.includes(x.data.component_id) &&
|
||||
sendLoading,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
<div className="size-6 flex items-center justify-center">
|
||||
<OperatorIcon
|
||||
className="size-4"
|
||||
name={nodeLabel as Operator}
|
||||
></OperatorIcon>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
<TimelineContent className="text-foreground rounded-lg border mb-5">
|
||||
<section key={'content_' + idx}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="bg-bg-card px-3"
|
||||
>
|
||||
<AccordionItem value={idx.toString()}>
|
||||
<AccordionTrigger
|
||||
hideDownIcon={isShare && !x.data?.thoughts}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>
|
||||
{!isShare && getNodeName(x.data?.component_name)}
|
||||
{isShare &&
|
||||
(typeMapLowerCase[
|
||||
toLowerCaseStringAndDeleteChar(
|
||||
nodeLabel,
|
||||
) as keyof typeof typeMap
|
||||
] ??
|
||||
nodeLabel)}
|
||||
</span>
|
||||
<span className="text-text-secondary text-xs">
|
||||
{getElapsedTime(x.data.component_id)
|
||||
.toString()
|
||||
.slice(0, 6)}
|
||||
{getElapsedTime(x.data.component_id) ? 's' : ''}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'border-background -end-1 -top-1 size-2 rounded-full',
|
||||
{ 'bg-state--success': isEmpty(x.data.error) },
|
||||
{ 'bg-state--error': !isEmpty(x.data.error) },
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Online</span>
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
{!isShare && (
|
||||
<AccordionContent>
|
||||
<div className="space-y-2">
|
||||
{!isShare && (
|
||||
<>
|
||||
<JsonViewer
|
||||
data={inputs}
|
||||
title="Input"
|
||||
></JsonViewer>
|
||||
|
||||
<JsonViewer
|
||||
data={outputs}
|
||||
title={'Output'}
|
||||
></JsonViewer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
)}
|
||||
{isShare && x.data?.thoughts && (
|
||||
<AccordionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="w-full h-[200px] break-words overflow-auto scrollbar-auto p-2 bg-muted">
|
||||
<HighLightMarkdown>
|
||||
{x.data.thoughts || ''}
|
||||
</HighLightMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
)}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
{hasTrace(x.data.component_id) && (
|
||||
<ToolTimelineItem
|
||||
key={'tool_' + idx}
|
||||
tools={filterTrace(x.data.component_id)}
|
||||
sendLoading={sendLoading}
|
||||
isShare={isShare}
|
||||
></ToolTimelineItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
);
|
||||
};
|
||||
370
web/src/pages/dataset/components/metedata/hook.ts
Normal file
370
web/src/pages/dataset/components/metedata/hook.ts
Normal file
@ -0,0 +1,370 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useSetDocumentMeta } from '@/hooks/use-document-request';
|
||||
import {
|
||||
getMetaDataService,
|
||||
updateMetaData,
|
||||
} from '@/services/knowledge-service';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import {
|
||||
IMetaDataReturnJSONSettings,
|
||||
IMetaDataReturnJSONType,
|
||||
IMetaDataReturnType,
|
||||
IMetaDataTableData,
|
||||
MetadataOperations,
|
||||
ShowManageMetadataModalProps,
|
||||
} from './interface';
|
||||
export enum MetadataType {
|
||||
Manage = 1,
|
||||
UpdateSingle = 2,
|
||||
Setting = 3,
|
||||
}
|
||||
export const util = {
|
||||
changeToMetaDataTableData(data: IMetaDataReturnType): IMetaDataTableData[] {
|
||||
return Object.entries(data).map(([key, value]) => {
|
||||
const values = value.map(([v]) => v.toString());
|
||||
console.log('values', values);
|
||||
return {
|
||||
field: key,
|
||||
description: '',
|
||||
values: values,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
|
||||
JSONToMetaDataTableData(
|
||||
data: Record<string, string | string[]>,
|
||||
): IMetaDataTableData[] {
|
||||
return Object.entries(data).map(([key, value]) => {
|
||||
return {
|
||||
field: key,
|
||||
description: '',
|
||||
values: value,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
|
||||
tableDataToMetaDataJSON(data: IMetaDataTableData[]): IMetaDataReturnJSONType {
|
||||
return data.reduce<IMetaDataReturnJSONType>((pre, cur) => {
|
||||
pre[cur.field] = cur.values;
|
||||
return pre;
|
||||
}, {});
|
||||
},
|
||||
|
||||
tableDataToMetaDataSettingJSON(
|
||||
data: IMetaDataTableData[],
|
||||
): IMetaDataReturnJSONSettings {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
key: item.field,
|
||||
description: item.description,
|
||||
enum: item.values,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
metaDataSettingJSONToMetaDataTableData(
|
||||
data: IMetaDataReturnJSONSettings,
|
||||
): IMetaDataTableData[] {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
field: item.key,
|
||||
description: item.description,
|
||||
values: item.enum,
|
||||
restrictDefinedValues: !!item.enum?.length,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const useMetadataOperations = () => {
|
||||
const [operations, setOperations] = useState<MetadataOperations>({
|
||||
deletes: [],
|
||||
updates: [],
|
||||
});
|
||||
|
||||
const addDeleteRow = useCallback((key: string) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
deletes: [...prev.deletes, { key }],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const addDeleteValue = useCallback((key: string, value: string) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
deletes: [...prev.deletes, { key, value }],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const addUpdateValue = useCallback(
|
||||
(key: string, value: string | string[]) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
updates: [...prev.updates, { key, value }],
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetOperations = useCallback(() => {
|
||||
setOperations({
|
||||
deletes: [],
|
||||
updates: [],
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
operations,
|
||||
addDeleteRow,
|
||||
addDeleteValue,
|
||||
addUpdateValue,
|
||||
resetOperations,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFetchMetaDataManageData = (
|
||||
type: MetadataType = MetadataType.Manage,
|
||||
) => {
|
||||
const { id } = useParams();
|
||||
// const [data, setData] = useState<IMetaDataTableData[]>([]);
|
||||
// const [loading, setLoading] = useState(false);
|
||||
// const fetchData = useCallback(async (): Promise<IMetaDataTableData[]> => {
|
||||
// setLoading(true);
|
||||
// const { data } = await getMetaDataService({
|
||||
// kb_id: id as string,
|
||||
// });
|
||||
// setLoading(false);
|
||||
// if (data?.data?.summary) {
|
||||
// return util.changeToMetaDataTableData(data.data.summary);
|
||||
// }
|
||||
// return [];
|
||||
// }, [id]);
|
||||
// useEffect(() => {
|
||||
// if (type === MetadataType.Manage) {
|
||||
// fetchData()
|
||||
// .then((res) => {
|
||||
// setData(res);
|
||||
// })
|
||||
// .catch((res) => {
|
||||
// console.error(res);
|
||||
// });
|
||||
// }
|
||||
// }, [type, fetchData]);
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useQuery<IMetaDataTableData[]>({
|
||||
queryKey: ['fetchMetaData', id],
|
||||
enabled: !!id && type === MetadataType.Manage,
|
||||
initialData: [],
|
||||
gcTime: 1000,
|
||||
queryFn: async () => {
|
||||
const { data } = await getMetaDataService({
|
||||
kb_id: id as string,
|
||||
});
|
||||
if (data?.data?.summary) {
|
||||
return util.changeToMetaDataTableData(data.data.summary);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageMetaDataModal = (
|
||||
metaData: IMetaDataTableData[] = [],
|
||||
type: MetadataType = MetadataType.Manage,
|
||||
otherData?: Record<string, any>,
|
||||
) => {
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { data, loading } = useFetchMetaDataManageData(type);
|
||||
|
||||
const [tableData, setTableData] = useState<IMetaDataTableData[]>(metaData);
|
||||
|
||||
const { operations, addDeleteRow, addDeleteValue, addUpdateValue } =
|
||||
useMetadataOperations();
|
||||
|
||||
const { setDocumentMeta } = useSetDocumentMeta();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setTableData(data);
|
||||
} else {
|
||||
setTableData([]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (metaData) {
|
||||
setTableData(metaData);
|
||||
} else {
|
||||
setTableData([]);
|
||||
}
|
||||
}, [metaData]);
|
||||
|
||||
const handleDeleteSingleValue = useCallback(
|
||||
(field: string, value: string) => {
|
||||
addDeleteValue(field, value);
|
||||
|
||||
setTableData((prevTableData) => {
|
||||
const newTableData = prevTableData.map((item) => {
|
||||
if (item.field === field) {
|
||||
return {
|
||||
...item,
|
||||
values: item.values.filter((v) => v !== value),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
// console.log('newTableData', newTableData, prevTableData);
|
||||
return newTableData;
|
||||
});
|
||||
},
|
||||
[addDeleteValue],
|
||||
);
|
||||
|
||||
const handleDeleteSingleRow = useCallback(
|
||||
(field: string) => {
|
||||
addDeleteRow(field);
|
||||
setTableData((prevTableData) => {
|
||||
const newTableData = prevTableData.filter(
|
||||
(item) => item.field !== field,
|
||||
);
|
||||
// console.log('newTableData', newTableData, prevTableData);
|
||||
return newTableData;
|
||||
});
|
||||
},
|
||||
[addDeleteRow],
|
||||
);
|
||||
|
||||
const handleSaveManage = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const { data: res } = await updateMetaData({
|
||||
kb_id: id as string,
|
||||
data: operations,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success(t('message.success'));
|
||||
callback();
|
||||
}
|
||||
},
|
||||
[operations, id, t],
|
||||
);
|
||||
|
||||
const handleSaveUpdateSingle = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const reqData = util.tableDataToMetaDataJSON(tableData);
|
||||
if (otherData?.id) {
|
||||
const ret = await setDocumentMeta({
|
||||
documentId: otherData?.id,
|
||||
meta: JSON.stringify(reqData),
|
||||
});
|
||||
if (ret === 0) {
|
||||
// message.success(t('message.success'));
|
||||
callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
[tableData, otherData, setDocumentMeta],
|
||||
);
|
||||
|
||||
const handleSaveSettings = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const data = util.tableDataToMetaDataSettingJSON(tableData);
|
||||
callback();
|
||||
|
||||
return data;
|
||||
},
|
||||
[tableData],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async ({ callback }: { callback: () => void }) => {
|
||||
switch (type) {
|
||||
case MetadataType.UpdateSingle:
|
||||
handleSaveUpdateSingle(callback);
|
||||
break;
|
||||
case MetadataType.Manage:
|
||||
handleSaveManage(callback);
|
||||
break;
|
||||
case MetadataType.Setting:
|
||||
return handleSaveSettings(callback);
|
||||
default:
|
||||
handleSaveManage(callback);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[handleSaveManage, type, handleSaveUpdateSingle, handleSaveSettings],
|
||||
);
|
||||
|
||||
return {
|
||||
tableData,
|
||||
setTableData,
|
||||
handleDeleteSingleValue,
|
||||
handleDeleteSingleRow,
|
||||
loading,
|
||||
handleSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageMetadata = () => {
|
||||
const [tableData, setTableData] = useState<IMetaDataTableData[]>([]);
|
||||
const [config, setConfig] = useState<ShowManageMetadataModalProps>(
|
||||
{} as ShowManageMetadataModalProps,
|
||||
);
|
||||
const {
|
||||
visible: manageMetadataVisible,
|
||||
showModal,
|
||||
hideModal: hideManageMetadataModal,
|
||||
} = useSetModalState();
|
||||
const showManageMetadataModal = useCallback(
|
||||
(config?: ShowManageMetadataModalProps) => {
|
||||
const { metadata } = config || {};
|
||||
if (metadata) {
|
||||
// const dataTemp = Object.entries(metadata).map(([key, value]) => {
|
||||
// return {
|
||||
// field: key,
|
||||
// description: '',
|
||||
// values: Array.isArray(value) ? value : [value],
|
||||
// } as IMetaDataTableData;
|
||||
// });
|
||||
setTableData(metadata);
|
||||
console.log('metadata-2', metadata);
|
||||
}
|
||||
console.log('metadata-3', metadata);
|
||||
if (config) {
|
||||
setConfig(config);
|
||||
}
|
||||
showModal();
|
||||
},
|
||||
[showModal],
|
||||
);
|
||||
return {
|
||||
manageMetadataVisible,
|
||||
showManageMetadataModal,
|
||||
hideManageMetadataModal,
|
||||
tableData,
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageValues = () => {
|
||||
const [updateValues, setUpdateValues] = useState<{
|
||||
field: string;
|
||||
values: string[];
|
||||
} | null>(null);
|
||||
return { updateValues, setUpdateValues };
|
||||
};
|
||||
80
web/src/pages/dataset/components/metedata/interface.ts
Normal file
80
web/src/pages/dataset/components/metedata/interface.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MetadataType } from './hook';
|
||||
export type IMetaDataReturnType = Record<string, Array<Array<string | number>>>;
|
||||
export type IMetaDataReturnJSONType = Record<
|
||||
string,
|
||||
Array<string | number> | string
|
||||
>;
|
||||
|
||||
export interface IMetaDataReturnJSONSettingItem {
|
||||
key: string;
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
}
|
||||
export type IMetaDataReturnJSONSettings = Array<IMetaDataReturnJSONSettingItem>;
|
||||
|
||||
export type IMetaDataTableData = {
|
||||
field: string;
|
||||
description: string;
|
||||
restrictDefinedValues?: boolean;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type IManageModalProps = {
|
||||
title: ReactNode;
|
||||
isShowDescription?: boolean;
|
||||
isDeleteSingleValue?: boolean;
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
tableData?: IMetaDataTableData[];
|
||||
isCanAdd: boolean;
|
||||
type: MetadataType;
|
||||
otherData?: Record<string, any>;
|
||||
isEditField?: boolean;
|
||||
isAddValue?: boolean;
|
||||
isShowValueSwitch?: boolean;
|
||||
isVerticalShowValue?: boolean;
|
||||
success?: (data: any) => void;
|
||||
};
|
||||
|
||||
export interface IManageValuesProps {
|
||||
title: ReactNode;
|
||||
visible: boolean;
|
||||
isEditField?: boolean;
|
||||
isAddValue?: boolean;
|
||||
isShowDescription?: boolean;
|
||||
isShowValueSwitch?: boolean;
|
||||
isVerticalShowValue?: boolean;
|
||||
data: IMetaDataTableData;
|
||||
hideModal: () => void;
|
||||
onSave: (data: IMetaDataTableData) => void;
|
||||
addUpdateValue: (key: string, value: string | string[]) => void;
|
||||
addDeleteValue: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
interface DeleteOperation {
|
||||
key: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface UpdateOperation {
|
||||
key: string;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
export interface MetadataOperations {
|
||||
deletes: DeleteOperation[];
|
||||
updates: UpdateOperation[];
|
||||
}
|
||||
export interface ShowManageMetadataModalOptions {
|
||||
title?: ReactNode | string;
|
||||
}
|
||||
export type ShowManageMetadataModalProps = Partial<IManageModalProps> & {
|
||||
metadata?: IMetaDataTableData[];
|
||||
isCanAdd: boolean;
|
||||
type: MetadataType;
|
||||
record?: Record<string, any>;
|
||||
options?: ShowManageMetadataModalOptions;
|
||||
title?: ReactNode | string;
|
||||
isDeleteSingleValue?: boolean;
|
||||
};
|
||||
373
web/src/pages/dataset/components/metedata/manage-modal.tsx
Normal file
373
web/src/pages/dataset/components/metedata/manage-modal.tsx
Normal file
@ -0,0 +1,373 @@
|
||||
import {
|
||||
ConfirmDeleteDialog,
|
||||
ConfirmDeleteDialogNode,
|
||||
} from '@/components/confirm-delete-dialog';
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { Plus, Settings, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useManageMetaDataModal } from './hook';
|
||||
import { IManageModalProps, IMetaDataTableData } from './interface';
|
||||
import { ManageValuesModal } from './manage-values-modal';
|
||||
export const ManageMetadataModal = (props: IManageModalProps) => {
|
||||
const {
|
||||
title,
|
||||
visible,
|
||||
hideModal,
|
||||
isDeleteSingleValue,
|
||||
tableData: originalTableData,
|
||||
isCanAdd,
|
||||
type: metadataType,
|
||||
otherData,
|
||||
isEditField,
|
||||
isAddValue,
|
||||
isShowDescription = false,
|
||||
isShowValueSwitch = false,
|
||||
isVerticalShowValue = true,
|
||||
success,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const [valueData, setValueData] = useState<IMetaDataTableData>({
|
||||
field: '',
|
||||
description: '',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const [currentValueIndex, setCurrentValueIndex] = useState<number>(0);
|
||||
const [deleteDialogContent, setDeleteDialogContent] = useState({
|
||||
visible: false,
|
||||
title: '',
|
||||
name: '',
|
||||
warnText: '',
|
||||
onOk: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
|
||||
const {
|
||||
tableData,
|
||||
setTableData,
|
||||
handleDeleteSingleValue,
|
||||
handleDeleteSingleRow,
|
||||
handleSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
} = useManageMetaDataModal(originalTableData, metadataType, otherData);
|
||||
|
||||
const {
|
||||
visible: manageValuesVisible,
|
||||
showModal: showManageValuesModal,
|
||||
hideModal: hideManageValuesModal,
|
||||
} = useSetModalState();
|
||||
const hideDeleteModal = () => {
|
||||
setDeleteDialogContent({
|
||||
visible: false,
|
||||
title: '',
|
||||
name: '',
|
||||
warnText: '',
|
||||
onOk: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
};
|
||||
const handAddValueRow = () => {
|
||||
setValueData({
|
||||
field: '',
|
||||
description: '',
|
||||
values: [],
|
||||
});
|
||||
setCurrentValueIndex(tableData.length || 0);
|
||||
showManageValuesModal();
|
||||
};
|
||||
const handleEditValueRow = useCallback(
|
||||
(data: IMetaDataTableData, index: number) => {
|
||||
setCurrentValueIndex(index);
|
||||
setValueData(data);
|
||||
showManageValuesModal();
|
||||
},
|
||||
[showManageValuesModal],
|
||||
);
|
||||
|
||||
const columns: ColumnDef<IMetaDataTableData>[] = useMemo(() => {
|
||||
const cols: ColumnDef<IMetaDataTableData>[] = [
|
||||
{
|
||||
accessorKey: 'field',
|
||||
header: () => <span>{t('knowledgeDetails.metadata.field')}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-accent-primary">
|
||||
{row.getValue('field')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: () => <span>{t('knowledgeDetails.metadata.description')}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm truncate max-w-32">
|
||||
{row.getValue('description')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'values',
|
||||
header: () => <span>{t('knowledgeDetails.metadata.values')}</span>,
|
||||
cell: ({ row }) => {
|
||||
const values = row.getValue('values') as Array<string>;
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{values.length > 0 &&
|
||||
values
|
||||
.filter((value: string, index: number) => index < 2)
|
||||
?.map((value: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
aria-label="Edit"
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
<div className="text-sm truncate max-w-24">
|
||||
{value}
|
||||
</div>
|
||||
{isDeleteSingleValue && (
|
||||
<Button
|
||||
variant={'delete'}
|
||||
className="p-0 bg-transparent"
|
||||
onClick={() => {
|
||||
handleDeleteSingleValue(
|
||||
row.getValue('field'),
|
||||
value,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{values.length > 2 && (
|
||||
<div className="text-text-secondary self-end">...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'action',
|
||||
header: () => <span>{t('knowledgeDetails.metadata.action')}</span>,
|
||||
meta: {
|
||||
cellClassName: 'w-12',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className=" flex opacity-0 group-hover:opacity-100 gap-2">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="bg-transparent px-1 py-0"
|
||||
onClick={() => {
|
||||
handleEditValueRow(row.original, row.index);
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</Button>
|
||||
<Button
|
||||
variant={'delete'}
|
||||
className="p-0 bg-transparent"
|
||||
onClick={() => {
|
||||
setDeleteDialogContent({
|
||||
visible: true,
|
||||
title:
|
||||
t('common.delete') +
|
||||
' ' +
|
||||
t('knowledgeDetails.metadata.metadata'),
|
||||
name: row.getValue('field'),
|
||||
warnText: t('knowledgeDetails.metadata.deleteWarn'),
|
||||
onOk: () => {
|
||||
hideDeleteModal();
|
||||
handleDeleteSingleRow(row.getValue('field'));
|
||||
},
|
||||
onCancel: () => {
|
||||
hideDeleteModal();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (!isShowDescription) {
|
||||
cols.splice(1, 1);
|
||||
}
|
||||
return cols;
|
||||
}, [
|
||||
handleDeleteSingleRow,
|
||||
t,
|
||||
handleDeleteSingleValue,
|
||||
isShowDescription,
|
||||
isDeleteSingleValue,
|
||||
handleEditValueRow,
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData as IMetaDataTableData[],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleSaveValues = (data: IMetaDataTableData) => {
|
||||
setTableData((prev) => {
|
||||
if (currentValueIndex >= prev.length) {
|
||||
return [...prev, data];
|
||||
} else {
|
||||
return prev.map((item, index) => {
|
||||
if (index === currentValueIndex) {
|
||||
return data;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
maskClosable={false}
|
||||
okText={t('common.save')}
|
||||
onOk={async () => {
|
||||
const res = await handleSave({ callback: hideModal });
|
||||
console.log('data', res);
|
||||
success?.(res);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{t('knowledgeDetails.metadata.metadata')}</div>
|
||||
{isCanAdd && (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
onClick={handAddValueRow}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Table rootClassName="max-h-[800px]">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
<Empty type={EmptyType.Data} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Modal>
|
||||
{manageValuesVisible && (
|
||||
<ManageValuesModal
|
||||
title={<div>{t('knowledgeDetails.metadata.editMetadata')}</div>}
|
||||
visible={manageValuesVisible}
|
||||
hideModal={hideManageValuesModal}
|
||||
data={valueData}
|
||||
onSave={handleSaveValues}
|
||||
addUpdateValue={addUpdateValue}
|
||||
addDeleteValue={addDeleteValue}
|
||||
isEditField={isEditField || isCanAdd}
|
||||
isAddValue={isAddValue || isCanAdd}
|
||||
isShowDescription={isShowDescription}
|
||||
isShowValueSwitch={isShowValueSwitch}
|
||||
isVerticalShowValue={isVerticalShowValue}
|
||||
// handleDeleteSingleValue={handleDeleteSingleValue}
|
||||
// handleDeleteSingleRow={handleDeleteSingleRow}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteDialogContent.visible && (
|
||||
<ConfirmDeleteDialog
|
||||
open={deleteDialogContent.visible}
|
||||
onCancel={deleteDialogContent.onCancel}
|
||||
onOk={deleteDialogContent.onOk}
|
||||
title={deleteDialogContent.title}
|
||||
content={{
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={deleteDialogContent.name}
|
||||
warnText={deleteDialogContent.warnText}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,250 @@
|
||||
import EditTag from '@/components/edit-tag';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IManageValuesProps, IMetaDataTableData } from './interface';
|
||||
|
||||
// Create a separate input component, wrapped with memo to avoid unnecessary re-renders
|
||||
const ValueInputItem = memo(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
onValueChange,
|
||||
onDelete,
|
||||
onBlur,
|
||||
}: {
|
||||
item: string;
|
||||
index: number;
|
||||
onValueChange: (index: number, value: string) => void;
|
||||
onDelete: (index: number) => void;
|
||||
onBlur: (index: number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={`value-item-${index}`}
|
||||
className="flex items-center gap-2.5 w-full"
|
||||
>
|
||||
<div className="flex-1 w-full">
|
||||
<Input
|
||||
value={item}
|
||||
onChange={(e) => onValueChange(index, e.target.value)}
|
||||
onBlur={() => onBlur(index)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="delete"
|
||||
className="border border-border-button px-1 h-6 w-6 rounded-sm"
|
||||
onClick={() => onDelete(index)}
|
||||
>
|
||||
<Trash2 size={14} className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ManageValuesModal = (props: IManageValuesProps) => {
|
||||
const {
|
||||
title,
|
||||
data,
|
||||
isEditField,
|
||||
visible,
|
||||
isAddValue,
|
||||
isShowDescription,
|
||||
isShowValueSwitch,
|
||||
isVerticalShowValue,
|
||||
hideModal,
|
||||
onSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
} = props;
|
||||
const [metaData, setMetaData] = useState(data);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Use functional update to avoid closure issues
|
||||
const handleChange = useCallback((field: string, value: any) => {
|
||||
setMetaData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Maintain separate state for each input box
|
||||
const [tempValues, setTempValues] = useState<string[]>([...data.values]);
|
||||
|
||||
useEffect(() => {
|
||||
setTempValues([...data.values]);
|
||||
setMetaData(data);
|
||||
}, [data]);
|
||||
|
||||
const handleHideModal = useCallback(() => {
|
||||
hideModal();
|
||||
setMetaData({} as IMetaDataTableData);
|
||||
}, [hideModal]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!metaData.restrictDefinedValues && isShowValueSwitch) {
|
||||
const newMetaData = { ...metaData, values: [] };
|
||||
onSave(newMetaData);
|
||||
} else {
|
||||
onSave(metaData);
|
||||
}
|
||||
handleHideModal();
|
||||
}, [metaData, onSave, handleHideModal, isShowValueSwitch]);
|
||||
|
||||
// Handle value changes, only update temporary state
|
||||
const handleValueChange = useCallback((index: number, value: string) => {
|
||||
setTempValues((prev) => {
|
||||
const newValues = [...prev];
|
||||
newValues[index] = value;
|
||||
|
||||
return newValues;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle blur event, synchronize to main state
|
||||
const handleValueBlur = useCallback(() => {
|
||||
addUpdateValue(metaData.field, [...tempValues]);
|
||||
handleChange('values', [...tempValues]);
|
||||
}, [handleChange, tempValues, metaData, addUpdateValue]);
|
||||
|
||||
// Handle delete operation
|
||||
const handleDelete = useCallback(
|
||||
(index: number) => {
|
||||
setTempValues((prev) => {
|
||||
const newTempValues = [...prev];
|
||||
addDeleteValue(metaData.field, newTempValues[index]);
|
||||
newTempValues.splice(index, 1);
|
||||
return newTempValues;
|
||||
});
|
||||
|
||||
// Synchronize to main state
|
||||
setMetaData((prev) => {
|
||||
const newMetaDataValues = [...prev.values];
|
||||
newMetaDataValues.splice(index, 1);
|
||||
return {
|
||||
...prev,
|
||||
values: newMetaDataValues,
|
||||
};
|
||||
});
|
||||
},
|
||||
[addDeleteValue, metaData],
|
||||
);
|
||||
|
||||
// Handle adding new value
|
||||
const handleAddValue = useCallback(() => {
|
||||
setTempValues((prev) => [...prev, '']);
|
||||
|
||||
// Synchronize to main state
|
||||
setMetaData((prev) => ({
|
||||
...prev,
|
||||
values: [...prev.values, ''],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleHideModal}
|
||||
className="!w-[460px]"
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{!isEditField && (
|
||||
<div className="text-base p-5 border border-border-button rounded-lg">
|
||||
{metaData.field}
|
||||
</div>
|
||||
)}
|
||||
{isEditField && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.fieldName')}</div>
|
||||
<div>
|
||||
<Input
|
||||
value={metaData.field}
|
||||
onChange={(e) => {
|
||||
handleChange('field', e.target?.value || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isShowDescription && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.description')}</div>
|
||||
<div>
|
||||
<Textarea
|
||||
value={metaData.description}
|
||||
onChange={(e) => {
|
||||
handleChange('description', e.target?.value || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isShowValueSwitch && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.restrictDefinedValues')}</div>
|
||||
<div>
|
||||
<Switch
|
||||
checked={metaData.restrictDefinedValues || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange('restrictDefinedValues', checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{metaData.restrictDefinedValues && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{t('knowledgeDetails.metadata.values')}</div>
|
||||
{isAddValue && isVerticalShowValue && (
|
||||
<div>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
onClick={handleAddValue}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isVerticalShowValue && (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{tempValues?.map((item, index) => {
|
||||
return (
|
||||
<ValueInputItem
|
||||
key={`value-item-${index}`}
|
||||
item={item}
|
||||
index={index}
|
||||
onValueChange={handleValueChange}
|
||||
onDelete={handleDelete}
|
||||
onBlur={handleValueBlur}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!isVerticalShowValue && (
|
||||
<EditTag
|
||||
value={metaData.values}
|
||||
onChange={(value) => handleChange('values', value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,11 @@
|
||||
import {
|
||||
FormFieldConfig,
|
||||
FormFieldType,
|
||||
RenderField,
|
||||
} from '@/components/dynamic-form';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
@ -13,8 +19,19 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FieldValues, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
ControllerRenderProps,
|
||||
FieldValues,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import {
|
||||
MetadataType,
|
||||
useManageMetadata,
|
||||
util,
|
||||
} from '../../components/metedata/hook';
|
||||
import { ManageMetadataModal } from '../../components/metedata/manage-modal';
|
||||
import {
|
||||
useHandleKbEmbedding,
|
||||
useHasParsedDocument,
|
||||
@ -304,3 +321,84 @@ export function OverlappedPercent() {
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoMetadata() {
|
||||
// get metadata field
|
||||
const form = useFormContext();
|
||||
const {
|
||||
manageMetadataVisible,
|
||||
showManageMetadataModal,
|
||||
hideManageMetadataModal,
|
||||
tableData,
|
||||
config: metadataConfig,
|
||||
} = useManageMetadata();
|
||||
const autoMetadataField: FormFieldConfig = {
|
||||
name: 'parser_config.enable_metadata',
|
||||
label: t('knowledgeConfiguration.autoMetadata'),
|
||||
type: FormFieldType.Custom,
|
||||
horizontal: true,
|
||||
defaultValue: true,
|
||||
|
||||
render: (fieldProps: ControllerRenderProps) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const metadata = form.getValues('parser_config.metadata');
|
||||
const tableMetaData =
|
||||
util.metaDataSettingJSONToMetaDataTableData(metadata);
|
||||
showManageMetadataModal({
|
||||
metadata: tableMetaData,
|
||||
isCanAdd: true,
|
||||
type: MetadataType.Setting,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings />
|
||||
{t('knowledgeConfiguration.settings')}
|
||||
</div>
|
||||
</Button>
|
||||
<Switch
|
||||
checked={fieldProps.value}
|
||||
onCheckedChange={fieldProps.onChange}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<RenderField field={autoMetadataField} />
|
||||
{manageMetadataVisible && (
|
||||
<ManageMetadataModal
|
||||
title={
|
||||
metadataConfig.title || (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-normal">
|
||||
{t('knowledgeDetails.metadata.metadataGenerationSettings')}
|
||||
</div>
|
||||
<div className="text-sm text-text-secondary">
|
||||
{t('knowledgeDetails.metadata.changesAffectNewParses')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
visible={manageMetadataVisible}
|
||||
hideModal={hideManageMetadataModal}
|
||||
// selectedRowKeys={selectedRowKeys}
|
||||
tableData={tableData}
|
||||
isCanAdd={metadataConfig.isCanAdd}
|
||||
isDeleteSingleValue={metadataConfig.isDeleteSingleValue}
|
||||
type={metadataConfig.type}
|
||||
otherData={metadataConfig.record}
|
||||
isShowDescription={true}
|
||||
isShowValueSwitch={true}
|
||||
isVerticalShowValue={false}
|
||||
success={(data) => {
|
||||
form.setValue('parser_config.metadata', data || []);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,7 +11,11 @@ import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
import { EnableTocToggle, OverlappedPercent } from './common-item';
|
||||
import {
|
||||
AutoMetadata,
|
||||
EnableTocToggle,
|
||||
OverlappedPercent,
|
||||
} from './common-item';
|
||||
|
||||
export function NaiveConfiguration() {
|
||||
return (
|
||||
@ -22,6 +26,7 @@ export function NaiveConfiguration() {
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
<ChildrenDelimiterForm />
|
||||
<EnableTocToggle />
|
||||
<AutoMetadata />
|
||||
<OverlappedPercent />
|
||||
</ConfigurationFormContainer>
|
||||
<ConfigurationFormContainer>
|
||||
|
||||
@ -83,6 +83,18 @@ export const formSchema = z
|
||||
path: ['entity_types'],
|
||||
},
|
||||
),
|
||||
metadata: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
key: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
enum: z.array(z.string().optional()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
enable_metadata: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
pagerank: z.number(),
|
||||
|
||||
@ -90,6 +90,8 @@ export default function DatasetSettings() {
|
||||
entity_types: initialEntityTypes,
|
||||
method: MethodValue.Light,
|
||||
},
|
||||
metadata: [],
|
||||
enable_metadata: false,
|
||||
},
|
||||
pipeline_id: '',
|
||||
parseType: 1,
|
||||
@ -238,11 +240,8 @@ export default function DatasetSettings() {
|
||||
}
|
||||
return connector;
|
||||
});
|
||||
console.log('🚀 ~ DatasetSettings ~ connectors:', connectors);
|
||||
setSourceData(connectors as IDataSourceNodeProps[]);
|
||||
form.setValue('connectors', connectors || []);
|
||||
// form.setValue('pipeline_name', data.name || '');
|
||||
// form.setValue('pipeline_avatar', data.avatar || '');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -58,7 +58,11 @@ export function SavingButton() {
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
try {
|
||||
let beValid = await form.formControl.trigger();
|
||||
let beValid = await form.trigger();
|
||||
if (!beValid) {
|
||||
const errors = form.formState.errors;
|
||||
console.error('Validation errors:', errors);
|
||||
}
|
||||
if (beValid) {
|
||||
form.handleSubmit(async (values) => {
|
||||
console.log('saveKnowledgeConfiguration: ', values);
|
||||
|
||||
@ -32,19 +32,20 @@ import { getExtension } from '@/utils/document-util';
|
||||
import { t } from 'i18next';
|
||||
import { pick } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { ShowManageMetadataModalProps } from '../components/metedata/interface';
|
||||
import ProcessLogModal from '../process-log-modal';
|
||||
import { useShowLog } from './hooks';
|
||||
import { SetMetaDialog } from './set-meta-dialog';
|
||||
import { useChangeDocumentParser } from './use-change-document-parser';
|
||||
import { useDatasetTableColumns } from './use-dataset-table-columns';
|
||||
import { useRenameDocument } from './use-rename-document';
|
||||
import { useSaveMeta } from './use-save-meta';
|
||||
|
||||
export type DatasetTableProps = Pick<
|
||||
ReturnType<typeof useFetchDocumentList>,
|
||||
'documents' | 'setPagination' | 'pagination' | 'loading'
|
||||
> &
|
||||
Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>;
|
||||
Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'> & {
|
||||
showManageMetadataModal: (config: ShowManageMetadataModalProps) => void;
|
||||
};
|
||||
|
||||
export function DatasetTable({
|
||||
documents,
|
||||
@ -52,6 +53,7 @@ export function DatasetTable({
|
||||
setPagination,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
showManageMetadataModal,
|
||||
}: DatasetTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
@ -78,20 +80,19 @@ export function DatasetTable({
|
||||
initialName,
|
||||
} = useRenameDocument();
|
||||
|
||||
const {
|
||||
showSetMetaModal,
|
||||
hideSetMetaModal,
|
||||
setMetaVisible,
|
||||
setMetaLoading,
|
||||
onSetMetaModalOk,
|
||||
metaRecord,
|
||||
} = useSaveMeta();
|
||||
// const {
|
||||
// hideSetMetaModal,
|
||||
// setMetaVisible,
|
||||
// setMetaLoading,
|
||||
// onSetMetaModalOk,
|
||||
// metaRecord,
|
||||
// } = useSaveMeta();
|
||||
const { showLog, logInfo, logVisible, hideLog } = useShowLog(documents);
|
||||
|
||||
const columns = useDatasetTableColumns({
|
||||
showChangeParserModal,
|
||||
showRenameModal,
|
||||
showSetMetaModal,
|
||||
showManageMetadataModal,
|
||||
showLog,
|
||||
});
|
||||
|
||||
@ -207,14 +208,14 @@ export function DatasetTable({
|
||||
></RenameDialog>
|
||||
)}
|
||||
|
||||
{setMetaVisible && (
|
||||
{/* {setMetaVisible && (
|
||||
<SetMetaDialog
|
||||
hideModal={hideSetMetaModal}
|
||||
loading={setMetaLoading}
|
||||
onOk={onSetMetaModalOk}
|
||||
initialMetaData={metaRecord.meta_fields}
|
||||
></SetMetaDialog>
|
||||
)}
|
||||
)} */}
|
||||
{logVisible && (
|
||||
<ProcessLogModal
|
||||
title={t('knowledgeDetails.fileLogs')}
|
||||
|
||||
@ -13,9 +13,11 @@ import {
|
||||
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { Pen, Upload } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useManageMetadata } from '../components/metedata/hook';
|
||||
import { ManageMetadataModal } from '../components/metedata/manage-modal';
|
||||
import { DatasetTable } from './dataset-table';
|
||||
import Generate from './generate-button/generate';
|
||||
import { useBulkOperateDataset } from './use-bulk-operate-dataset';
|
||||
@ -61,6 +63,14 @@ export default function Dataset() {
|
||||
showCreateModal,
|
||||
} = useCreateEmptyDocument();
|
||||
|
||||
const {
|
||||
manageMetadataVisible,
|
||||
showManageMetadataModal,
|
||||
hideManageMetadataModal,
|
||||
tableData,
|
||||
config: metadataConfig,
|
||||
} = useManageMetadata();
|
||||
|
||||
const { rowSelection, rowSelectionIsEmpty, setRowSelection, selectedCount } =
|
||||
useRowSelection();
|
||||
|
||||
@ -91,6 +101,18 @@ export default function Dataset() {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
preChildren={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
onClick={() => showManageMetadataModal()}
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
<Pen size={14} />
|
||||
{t('knowledgeDetails.metadata.metadata')}
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@ -119,6 +141,7 @@ export default function Dataset() {
|
||||
setPagination={setPagination}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
showManageMetadataModal={showManageMetadataModal}
|
||||
loading={loading}
|
||||
></DatasetTable>
|
||||
{documentUploadVisible && (
|
||||
@ -137,6 +160,30 @@ export default function Dataset() {
|
||||
title={'File Name'}
|
||||
></RenameDialog>
|
||||
)}
|
||||
{manageMetadataVisible && (
|
||||
<ManageMetadataModal
|
||||
title={
|
||||
metadataConfig.title || (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-normal">
|
||||
{t('knowledgeDetails.metadata.manageMetadata')}
|
||||
</div>
|
||||
<div className="text-sm text-text-secondary">
|
||||
{t('knowledgeDetails.metadata.manageMetadataForDataset')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
visible={manageMetadataVisible}
|
||||
hideModal={hideManageMetadataModal}
|
||||
// selectedRowKeys={selectedRowKeys}
|
||||
tableData={tableData}
|
||||
isCanAdd={metadataConfig.isCanAdd}
|
||||
isDeleteSingleValue={metadataConfig.isDeleteSingleValue}
|
||||
type={metadataConfig.type}
|
||||
otherData={metadataConfig.record}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -16,20 +16,23 @@ import { formatDate } from '@/utils/date';
|
||||
import { ColumnDef } from '@tanstack/table-core';
|
||||
import { ArrowUpDown, MonitorUp } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MetadataType, util } from '../components/metedata/hook';
|
||||
import { ShowManageMetadataModalProps } from '../components/metedata/interface';
|
||||
import { DatasetActionCell } from './dataset-action-cell';
|
||||
import { ParsingStatusCell } from './parsing-status-cell';
|
||||
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
|
||||
import { UseRenameDocumentShowType } from './use-rename-document';
|
||||
import { UseSaveMetaShowType } from './use-save-meta';
|
||||
|
||||
type UseDatasetTableColumnsType = UseChangeDocumentParserShowType &
|
||||
UseRenameDocumentShowType &
|
||||
UseSaveMetaShowType & { showLog: (record: IDocumentInfo) => void };
|
||||
UseRenameDocumentShowType & {
|
||||
showLog: (record: IDocumentInfo) => void;
|
||||
showManageMetadataModal: (config: ShowManageMetadataModalProps) => void;
|
||||
};
|
||||
|
||||
export function useDatasetTableColumns({
|
||||
showChangeParserModal,
|
||||
showRenameModal,
|
||||
showSetMetaModal,
|
||||
showManageMetadataModal,
|
||||
showLog,
|
||||
}: UseDatasetTableColumnsType) {
|
||||
const { t } = useTranslation('translation', {
|
||||
@ -174,7 +177,26 @@ export function useDatasetTableColumns({
|
||||
<ParsingStatusCell
|
||||
record={row.original}
|
||||
showChangeParserModal={showChangeParserModal}
|
||||
showSetMetaModal={showSetMetaModal}
|
||||
showSetMetaModal={(row) =>
|
||||
showManageMetadataModal({
|
||||
metadata: util.JSONToMetaDataTableData(row.meta_fields || {}),
|
||||
isCanAdd: true,
|
||||
type: MetadataType.UpdateSingle,
|
||||
record: row,
|
||||
title: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-normal">
|
||||
{t('metadata.editMetadata')}
|
||||
</div>
|
||||
<div className="text-sm text-text-secondary">
|
||||
{t('metadata.editMetadataForDataset')}
|
||||
{row.name}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
isDeleteSingleValue: true,
|
||||
})
|
||||
}
|
||||
showLog={showLog}
|
||||
></ParsingStatusCell>
|
||||
);
|
||||
|
||||
@ -16,18 +16,10 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { LanguageTranslationMap } from '@/constants/common';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
@ -42,13 +34,6 @@ const FormId = 'dataset-creating-form';
|
||||
export function InputForm({ onOk }: IModalProps<any>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const languageOptions = useMemo(() => {
|
||||
return Object.keys(LanguageTranslationMap).map((x) => ({
|
||||
label: x,
|
||||
value: x,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
name: z
|
||||
@ -66,7 +51,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
.trim(),
|
||||
parser_id: z.string().optional(),
|
||||
pipeline_id: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
// When parseType === 1, parser_id is required
|
||||
@ -80,8 +64,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
path: ['parser_id'],
|
||||
});
|
||||
}
|
||||
|
||||
console.log('form-data', data);
|
||||
// When parseType === 1, pipline_id required
|
||||
if (data.parseType === 2 && !data.pipeline_id) {
|
||||
ctx.addIssue({
|
||||
@ -99,7 +81,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
parseType: 1,
|
||||
parser_id: '',
|
||||
embd_id: '',
|
||||
language: 'English',
|
||||
},
|
||||
});
|
||||
|
||||
@ -147,33 +128,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.language')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t('common.languagePlaceholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{languageOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EmbeddingModelItem line={2} isEdit={false} />
|
||||
<ParseTypeItem />
|
||||
{parseType === 1 && <ChunkMethodItem></ChunkMethodItem>}
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
IAgentLogsRequest,
|
||||
IPipeLineListRequest,
|
||||
} from '@/interfaces/database/agent';
|
||||
import { IAgentWebhookTraceRequest } from '@/interfaces/request/agent';
|
||||
import api from '@/utils/api';
|
||||
import { registerNextServer } from '@/utils/register-server';
|
||||
import request from '@/utils/request';
|
||||
@ -143,4 +144,11 @@ export const fetchPipeLineList = (params: IPipeLineListRequest) => {
|
||||
return request.get(api.listCanvas, { params: params });
|
||||
};
|
||||
|
||||
export const fetchWebhookTrace = (
|
||||
id: string,
|
||||
params: IAgentWebhookTraceRequest,
|
||||
) => {
|
||||
return request.get(api.fetchWebhookTrace(id), { params: params });
|
||||
};
|
||||
|
||||
export default agentService;
|
||||
|
||||
@ -215,6 +215,10 @@ const methods = {
|
||||
url: check_embedding,
|
||||
method: 'post',
|
||||
},
|
||||
// getMetaData: {
|
||||
// url: getMetaData,
|
||||
// method: 'get',
|
||||
// },
|
||||
};
|
||||
|
||||
const kbService = registerServer<keyof typeof methods>(methods, request);
|
||||
@ -251,6 +255,11 @@ export const listDocument = (
|
||||
export const documentFilter = (kb_id: string) =>
|
||||
request.post(api.get_dataset_filter, { kb_id });
|
||||
|
||||
export const getMetaDataService = ({ kb_id }: { kb_id: string }) =>
|
||||
request.post(api.getMetaData, { data: { kb_id } });
|
||||
export const updateMetaData = ({ kb_id, data }: { kb_id: string; data: any }) =>
|
||||
request.post(api.updateMetaData, { data: { kb_id, data } });
|
||||
|
||||
export const listDataPipelineLogDocument = (
|
||||
params?: IFetchKnowledgeListRequestParams,
|
||||
body?: IFetchDocumentListRequestBody,
|
||||
|
||||
@ -77,6 +77,8 @@ export default {
|
||||
unbindPipelineTask: ({ kb_id, type }: { kb_id: string; type: string }) =>
|
||||
`${api_host}/kb/unbind_task?kb_id=${kb_id}&pipeline_task_type=${type}`,
|
||||
pipelineRerun: `${api_host}/canvas/rerun`,
|
||||
getMetaData: `${api_host}/document/metadata/summary`,
|
||||
updateMetaData: `${api_host}/document/metadata/update`,
|
||||
|
||||
// tags
|
||||
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,
|
||||
@ -202,6 +204,9 @@ export default {
|
||||
prompt: `${api_host}/canvas/prompts`,
|
||||
cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`,
|
||||
downloadFile: `${api_host}/canvas/download`,
|
||||
testWebhook: (id: string) => `${ExternalApi}${api_host}/webhook_test/${id}`,
|
||||
fetchWebhookTrace: (id: string) =>
|
||||
`${ExternalApi}${api_host}/webhook_trace/${id}`,
|
||||
|
||||
// mcp server
|
||||
listMcpServer: `${api_host}/mcp_server/list`,
|
||||
|
||||
Reference in New Issue
Block a user