Feature/docs generator (#11858)

### Type of change

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


### What problem does this PR solve?

This PR introduces a new Docs Generator agent component for producing
downloadable PDF, DOCX, or TXT files from Markdown content generated
within a RAGFlow workflow.

### **Key Features**

**Backend**

- New component: DocsGenerator (agent/component/docs_generator.py)
- 
- Markdown → PDF/DOCX/TXT conversion
- 
- Supports tables, lists, code blocks, headings, and rich formatting
- 
- Configurable document style (fonts, margins, colors, page size,
orientation)
- 
- Optional header logo and footer with page numbers/timestamps
- 

**Frontend**

- New configuration UI for the Docs Generator
- 
- Download button integrated into the chat interface
- 
- Output wired to the Message component
- 
- Full i18n support

**Documentation**

Added component guide:
docs/guides/agent/agent_component_reference/docs_generator.md

**Usage**

Add the Docs Generator to a workflow, connect Markdown output from an
upstream component, configure metadata/style, and feed its output into
the Message component. Users will see a document download button
directly in the chat.

**Contributor Note**

We have been following RAGFlow since more than a year and half now and
have worked extensively on personalizing the framework and integrating
it into several of our internal systems. Over the past year and a half,
we have built multiple platforms that rely on RAGFlow as a core
component, which has given us a strong appreciation for how flexible and
powerful the project is.

We also previously contributed the full Italian translation, and we were
glad to see it accepted. This new Docs Generator component was created
for our own production needs, and we believe that it may be useful for
many others in the community as well.

We want to sincerely thank the entire RAGFlow team for the remarkable
work you have done and continue to do. If there are opportunities to
contribute further, we would be glad to help whenever we have time
available. It would be a pleasure to support the project in any way we
can.

If appropriate, we would be glad to be listed among the project’s
contributors, but in any case we look forward to continuing to support
and contribute to the project.

PentaFrame Development Team

---------

Co-authored-by: PentaFrame <info@pentaframe.it>
Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
PentaFDevs
2025-12-12 07:59:43 +01:00
committed by GitHub
parent 6560388f2b
commit f9510edbbc
29 changed files with 3043 additions and 102 deletions

View File

@ -52,7 +52,8 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt install -y nginx unzip curl wget git vim less && \
apt install -y ghostscript && \
apt install -y pandoc && \
apt install -y texlive
apt install -y texlive && \
apt install -y fonts-freefont-ttf fonts-noto-cjk
# Install uv
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,241 @@
---
sidebar_position: 35
slug: /docs_generator
---
# Docs Generator component
A component that generates downloadable PDF, DOCX, or TXT documents from markdown-style content with full Unicode support.
---
The **Docs Generator** component enables you to create professional documents directly within your agent workflow. It accepts markdown-formatted text and converts it into downloadable files, making it ideal for generating reports, summaries, or any structured document output.
## Key features
- **Multiple output formats**: PDF, DOCX, and TXT
- **Full Unicode support**: Automatic font switching for CJK (Chinese, Japanese, Korean), Arabic, Hebrew, and other non-Latin scripts
- **Rich formatting**: Headers, lists, tables, code blocks, and more
- **Customizable styling**: Fonts, margins, page size, and orientation
- **Document extras**: Logo, watermark, page numbers, and timestamps
- **Direct download**: Generates a download button for the chat interface
## Prerequisites
- Content to be converted into a document (typically from an **Agent** or other text-generating component).
## Examples
You can pair an **Agent** component with the **Docs Generator** to create dynamic documents based on user queries. The **Agent** generates the content, and the **Docs Generator** converts it into a downloadable file. Connect the output to a **Message** component to display the download button in the chat.
A typical workflow looks like:
```
Begin → Agent → Docs Generator → Message
```
In the **Message** component, reference the `download` output variable from the **Docs Generator** to display a download button in the chat interface.
## Configurations
### Content
The main text content to include in the document. Supports markdown formatting:
- **Bold**: `**text**` or `__text__`
- **Italic**: `*text*` or `_text_`
- **Inline code**: `` `code` ``
- **Headings**: `# Heading 1`, `## Heading 2`, `### Heading 3`
- **Bullet lists**: `- item` or `* item`
- **Numbered lists**: `1. item`
- **Tables**: `| Column 1 | Column 2 |`
- **Horizontal lines**: `---`
- **Code blocks**: ` ``` code ``` `
:::tip NOTE
Click **(x)** or type `/` to insert variables from upstream components.
:::
### Title
Optional. The document title displayed at the top of the generated file.
### Subtitle
Optional. A subtitle displayed below the title.
### Output format
The file format for the generated document:
- **PDF** (default): Portable Document Format with full styling support.
- **DOCX**: Microsoft Word format.
- **TXT**: Plain text format.
### Logo image
Optional. A logo image to display at the top of the document. You can either:
- Upload an image file using the file picker
- Paste an image path, URL, or base64-encoded data
### Logo position
The horizontal position of the logo:
- **left** (default)
- **center**
- **right**
### Logo dimensions
- **Logo width**: Width in inches (default: `2.0`)
- **Logo height**: Height in inches (default: `1.0`)
### Font family
The font used throughout the document:
- **Helvetica** (default)
- **Times-Roman**
- **Courier**
- **Helvetica-Bold**
- **Times-Bold**
### Font size
The base font size in points. Defaults to `12`.
### Title font size
The font size for the document title. Defaults to `24`.
### Page size
The paper size for the document:
- **A4** (default)
- **Letter**
### Orientation
The page orientation:
- **Portrait** (default)
- **Landscape**
### Margins
Page margins in inches:
- **Margin top**: Defaults to `1.0`
- **Margin bottom**: Defaults to `1.0`
- **Margin left**: Defaults to `1.0`
- **Margin right**: Defaults to `1.0`
### Filename
Optional. Custom filename for the generated document. If left empty, a filename is auto-generated with a timestamp.
### Output directory
The server directory where generated documents are saved. Defaults to `/tmp/pdf_outputs`.
### Add page numbers
When enabled, page numbers are added to the footer of each page. Defaults to `true`.
### Add timestamp
When enabled, a generation timestamp is added to the document footer. Defaults to `true`.
### Watermark text
Optional. Text to display as a diagonal watermark across each page. Useful for marking documents as "Draft", "Confidential", etc.
## Output
The **Docs Generator** component provides the following output variables:
| Variable name | Type | Description |
| ------------- | --------- | --------------------------------------------------------------------------- |
| `file_path` | `string` | The server path where the generated document is saved. |
| `pdf_base64` | `string` | The document content encoded in base64 format. |
| `download` | `string` | JSON containing download information for the chat interface. |
| `success` | `boolean` | Indicates whether the document was generated successfully. |
### Displaying the download button
To display a download button in the chat, add a **Message** component after the **Docs Generator** and reference the `download` variable:
1. Connect the **Docs Generator** output to a **Message** component.
2. In the **Message** component's content field, type `/` and select `{Docs Generator_0@download}`.
3. When the agent runs, a download button will appear in the chat, allowing users to download the generated document.
The download button automatically handles:
- File type detection (PDF, DOCX, TXT)
- Proper MIME type for browser downloads
- Base64 decoding for direct file delivery
## Unicode and multi-language support
The **Docs Generator** includes intelligent font handling for international content:
### How it works
1. **Content analysis**: The component scans the text for non-Latin characters.
2. **Automatic font switching**: When CJK or other complex scripts are detected, the system automatically switches to a compatible CID font (STSong-Light for Chinese, HeiseiMin-W3 for Japanese, HYSMyeongJo-Medium for Korean).
3. **Latin content**: For documents containing only Latin characters (including extended Latin, Cyrillic, and Greek), the user-selected font family is used.
### Supported scripts
| Script | Unicode Range | Font Used |
| ------ | ------------- | --------- |
| Chinese (CJK) | U+4E00U+9FFF | STSong-Light |
| Japanese (Hiragana/Katakana) | U+3040U+30FF | HeiseiMin-W3 |
| Korean (Hangul) | U+AC00U+D7AF | HYSMyeongJo-Medium |
| Arabic | U+0600U+06FF | CID font fallback |
| Hebrew | U+0590U+05FF | CID font fallback |
| Devanagari (Hindi) | U+0900U+097F | CID font fallback |
| Thai | U+0E00U+0E7F | CID font fallback |
### Font installation
For full multi-language support in self-hosted deployments, ensure Unicode fonts are installed:
**Linux (Debian/Ubuntu):**
```bash
apt-get install fonts-freefont-ttf fonts-noto-cjk
```
**Docker:** The official RAGFlow Docker image includes these fonts. For custom images, add the font packages to your Dockerfile:
```dockerfile
RUN apt-get update && apt-get install -y fonts-freefont-ttf fonts-noto-cjk
```
:::tip NOTE
CID fonts (STSong-Light, HeiseiMin-W3, etc.) are built into ReportLab and do not require additional installation. They are used automatically when CJK content is detected.
:::
## Troubleshooting
### Characters appear as boxes or question marks
This indicates missing font support. Ensure:
1. The content contains supported Unicode characters.
2. For self-hosted deployments, Unicode fonts are installed on the server.
3. The document is being viewed in a PDF reader that supports embedded fonts.
### Download button not appearing
Ensure:
1. The **Message** component is connected after the **Docs Generator**.
2. The `download` variable is correctly referenced using `/` (which appears as `{Docs Generator_0@download}` when copied).
3. The document generation completed successfully (check `success` output).
### Large tables not rendering correctly
For tables with many columns or large cell content:
- The component automatically converts wide tables to a definition list format for better readability.
- Consider splitting large tables into multiple smaller tables.
- Use landscape orientation for wide tables.

View File

@ -154,8 +154,10 @@ dependencies = [
"exceptiongroup>=1.3.0,<2.0.0",
"ffmpeg-python>=0.2.0",
"imageio-ffmpeg>=0.6.0",
"reportlab>=4.4.1",
"jinja2>=3.1.0",
"boxsdk>=10.1.0",
"aiosmtplib>=5.0.0",
"aiosmtplib>=5.0.0"
]
[dependency-groups]

38
uv.lock generated
View File

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.12, <3.15"
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'darwin'",
@ -3279,7 +3279,7 @@ wheels = [
[[package]]
name = "jupyter-client"
version = "8.6.3"
version = "8.7.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "jupyter-core" },
@ -3288,9 +3288,9 @@ dependencies = [
{ name = "tornado" },
{ name = "traitlets" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" },
]
[[package]]
@ -5979,6 +5979,7 @@ dependencies = [
{ name = "infinity-emb" },
{ name = "infinity-sdk" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "jira" },
{ name = "json-repair" },
{ name = "langfuse" },
@ -6036,6 +6037,7 @@ dependencies = [
{ name = "ranx" },
{ name = "readability-lxml" },
{ name = "replicate" },
{ name = "reportlab" },
{ name = "requests" },
{ name = "roman-numbers" },
{ name = "ruamel-base" },
@ -6148,6 +6150,7 @@ requires-dist = [
{ name = "infinity-emb", specifier = ">=0.0.66,<0.0.67" },
{ name = "infinity-sdk", specifier = "==0.6.11" },
{ name = "itsdangerous", specifier = "==2.1.2" },
{ name = "jinja2", specifier = ">=3.1.0" },
{ name = "jira", specifier = "==3.10.5" },
{ name = "json-repair", specifier = "==0.35.0" },
{ name = "langfuse", specifier = ">=2.60.0" },
@ -6205,6 +6208,7 @@ requires-dist = [
{ name = "ranx", specifier = "==0.3.20" },
{ name = "readability-lxml", specifier = ">=0.8.4,<1.0.0" },
{ name = "replicate", specifier = "==0.31.0" },
{ name = "reportlab", specifier = ">=4.4.1" },
{ name = "requests", specifier = ">=2.32.3,<3.0.0" },
{ name = "roman-numbers", specifier = "==1.0.2" },
{ name = "ruamel-base", specifier = "==1.0.0" },
@ -7409,21 +7413,21 @@ wheels = [
[[package]]
name = "tornado"
version = "6.5.2"
version = "6.5.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/2e/3d22d478f27cb4b41edd4db7f10cd7846d0a28ea443342de3dba97035166/tornado-6.5.3.tar.gz", hash = "sha256:16abdeb0211796ffc73765bc0a20119712d68afeeaf93d1a3f2edf6b3aee8d5a", size = 513348, upload-time = "2025-12-11T04:16:42.225Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/e9/bf22f66e1d5d112c0617974b5ce86666683b32c09b355dfcd59f8d5c8ef6/tornado-6.5.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2dd7d7e8d3e4635447a8afd4987951e3d4e8d1fb9ad1908c54c4002aabab0520", size = 443860, upload-time = "2025-12-11T04:16:26.638Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/9c/594b631f0b8dc5977080c7093d1e96f1377c10552577d2c31bb0208c9362/tornado-6.5.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5977a396f83496657779f59a48c38096ef01edfe4f42f1c0634b791dde8165d0", size = 442118, upload-time = "2025-12-11T04:16:28.32Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/f6/685b869f5b5b9d9547571be838c6106172082751696355b60fc32a4988ed/tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f72ac800be2ac73ddc1504f7aa21069a4137e8d70c387172c063d363d04f2208", size = 445700, upload-time = "2025-12-11T04:16:29.64Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/4c/f0d19edf24912b7f21ae5e941f7798d132ad4d9b71441c1e70917a297265/tornado-6.5.3-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43c4fc4f5419c6561cfb8b884a8f6db7b142787d47821e1a0e1296253458265", size = 445041, upload-time = "2025-12-11T04:16:30.799Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/2b/e02da94f4a4aef2bb3b923c838ef284a77548a5f06bac2a8682b36b4eead/tornado-6.5.3-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8b3fed4b3afb65d542d7702ac8767b567e240f6a43020be8eaef59328f117b", size = 445270, upload-time = "2025-12-11T04:16:32.316Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/e2/7a7535d23133443552719dba526dacbb7415f980157da9f14950ddb88ad6/tornado-6.5.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dbc4b4c32245b952566e17a20d5c1648fbed0e16aec3fc7e19f3974b36e0e47c", size = 445957, upload-time = "2025-12-11T04:16:33.913Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/1f/9ff92eca81ff17a86286ec440dcd5eab0400326eb81761aa9a4eecb1ffb9/tornado-6.5.3-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:db238e8a174b4bfd0d0238b8cfcff1c14aebb4e2fcdafbf0ea5da3b81caceb4c", size = 445371, upload-time = "2025-12-11T04:16:35.093Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/b1/1d03ae4526a393b0b839472a844397337f03c7f3a1e6b5c82241f0e18281/tornado-6.5.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:892595c100cd9b53a768cbfc109dfc55dec884afe2de5290611a566078d9692d", size = 445348, upload-time = "2025-12-11T04:16:36.679Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/7d/7c181feadc8941f418d0d26c3790ee34ffa4bd0a294bc5201d44ebd19c1e/tornado-6.5.3-cp39-abi3-win32.whl", hash = "sha256:88141456525fe291e47bbe1ba3ffb7982549329f09b4299a56813923af2bd197", size = 446433, upload-time = "2025-12-11T04:16:38.332Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/98/4f7f938606e21d0baea8c6c39a7c8e95bdf8e50b0595b1bb6f0de2af7a6e/tornado-6.5.3-cp39-abi3-win_amd64.whl", hash = "sha256:ba4b513d221cc7f795a532c1e296f36bcf6a60e54b15efd3f092889458c69af1", size = 446842, upload-time = "2025-12-11T04:16:39.867Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/27/0e3fca4c4edf33fb6ee079e784c63961cd816971a45e5e4cacebe794158d/tornado-6.5.3-cp39-abi3-win_arm64.whl", hash = "sha256:278c54d262911365075dd45e0b6314308c74badd6ff9a54490e7daccdd5ed0ea", size = 445863, upload-time = "2025-12-11T04:16:41.099Z" },
]
[[package]]

View File

@ -14,6 +14,11 @@ import { cn } from '@/lib/utils';
import MarkdownContent from '../markdown-content';
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
import { UploadedMessageFiles } from '../next-message-item/uploaded-message-files';
import {
PDFDownloadButton,
extractPDFDownloadInfo,
removePDFDownloadInfo,
} from '../pdf-download-button';
import { RAGFlowAvatar } from '../ragflow-avatar';
import { useTheme } from '../theme-provider';
import { AssistantGroupButton, UserGroupButton } from './group-button';
@ -61,6 +66,20 @@ const MessageItem = ({
return reference?.doc_aggs ?? [];
}, [reference?.doc_aggs]);
// Extract PDF download info from message content
const pdfDownloadInfo = useMemo(
() => extractPDFDownloadInfo(item.content),
[item.content],
);
// If we have PDF download info, extract the remaining text
const messageContent = useMemo(() => {
if (!pdfDownloadInfo) return item.content;
// Remove the JSON part from the content to avoid showing it
return removePDFDownloadInfo(item.content, pdfDownloadInfo);
}, [item.content, pdfDownloadInfo]);
const handleRegenerateMessage = useCallback(() => {
regenerateMessage?.(item);
}, [regenerateMessage, item]);
@ -122,6 +141,16 @@ const MessageItem = ({
></UserGroupButton>
)}
{/* Show PDF download button if download info is present */}
{pdfDownloadInfo && (
<PDFDownloadButton
downloadInfo={pdfDownloadInfo}
className="mb-2"
/>
)}
{/* Show message content if there's any text besides the download */}
{messageContent && (
<div
className={cn(
isAssistant
@ -134,11 +163,12 @@ const MessageItem = ({
>
<MarkdownContent
loading={loading}
content={item.content}
content={messageContent}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
)}
{isAssistant && referenceDocumentList.length > 0 && (
<ReferenceDocumentList
list={referenceDocumentList}

View File

@ -4,6 +4,7 @@ import {
IMessage,
IReferenceChunk,
IReferenceObject,
UploadResponseDataType,
} from '@/interfaces/database/chat';
import classNames from 'classnames';
import {
@ -24,6 +25,11 @@ import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline';
import { isEmpty } from 'lodash';
import { Atom, ChevronDown, ChevronUp } from 'lucide-react';
import MarkdownContent from '../next-markdown-content';
import {
PDFDownloadButton,
extractPDFDownloadInfo,
removePDFDownloadInfo,
} from '../pdf-download-button';
import { RAGFlowAvatar } from '../ragflow-avatar';
import { useTheme } from '../theme-provider';
import { Button } from '../ui/button';
@ -95,6 +101,20 @@ function MessageItem({
return Object.values(docs);
}, [reference?.doc_aggs]);
// Extract PDF download info from message content
const pdfDownloadInfo = useMemo(
() => extractPDFDownloadInfo(item.content),
[item.content],
);
// If we have PDF download info, extract the remaining text
const messageContent = useMemo(() => {
if (!pdfDownloadInfo) return item.content;
// Remove the JSON part from the content to avoid showing it
return removePDFDownloadInfo(item.content, pdfDownloadInfo);
}, [item.content, pdfDownloadInfo]);
const handleRegenerateMessage = useCallback(() => {
regenerateMessage?.(item);
}, [regenerateMessage, item]);
@ -219,6 +239,16 @@ function MessageItem({
/>
</div>
)}
{/* Show PDF download button if download info is present */}
{pdfDownloadInfo && (
<PDFDownloadButton
downloadInfo={pdfDownloadInfo}
className="mb-2"
/>
)}
{/* Show message content if there's any text besides the download */}
{messageContent && (
<div
className={cn({
[theme === 'dark'
@ -230,17 +260,18 @@ function MessageItem({
>
{item.data ? (
children
) : sendLoading && isEmpty(item.content) ? (
) : sendLoading && isEmpty(messageContent) ? (
<>{!isShare && 'running...'}</>
) : (
<MarkdownContent
loading={loading}
content={item.content}
content={messageContent}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
)}
</div>
)}
{isAssistant && referenceDocuments.length > 0 && (
<ReferenceDocumentList
list={referenceDocuments}
@ -248,7 +279,9 @@ function MessageItem({
)}
{isUser && (
<UploadedMessageFiles files={item.files}></UploadedMessageFiles>
<UploadedMessageFiles
files={item.files as File[] | UploadResponseDataType[]}
></UploadedMessageFiles>
)}
{/* {isAssistant && item.attachment && item.attachment.doc_id && (
<div className="w-full flex items-center justify-end">

View File

@ -0,0 +1,196 @@
import { Button } from '@/components/ui/button';
import { Download, FileText } from 'lucide-react';
import { useCallback } from 'react';
interface DocumentDownloadInfo {
filename: string;
base64: string;
mime_type: string;
}
interface DocumentDownloadButtonProps {
downloadInfo: DocumentDownloadInfo;
className?: string;
}
export function PDFDownloadButton({
downloadInfo,
className,
}: DocumentDownloadButtonProps) {
const handleDownload = useCallback(() => {
try {
// Convert base64 to blob
const byteCharacters = atob(downloadInfo.base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: downloadInfo.mime_type });
// Create download link
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = downloadInfo.filename;
document.body.appendChild(link);
link.click();
// Cleanup
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading document:', error);
}
}, [downloadInfo]);
// Determine document type from mime_type or filename
const getDocumentType = () => {
if (downloadInfo.mime_type === 'application/pdf') return 'PDF Document';
if (
downloadInfo.mime_type ===
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
return 'Word Document';
if (downloadInfo.mime_type === 'text/plain') return 'Text Document';
// Fallback to file extension
const ext = downloadInfo.filename.split('.').pop()?.toUpperCase();
if (ext === 'PDF') return 'PDF Document';
if (ext === 'DOCX') return 'Word Document';
if (ext === 'TXT') return 'Text Document';
return 'Document';
};
return (
<div
className={`flex items-center gap-3 p-4 border rounded-lg bg-background-card ${className || ''}`}
>
<div className="flex-shrink-0">
<div className="p-2 bg-accent-primary/10 rounded-lg">
<FileText className="w-6 h-6 text-accent-primary" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">
{downloadInfo.filename}
</div>
<div className="text-xs text-muted-foreground">{getDocumentType()}</div>
</div>
<Button
onClick={handleDownload}
size="sm"
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
Download
</Button>
</div>
);
}
// Helper function to detect if content contains document download info
export function extractPDFDownloadInfo(
content: string,
): DocumentDownloadInfo | null {
try {
// Try to parse as JSON first (for pure JSON content)
const parsed = JSON.parse(content);
if (parsed && parsed.filename && parsed.base64 && parsed.mime_type) {
// Accept PDF, DOCX, and TXT formats
const validMimeTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
];
if (validMimeTypes.includes(parsed.mime_type)) {
return parsed as DocumentDownloadInfo;
}
}
} catch {
// If direct parsing fails, try to extract JSON object from mixed content
// Look for a JSON object that contains the required fields
// This regex finds a balanced JSON object by counting braces
const startPattern = /\{[^{}]*"filename"[^{}]*:/g;
let match;
while ((match = startPattern.exec(content)) !== null) {
const startIndex = match.index;
let braceCount = 0;
let endIndex = startIndex;
// Find the matching closing brace
for (let i = startIndex; i < content.length; i++) {
if (content[i] === '{') braceCount++;
if (content[i] === '}') braceCount--;
if (braceCount === 0) {
endIndex = i + 1;
break;
}
}
if (endIndex > startIndex) {
try {
const jsonStr = content.substring(startIndex, endIndex);
const parsed = JSON.parse(jsonStr);
if (parsed && parsed.filename && parsed.base64 && parsed.mime_type) {
// Accept PDF, DOCX, and TXT formats
const validMimeTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
];
if (validMimeTypes.includes(parsed.mime_type)) {
return parsed as DocumentDownloadInfo;
}
}
} catch {
// This wasn't valid JSON, continue searching
}
}
}
}
return null;
}
// Helper function to remove document download info from content
export function removePDFDownloadInfo(
content: string,
downloadInfo: DocumentDownloadInfo,
): string {
try {
// First, check if the entire content is just the JSON (most common case)
try {
const parsed = JSON.parse(content);
if (
parsed &&
parsed.filename === downloadInfo.filename &&
parsed.base64 === downloadInfo.base64
) {
// The entire content is just the download JSON, return empty
return '';
}
} catch {
// Content is not pure JSON, continue with removal
}
// Try to remove the JSON string from content
const jsonStr = JSON.stringify(downloadInfo);
let cleaned = content.replace(jsonStr, '').trim();
// Also try with pretty-printed JSON (with indentation)
const prettyJsonStr = JSON.stringify(downloadInfo, null, 2);
cleaned = cleaned.replace(prettyJsonStr, '').trim();
// Also try to find and remove JSON object pattern from mixed content
// This handles cases where the JSON might have different formatting
const startPattern = /\{[^{}]*"filename"[^{}]*"base64"[^{}]*\}/g;
cleaned = cleaned.replace(startPattern, '').trim();
return cleaned;
} catch {
return content;
}
}

View File

@ -101,6 +101,7 @@ export enum Operator {
UserFillUp = 'UserFillUp',
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
PDFGenerator = 'PDFGenerator',
Placeholder = 'Placeholder',
DataOperations = 'DataOperations',
ListOperations = 'ListOperations',

View File

@ -878,6 +878,27 @@ export default {
searXNG: 'SearXNG',
searXNGDescription:
'Eine Komponente, die auf https://searxng.org/ sucht und Ihnen ermöglicht, die Anzahl der Suchergebnisse mit TopN anzugeben. Sie ergänzt die vorhandenen Wissensdatenbanken.',
pdfGenerator: 'Dokumentengenerator',
pDFGenerator: 'Dokumentengenerator',
pdfGeneratorDescription: `Eine Komponente, die Dokumente (PDF, DOCX, TXT) aus markdown-formatierten Inhalten mit anpassbarem Stil, Bildern und Tabellen generiert. Unterstützt: **fett**, *kursiv*, # Überschriften, - Listen, Tabellen mit | Syntax.`,
pDFGeneratorDescription: `Eine Komponente, die Dokumente (PDF, DOCX, TXT) aus markdown-formatierten Inhalten mit anpassbarem Stil, Bildern und Tabellen generiert. Unterstützt: **fett**, *kursiv*, # Überschriften, - Listen, Tabellen mit | Syntax.`,
subtitle: 'Untertitel',
logoImage: 'Logo-Bild',
logoPosition: 'Logo-Position',
logoWidth: 'Logo-Breite',
logoHeight: 'Logo-Höhe',
fontFamily: 'Schriftfamilie',
fontSize: 'Schriftgröße',
titleFontSize: 'Titel-Schriftgröße',
pageSize: 'Seitengröße',
orientation: 'Ausrichtung',
marginTop: 'Oberer Rand',
marginBottom: 'Unterer Rand',
filename: 'Dateiname',
outputDirectory: 'Ausgabeverzeichnis',
addPageNumbers: 'Seitenzahlen hinzufügen',
addTimestamp: 'Zeitstempel hinzufügen',
watermarkText: 'Wasserzeichentext',
channel: 'Kanal',
channelTip:
'Führt eine Textsuche oder Nachrichtensuche für die Eingabe der Komponente durch',

View File

@ -1307,6 +1307,27 @@ Example: Virtual Hosted Style`,
searXNG: 'SearXNG',
searXNGDescription:
'A component that searches via your provided SearXNG instance URL. Specify TopN and the instance URL.',
pdfGenerator: 'Docs Generator',
pDFGenerator: 'Docs Generator',
pdfGeneratorDescription: `A component that generates documents (PDF, DOCX, TXT) from markdown-formatted content with customizable styling, images, and tables. Supports: **bold**, *italic*, # headings, - lists, tables with | syntax.`,
pDFGeneratorDescription: `A component that generates documents (PDF, DOCX, TXT) from markdown-formatted content with customizable styling, images, and tables. Supports: **bold**, *italic*, # headings, - lists, tables with | syntax.`,
subtitle: 'Subtitle',
logoImage: 'Logo Image',
logoPosition: 'Logo Position',
logoWidth: 'Logo Width',
logoHeight: 'Logo Height',
fontFamily: 'Font Family',
fontSize: 'Font Size',
titleFontSize: 'Title Font Size',
pageSize: 'Page Size',
orientation: 'Orientation',
marginTop: 'Margin Top',
marginBottom: 'Margin Bottom',
filename: 'Filename',
outputDirectory: 'Output Directory',
addPageNumbers: 'Add Page Numbers',
addTimestamp: 'Add Timestamp',
watermarkText: 'Watermark Text',
channel: 'Channel',
channelTip: `Perform text search or news search on the component's input`,
text: 'Text',
@ -1690,7 +1711,6 @@ This delimiter is used to split the input text into several text pieces echo of
datatype: 'MINE type of the HTTP request',
insertVariableTip: `Enter / Insert variables`,
historyversion: 'Version history',
filename: 'File name',
version: {
created: 'Created',
details: 'Version details',

View File

@ -578,15 +578,31 @@ export default {
'Este componente se usa para obtener resultados de búsqueda de www.baidu.com. Típicamente, actúa como un complemento a las bases de conocimiento. Top N especifica el número de resultados de búsqueda que necesitas ajustar.',
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'Un componente que recupera resultados de búsqueda de duckduckgo.com, con TopN especificando el número de resultados de búsqueda. Complementa las bases de conocimiento existentes.',
'Un componente que busca en duckduckgo.com, permitiéndote especificar el número de resultados de búsqueda usando TopN. Supplementa las bases de conocimiento existentes.',
searXNG: 'SearXNG',
searXNGDescription:
'Un componente que realiza búsquedas mediante la URL de la instancia de SearXNG que usted proporcione. Especifique TopN y la URL de la instancia.',
channel: 'Canal',
channelTip:
'Realizar búsqueda de texto o búsqueda de noticias en la entrada del componente.',
text: 'Texto',
news: 'Noticias',
'Un componente que busca a través de la URL de la instancia SearXNG que proporcionas. Especifica TopN y la URL de la instancia.',
pdfGenerator: 'Generador de Documentos',
pDFGenerator: 'Generador de Documentos',
pdfGeneratorDescription: `Un componente que genera documentos (PDF, DOCX, TXT) desde contenido formateado en markdown con estilo personalizable, imágenes y tablas. Soporta: **negrita**, *cursiva*, # encabezados, - listas, tablas con sintaxis |.`,
pDFGeneratorDescription: `Un componente que genera documentos (PDF, DOCX, TXT) desde contenido formateado en markdown con estilo personalizable, imágenes y tablas. Soporta: **negrita**, *cursiva*, # encabezados, - listas, tablas con sintaxis |.`,
subtitle: 'Subtítulo',
logoImage: 'Imagen Logo',
logoPosition: 'Posición Logo',
logoWidth: 'Ancho Logo',
logoHeight: 'Alto Logo',
fontFamily: 'Familia Fuente',
fontSize: 'Tamaño Fuente',
titleFontSize: 'Tamaño Fuente Título',
pageSize: 'Tamaño Página',
orientation: 'Orientación',
marginTop: 'Margen Superior',
marginBottom: 'Margen Inferior',
filename: 'Nombre Archivo',
outputDirectory: 'Directorio Salida',
addPageNumbers: 'Agregar Números Página',
addTimestamp: 'Agregar Timestamp',
watermarkText: 'Texto Marca Agua',
messageHistoryWindowSize:
'Tamaño de la ventana del historial de mensajes',
messageHistoryWindowSizeTip:

View File

@ -788,15 +788,31 @@ export default {
'Un composant qui recherche sur baidu.com, utilisant TopN pour spécifier le nombre de résultats. Il complète les bases de connaissances existantes.',
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'Un composant qui recherche sur duckduckgo.com, vous permettant de spécifier le nombre de résultats avec TopN. Il complète les bases de connaissances existantes.',
'Un composant qui recherche sur duckduckgo.com, vous permettant de spécifier le nombre de résultats de recherche avec TopN. Il complète les bases de connaissances existantes.',
searXNG: 'SearXNG',
searXNGDescription:
"Un composant qui effectue des recherches via la URL de l'instance de SearXNG que vous fournissez. Spécifiez TopN et l'URL de l'instance.",
channel: 'Canal',
channelTip:
"Effectuer une recherche de texte ou d'actualités sur l'entrée du composant",
text: 'Texte',
news: 'Actualités',
pdfGenerator: 'Générateur de Documents',
pDFGenerator: 'Générateur de Documents',
pdfGeneratorDescription: `Un composant qui génère des documents (PDF, DOCX, TXT) à partir de contenu formaté en markdown avec un style personnalisable, des images et des tableaux. Prend en charge : **gras**, *italique*, # titres, - listes, tableaux avec syntaxe |.`,
pDFGeneratorDescription: `Un composant qui génère des documents (PDF, DOCX, TXT) à partir de contenu formaté en markdown avec un style personnalisable, des images et des tableaux. Prend en charge : **gras**, *italique*, # titres, - listes, tableaux avec syntaxe |.`,
subtitle: 'Sous-titre',
logoImage: 'Image Logo',
logoPosition: 'Position Logo',
logoWidth: 'Largeur Logo',
logoHeight: 'Hauteur Logo',
fontFamily: 'Famille Police',
fontSize: 'Taille Police',
titleFontSize: 'Taille Police Titre',
pageSize: 'Taille Page',
orientation: 'Orientation',
marginTop: 'Marge Supérieure',
marginBottom: 'Marge Inférieure',
filename: 'Nom Fichier',
outputDirectory: 'Répertoire Sortie',
addPageNumbers: 'Ajouter Numéros Page',
addTimestamp: 'Ajouter Timestamp',
watermarkText: 'Texte Filigrane',
messageHistoryWindowSize:
"Taille de la fenêtre d'historique des messages",
messageHistoryWindowSizeTip:
@ -1173,7 +1189,6 @@ export default {
datatype: 'Type MIME de la requête HTTP',
insertVariableTip: `Entrer / Insérer des variables`,
historyversion: 'Historique des versions',
filename: 'Nom du fichier',
version: {
created: 'Créé',
details: 'Détails de la version',

View File

@ -770,6 +770,27 @@ export default {
searXNG: 'SearXNG',
searXNGDescription:
'Komponen yang melakukan pencarian menggunakan URL instance SearXNG yang Anda berikan. Spesifikasikan TopN dan URL instance.',
pdfGenerator: 'Pembuat Dokumen',
pDFGenerator: 'Pembuat Dokumen',
pdfGeneratorDescription: `Komponen yang menghasilkan dokumen (PDF, DOCX, TXT) dari konten berformat markdown dengan gaya yang dapat disesuaikan, gambar, dan tabel. Mendukung: **tebal**, *miring*, # judul, - daftar, tabel dengan sintaks |.`,
pDFGeneratorDescription: `Komponen yang menghasilkan dokumen (PDF, DOCX, TXT) dari konten berformat markdown dengan gaya yang dapat disesuaikan, gambar, dan tabel. Mendukung: **tebal**, *miring*, # judul, - daftar, tabel dengan sintaks |.`,
subtitle: 'Subjudul',
logoImage: 'Gambar Logo',
logoPosition: 'Posisi Logo',
logoWidth: 'Lebar Logo',
logoHeight: 'Tinggi Logo',
fontFamily: 'Keluarga Font',
fontSize: 'Ukuran Font',
titleFontSize: 'Ukuran Font Judul',
pageSize: 'Ukuran Halaman',
orientation: 'Orientasi',
marginTop: 'Margin Atas',
marginBottom: 'Margin Bawah',
filename: 'Nama File',
outputDirectory: 'Direktori Output',
addPageNumbers: 'Tambahkan Nomor Halaman',
addTimestamp: 'Tambahkan Timestamp',
watermarkText: 'Teks Watermark',
channel: 'Saluran',
channelTip: `Lakukan pencarian teks atau pencarian berita pada input komponen`,
text: 'Teks',

View File

@ -930,6 +930,30 @@ Quanto sopra è il contenuto che devi riassumere.`,
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'Un componente che cerca da duckduckgo.com, permettendo di specificare il numero di risultati di ricerca usando TopN.',
searXNG: 'SearXNG',
searXNGDescription:
'Un componente che cerca tramite lURL dellistanza SearXNG fornita. Specifica TopN e lURL dellistanza.',
pdfGenerator: 'Generatore Documenti',
pDFGenerator: 'Generatore Documenti',
pdfGeneratorDescription: `Un componente che genera documenti (PDF, DOCX, TXT) da contenuti formattati in markdown con stile personalizzabile, immagini e tabelle. Supporta: **grassetto**, *corsivo*, # titoli, - elenchi, tabelle con sintassi |.`,
pDFGeneratorDescription: `Un componente che genera documenti (PDF, DOCX, TXT) da contenuti formattati in markdown con stile personalizzabile, immagini e tabelle. Supporta: **grassetto**, *corsivo*, # titoli, - elenchi, tabelle con sintassi |.`,
subtitle: 'Sottotitolo',
logoImage: 'Immagine Logo',
logoPosition: 'Posizione Logo',
logoWidth: 'Larghezza Logo',
logoHeight: 'Altezza Logo',
fontFamily: 'Famiglia Font',
fontSize: 'Dimensione Font',
titleFontSize: 'Dimensione Font Titolo',
pageSize: 'Dimensione Pagina',
orientation: 'Orientamento',
marginTop: 'Margine Superiore',
marginBottom: 'Margine Inferiore',
filename: 'Nome File',
outputDirectory: 'Directory Output',
addPageNumbers: 'Aggiungi Numeri Pagina',
addTimestamp: 'Aggiungi Timestamp',
watermarkText: 'Testo Filigrana',
channel: 'Canale',
channelTip: `Esegui ricerca testo o notizie sull'input del componente`,
text: 'Testo',

View File

@ -795,11 +795,27 @@ export default {
searXNG: 'SearXNG',
searXNGDescription:
'SearXNGのインスタンスURLを提供して検索を行うコンポーネント。TopNとインスタンスURLを指定してください。',
channel: 'チャンネル',
channelTip: `コンポーネントの入力に対してテキスト検索またはニュース検索を実行します`,
text: 'テキスト',
news: 'ニュース',
messageHistoryWindowSize: 'メッセージウィンドウサイズ',
pdfGenerator: 'ドキュメント生成',
pDFGenerator: 'ドキュメント生成',
pdfGeneratorDescription: `マークダウン形式のコンテンツからドキュメントPDF、DOCX、TXTを生成するコンポーネント。カスタムスタイル、画像、テーブルをサポート。サポート**太字**、*斜体*、# 見出し、- リスト、| 構文のテーブル。`,
pDFGeneratorDescription: `マークダウン形式のコンテンツからドキュメントPDF、DOCX、TXTを生成するコンポーネント。カスタムスタイル、画像、テーブルをサポート。サポート**太字**、*斜体*、# 見出し、- リスト、| 構文のテーブル。`,
subtitle: 'サブタイトル',
logoImage: 'ロゴ画像',
logoPosition: 'ロゴ位置',
logoWidth: 'ロゴ幅',
logoHeight: 'ロゴ高さ',
fontFamily: 'フォントファミリー',
fontSize: 'フォントサイズ',
titleFontSize: 'タイトルフォントサイズ',
pageSize: 'ページサイズ',
orientation: '向き',
marginTop: '上余白',
marginBottom: '下余白',
filename: 'ファイル名',
outputDirectory: '出力ディレクトリ',
addPageNumbers: 'ページ番号を追加',
addTimestamp: 'タイムスタンプを追加',
watermarkText: '透かしテキスト',
messageHistoryWindowSizeTip:
'LLMに表示される会話履歴のウィンドウサイズ。大きいほど良いですが、LLMの最大トークン制限に注意してください。',
wikipedia: 'Wikipedia',

View File

@ -737,11 +737,27 @@ export default {
searXNG: 'SearXNG',
searXNGDescription:
'Um componente que realiza buscas via URL da instância SearXNG que você fornece. Especifique TopN e URL da instância.',
channel: 'Canal',
channelTip: `Realize uma busca por texto ou por notícias na entrada do componente`,
text: 'Texto',
news: 'Notícias',
messageHistoryWindowSize: 'Tamanho da janela de mensagens',
pdfGenerator: 'Gerador de Documentos',
pDFGenerator: 'Gerador de Documentos',
pdfGeneratorDescription: `Um componente que gera documentos (PDF, DOCX, TXT) de conteúdo formatado em markdown com estilo personalizável, imagens e tabelas. Suporta: **negrito**, *itálico*, # títulos, - listas, tabelas com sintaxe |.`,
pDFGeneratorDescription: `Um componente que gera documentos (PDF, DOCX, TXT) de conteúdo formatado em markdown com estilo personalizável, imagens e tabelas. Suporta: **negrito**, *itálico*, # títulos, - listas, tabelas com sintaxe |.`,
subtitle: 'Subtítulo',
logoImage: 'Imagem Logo',
logoPosition: 'Posição Logo',
logoWidth: 'Largura Logo',
logoHeight: 'Altura Logo',
fontFamily: 'Família Fonte',
fontSize: 'Tamanho Fonte',
titleFontSize: 'Tamanho Fonte Título',
pageSize: 'Tamanho Página',
orientation: 'Orientação',
marginTop: 'Margem Superior',
marginBottom: 'Margem Inferior',
filename: 'Nome Arquivo',
outputDirectory: 'Diretório Saída',
addPageNumbers: 'Adicionar Números Página',
addTimestamp: 'Adicionar Timestamp',
watermarkText: 'Texto Marca Dágua',
messageHistoryWindowSizeTip:
'O tamanho da janela do histórico de conversa visível para o LLM. Quanto maior, melhor, mas fique atento ao limite máximo de tokens do LLM.',
wikipedia: 'Wikipedia',

View File

@ -1223,6 +1223,27 @@ export default {
searXNG: 'SearXNG',
searXNGDescription:
'Компонент, который выполняет поиск через ваш предоставленный URL экземпляра SearXNG. Укажите TopN и URL экземпляра.',
pdfGenerator: 'Генератор документов',
pDFGenerator: 'Генератор документов',
pdfGeneratorDescription: `Компонент, который генерирует документы (PDF, DOCX, TXT) из содержимого в формате markdown с настраиваемым стилем, изображениями и таблицами. Поддерживает: **жирный**, *курсив*, # заголовки, - списки, таблицы с синтаксисом |.`,
pDFGeneratorDescription: `Компонент, который генерирует документы (PDF, DOCX, TXT) из содержимого в формате markdown с настраиваемым стилем, изображениями и таблицами. Поддерживает: **жирный**, *курсив*, # заголовки, - списки, таблицы с синтаксисом |.`,
subtitle: 'Подзаголовок',
logoImage: 'Изображение логотипа',
logoPosition: 'Позиция логотипа',
logoWidth: 'Ширина логотипа',
logoHeight: 'Высота логотипа',
fontFamily: 'Семейство шрифтов',
fontSize: 'Размер шрифта',
titleFontSize: 'Размер шрифта заголовка',
pageSize: 'Размер страницы',
orientation: 'Ориентация',
marginTop: 'Верхний отступ',
marginBottom: 'Нижний отступ',
filename: 'Имя файла',
outputDirectory: 'Выходной каталог',
addPageNumbers: 'Добавить номера страниц',
addTimestamp: 'Добавить временную метку',
watermarkText: 'Текст водяного знака',
channel: 'Канал',
channelTip: `Выполняет текстовый поиск или поиск новостей на входе компонента`,
text: 'Текст',
@ -1604,7 +1625,6 @@ export default {
datatype: 'MIME тип HTTP запроса',
insertVariableTip: `Введите / Вставьте переменные`,
historyversion: 'История версий',
filename: 'Имя файла',
version: {
created: 'Создано',
details: 'Детали версии',

View File

@ -821,15 +821,31 @@ export default {
baiduDescription: `Thành phần này được sử dụng để lấy kết quả tìm kiếm từ www.baidu.com. Thông thường, nó hoạt động như một phần bổ sung cho các cơ sở kiến thức. Top N chỉ định số lượng kết quả tìm kiếm bạn cần điều chỉnh.`,
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'Một thành phần truy xuất kết quả tìm kiếm t duckduckgo.com, với TopN xác định số lượng kết quả tìm kiếm. Nó bổ sung cho các cơ sở kiến thức hiện có.',
'Một thành phần tìm kiếm trên duckduckgo.com, cho phép bạn chỉ định số lượng kết quả tìm kiếm sử dụng TopN. Nó bổ sung cho các cơ sở kiến thức hiện có.',
searXNG: 'SearXNG',
searXNGDescription:
'Một thành phần truy xuất kết quả tìm kiếm từ searxng.com, với TopN xác định số lượng kết quả tìm kiếm. Nó bổ sung cho các cơ sở kiến thức hin .',
channel: 'Kênh',
channelTip: `Thực hiện tìm kiếm văn bản hoặc tìm kiếm tin tức trên đầu vào của thành phần`,
text: 'Văn bản',
news: 'Tin tức',
messageHistoryWindowSize: 'Cửa sổ lịch sử tin nhắn',
'Một thành phần tìm kiếm thông qua URL phiên bản SearXNG bạn cung cấp. Chỉ định TopN và URL phiên bản.',
pdfGenerator: 'Trình tạo Tài liệu',
pDFGenerator: 'Trình tạo Tài liệu',
pdfGeneratorDescription: `Một thành phần tạo tài liệu (PDF, DOCX, TXT) từ nội dung định dạng markdown với kiểu tùy chỉnh, hình ảnh và bảng. Hỗ trợ: **in đậm**, *in nghiêng*, # tiêu đề, - danh sách, bảng với cú pháp |.`,
pDFGeneratorDescription: `Một thành phần tạo tài liệu (PDF, DOCX, TXT) từ nội dung định dạng markdown với kiểu tùy chỉnh, hình ảnh và bảng. Hỗ trợ: **in đậm**, *in nghiêng*, # tiêu đề, - danh sách, bảng với cú pháp |.`,
subtitle: 'Phụ đề',
logoImage: 'Hình ảnh Logo',
logoPosition: 'Vị trí Logo',
logoWidth: 'Chiều rộng Logo',
logoHeight: 'Chiều cao Logo',
fontFamily: 'Họ phông chữ',
fontSize: 'Kích thước phông chữ',
titleFontSize: 'Kích thước phông chữ tiêu đề',
pageSize: 'Kích thước trang',
orientation: 'Hướng',
marginTop: 'Lề trên',
marginBottom: 'Lề dưới',
filename: 'Tên tệp',
outputDirectory: 'Thư mục đầu ra',
addPageNumbers: 'Thêm số trang',
addTimestamp: 'Thêm dấu thời gian',
watermarkText: 'Văn bản watermark',
messageHistoryWindowSizeTip:
'Kích thước cửa sổ lịch sử cuộc trò chuyện hiển thị với LLM. Càng lớn càng tốt, nhưng hãy chú ý đến giới hạn tối đa số token của LLM.',
wikipedia: 'Wikipedia',

View File

@ -849,15 +849,31 @@ export default {
baiduDescription: `此組件用於取得www.baidu.com的搜尋結果一般作為知識庫的補充Top N指定需要採納的搜尋結果數。`,
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'此件用於從 www.duckduckgo.com 取得搜尋結果通常,它作為知識庫的補充。 Top N 指定您需要採用的搜尋結果。',
'此件用於從 www.duckduckgo.com 取得搜尋結果通常充當知識庫的補充。Top N 指定搜尋結果的數量。',
searXNG: 'SearXNG',
searXNGDescription:
'組件過您提供的 SearXNG 實例地址進行搜。請設 Top N 和實例 URL。',
channel: '頻道',
channelTip: '針對該組件的輸入進行文字搜尋或新聞搜索',
text: '文字',
news: '新聞',
messageHistoryWindowSize: '歷史訊息視窗大小',
'組件過您提供的 SearXNG 實例 URL 進行搜。請設 Top N 和實例 URL。',
pdfGenerator: '文檔生成器',
pPDFGenerator: '文檔生成器',
pdfGeneratorDescription: `該組件從 markdown 格式的內容生成文檔PDF、DOCX、TXT支援自定義樣式、圖片和表格。支援**粗體**、*斜體*、# 標題、- 列表、使用 | 語法的表格。`,
pPDFGeneratorDescription: `該組件從 markdown 格式的內容生成文檔PDF、DOCX、TXT支援自定義樣式、圖片和表格。支援**粗體**、*斜體*、# 標題、- 列表、使用 | 語法的表格。`,
subtitle: '副標題',
logoImage: '標誌圖片',
logoPosition: '標誌位置',
logoWidth: '標誌寬度',
logoHeight: '標誌高度',
fontFamily: '字體系列',
fontSize: '字體大小',
titleFontSize: '標題字體大小',
pageSize: '頁面大小',
orientation: '方向',
marginTop: '上邊距',
marginBottom: '下邊距',
filename: '檔名',
outputDirectory: '輸出目錄',
addPageNumbers: '添加頁碼',
addTimestamp: '添加時間戳',
watermarkText: '浮水印文字',
messageHistoryWindowSizeTip:
'LLM 需要查看的對話歷史視窗大小。越大越好,但要注意 LLM 的最大 Token 數。',
wikipedia: '維基百科',

View File

@ -1187,6 +1187,27 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
searXNG: 'SearXNG',
searXNGDescription:
'该组件通过您提供的 SearXNG 实例地址进行搜索。请设置 Top N 和实例 URL。',
pdfGenerator: '文档生成器',
pDFGenerator: '文档生成器',
pdfGeneratorDescription: `该组件从 markdown 格式的内容生成文档PDF、DOCX、TXT支持自定义样式、图片和表格。支持**粗体**、*斜体*、# 标题、- 列表、使用 | 语法的表格。`,
pDFGeneratorDescription: `该组件从 markdown 格式的内容生成文档PDF、DOCX、TXT支持自定义样式、图片和表格。支持**粗体**、*斜体*、# 标题、- 列表、使用 | 语法的表格。`,
subtitle: '副标题',
logoImage: '标志图片',
logoPosition: '标志位置',
logoWidth: '标志宽度',
logoHeight: '标志高度',
fontFamily: '字体系列',
fontSize: '字体大小',
titleFontSize: '标题字体大小',
pageSize: '页面大小',
orientation: '方向',
marginTop: '上边距',
marginBottom: '下边距',
filename: '文件名',
outputDirectory: '输出目录',
addPageNumbers: '添加页码',
addTimestamp: '添加时间戳',
watermarkText: '水印文本',
channel: '频道',
channelTip: '针对该组件的输入进行文本搜索或新闻搜索',
text: '文本',

View File

@ -122,6 +122,7 @@ export function AccordionOperators({
Operator.Invoke,
Operator.WenCai,
Operator.SearXNG,
Operator.PDFGenerator,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}

View File

@ -932,6 +932,71 @@ export enum AgentVariableType {
Conversation = 'conversation',
}
// PDF Generator enums
export enum PDFGeneratorFontFamily {
Helvetica = 'Helvetica',
TimesRoman = 'Times-Roman',
Courier = 'Courier',
HelveticaBold = 'Helvetica-Bold',
TimesBold = 'Times-Bold',
}
export enum PDFGeneratorLogoPosition {
Left = 'left',
Center = 'center',
Right = 'right',
}
export enum PDFGeneratorPageSize {
A4 = 'A4',
Letter = 'Letter',
}
export enum PDFGeneratorOrientation {
Portrait = 'portrait',
Landscape = 'landscape',
}
export const initialPDFGeneratorValues = {
output_format: 'pdf',
content: '',
title: '',
subtitle: '',
header_text: '',
footer_text: '',
logo_image: '',
logo_position: PDFGeneratorLogoPosition.Left,
logo_width: 2.0,
logo_height: 1.0,
font_family: PDFGeneratorFontFamily.Helvetica,
font_size: 12,
title_font_size: 24,
heading1_font_size: 18,
heading2_font_size: 16,
heading3_font_size: 14,
text_color: '#000000',
title_color: '#000000',
page_size: PDFGeneratorPageSize.A4,
orientation: PDFGeneratorOrientation.Portrait,
margin_top: 1.0,
margin_bottom: 1.0,
margin_left: 1.0,
margin_right: 1.0,
line_spacing: 1.2,
filename: '',
output_directory: '/tmp/pdf_outputs',
add_page_numbers: true,
add_timestamp: true,
watermark_text: '',
enable_toc: false,
outputs: {
file_path: { type: 'string', value: '' },
pdf_base64: { type: 'string', value: '' },
download: { type: 'string', value: '' },
success: { type: 'boolean', value: false },
},
};
export enum WebhookMethod {
Post = 'POST',
Get = 'GET',

View File

@ -22,6 +22,7 @@ import ListOperationsForm from '../form/list-operations-form';
import LoopForm from '../form/loop-form';
import MessageForm from '../form/message-form';
import ParserForm from '../form/parser-form';
import PDFGeneratorForm from '../form/pdf-generator-form';
import PubMedForm from '../form/pubmed-form';
import RetrievalForm from '../form/retrieval-form/next';
import RewriteQuestionForm from '../form/rewrite-question-form';
@ -110,6 +111,9 @@ export const FormConfigMap = {
[Operator.SearXNG]: {
component: SearXNGForm,
},
[Operator.PDFGenerator]: {
component: PDFGeneratorForm,
},
[Operator.Note]: {
component: () => <></>,
},

View File

@ -0,0 +1,535 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { memo, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
PDFGeneratorFontFamily,
PDFGeneratorLogoPosition,
PDFGeneratorOrientation,
PDFGeneratorPageSize,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output, transferOutputs } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
function PDFGeneratorForm({ node }: INextOperatorForm) {
const values = useValues(node);
const FormSchema = z.object({
output_format: z.string().default('pdf'),
content: z.string().min(1, 'Content is required'),
title: z.string().optional(),
subtitle: z.string().optional(),
header_text: z.string().optional(),
footer_text: z.string().optional(),
logo_image: z.string().optional(),
logo_position: z.string(),
logo_width: z.number(),
logo_height: z.number(),
font_family: z.string(),
font_size: z.number(),
title_font_size: z.number(),
heading1_font_size: z.number(),
heading2_font_size: z.number(),
heading3_font_size: z.number(),
text_color: z.string(),
title_color: z.string(),
page_size: z.string(),
orientation: z.string(),
margin_top: z.number(),
margin_bottom: z.number(),
margin_left: z.number(),
margin_right: z.number(),
line_spacing: z.number(),
filename: z.string().optional(),
output_directory: z.string(),
add_page_numbers: z.boolean(),
add_timestamp: z.boolean(),
watermark_text: z.string().optional(),
enable_toc: z.boolean(),
outputs: z
.object({
file_path: z.object({ type: z.string() }),
pdf_base64: z.object({ type: z.string() }),
success: z.object({ type: z.string() }),
})
.optional(),
});
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const outputList = useMemo(() => {
return transferOutputs(values.outputs);
}, [values.outputs]);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
{/* Output Format Selection */}
<FormField
control={form.control}
name="output_format"
render={({ field }) => (
<FormItem>
<FormLabel>Output Format</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={[
{ label: 'PDF', value: 'pdf' },
{ label: 'DOCX', value: 'docx' },
{ label: 'TXT', value: 'txt' },
]}
></RAGFlowSelect>
</FormControl>
<FormDescription>
Choose the output document format
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Content Section */}
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.content')}</FormLabel>
<FormControl>
<PromptEditor
{...field}
showToolbar={true}
placeholder="Enter content with markdown formatting...&#10;&#10;**Bold text**, *italic*, # Heading, - List items, etc."
></PromptEditor>
</FormControl>
<FormDescription>
<div className="text-xs space-y-1">
<div>
<strong>Markdown support:</strong> **bold**, *italic*,
`code`, # Heading 1, ## Heading 2
</div>
<div>
<strong>Lists:</strong> - bullet or 1. numbered
</div>
<div>
<strong>Tables:</strong> | Column 1 | Column 2 | (use | to
separate columns, &lt;br&gt; or \n for line breaks in
cells)
</div>
<div>
<strong>Other:</strong> --- for horizontal line, ``` for
code blocks
</div>
</div>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Title & Subtitle */}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.title')}</FormLabel>
<FormControl>
<Input {...field} placeholder="Document title (optional)" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subtitle"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.subtitle')}</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Document subtitle (optional)"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Logo Settings */}
<FormField
control={form.control}
name="logo_image"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.logoImage')}</FormLabel>
<FormControl>
<div className="space-y-2">
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
field.onChange(reader.result as string);
};
reader.readAsDataURL(file);
}
}}
className="cursor-pointer"
/>
<Input
{...field}
placeholder="Or paste image path/URL/base64"
className="mt-2"
/>
</div>
</FormControl>
<FormDescription>
Upload an image file or paste a file path/URL/base64
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo_position"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.logoPosition')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={Object.values(PDFGeneratorLogoPosition).map(
(val) => ({ label: val, value: val }),
)}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="logo_width"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.logoWidth')} (inches)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
step="0.1"
onChange={(e) =>
field.onChange(parseFloat(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo_height"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.logoHeight')} (inches)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
step="0.1"
onChange={(e) =>
field.onChange(parseFloat(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Font Settings */}
<FormField
control={form.control}
name="font_family"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.fontFamily')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={Object.values(PDFGeneratorFontFamily).map(
(val) => ({ label: val, value: val }),
)}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="font_size"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.fontSize')}</FormLabel>
<FormControl>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(parseInt(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title_font_size"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.titleFontSize')}</FormLabel>
<FormControl>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(parseInt(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Page Settings */}
<FormField
control={form.control}
name="page_size"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.pageSize')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={Object.values(PDFGeneratorPageSize).map((val) => ({
label: val,
value: val,
}))}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="orientation"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.orientation')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={Object.values(PDFGeneratorOrientation).map(
(val) => ({ label: val, value: val }),
)}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Margins */}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="margin_top"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.marginTop')} (inches)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
step="0.1"
onChange={(e) =>
field.onChange(parseFloat(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="margin_bottom"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.marginBottom')} (inches)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
step="0.1"
onChange={(e) =>
field.onChange(parseFloat(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Output Settings */}
<FormField
control={form.control}
name="filename"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.filename')}</FormLabel>
<FormControl>
<Input
{...field}
placeholder="document.pdf (auto-generated if empty)"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="output_directory"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.outputDirectory')}</FormLabel>
<FormControl>
<Input {...field} placeholder="/tmp/pdf_outputs" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Additional Options */}
<FormField
control={form.control}
name="add_page_numbers"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>{t('flow.addPageNumbers')}</FormLabel>
<FormDescription>
Add page numbers to the document
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="add_timestamp"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>{t('flow.addTimestamp')}</FormLabel>
<FormDescription>
Add generation timestamp to the document
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="watermark_text"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.watermarkText')}</FormLabel>
<FormControl>
<Input {...field} placeholder="Watermark text (optional)" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="outputs"
render={() => <div></div>}
/>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(PDFGeneratorForm);

View File

@ -0,0 +1,11 @@
import { useMemo } from 'react';
import { Node } from 'reactflow';
import { initialPDFGeneratorValues } from '../../constant';
export const useValues = (node?: Node) => {
const values = useMemo(() => {
return node?.data.form ?? initialPDFGeneratorValues;
}, [node?.data.form]);
return values;
};

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { UseFormReturn } from 'react-hook-form';
import useGraphStore from '../../store';
export const useWatchFormChange = (
nodeId: string | undefined,
form: UseFormReturn<any>,
) => {
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
const { unsubscribe } = form.watch((value) => {
if (nodeId) {
updateNodeForm(nodeId, value);
}
});
return () => unsubscribe();
}, [form, nodeId, updateNodeForm]);
};

View File

@ -16,6 +16,7 @@ import { IconFontFill } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import {
FileCode,
FileText,
HousePlus,
Infinity as InfinityIcon,
LogOut,
@ -67,6 +68,7 @@ export const LucideIconMap = {
[Operator.DataOperations]: FileCode,
[Operator.Loop]: InfinityIcon,
[Operator.ExitLoop]: LogOut,
[Operator.PDFGenerator]: FileText,
};
const Empty = () => {

View File

@ -7,11 +7,15 @@ export function useSelectFilters() {
const { data } = useFetchAgentList({});
const canvasCategory = useMemo(() => {
return groupListByType(data.canvas, 'canvas_category', 'canvas_category');
}, [data.canvas]);
return groupListByType(
data?.canvas ?? [],
'canvas_category',
'canvas_category',
);
}, [data?.canvas]);
const filters: FilterCollection[] = [
buildOwnersFilter(data.canvas),
buildOwnersFilter(data?.canvas ?? []),
{
field: 'canvasCategory',
list: canvasCategory,