Compare commits

..

7 Commits

Author SHA1 Message Date
55c0468ac9 Include document_id in knowledgebase info retrieval (#12041)
### What problem does this PR solve?
After a file in the file list is associated with a knowledge base, the
knowledge base document ID is returned


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-12-19 19:32:24 +08:00
eeb36a5ce7 Feature: Implement metadata functionality (#12049)
### What problem does this PR solve?

Feature: Implement metadata functionality

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-12-19 19:13:33 +08:00
aceca266ff Feat: Images appearing consecutively in the dialogue are merged and displayed in a carousel. #10427 (#12051)
### What problem does this PR solve?

Feat: Images appearing consecutively in the dialogue are merged and
displayed in a carousel. #10427
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-19 19:13:18 +08:00
d82e502a71 Add AI Badgr as OpenAI-compatible chat model provider (#12018)
## What problem does this PR solve?

Adds AI Badgr as an optional LLM provider in RAGFlow. Users can use AI
Badgr for chat completions and embeddings via its OpenAI-compatible API.

**Background:**
- AI Badgr provides OpenAI-compatible endpoints (`/v1/chat/completions`,
`/v1/embeddings`, `/v1/models`)
- Previously, RAGFlow didn't support AI Badgr
- This PR adds support following the existing provider pattern (e.g.,
CometAPI, DeerAPI)

**Implementation details:**
- Added AI Badgr to the provider registry and configuration
- Supports chat completions (via LiteLLMBase) and embeddings (via
AIBadgrEmbed)
- Uses standard API key authentication
- Base URL: `https://aibadgr.com/api/v1`
- Environment variables: `AIBADGR_API_KEY`, `AIBADGR_BASE_URL`
(optional)

## Type of change

- [x] New Feature (non-breaking change which adds functionality)

This is a new feature that adds support for a new provider without
changing existing functionality.

---------

Co-authored-by: michaelmanley <55236695+michaelbrinkworth@users.noreply.github.com>
2025-12-19 17:45:20 +08:00
0494b92371 Feat: Display error messages from intermediate nodes. #10427 (#12038)
### What problem does this PR solve?

Feat: Display error messages from intermediate nodes. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-19 17:44:45 +08:00
8683a5b1b7 Docs: How to call MinerU as a remote service (#12004)
### Type of change

- [x] Documentation Update
2025-12-19 17:06:32 +08:00
4cbe470089 Feat: Display error messages from intermediate nodes of the webhook. #10427 (#11954)
### What problem does this PR solve?

Feat: Remove HMAC from the webhook #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-19 12:56:56 +08:00
61 changed files with 2743 additions and 528 deletions

View File

@ -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

View File

@ -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 (&ge; 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?

View File

@ -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

View File

@ -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 (&ge; 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

View File

@ -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

View File

@ -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
```
![placeholder-image](https://github.com/infiniflow/ragflow-docs/blob/040e4acd4c1eac6dc73dc44e934a6518de78d097/images/google_drive/image8.png?raw=true)
### If running from source:
- If running from source:
**Authorized JavaScript origin:**
```
http://localhost:9222
```
![placeholder-image](https://github.com/infiniflow/ragflow-docs/blob/040e4acd4c1eac6dc73dc44e934a6518de78d097/images/google_drive/image9.png?raw=true)
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.
![placeholder-image](https://github.com/infiniflow/ragflow-docs/blob/040e4acd4c1eac6dc73dc44e934a6518de78d097/images/google_drive/image10.png?raw=true)

View File

@ -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 (&ge; 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

View File

@ -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.
:::

View 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
View File

@ -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",

View File

@ -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",

View File

@ -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>
);
}

View File

@ -691,7 +691,7 @@ const DynamicForm = {
useImperativeHandle(
ref,
() => ({
submit: form.handleSubmit,
submit: form.handleSubmit(onSubmit),
getValues: form.getValues,
reset: (values?: T) => {
if (values) {

View File

@ -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>

View 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;

View File

@ -25,7 +25,7 @@
display: block;
object-fit: contain;
max-width: 100%;
max-height: 6vh;
max-height: 10vh;
}
.referenceImagePreview {

View File

@ -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 (

View 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);
});
};

View File

@ -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);
}

View File

@ -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

View 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,
};

View File

@ -196,7 +196,7 @@ export enum SwitchLogicOperator {
Or = 'or',
}
export const WebhookAlgorithmList = [
export const WebhookJWTAlgorithmList = [
'hs256',
'hs384',
'hs512',

View File

@ -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,
};
};

View File

@ -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;
}

View File

@ -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.
}

View File

@ -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:

View File

@ -267,6 +267,8 @@ export default {
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
},
knowledgeConfiguration: {
settings: '设置',
autoMetadata: '自动元数据',
mineruOptions: 'MinerU 选项',
mineruParseMethod: '解析方法',
mineruParseMethodTip:

View File

@ -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,

View File

@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) {
const form = useForm({
defaultValues: values,
resolver: zodResolver(BeginFormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);

View File

@ -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(),
})

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -49,7 +49,7 @@ export function WebhookResponse() {
name="response.body_template"
label={t('flow.webhook.bodyTemplate')}
>
<Textarea></Textarea>
<Textarea className="overflow-auto"></Textarea>
</RAGFlowFormItem>
</>
)}

View 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>
);
}

View 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;
}

View File

@ -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>
);
}

View File

@ -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,
},
},
];

View File

@ -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

View File

@ -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'),

View File

@ -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,
};
}

View 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;

View 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>
);
};

View 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 };
};

View 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;
};

View 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}
/>
),
}}
/>
)}
</>
);
};

View File

@ -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>
);
};

View File

@ -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 || []);
}}
/>
)}
</>
);
}

View File

@ -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>

View File

@ -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(),

View File

@ -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 || '');
}
};

View File

@ -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);

View File

@ -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')}

View File

@ -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>
</>
);

View File

@ -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>
);

View File

@ -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>}

View File

@ -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;

View File

@ -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,

View File

@ -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`,