feat: 添加信息图插件,并更新相关插件模板和开发文档。
This commit is contained in:
@@ -117,6 +117,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
# Add other configuration fields as needed
|
||||
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
|
||||
|
||||
@@ -175,6 +179,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -278,10 +297,30 @@ class Action:
|
||||
|
||||
# 2. Input Validation
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
return body # Or handle error
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body # Or handle error
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
warning_msg = f"Text too short ({len(original_content)} chars). Minimum required: {self.valves.MIN_TEXT_LENGTH}."
|
||||
|
||||
@@ -117,6 +117,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
# 根据需要添加其他配置字段
|
||||
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
|
||||
|
||||
@@ -186,6 +190,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式。"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -289,10 +308,30 @@ class Action:
|
||||
|
||||
# 2. 输入验证
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
return body # 或者处理错误
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# 根据 MESSAGE_COUNT 获取最近 N 条消息
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# 聚合选中消息的内容,带标签
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body # 或者处理错误
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
warning_msg = f"文本过短 ({len(original_content)} 字符)。最少需要: {self.valves.MIN_TEXT_LENGTH}。"
|
||||
|
||||
67
plugins/actions/infographic/README.md
Normal file
67
plugins/actions/infographic/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 信息图 - OpenWebUI Action 插件
|
||||
|
||||
将文本内容智能转换为美观的信息图,基于蚂蚁集团 AntV Infographic 引擎。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🤖 **AI 驱动**: 使用 LLM 自动分析文本内容并生成信息图语法
|
||||
- 📊 **多种模板**: 支持列表、流程、层级等多种信息图类型
|
||||
- 🎨 **自动图标**: 使用 `ref:search` 语法自动匹配高质量图标
|
||||
- 💾 **多格式导出**: 支持下载 SVG、PNG 和独立 HTML 文件
|
||||
- 🎯 **零配置**: 开箱即用,无需额外设置
|
||||
|
||||
## 安装
|
||||
|
||||
1. 将 `信息图.py` 文件复制到 Open WebUI 的插件目录:
|
||||
```
|
||||
plugins/actions/infographic/
|
||||
```
|
||||
|
||||
2. 重启 Open WebUI 或在管理界面重新加载插件
|
||||
|
||||
3. 在聊天界面的 Action 菜单中即可看到 "信息图" 选项
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 在聊天框输入需要可视化的文本内容(建议 100 字符以上)
|
||||
2. 点击 "信息图" Action 按钮
|
||||
3. AI 将自动分析文本并生成信息图
|
||||
4. 可以下载 SVG、PNG 或 HTML 格式的文件
|
||||
|
||||
### 示例文本
|
||||
|
||||
```
|
||||
我们的产品开发流程包括三个主要阶段:
|
||||
1. 需求分析 - 收集和分析用户需求,确定产品方向
|
||||
2. 设计开发 - 完成 UI/UX 设计和前后端开发
|
||||
3. 测试上线 - 进行质量验证并正式发布
|
||||
```
|
||||
|
||||
## 配置选项(Valves)
|
||||
|
||||
- **SHOW_STATUS**: 是否显示操作状态更新(默认: True)
|
||||
- **MODEL_ID**: 用于分析的 LLM 模型 ID(默认: 使用当前对话模型)
|
||||
- **MIN_TEXT_LENGTH**: 最小文本长度要求(默认: 100 字符)
|
||||
- **CLEAR_PREVIOUS_HTML**: 是否清除之前的插件输出(默认: False)
|
||||
|
||||
## 支持的信息图类型
|
||||
|
||||
插件会根据文本内容自动选择最合适的模板:
|
||||
|
||||
- **列表型**: `list-row-horizontal-icon-arrow`, `list-grid`
|
||||
- **层级型**: `tree-vertical`, `tree-horizontal`
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Python, OpenWebUI Action API
|
||||
- **前端**: AntV Infographic (CDN)
|
||||
- **AI**: 自定义提示词工程
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 致谢
|
||||
|
||||
- [AntV Infographic](https://infographic.antv.vision/) - 信息图渲染引擎
|
||||
- [Open WebUI](https://github.com/open-webui/open-webui) - AI 聊天界面
|
||||
43
plugins/actions/infographic/README_CN.md
Normal file
43
plugins/actions/infographic/README_CN.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AntV Infographic 智能信息图插件
|
||||
|
||||
将文本内容一键转换为精美的信息图。支持列表、层级、流程、关系、对比、分析、图表等多种可视化形式。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **智能分析**: 自动识别文本结构,选择最合适的图表模板。
|
||||
- **丰富模板**: 支持 20+ 种 AntV 信息图模板,涵盖列表、树图、思维导图、流程图、桑基图、SWOT、象限图、柱状图、饼图等。
|
||||
- **自动配图**: 智能搜索并匹配合适的图标。
|
||||
- **多格式导出**: 支持导出为 SVG, PNG, HTML 格式。
|
||||
- **多语言支持**: 输出语言跟随用户设定。
|
||||
|
||||
## 使用方法
|
||||
|
||||
在 Open WebUI 聊天框中,直接输入文本或上传文档,然后启用该插件。插件会自动分析内容并生成信息图。
|
||||
|
||||
### 支持的图表类型
|
||||
|
||||
#### 1. 列表与层级
|
||||
- **列表**: 网格卡片 (`list-grid`), 垂直列表 (`list-vertical`)
|
||||
- **树图**: 垂直树 (`tree-vertical`), 水平树 (`tree-horizontal`)
|
||||
- **思维导图**: `mindmap`
|
||||
|
||||
#### 2. 顺序与关系
|
||||
- **流程**: 路线图 (`sequence-roadmap`), 之字形流程 (`sequence-zigzag`), 水平流程 (`sequence-horizontal`)
|
||||
- **关系**: 桑基图 (`relation-sankey`), 循环关系 (`relation-circle`)
|
||||
|
||||
#### 3. 对比与分析
|
||||
- **对比**: 二元对比 (`compare-binary`), 对比表 (`compare-table`)
|
||||
- **分析**: SWOT 分析 (`compare-swot`), 象限图 (`quadrant-quarter`)
|
||||
|
||||
#### 4. 图表与数据
|
||||
- **统计**: 统计卡片 (`statistic-card`)
|
||||
- **图表**: 柱状图 (`chart-bar`), 条形图 (`chart-column`), 折线图 (`chart-line`), 饼图 (`chart-pie`), 环形图 (`chart-doughnut`)
|
||||
|
||||
## 安装
|
||||
|
||||
将 `infographic.py` (英文版) 或 `信息图.py` (中文版) 放入 Open WebUI 的插件目录即可。
|
||||
|
||||
## 依赖
|
||||
|
||||
- 插件依赖 `@antv/infographic` 库 (通过 CDN 加载)。
|
||||
- 需要联网权限以加载 CDN 资源和图标。
|
||||
43
plugins/actions/infographic/README_EN.md
Normal file
43
plugins/actions/infographic/README_EN.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AntV Infographic Plugin
|
||||
|
||||
Transform text content into beautiful infographics with a single click. Supports lists, hierarchies, processes, relationships, comparisons, analysis, charts, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Analysis**: Automatically identifies text structure and selects the best template.
|
||||
- **Rich Templates**: Supports 20+ AntV infographic templates, including lists, trees, mind maps, roadmaps, Sankey diagrams, SWOT, quadrant charts, bar charts, pie charts, etc.
|
||||
- **Auto Icons**: Intelligently searches and matches appropriate icons.
|
||||
- **Multi-format Export**: Export as SVG, PNG, or HTML.
|
||||
- **Multi-language**: Output language follows user settings.
|
||||
|
||||
## Usage
|
||||
|
||||
In the Open WebUI chat interface, simply input text or upload a document, then enable this plugin. The plugin will analyze the content and generate an infographic.
|
||||
|
||||
### Supported Chart Types
|
||||
|
||||
#### 1. List & Hierarchy
|
||||
- **List**: Grid Cards (`list-grid`), Vertical List (`list-vertical`)
|
||||
- **Tree**: Vertical Tree (`tree-vertical`), Horizontal Tree (`tree-horizontal`)
|
||||
- **Mindmap**: `mindmap`
|
||||
|
||||
#### 2. Sequence & Relationship
|
||||
- **Process**: Roadmap (`sequence-roadmap`), Zigzag Process (`sequence-zigzag`), Horizontal Process (`sequence-horizontal`)
|
||||
- **Relationship**: Sankey Diagram (`relation-sankey`), Circular Relationship (`relation-circle`)
|
||||
|
||||
#### 3. Comparison & Analysis
|
||||
- **Comparison**: Binary Comparison (`compare-binary`), Comparison Table (`compare-table`)
|
||||
- **Analysis**: SWOT Analysis (`compare-swot`), Quadrant Chart (`quadrant-quarter`)
|
||||
|
||||
#### 4. Charts & Data
|
||||
- **Statistics**: Statistic Cards (`statistic-card`)
|
||||
- **Charts**: Bar Chart (`chart-bar`), Column Chart (`chart-column`), Line Chart (`chart-line`), Pie Chart (`chart-pie`), Doughnut Chart (`chart-doughnut`)
|
||||
|
||||
## Installation
|
||||
|
||||
Place `infographic.py` (English version) or `信息图.py` (Chinese version) into your Open WebUI plugins directory.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `@antv/infographic` library (loaded via CDN).
|
||||
- Requires internet access to load CDN resources and icons.
|
||||
277
plugins/actions/infographic/debug_card_spacing.html
Normal file
277
plugins/actions/infographic/debug_card_spacing.html
Normal file
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>卡片间距调试工具</title>
|
||||
<script src="https://registry.npmmirror.com/@antv/infographic/0.2.1/files/dist/infographic.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.debug-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 320px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: block;
|
||||
margin: 16px 0 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.controls .info {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#infographic-container {
|
||||
min-height: 500px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 基础样式 */
|
||||
#infographic-container svg text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
/* 主标题样式 */
|
||||
#infographic-container svg foreignObject[data-element-type="title"]>* {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* 页面副标题样式 */
|
||||
#infographic-container svg foreignObject[data-element-type="desc"]>* {
|
||||
font-size: 0.6em !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* 卡片标题和描述样式(这里是重点调试区域) */
|
||||
#infographic-container svg foreignObject[data-element-type="item-label"]>* {
|
||||
font-size: 0.6em !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject[data-element-type="item-desc"]>* {
|
||||
line-height: 1.4 !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="debug-container">
|
||||
<div class="controls">
|
||||
<h2>🔧 卡片间距调试面板</h2>
|
||||
|
||||
<label>卡片标题与描述间距 (margin-top)</label>
|
||||
<input type="text" id="label-desc-gap" value="8px" placeholder="例如: 8px, 12px, 1em">
|
||||
|
||||
<label>卡片标题字体大小</label>
|
||||
<input type="text" id="label-size" value="0.6em" placeholder="例如: 0.6em, 12px">
|
||||
|
||||
<label>卡片描述字体大小</label>
|
||||
<input type="text" id="desc-size" value="0.5em" placeholder="例如: 0.5em, 10px">
|
||||
|
||||
<label>卡片内边距 (padding)</label>
|
||||
<input type="text" id="card-padding" value="8px" placeholder="例如: 8px, 12px">
|
||||
|
||||
<button onclick="applyStyles()">应用样式</button>
|
||||
<button onclick="reRender()" style="background: #10b981; margin-top: 8px;">重新渲染</button>
|
||||
|
||||
<div class="info">
|
||||
<strong>说明:</strong><br>
|
||||
• 标题与描述间距:控制标题和内容之间的垂直间距<br>
|
||||
• 字体大小:分别控制标题和描述的文字大小<br>
|
||||
• 内边距:控制整个卡片内的留白
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<div id="infographic-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyStyles() {
|
||||
console.log('[Debug] 应用卡片样式...');
|
||||
|
||||
const labelDescGap = document.getElementById('label-desc-gap').value;
|
||||
const labelSize = document.getElementById('label-size').value;
|
||||
const descSize = document.getElementById('desc-size').value;
|
||||
const cardPadding = document.getElementById('card-padding').value;
|
||||
|
||||
const container = document.getElementById('infographic-container');
|
||||
|
||||
// 卡片标题
|
||||
const itemLabels = container.querySelectorAll('svg foreignObject[data-element-type="item-label"]');
|
||||
itemLabels.forEach(fo => {
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
if (firstChild) {
|
||||
firstChild.style.setProperty('font-size', labelSize, 'important');
|
||||
firstChild.style.setProperty('margin-bottom', labelDescGap, 'important');
|
||||
}
|
||||
});
|
||||
|
||||
// 卡片描述
|
||||
const itemDescs = container.querySelectorAll('svg foreignObject[data-element-type="item-desc"]');
|
||||
itemDescs.forEach(fo => {
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
if (firstChild) {
|
||||
firstChild.style.setProperty('font-size', descSize, 'important');
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试调整卡片整体内边距(通过调整 g 元素的 padding)
|
||||
const cardGroups = container.querySelectorAll('svg g[data-element-type="items-group"] > g');
|
||||
cardGroups.forEach(group => {
|
||||
// 这里可能需要通过调整内部 g 的 transform 来实现
|
||||
// AntV 的布局比较复杂,padding 可能需要通过其他方式实现
|
||||
});
|
||||
|
||||
console.log('[Debug] 卡片样式已应用:', {
|
||||
labelDescGap,
|
||||
labelSize,
|
||||
descSize,
|
||||
cardPadding
|
||||
});
|
||||
}
|
||||
|
||||
function reRender() {
|
||||
const container = document.getElementById('infographic-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
const syntaxContent = `infographic list-grid-compact-card
|
||||
data
|
||||
title 🚀 功能亮点说明
|
||||
desc 系统核心逻辑与交互视觉特性
|
||||
items
|
||||
- label 核心流程演示
|
||||
icon mdi/database-cog
|
||||
desc 包含4个数据源、4条分流管道(运费/商品/退货)以及UNION ALL与GROUP BY聚合节点
|
||||
- label 灵动动画效果
|
||||
icon mdi/motion-play
|
||||
desc 支持5个订单多色并行流转、管道霓虹闪烁光晕、节点旋转及仪表盘数值跳动反馈
|
||||
- label 极简交互控制
|
||||
icon mdi/cursor-default-click
|
||||
desc 内置开始/暂停/重置逻辑,支持1x至4x倍速调整,并集成实时仪表板与日志系统
|
||||
- label 赛博朋克风格
|
||||
icon mdi/palette
|
||||
desc 采用深蓝紫色调、霓虹绿文字影、网格背景以及渐变发光边框,打造极客视觉体验`;
|
||||
|
||||
if (typeof AntVInfographic === 'undefined') {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ AntV Infographic 库未加载</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container',
|
||||
padding: 24,
|
||||
});
|
||||
instance.render(syntaxContent);
|
||||
console.log('[Debug] 渲染完成');
|
||||
|
||||
// 渲染完成后自动应用样式
|
||||
setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ 渲染错误: ' + error.message + '</div>';
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后自动渲染
|
||||
window.onload = function () {
|
||||
setTimeout(reRender, 500);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
342
plugins/actions/infographic/debug_styles.html
Normal file
342
plugins/actions/infographic/debug_styles.html
Normal file
@@ -0,0 +1,342 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>信息图样式调试</title>
|
||||
<script src="https://registry.npmmirror.com/@antv/infographic/0.2.1/files/dist/infographic.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.debug-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: block;
|
||||
margin: 12px 0 4px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.controls input,
|
||||
.controls select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#infographic-container {
|
||||
min-height: 500px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* ========== 可调试的样式 ========== */
|
||||
/* 这些样式可以直接在这里修改,刷新页面即可看到效果 */
|
||||
|
||||
/* 基础字体 */
|
||||
#infographic-container svg text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
/* 标题样式 - 根据需要调整 */
|
||||
#infographic-container svg foreignObject h1,
|
||||
#infographic-container svg foreignObject [class*="title"] {
|
||||
font-size: 0.8em !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject h2,
|
||||
#infographic-container svg foreignObject [class*="subtitle"] {
|
||||
font-size: 0.85em !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* ========== 调试样式结束 ========== */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="debug-container">
|
||||
<div class="controls">
|
||||
<h2>🔧 样式调试面板</h2>
|
||||
|
||||
<label>一级标题字体大小</label>
|
||||
<input type="text" id="h1-size" value="1.2em" placeholder="例如: 1.2em, 20px">
|
||||
|
||||
<label>二级标题字体大小</label>
|
||||
<input type="text" id="h2-size" value="0.6em" placeholder="例如: 0.6em, 12px">
|
||||
|
||||
<label>标题换行设置</label>
|
||||
<select id="title-wrap">
|
||||
<option value="nowrap">不换行 (nowrap)</option>
|
||||
<option value="normal">自动换行 (normal)</option>
|
||||
</select>
|
||||
|
||||
<label>行高</label>
|
||||
<input type="text" id="line-height" value="1.4" placeholder="例如: 1.4, 1.6">
|
||||
|
||||
<button onclick="applyStyles()">应用样式</button>
|
||||
<button onclick="reRender()" style="background: #10b981; margin-top: 8px;">重新渲染</button>
|
||||
|
||||
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
||||
|
||||
<label>自定义语法内容</label>
|
||||
<textarea id="syntax-input" rows="10" style="width: 100%; font-size: 12px; font-family: monospace;">infographic list-grid
|
||||
data
|
||||
title 2025职业足球运动员成功要素解析
|
||||
desc 在高度竞争的现代足球体系下,从天赋到职业的进化路径
|
||||
items
|
||||
- label 核心入场券:天赋与身体
|
||||
desc 速度、爆发力及极佳的球感是底线。基本功必须化为肌肉记忆。
|
||||
icon mdi/lightning-bolt
|
||||
- label 决定上限:极度自律
|
||||
desc 包含严苛的饮食、睡眠管理及强大的抗压心态。
|
||||
icon mdi/shield-star
|
||||
- label 现代智慧:球商 (IQ)
|
||||
desc 复杂的战术理解力与阅读比赛能力。
|
||||
icon mdi/brain
|
||||
- label 隐形推手:机遇与平台
|
||||
desc 专业的青训体系、能发掘你的伯乐。
|
||||
icon mdi/star-shooting</textarea>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<div id="infographic-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyStyles() {
|
||||
console.log('[Debug] 应用样式到已渲染的元素...');
|
||||
|
||||
const h1Size = document.getElementById('h1-size').value;
|
||||
const h2Size = document.getElementById('h2-size').value;
|
||||
const titleWrap = document.getElementById('title-wrap').value;
|
||||
const lineHeight = document.getElementById('line-height').value;
|
||||
|
||||
const container = document.getElementById('infographic-container');
|
||||
|
||||
// 直接操作 SVG 内的所有文本元素
|
||||
const allTexts = container.querySelectorAll('svg text');
|
||||
allTexts.forEach(text => {
|
||||
text.style.setProperty('font-size', h1Size, 'important');
|
||||
});
|
||||
|
||||
// 操作 foreignObject 内的所有元素
|
||||
const foreignObjects = container.querySelectorAll('svg foreignObject');
|
||||
let mainTitleCount = 0;
|
||||
let itemLabelCount = 0;
|
||||
let itemDescCount = 0;
|
||||
let otherCount = 0;
|
||||
|
||||
foreignObjects.forEach(fo => {
|
||||
// 检查 foreignObject 自身的 data-element-type 属性
|
||||
const elementType = fo.getAttribute('data-element-type');
|
||||
|
||||
// 设置 foreignObject 本身的行高
|
||||
fo.style.setProperty('line-height', lineHeight, 'important');
|
||||
|
||||
// 获取内部的第一个元素(通常是 span)
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
|
||||
if (!firstChild) return;
|
||||
|
||||
// 根据 foreignObject 的类型设置样式
|
||||
if (elementType === 'title') {
|
||||
// 主标题
|
||||
mainTitleCount++;
|
||||
firstChild.style.setProperty('font-size', h1Size, 'important');
|
||||
firstChild.style.setProperty('font-weight', 'bold', 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'desc') {
|
||||
// 页面副标题(主标题下方的描述)
|
||||
itemLabelCount++; // 归入副标题计数
|
||||
firstChild.style.setProperty('font-size', h2Size, 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'item-label') {
|
||||
// 卡片标题
|
||||
itemLabelCount++;
|
||||
firstChild.style.setProperty('font-size', h2Size, 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'item-desc') {
|
||||
// 卡片描述
|
||||
itemDescCount++;
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
} else {
|
||||
// 其他元素
|
||||
otherCount++;
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Debug] foreignObject 处理:', '主标题:', mainTitleCount, '卡片标题:', itemLabelCount, '卡片描述:', itemDescCount, '其他:', otherCount);
|
||||
|
||||
console.log('[Debug] 样式已应用到', allTexts.length, '个文本元素和', foreignObjects.length, '个 foreignObject');
|
||||
}
|
||||
|
||||
function reRender() {
|
||||
const container = document.getElementById('infographic-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
let syntaxContent = document.getElementById('syntax-input').value.trim();
|
||||
|
||||
if (typeof AntVInfographic === 'undefined') {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ AntV Infographic 库未加载</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用模板映射(与插件保持一致)
|
||||
const TEMPLATE_MAPPING = {
|
||||
'list-grid': 'list-grid-compact-card',
|
||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
||||
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
|
||||
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
|
||||
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
||||
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
|
||||
'relation-sankey': 'relation-sankey-simple',
|
||||
'relation-circle': 'relation-circle-icon-badge',
|
||||
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
||||
'compare-swot': 'compare-swot',
|
||||
'compare-table': 'compare-table-simple',
|
||||
'quadrant-quarter': 'quadrant-quarter-simple-card',
|
||||
'statistic-card': 'list-grid-compact-card',
|
||||
'chart-bar': 'chart-bar-plain-text',
|
||||
'chart-column': 'chart-column-simple',
|
||||
'chart-line': 'chart-line-plain-text',
|
||||
'chart-area': 'chart-area-simple',
|
||||
'chart-pie': 'chart-pie-plain-text',
|
||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {
|
||||
const regex = new RegExp(`infographic\\s+${key}(?=\\s|$)`, 'i');
|
||||
if (regex.test(syntaxContent)) {
|
||||
console.log(`[Debug] 模板映射: ${key} -> ${value}`);
|
||||
syntaxContent = syntaxContent.replace(regex, `infographic ${value}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container',
|
||||
padding: 24,
|
||||
});
|
||||
instance.render(syntaxContent);
|
||||
console.log('[Debug] 渲染完成');
|
||||
|
||||
// 渲染完成后应用样式
|
||||
setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ 渲染错误: ' + error.message + '</div>';
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后自动渲染
|
||||
window.onload = function () {
|
||||
setTimeout(reRender, 500);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1098
plugins/actions/infographic/infographic.py
Normal file
1098
plugins/actions/infographic/infographic.py
Normal file
File diff suppressed because one or more lines are too long
194
plugins/actions/infographic/infographic_templates.json
Normal file
194
plugins/actions/infographic/infographic_templates.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"templates": [
|
||||
"chart-bar-basic-bar",
|
||||
"chart-bar-basic-column",
|
||||
"chart-bar-grouped-bar",
|
||||
"chart-bar-grouped-column",
|
||||
"chart-bar-percent-stacked-bar",
|
||||
"chart-bar-percent-stacked-column",
|
||||
"chart-bar-stacked-bar",
|
||||
"chart-bar-stacked-column",
|
||||
"chart-line-basic-line",
|
||||
"chart-line-curved-line",
|
||||
"chart-line-multi-line",
|
||||
"chart-line-step-line",
|
||||
"chart-pie-basic-donut",
|
||||
"chart-pie-basic-pie",
|
||||
"chart-pie-compact-card",
|
||||
"chart-pie-donut-compact-card",
|
||||
"chart-wordcloud-basic",
|
||||
"compare-binary-circle-progress",
|
||||
"compare-binary-compact-card",
|
||||
"compare-binary-dashed-arrow-compact-card",
|
||||
"compare-binary-dashed-line-compact-card",
|
||||
"compare-binary-horizontal-icon-arrow",
|
||||
"compare-binary-simple-horizontal-arrow",
|
||||
"compare-binary-simple-vertical-arrow",
|
||||
"compare-binary-tech-style-compact-card",
|
||||
"compare-hierarchy-compact-card",
|
||||
"compare-hierarchy-dashed-arrow-compact-card",
|
||||
"compare-hierarchy-dashed-line-compact-card",
|
||||
"compare-hierarchy-horizontal-icon-arrow",
|
||||
"compare-hierarchy-simple-horizontal-arrow",
|
||||
"compare-hierarchy-simple-vertical-arrow",
|
||||
"compare-hierarchy-tech-style-compact-card",
|
||||
"compare-swot-compact-card",
|
||||
"compare-swot-dashed-arrow-compact-card",
|
||||
"compare-swot-dashed-line-compact-card",
|
||||
"compare-swot-horizontal-icon-arrow",
|
||||
"compare-swot-simple-horizontal-arrow",
|
||||
"compare-swot-simple-vertical-arrow",
|
||||
"compare-swot-tech-style-compact-card",
|
||||
"hierarchy-mindmap-branch-gradient-capsule-item",
|
||||
"hierarchy-mindmap-branch-gradient-circle-progress",
|
||||
"hierarchy-mindmap-branch-gradient-compact-card",
|
||||
"hierarchy-mindmap-branch-gradient-lined-palette",
|
||||
"hierarchy-mindmap-branch-gradient-rounded-rect",
|
||||
"hierarchy-mindmap-level-gradient-capsule-item",
|
||||
"hierarchy-mindmap-level-gradient-circle-progress",
|
||||
"hierarchy-mindmap-level-gradient-compact-card",
|
||||
"hierarchy-mindmap-level-gradient-lined-palette",
|
||||
"hierarchy-mindmap-level-gradient-rounded-rect",
|
||||
"hierarchy-tree-bt-curved-line-badge-card",
|
||||
"hierarchy-tree-bt-curved-line-capsule-item",
|
||||
"hierarchy-tree-bt-curved-line-compact-card",
|
||||
"hierarchy-tree-bt-curved-line-ribbon-card",
|
||||
"hierarchy-tree-bt-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-bt-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-bt-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-bt-dashed-line-badge-card",
|
||||
"hierarchy-tree-bt-dashed-line-capsule-item",
|
||||
"hierarchy-tree-bt-dashed-line-compact-card",
|
||||
"hierarchy-tree-bt-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-bt-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-bt-distributed-origin-badge-card",
|
||||
"hierarchy-tree-bt-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-bt-distributed-origin-compact-card",
|
||||
"hierarchy-tree-bt-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-bt-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-bt-tech-style-badge-card",
|
||||
"hierarchy-tree-bt-tech-style-capsule-item",
|
||||
"hierarchy-tree-bt-tech-style-compact-card",
|
||||
"hierarchy-tree-bt-tech-style-ribbon-card",
|
||||
"hierarchy-tree-bt-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-curved-line-badge-card",
|
||||
"hierarchy-tree-curved-line-capsule-item",
|
||||
"hierarchy-tree-curved-line-compact-card",
|
||||
"hierarchy-tree-curved-line-ribbon-card",
|
||||
"hierarchy-tree-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-dashed-line-badge-card",
|
||||
"hierarchy-tree-dashed-line-capsule-item",
|
||||
"hierarchy-tree-dashed-line-compact-card",
|
||||
"hierarchy-tree-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-distributed-origin-badge-card",
|
||||
"hierarchy-tree-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-distributed-origin-compact-card",
|
||||
"hierarchy-tree-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-lr-curved-line-badge-card",
|
||||
"hierarchy-tree-lr-curved-line-capsule-item",
|
||||
"hierarchy-tree-lr-curved-line-compact-card",
|
||||
"hierarchy-tree-lr-curved-line-ribbon-card",
|
||||
"hierarchy-tree-lr-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-lr-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-lr-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-lr-dashed-line-badge-card",
|
||||
"hierarchy-tree-lr-dashed-line-capsule-item",
|
||||
"hierarchy-tree-lr-dashed-line-compact-card",
|
||||
"hierarchy-tree-lr-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-lr-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-lr-distributed-origin-badge-card",
|
||||
"hierarchy-tree-lr-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-lr-distributed-origin-compact-card",
|
||||
"hierarchy-tree-lr-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-lr-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-lr-tech-style-badge-card",
|
||||
"hierarchy-tree-lr-tech-style-capsule-item",
|
||||
"hierarchy-tree-lr-tech-style-compact-card",
|
||||
"hierarchy-tree-lr-tech-style-ribbon-card",
|
||||
"hierarchy-tree-lr-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-rl-curved-line-badge-card",
|
||||
"hierarchy-tree-rl-curved-line-capsule-item",
|
||||
"hierarchy-tree-rl-curved-line-compact-card",
|
||||
"hierarchy-tree-rl-curved-line-ribbon-card",
|
||||
"hierarchy-tree-rl-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-rl-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-rl-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-rl-dashed-line-badge-card",
|
||||
"hierarchy-tree-rl-dashed-line-capsule-item",
|
||||
"hierarchy-tree-rl-dashed-line-compact-card",
|
||||
"hierarchy-tree-rl-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-rl-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-rl-distributed-origin-badge-card",
|
||||
"hierarchy-tree-rl-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-rl-distributed-origin-compact-card",
|
||||
"hierarchy-tree-rl-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-rl-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-rl-tech-style-badge-card",
|
||||
"hierarchy-tree-rl-tech-style-capsule-item",
|
||||
"hierarchy-tree-rl-tech-style-compact-card",
|
||||
"hierarchy-tree-rl-tech-style-ribbon-card",
|
||||
"hierarchy-tree-rl-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-tech-style-badge-card",
|
||||
"hierarchy-tree-tech-style-capsule-item",
|
||||
"hierarchy-tree-tech-style-compact-card",
|
||||
"hierarchy-tree-tech-style-ribbon-card",
|
||||
"hierarchy-tree-tech-style-rounded-rect-node",
|
||||
"list-column-done-list",
|
||||
"list-column-simple-vertical-arrow",
|
||||
"list-column-vertical-icon-arrow",
|
||||
"list-grid-badge-card",
|
||||
"list-grid-candy-card-lite",
|
||||
"list-grid-circular-progress",
|
||||
"list-grid-compact-card",
|
||||
"list-grid-done-list",
|
||||
"list-grid-horizontal-icon-arrow",
|
||||
"list-grid-progress-card",
|
||||
"list-grid-ribbon-card",
|
||||
"list-grid-simple",
|
||||
"list-pyramid-badge-card",
|
||||
"list-pyramid-compact-card",
|
||||
"list-pyramid-rounded-rect-node",
|
||||
"list-row-circular-progress",
|
||||
"list-row-horizontal-icon-arrow",
|
||||
"list-row-horizontal-icon-line",
|
||||
"list-row-simple-horizontal-arrow",
|
||||
"list-row-simple-illus",
|
||||
"list-sector-half-plain-text",
|
||||
"list-sector-plain-text",
|
||||
"list-sector-simple",
|
||||
"quadrant-scatter-basic",
|
||||
"quadrant-scatter-compact-card",
|
||||
"quadrant-scatter-dashed-arrow-compact-card",
|
||||
"quadrant-scatter-dashed-line-compact-card",
|
||||
"quadrant-scatter-horizontal-icon-arrow",
|
||||
"quadrant-scatter-simple-horizontal-arrow",
|
||||
"quadrant-scatter-simple-vertical-arrow",
|
||||
"quadrant-scatter-tech-style-compact-card",
|
||||
"relation-graph-circular-layout",
|
||||
"relation-graph-force-directed",
|
||||
"sequence-circular-basic",
|
||||
"sequence-roadmap-vertical-simple",
|
||||
"sequence-snake-basic",
|
||||
"sequence-stairs-basic",
|
||||
"sequence-steps-basic",
|
||||
"sequence-timeline-basic",
|
||||
"sequence-timeline-done-list",
|
||||
"sequence-zigzag-basic"
|
||||
]
|
||||
}
|
||||
688
plugins/actions/infographic/test_infographic.html
Normal file
688
plugins/actions/infographic/test_infographic.html
Normal file
@@ -0,0 +1,688 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AntV Infographic Debug Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.infographic-render-container {
|
||||
height: 800px;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #e0f2fe;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
/* 保留换行符 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>AntV Infographic 综合测试台</h1>
|
||||
<div class="controls">
|
||||
<label>选择测试用例:</label>
|
||||
<select id="case-selector">
|
||||
<option value="case-list-basic">Case A: List Basic (Standard)</option>
|
||||
<option value="case-tree-vertical">Case B: Tree Vertical (Hierarchy)</option>
|
||||
<option value="case-theme-extended">Case C: Theme Extended (Bg/Text Color)</option>
|
||||
<option value="case-tree-horizontal">Case D: Tree Horizontal</option>
|
||||
<option value="case-list-children">Case E: List with Children</option>
|
||||
<option value="case-tree-indented">Case F: Indented Tree</option>
|
||||
<option value="case-mindmap">Case G: Mindmap</option>
|
||||
<option value="case-l">Case L: List Grid (MiniMax)</option>
|
||||
<option value="case-list-vertical">Case H: List Vertical</option>
|
||||
<option value="case-statistic-card">Case I: Statistic Card</option>
|
||||
<option value="case-k">Case K: 柠檬霜 Controversy (Roadmap)</option>
|
||||
<option value="case-l">Case L: List Grid (MiniMax)</option>
|
||||
<option value="case-llm-list-grid">LLM: List Grid (Features)</option>
|
||||
<option value="case-llm-tree-vertical">LLM: Tree Vertical (Hierarchy)</option>
|
||||
<option value="case-llm-statistic-card">LLM: Statistic Card (Metrics)</option>
|
||||
<option value="case-llm-mindmap">LLM: Mindmap (Brainstorming)</option>
|
||||
<option value="case-chart-bar">Case M: Chart Bar</option>
|
||||
<option value="case-compare-swot">Case N: Comparison SWOT</option>
|
||||
<option value="case-relation-sankey">Case O: Relationship Sankey</option>
|
||||
<option value="case-quadrant-quarter">Case P: Quadrant Analysis</option>
|
||||
</select>
|
||||
<button onclick="runTest()">运行测试</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status-log">准备就绪...</div>
|
||||
<div id="infographic-container-test" class="infographic-render-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Case A: 基础列表 -->
|
||||
<script type="text/template" id="case-list-basic">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 基础列表测试
|
||||
items
|
||||
- label 步骤一
|
||||
icon ref:search:one
|
||||
- label 步骤二
|
||||
icon ref:search:two
|
||||
theme
|
||||
colorPrimary #3b82f6
|
||||
palette #3b82f6 #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case B: 垂直树 (尝试添加 ID) -->
|
||||
<script type="text/template" id="case-tree-vertical">
|
||||
infographic tree-vertical
|
||||
data
|
||||
title 垂直树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root
|
||||
icon ref:search:root
|
||||
children
|
||||
- label 子节点 A
|
||||
id child-a
|
||||
- label 子节点 B
|
||||
id child-b
|
||||
theme
|
||||
colorPrimary #10b981
|
||||
palette #10b981 #34d399
|
||||
</script>
|
||||
|
||||
<!-- Case C: 扩展主题 (测试清理逻辑) -->
|
||||
<script type="text/template" id="case-theme-extended">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 扩展主题测试
|
||||
items
|
||||
- label 暗色模式
|
||||
icon ref:search:moon
|
||||
theme
|
||||
colorPrimary #6366f1
|
||||
palette #6366f1 #8b5cf6
|
||||
backgroundColor #1e293b
|
||||
textColor #f8fafc
|
||||
roughness 0.5
|
||||
</script>
|
||||
|
||||
<!-- Case D: 水平树 -->
|
||||
<script type="text/template" id="case-tree-horizontal">
|
||||
infographic tree-horizontal
|
||||
data
|
||||
title 水平树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root-h
|
||||
children
|
||||
- label 子节点 1
|
||||
id child-1
|
||||
- label 子节点 2
|
||||
id child-2
|
||||
theme
|
||||
colorPrimary #f59e0b
|
||||
</script>
|
||||
|
||||
<!-- Case E: 列表带子节点 (测试兼容性) -->
|
||||
<script type="text/template" id="case-list-children">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 列表带子节点
|
||||
items
|
||||
- label 父项 1
|
||||
children
|
||||
- label 子项 1.1
|
||||
- label 父项 2
|
||||
theme
|
||||
colorPrimary #ec4899
|
||||
</script>
|
||||
|
||||
<!-- Case F: Indented Tree (缩进树) -->
|
||||
<script type="text/template" id="case-tree-indented">
|
||||
infographic tree-indented
|
||||
data
|
||||
title 缩进树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root-i
|
||||
children
|
||||
- label 子节点 1
|
||||
id child-i-1
|
||||
- label 子节点 2
|
||||
id child-i-2
|
||||
theme
|
||||
colorPrimary #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case G: Mindmap -->
|
||||
<script type="text/template" id="case-mindmap">
|
||||
infographic mindmap
|
||||
data
|
||||
title 脑图测试
|
||||
items
|
||||
- label 中心主题
|
||||
children
|
||||
- label 主题 A
|
||||
children
|
||||
- label 子主题 A1
|
||||
- label 子主题 A2
|
||||
- label 主题 B
|
||||
theme
|
||||
colorPrimary #06b6d4
|
||||
</script>
|
||||
|
||||
<!-- Case H: List Vertical (垂直列表) -->
|
||||
<script type="text/template" id="case-list-vertical">
|
||||
infographic list-vertical
|
||||
data
|
||||
title 垂直列表测试
|
||||
items
|
||||
- label 第一项
|
||||
desc 描述文本 1
|
||||
icon ref:search:one
|
||||
- label 第二项
|
||||
desc 描述文本 2
|
||||
icon ref:search:two
|
||||
theme
|
||||
colorPrimary #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case I: Statistic Card (统计卡片) -->
|
||||
<script type="text/template" id="case-statistic-card">
|
||||
infographic statistic-card
|
||||
data
|
||||
title 核心指标
|
||||
items
|
||||
- label 总用户数
|
||||
value 1,234
|
||||
icon ref:search:users
|
||||
- label 日活
|
||||
value 89%
|
||||
icon ref:search:activity
|
||||
theme
|
||||
colorPrimary #10b981
|
||||
</script>
|
||||
|
||||
<!-- Case J: Official Tree Example -->
|
||||
<script type="text/template" id="case-official-tree">
|
||||
infographic hierarchy-tree-tech-style-capsule-item
|
||||
data
|
||||
title 官方树形示例
|
||||
items
|
||||
- label 根节点
|
||||
icon ref:search:root
|
||||
children
|
||||
- label 子节点 A
|
||||
icon ref:search:a
|
||||
children
|
||||
- label 叶子 A1
|
||||
- label 叶子 A2
|
||||
- label 子节点 B
|
||||
icon ref:search:b
|
||||
theme
|
||||
colorPrimary #1677ff
|
||||
</script>
|
||||
|
||||
<!-- Case K: User Fail Case (Lemon Frost) -->
|
||||
<script type="text/template" id="case-user-fail">
|
||||
infographic list-vertical
|
||||
data
|
||||
title 柠檬霜豹纹守宫:美丽的诅咒
|
||||
desc 揭露极端审美背后的遗传缺陷、伦理代价与商业真相
|
||||
items
|
||||
- label 审美霸权与病态追求
|
||||
desc 追求极致黄色而筛选出致癌基因,让生命背负终身癌症风险,是伦理上的霸权行为。
|
||||
icon mdi/eye-off-outline
|
||||
- label 致命的基因多效性
|
||||
desc 与“谜”品系不同,柠檬霜导致的是实体肿瘤溃烂。致癌机制与颜色性状不可剥离。
|
||||
icon mdi/dna
|
||||
- label 商业层面的误导欺诈
|
||||
desc 所谓“低肿瘤率”或“改良版”多为营销谎言。只要表现出性状,就注定携带致癌机制。
|
||||
icon mdi/alert-octagon
|
||||
- label 科学研究与宠物价值
|
||||
desc 其价值应仅限于作为黑色素瘤的医学研究模型,而非在宠物交易市场中流通。
|
||||
icon mdi/microscope
|
||||
- label 行动呼吁:坚决拒绝
|
||||
desc 拒绝购买与繁育,将柠檬霜留在教科书中作为警示,而非留在饲养箱中承受痛苦。
|
||||
icon mdi/cancel
|
||||
theme
|
||||
colorPrimary #FFD700
|
||||
colorBg #1A1A1A
|
||||
textColor #FFFFFF
|
||||
palette #FFD700 #FF4D4F #E6E6E6
|
||||
stylize rough
|
||||
roughness 0.2
|
||||
</script>
|
||||
|
||||
<!-- Case L: List Grid (MiniMax) -->
|
||||
<script type="text/template" id="case-l">
|
||||
list-grid
|
||||
data
|
||||
title MiniMax 2025 模型矩阵全解析
|
||||
desc 以 abab 7 为核心的国产顶尖大模型
|
||||
items
|
||||
- label 极致 MoE 架构优化
|
||||
desc 国内首批 MoE 路线坚定者。
|
||||
icon mdi/cpu-64-bit
|
||||
- label Video-01 视频生成
|
||||
desc 杀手锏级多模态能力。
|
||||
icon mdi/movie-filter
|
||||
- label 情感陪伴与角色扮演
|
||||
desc 源自星野基因。
|
||||
icon mdi/emoticon-heart
|
||||
- label 超长上下文与精准召回
|
||||
desc 支持 128k+ 窗口。
|
||||
icon mdi/database-search
|
||||
theme
|
||||
colorPrimary #6366f1
|
||||
colorBg #ffffff
|
||||
textColor #1f2937
|
||||
</script>
|
||||
|
||||
<!-- LLM Generated Cases for Verification -->
|
||||
<script type="text/template" id="case-llm-list-grid">
|
||||
infographic list-grid
|
||||
data
|
||||
title MiniMax 2025 模型矩阵全解析
|
||||
desc 极致架构与多模态能力的全面进阶
|
||||
items
|
||||
- label 极致 MoE 架构优化
|
||||
desc 国内首批 MoE 路线坚定者,兼顾模型性能与推理效率
|
||||
icon mdi/layers-triple
|
||||
- label Video-01 视频生成
|
||||
desc 杀手锏级多模态能力,支持高品质视频内容创作
|
||||
icon mdi/video-vintage
|
||||
- label 情感陪伴与角色扮演
|
||||
desc 源自星野基因,提供深度的情感连接与人格化交互
|
||||
icon mdi/robot-happy
|
||||
- label 超长上下文与精准召回
|
||||
desc 支持 128k+ 超长窗口,实现海量信息的精准处理
|
||||
icon mdi/file-find
|
||||
theme
|
||||
colorPrimary #0052D9
|
||||
colorBg #F2F3F5
|
||||
palette #0052D9 #2BA471 #E37318 #D54941
|
||||
stylize flat
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-tree-vertical">
|
||||
infographic tree-vertical
|
||||
data
|
||||
title 公司组织架构
|
||||
desc 2025年度企业内部编制示意图
|
||||
items
|
||||
- label 研发部
|
||||
icon mdi/xml
|
||||
children
|
||||
- label 后端组
|
||||
icon mdi/server
|
||||
- label 前端组
|
||||
icon mdi/language-javascript
|
||||
- label 市场部
|
||||
icon mdi/chart-bell-curve
|
||||
children
|
||||
- label 销售组
|
||||
icon mdi/account-cash
|
||||
- label 推广组
|
||||
icon mdi/bullhorn
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-statistic-card">
|
||||
infographic statistic-card
|
||||
data
|
||||
title 2024年Q4 核心指标
|
||||
desc 统计日期:2024年第四季度
|
||||
items
|
||||
- label 总用户数
|
||||
value 1,234,567
|
||||
icon mdi/account-group
|
||||
- label 日活跃用户
|
||||
value 89%
|
||||
icon mdi/chart-line
|
||||
- label 营收增长
|
||||
value +45%
|
||||
icon mdi/trending-up
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-mindmap">
|
||||
infographic mindmap
|
||||
data
|
||||
title 人工智能应用领域
|
||||
desc 核心应用场景分类与展示
|
||||
items
|
||||
- label 生成式 AI
|
||||
icon mdi/brain
|
||||
children
|
||||
- label 文本生成
|
||||
icon mdi/text-recognition
|
||||
- label 图像生成
|
||||
icon mdi/image-multiple
|
||||
- label 视频生成
|
||||
icon mdi/video-input-component
|
||||
- label 预测性 AI
|
||||
icon mdi/chart-line
|
||||
children
|
||||
- label 股市预测
|
||||
icon mdi/finance
|
||||
- label 天气预报
|
||||
icon mdi/weather-partly-cloudy
|
||||
- label 决策 AI
|
||||
icon mdi/robot
|
||||
children
|
||||
- label 自动驾驶
|
||||
icon mdi/car-autonomous
|
||||
- label 游戏博弈
|
||||
icon mdi/controller-classic
|
||||
theme
|
||||
colorPrimary #2F54EB
|
||||
colorBg #FFFFFF
|
||||
</script>
|
||||
|
||||
<!-- New Styles Test Cases -->
|
||||
<script type="text/template" id="case-chart-bar">
|
||||
infographic chart-bar
|
||||
data
|
||||
title 2023年季度营收分析
|
||||
desc 单位:亿元
|
||||
items
|
||||
- label 第一季度
|
||||
value 12.5
|
||||
- label 第二季度
|
||||
value 15.8
|
||||
- label 第三季度
|
||||
value 14.2
|
||||
- label 第四季度
|
||||
value 18.9
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-relation-circle">
|
||||
infographic relation-circle
|
||||
data
|
||||
title 生态循环
|
||||
items
|
||||
- label 生产者
|
||||
- label 消费者
|
||||
- label 分解者
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-compare-swot">
|
||||
infographic compare-swot
|
||||
data
|
||||
title 某初创咖啡品牌 SWOT 分析
|
||||
items
|
||||
- label 优势
|
||||
children
|
||||
- label 产品口味独特
|
||||
- label 选址精准
|
||||
- label 劣势
|
||||
children
|
||||
- label 品牌知名度低
|
||||
- label 资金压力大
|
||||
- label 机会
|
||||
children
|
||||
- label 线上外卖市场增长
|
||||
- label 年轻人对精品咖啡需求增加
|
||||
- label 威胁
|
||||
children
|
||||
- label 行业巨头价格战
|
||||
- label 原材料成本上涨
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-relation-sankey">
|
||||
infographic relation-sankey
|
||||
data
|
||||
title 家庭月度开支流向
|
||||
desc 2025年12月家庭总收入 10000 元分配明细
|
||||
items
|
||||
- label 总收入
|
||||
value 10000
|
||||
children
|
||||
- label 房贷
|
||||
value 4000
|
||||
- label 日常消费
|
||||
value 3000
|
||||
children
|
||||
- label 餐饮
|
||||
value 2000
|
||||
- label 交通
|
||||
value 1000
|
||||
- label 教育培训
|
||||
value 2000
|
||||
- label 存入银行
|
||||
value 1000
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-quadrant-quarter">
|
||||
infographic quadrant-quarter
|
||||
data
|
||||
title 个人任务象限分析
|
||||
desc 2025年12月28日 任务优先级管理
|
||||
items
|
||||
- label 修复线上紧急 Bug
|
||||
desc 紧急且重要:立即优先处理
|
||||
illus mdi/bug
|
||||
- label 准备下午的客户会议
|
||||
desc 紧急且重要:核心商务产出
|
||||
illus mdi/account-group
|
||||
- label 制定年度学习计划
|
||||
desc 重要不紧急:长期价值积累
|
||||
illus mdi/calendar-check
|
||||
- label 健身锻炼
|
||||
desc 重要不紧急:身体健康投资
|
||||
illus mdi/dumbbell
|
||||
- label 回复琐碎的邮件
|
||||
desc 紧急不重要:低价值干扰
|
||||
illus mdi/email-outline
|
||||
- label 接听推销电话
|
||||
desc 紧急不重要:时间黑洞
|
||||
illus mdi/phone-cancel
|
||||
- label 刷社交媒体
|
||||
desc 不紧急不重要:消遣娱乐
|
||||
illus mdi/instagram
|
||||
- label 看无聊的综艺
|
||||
desc 不紧急不重要:无谓消耗
|
||||
illus mdi/television-classic
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-compare-binary">
|
||||
infographic compare-binary
|
||||
data
|
||||
title IDE vs Open WebUI
|
||||
desc 核心差异对比
|
||||
items
|
||||
- label IDE
|
||||
desc 代码工程中心
|
||||
children
|
||||
- label 深度上下文
|
||||
- label 实时补全
|
||||
- label Open WebUI
|
||||
desc 对话交互中心
|
||||
children
|
||||
- label 模型管理
|
||||
- label 知识库 RAG
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/@antv/infographic@latest/dist/infographic.min.js"></script>
|
||||
<script>
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
const el = document.getElementById('status-log');
|
||||
el.textContent += '\n' + msg;
|
||||
}
|
||||
|
||||
function logError(err) {
|
||||
console.error('Detailed Error:', err);
|
||||
let msg = `❌ 内部错误: ${err.message}`;
|
||||
if (err.stack) msg += `\nStack: ${err.stack}`;
|
||||
for (const key in err) {
|
||||
if (key !== 'message' && key !== 'stack') {
|
||||
msg += `\n${key}: ${JSON.stringify(err[key])}`;
|
||||
}
|
||||
}
|
||||
log(msg);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('status-log').textContent = '准备就绪...';
|
||||
}
|
||||
|
||||
window.listTemplates = () => {
|
||||
clearLog();
|
||||
log('[Explore] 正在获取模版列表...');
|
||||
if (typeof AntVInfographic !== 'undefined') {
|
||||
if (typeof AntVInfographic.getTemplates === 'function') {
|
||||
const templates = AntVInfographic.getTemplates();
|
||||
log(`[Explore] getTemplates() result (${templates.length}):\n${JSON.stringify(templates, null, 2)}`);
|
||||
console.log(templates);
|
||||
} else {
|
||||
log('❌ AntVInfographic.getTemplates is not a function.');
|
||||
log('Available keys: ' + Object.keys(AntVInfographic).join(', '));
|
||||
}
|
||||
} else {
|
||||
log('❌ AntVInfographic is undefined.');
|
||||
}
|
||||
};
|
||||
|
||||
window.runTest = () => {
|
||||
clearLog();
|
||||
const selector = document.getElementById('case-selector');
|
||||
const caseId = selector.value;
|
||||
const sourceEl = document.getElementById(caseId);
|
||||
const containerEl = document.getElementById('infographic-container-test');
|
||||
|
||||
if (!sourceEl) return log('❌ 未找到测试用例');
|
||||
|
||||
// 重置容器
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.style = '';
|
||||
|
||||
let syntaxContent = sourceEl.textContent.trim();
|
||||
log(`[Test] 运行用例: ${caseId} (${selector.options[selector.selectedIndex].text})`);
|
||||
log(`[Step 1] 原始内容:\n${syntaxContent}`);
|
||||
|
||||
// --- 模拟插件的清理逻辑 ---
|
||||
const bgMatch = syntaxContent.match(/backgroundColor\s+(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|[a-zA-Z]+)/);
|
||||
if (bgMatch && bgMatch[1]) {
|
||||
containerEl.style.backgroundColor = bgMatch[1];
|
||||
}
|
||||
const textMatch = syntaxContent.match(/textColor\s+(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|[a-zA-Z]+)/);
|
||||
if (textMatch && textMatch[1]) {
|
||||
containerEl.style.color = textMatch[1];
|
||||
}
|
||||
|
||||
// 使用更强的正则:匹配 stylize 及其后续缩进的行
|
||||
const nl = String.fromCharCode(10);
|
||||
const cleanRegex = new RegExp('^\\s*(roughness|stylize|backgroundColor|textColor|colorBg).*(' + nl + '\\s+.*)*', 'gm');
|
||||
if (cleanRegex.test(syntaxContent)) {
|
||||
log('[Fix] 移除扩展 Theme 属性...');
|
||||
syntaxContent = syntaxContent.replace(cleanRegex, '');
|
||||
}
|
||||
|
||||
// 2. 模板映射配置
|
||||
const TEMPLATE_MAPPING = {
|
||||
// 列表与层级
|
||||
'list-grid': 'list-grid-compact-card',
|
||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
||||
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
|
||||
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
|
||||
|
||||
// 顺序与关系
|
||||
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
||||
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
|
||||
'relation-sankey': 'relation-sankey-simple', // 暂无直接对应,保留原值或需移除
|
||||
'relation-circle': 'relation-circle-icon-badge',
|
||||
|
||||
// 对比与分析
|
||||
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
||||
'compare-swot': 'compare-swot',
|
||||
'compare-table': 'compare-table-simple', // 暂无直接对应
|
||||
'quadrant-quarter': 'quadrant-quarter-simple-card',
|
||||
|
||||
// 图表与数据
|
||||
'statistic-card': 'list-grid-compact-card',
|
||||
'chart-bar': 'chart-bar-plain-text',
|
||||
'chart-column': 'chart-column-simple',
|
||||
'chart-line': 'chart-line-plain-text',
|
||||
'chart-area': 'chart-area-simple', // 暂无直接对应
|
||||
'chart-pie': 'chart-pie-plain-text',
|
||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||
};
|
||||
|
||||
// 3. 应用映射策略
|
||||
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {
|
||||
const regex = new RegExp(`infographic\\s+${key}(?=\\s|$)`, 'i');
|
||||
if (regex.test(syntaxContent)) {
|
||||
log(`[Fix] 自动映射模板: ${key} -> ${value}`);
|
||||
syntaxContent = syntaxContent.replace(regex, `infographic ${value}`);
|
||||
break; // 找到一个匹配后即停止
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底检查:确保以 infographic 开头
|
||||
if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {
|
||||
const firstWord = syntaxContent.trim().split(/\s+/)[0].toLowerCase();
|
||||
if (!['data', 'theme', 'design', 'items'].includes(firstWord)) {
|
||||
log('[Fix] 检测到缺失 infographic 前缀,自动补全');
|
||||
syntaxContent = 'infographic ' + syntaxContent;
|
||||
}
|
||||
}
|
||||
|
||||
syntaxContent = syntaxContent.trim();
|
||||
log(`[Step 2] 清理后内容:\n${syntaxContent}`);
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container-test',
|
||||
padding: 24,
|
||||
});
|
||||
|
||||
instance.on('error', (err) => {
|
||||
logError(err);
|
||||
});
|
||||
|
||||
instance.render(syntaxContent);
|
||||
|
||||
if (instance.node) {
|
||||
log('✅ 渲染成功 (instance.node 存在)');
|
||||
if (!containerEl.querySelector('svg')) {
|
||||
log('⚠️ 自动挂载失败,手动挂载...');
|
||||
containerEl.appendChild(instance.node);
|
||||
}
|
||||
} else {
|
||||
log('❌ 渲染失败 (instance.node 为空)');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
395
plugins/actions/infographic/verify_generation.py
Normal file
395
plugins/actions/infographic/verify_generation.py
Normal file
@@ -0,0 +1,395 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
import pathlib
|
||||
|
||||
# Load .env from the same directory as this script
|
||||
env_path = pathlib.Path(__file__).parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
# =================================================================
|
||||
# Configuration
|
||||
# =================================================================
|
||||
|
||||
API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
BASE_URL = os.getenv("OPENAI_BASE_URL")
|
||||
MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") # Default to gpt-4o if not set
|
||||
|
||||
if not API_KEY or not BASE_URL:
|
||||
print(
|
||||
"Error: OPENAI_API_KEY and OPENAI_BASE_URL environment variables must be set."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# =================================================================
|
||||
# Prompts (Extracted from 信息图.py)
|
||||
# =================================================================
|
||||
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
|
||||
### Syntax Rules
|
||||
- Entry uses `infographic <template-name>`
|
||||
- Key-value pairs are separated by spaces, **absolutely NO colons allowed**
|
||||
- Use two spaces for indentation
|
||||
- Object arrays use `-` with line breaks
|
||||
|
||||
⚠️ **IMPORTANT WARNING: This is NOT YAML format!**
|
||||
- ❌ Wrong: `children:` `items:` `data:` (with colons)
|
||||
- ✅ Correct: `children` `items` `data` (without colons)
|
||||
|
||||
### Template Library & Selection Guide
|
||||
|
||||
#### 1. List & Hierarchy (Text-heavy)
|
||||
- **Linear & Short (Steps/Phases)** -> `list-row-horizontal-icon-arrow`
|
||||
- **Linear & Long (Rankings/Details)** -> `list-vertical`
|
||||
- **Grouped / Parallel (Features/Catalog)** -> `list-grid`
|
||||
- **Hierarchical (Org Chart/Taxonomy)** -> `tree-vertical` or `tree-horizontal`
|
||||
- **Central Idea (Brainstorming)** -> `mindmap`
|
||||
|
||||
#### 2. Sequence & Relationship (Flow-based)
|
||||
- **Time-based (History/Plan)** -> `sequence-roadmap-vertical-simple`
|
||||
- **Process Flow (Complex)** -> `sequence-zigzag` or `sequence-horizontal`
|
||||
- **Resource Flow / Distribution** -> `relation-sankey`
|
||||
- **Circular Relationship** -> `relation-circle`
|
||||
|
||||
#### 3. Comparison & Analysis
|
||||
- **Binary Comparison (A vs B)** -> `compare-binary`
|
||||
- **SWOT Analysis** -> `compare-swot`
|
||||
- **Multi-item Comparison Table** -> `compare-table`
|
||||
- **Quadrant Analysis (Importance vs Urgency)** -> `quadrant-quarter`
|
||||
|
||||
#### 4. Charts & Data (Metric-heavy)
|
||||
- **Key Metrics / Data Cards** -> `statistic-card`
|
||||
- **Distribution / Comparison** -> `chart-bar` or `chart-column`
|
||||
- **Trend over Time** -> `chart-line` or `chart-area`
|
||||
- **Proportion / Part-to-Whole** -> `chart-pie` or `chart-doughnut`
|
||||
|
||||
### Infographic Syntax Guide
|
||||
|
||||
#### 1. Structure
|
||||
- **Entry**: `infographic <template-name>`
|
||||
- **Blocks**: `data`, `theme`, `design` (optional)
|
||||
- **Format**: Key-value pairs separated by spaces, 2-space indentation.
|
||||
- **Arrays**: Object arrays use `-` (newline), simple arrays use inline values.
|
||||
|
||||
#### 2. Data Block (`data`)
|
||||
- `title`: Main title
|
||||
- `desc`: Subtitle or description
|
||||
- `items`: List of data items
|
||||
- - `label`: Item title
|
||||
- - `value`: Numerical value (required for Charts/Stats)
|
||||
- - `desc`: Item description (optional)
|
||||
- - `icon`: Icon name (e.g., `mdi/rocket-launch`)
|
||||
- - `time`: Time label (Optional, for Roadmap/Sequence)
|
||||
- - `children`: Nested items (ONLY for Tree/Mindmap/Sankey/SWOT)
|
||||
- - `illus`: Illustration name (ONLY for Quadrant)
|
||||
|
||||
#### 3. Theme Block (`theme`)
|
||||
- `colorPrimary`: Main color (Hex)
|
||||
- `colorBg`: Background color (Hex)
|
||||
- `palette`: Color list (Space separated)
|
||||
- `textColor`: Text color (Hex)
|
||||
- `stylize`: Style effect (e.g., `rough`, `flat`)
|
||||
|
||||
### Examples
|
||||
|
||||
#### Chart (Bar Chart)
|
||||
infographic chart-bar
|
||||
data
|
||||
title Revenue Growth
|
||||
desc Monthly revenue in 2024
|
||||
items
|
||||
- label Jan
|
||||
value 1200
|
||||
- label Feb
|
||||
value 1500
|
||||
- label Mar
|
||||
value 1800
|
||||
|
||||
#### Comparison (SWOT)
|
||||
infographic compare-swot
|
||||
data
|
||||
title Project SWOT
|
||||
items
|
||||
- label Strengths
|
||||
children
|
||||
- label Strong team
|
||||
- label Innovative tech
|
||||
- label Weaknesses
|
||||
children
|
||||
- label Limited budget
|
||||
- label Opportunities
|
||||
children
|
||||
- label Emerging market
|
||||
- label Threats
|
||||
children
|
||||
- label High competition
|
||||
|
||||
#### Relationship (Sankey)
|
||||
infographic relation-sankey
|
||||
data
|
||||
title Energy Flow
|
||||
items
|
||||
- label Solar
|
||||
value 100
|
||||
children
|
||||
- label Grid
|
||||
value 60
|
||||
- label Battery
|
||||
value 40
|
||||
- label Wind
|
||||
value 80
|
||||
children
|
||||
- label Grid
|
||||
value 80
|
||||
|
||||
#### Quadrant (Importance vs Urgency)
|
||||
infographic quadrant-quarter
|
||||
data
|
||||
title Task Management
|
||||
items
|
||||
- label Critical Bug
|
||||
desc Fix immediately
|
||||
illus mdi/bug
|
||||
- label Feature Request
|
||||
desc Plan for next sprint
|
||||
illus mdi/star
|
||||
|
||||
### Output Rules
|
||||
1. **Strict Syntax**: Follow the indentation and formatting rules exactly.
|
||||
2. **No Explanations**: Output ONLY the syntax code block.
|
||||
3. **Language**: Use the user's requested language for content.
|
||||
"""
|
||||
|
||||
USER_PROMPT_GENERATE_INFOGRAPHIC = """
|
||||
请分析以下文本内容,将其核心信息转换为 AntV Infographic 语法格式。
|
||||
|
||||
---
|
||||
**用户上下文信息:**
|
||||
用户姓名: {user_name}
|
||||
当前日期时间: {current_date_time_str}
|
||||
用户语言: {user_language}
|
||||
---
|
||||
|
||||
**文本内容:**
|
||||
{long_text_content}
|
||||
|
||||
请根据文本特点选择最合适的信息图模板,并输出规范的 infographic 语法。注意保持正确的缩进格式(两个空格)。
|
||||
"""
|
||||
|
||||
# =================================================================
|
||||
# Test Cases
|
||||
# =================================================================
|
||||
|
||||
TEST_CASES = [
|
||||
{
|
||||
"name": "List Grid (Features)",
|
||||
"content": """
|
||||
MiniMax 2025 模型矩阵全解析:
|
||||
1. 极致 MoE 架构优化:国内首批 MoE 路线坚定者。
|
||||
2. Video-01 视频生成:杀手锏级多模态能力。
|
||||
3. 情感陪伴与角色扮演:源自星野基因。
|
||||
4. 超长上下文与精准召回:支持 128k+ 窗口。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Tree Vertical (Hierarchy)",
|
||||
"content": """
|
||||
公司组织架构:
|
||||
- 研发部
|
||||
- 后端组
|
||||
- 前端组
|
||||
- 市场部
|
||||
- 销售组
|
||||
- 推广组
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Statistic Card (Metrics)",
|
||||
"content": """
|
||||
2024年Q4 核心指标:
|
||||
- 总用户数:1,234,567
|
||||
- 日活跃用户:89%
|
||||
- 营收增长:+45%
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Mindmap (Brainstorming)",
|
||||
"content": """
|
||||
人工智能的应用领域:
|
||||
- 生成式 AI:文本生成、图像生成、视频生成
|
||||
- 预测性 AI:股市预测、天气预报
|
||||
- 决策 AI:自动驾驶、游戏博弈
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "SWOT Analysis",
|
||||
"content": """
|
||||
某初创咖啡品牌的 SWOT 分析:
|
||||
优势:产品口味独特,选址精准。
|
||||
劣势:品牌知名度低,资金压力大。
|
||||
机会:线上外卖市场增长,年轻人对精品咖啡需求增加。
|
||||
威胁:行业巨头价格战,原材料成本上涨。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Sankey (Relationship)",
|
||||
"content": """
|
||||
家庭月度开支流向:
|
||||
总收入 10000 元。
|
||||
其中 4000 元用于房贷。
|
||||
3000 元用于日常消费(包括 2000 元餐饮,1000 元交通)。
|
||||
2000 元用于教育培训。
|
||||
1000 元存入银行。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Quadrant (Analysis)",
|
||||
"content": """
|
||||
个人任务象限分析:
|
||||
- 紧急且重要:修复线上紧急 Bug,准备下午的客户会议。
|
||||
- 重要不紧急:制定年度学习计划,健身锻炼。
|
||||
- 紧急不重要:回复琐碎的邮件,接听推销电话。
|
||||
- 不紧急不重要:刷社交媒体,看无聊的综艺。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Chart Bar (Data)",
|
||||
"content": """
|
||||
2023年各季度营收情况(亿元):
|
||||
第一季度:12.5
|
||||
第二季度:15.8
|
||||
第三季度:14.2
|
||||
第四季度:18.9
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
||||
# =================================================================
|
||||
# Helper Functions
|
||||
# =================================================================
|
||||
|
||||
|
||||
def generate_infographic(content):
|
||||
now = datetime.now()
|
||||
current_date_time_str = now.strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
|
||||
formatted_user_prompt = USER_PROMPT_GENERATE_INFOGRAPHIC.format(
|
||||
user_name="TestUser",
|
||||
current_date_time_str=current_date_time_str,
|
||||
user_language="zh-CN",
|
||||
long_text_content=content,
|
||||
)
|
||||
|
||||
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT},
|
||||
{"role": "user", "content": formatted_user_prompt},
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/chat/completions", headers=headers, json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
print(f"API Request Failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def validate_syntax(syntax):
|
||||
if not syntax:
|
||||
return False, "Empty output"
|
||||
|
||||
# Basic checks
|
||||
if "infographic" not in syntax.lower():
|
||||
return False, "Missing 'infographic' keyword"
|
||||
|
||||
if "data" not in syntax.lower() and "items" not in syntax.lower():
|
||||
return False, "Missing 'data' or 'items' block"
|
||||
|
||||
# Check for colons in keys (simple heuristic)
|
||||
lines = syntax.split("\n")
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
# Ignore lines that are likely values or descriptions containing colons
|
||||
first_word = stripped.split()[0]
|
||||
if first_word in ["title", "desc", "label", "value", "time", "icon"]:
|
||||
continue # These can have colons in value
|
||||
|
||||
# Check for key: pattern at start of line
|
||||
if re.match(r"^\w+:", stripped):
|
||||
return False, f"Found colon in key: {stripped}"
|
||||
|
||||
return True, "Syntax looks valid"
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Main Execution
|
||||
# =================================================================
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Starting Infographic Generation Verification...")
|
||||
print(f"API: {BASE_URL}")
|
||||
print(f"Model: {MODEL}")
|
||||
print("-" * 50)
|
||||
|
||||
results = []
|
||||
|
||||
for case in TEST_CASES:
|
||||
print(f"\nTesting Case: {case['name']}")
|
||||
print("Generating...")
|
||||
|
||||
output = generate_infographic(case["content"])
|
||||
|
||||
if output:
|
||||
# Clean output (remove markdown code blocks if present)
|
||||
clean_output = output
|
||||
if "```" in output:
|
||||
match = re.search(
|
||||
r"```(?:infographic|mermaid)?\s*(.*?)\s*```", output, re.DOTALL
|
||||
)
|
||||
if match:
|
||||
clean_output = match.group(1).strip()
|
||||
|
||||
print(f"Output Preview:\n{clean_output[:200]}...")
|
||||
|
||||
is_valid, message = validate_syntax(clean_output)
|
||||
if is_valid:
|
||||
print(f"✅ Validation Passed: {message}")
|
||||
results.append({"name": case["name"], "status": "PASS"})
|
||||
else:
|
||||
print(f"❌ Validation Failed: {message}")
|
||||
print(f"Full Output:\n{output}")
|
||||
results.append({"name": case["name"], "status": "FAIL"})
|
||||
else:
|
||||
print("❌ Generation Failed")
|
||||
results.append({"name": case["name"], "status": "ERROR"})
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Summary")
|
||||
print("=" * 50)
|
||||
for res in results:
|
||||
print(f"{res['name']}: {res['status']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1155
plugins/actions/infographic/信息图.py
Normal file
1155
plugins/actions/infographic/信息图.py
Normal file
File diff suppressed because one or more lines are too long
@@ -95,6 +95,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -111,13 +115,32 @@ class Action:
|
||||
if not __event_emitter__:
|
||||
return body
|
||||
|
||||
# Get the last user message
|
||||
# Get messages based on MESSAGE_COUNT
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# Usually the action is triggered on the last message
|
||||
target_message = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
|
||||
target_message = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
# Check text length
|
||||
text_length = len(target_message)
|
||||
@@ -140,9 +163,18 @@ class Action:
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ Generating Flash Card...", "info"
|
||||
)
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ Flash Card: Starting generation...", done=False
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. Extract information using LLM
|
||||
await self._emit_status(
|
||||
__event_emitter__,
|
||||
"⚡ Flash Card: Calling AI model to analyze content...",
|
||||
done=False,
|
||||
)
|
||||
|
||||
user_id = __user__.get("id") if __user__ else "default"
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
|
||||
@@ -187,6 +219,12 @@ Important Principles:
|
||||
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__,
|
||||
"⚡ Flash Card: AI analysis complete, parsing data...",
|
||||
done=False,
|
||||
)
|
||||
|
||||
# Parse JSON
|
||||
try:
|
||||
# simple cleanup in case of markdown code blocks
|
||||
@@ -198,14 +236,20 @@ Important Principles:
|
||||
card_data = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse JSON: {e}, content: {content}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ Flash Card: Data parsing failed", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__,
|
||||
"Failed to generate card data, please try again.",
|
||||
"❌ Failed to generate card data, please try again.",
|
||||
"error",
|
||||
)
|
||||
return body
|
||||
|
||||
# 2. Generate HTML components
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ Flash Card: Rendering card...", done=False
|
||||
)
|
||||
card_content, card_style = self.generate_html_card_components(card_data)
|
||||
|
||||
# 3. Append to message
|
||||
@@ -245,6 +289,9 @@ Important Principles:
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "✅ Flash Card: Generation complete!", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ Flash Card generated successfully!", "success"
|
||||
)
|
||||
@@ -253,8 +300,13 @@ Important Principles:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating knowledge card: {e}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ Flash Card: Generation failed", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, f"Error generating knowledge card: {str(e)}", "error"
|
||||
__event_emitter__,
|
||||
f"❌ Error generating knowledge card: {str(e)}",
|
||||
"error",
|
||||
)
|
||||
return body
|
||||
|
||||
@@ -277,6 +329,21 @@ Important Principles:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
|
||||
@@ -92,6 +92,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -108,13 +112,32 @@ class Action:
|
||||
if not __event_emitter__:
|
||||
return body
|
||||
|
||||
# Get the last user message
|
||||
# Get messages based on MESSAGE_COUNT
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# Usually the action is triggered on the last message
|
||||
target_message = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
|
||||
target_message = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
# Check text length
|
||||
text_length = len(target_message)
|
||||
@@ -135,9 +158,14 @@ class Action:
|
||||
|
||||
# Notify user that we are generating the card
|
||||
await self._emit_notification(__event_emitter__, "⚡ 正在生成闪记卡...", "info")
|
||||
await self._emit_status(__event_emitter__, "⚡ 闪记卡: 开始生成...", done=False)
|
||||
|
||||
try:
|
||||
# 1. Extract information using LLM
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: 正在调用 AI 模型分析内容...", done=False
|
||||
)
|
||||
|
||||
user_id = __user__.get("id") if __user__ else "default"
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
|
||||
@@ -182,6 +210,10 @@ class Action:
|
||||
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: AI 分析完成,正在解析数据...", done=False
|
||||
)
|
||||
|
||||
# Parse JSON
|
||||
try:
|
||||
# simple cleanup in case of markdown code blocks
|
||||
@@ -193,12 +225,18 @@ class Action:
|
||||
card_data = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse JSON: {e}, content: {content}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ 闪记卡: 数据解析失败", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "生成卡片数据失败,请重试。", "error"
|
||||
__event_emitter__, "❌ 生成卡片数据失败,请重试。", "error"
|
||||
)
|
||||
return body
|
||||
|
||||
# 2. Generate HTML components
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: 正在渲染卡片...", done=False
|
||||
)
|
||||
card_content, card_style = self.generate_html_card_components(card_data)
|
||||
|
||||
# 3. Append to message
|
||||
@@ -238,6 +276,9 @@ class Action:
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "✅ 闪记卡: 生成完成!", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ 闪记卡生成成功!", "success"
|
||||
)
|
||||
@@ -246,8 +287,9 @@ class Action:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating knowledge card: {e}")
|
||||
await self._emit_status(__event_emitter__, "❌ 闪记卡: 生成失败", done=True)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, f"生成知识卡片时出错: {str(e)}", "error"
|
||||
__event_emitter__, f"❌ 生成知识卡片时出错: {str(e)}", "error"
|
||||
)
|
||||
return body
|
||||
|
||||
@@ -270,6 +312,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: Smart Mind Map
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
|
||||
version: 0.7.3
|
||||
version: 0.7.4
|
||||
description: Intelligently analyzes long texts and generates interactive mind maps, supporting SVG/Markdown export.
|
||||
"""
|
||||
|
||||
@@ -81,17 +81,14 @@ HTML_WRAPPER_TEMPLATE = """
|
||||
width: 100%;
|
||||
}
|
||||
.plugin-item {
|
||||
flex: 1 1 400px; /* Default width, allows shrinking/growing */
|
||||
flex: 1 1 400px; /* Default width, allows stretching */
|
||||
min-width: 300px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.plugin-item:hover {
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.plugin-item { flex: 1 1 100%; }
|
||||
@@ -128,7 +125,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--card-bg-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
@@ -169,7 +165,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
background-size: 20px 20px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -287,7 +282,8 @@ SCRIPT_TEMPLATE_MINDMAP = """
|
||||
try {
|
||||
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.height = '500px';
|
||||
svgEl.style.height = 'auto';
|
||||
svgEl.style.minHeight = '300px';
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.appendChild(svgEl);
|
||||
|
||||
@@ -410,6 +406,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -453,6 +453,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -544,18 +559,40 @@ class Action:
|
||||
)
|
||||
|
||||
messages = body.get("messages")
|
||||
if (
|
||||
not messages
|
||||
or not isinstance(messages, list)
|
||||
or not messages[-1].get("content")
|
||||
):
|
||||
if not messages or not isinstance(messages, list):
|
||||
error_message = "Unable to retrieve valid user message content."
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "Unable to retrieve valid user message content."
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
parts = re.split(r"```html.*?```", original_content, flags=re.DOTALL)
|
||||
long_text_content = ""
|
||||
if parts:
|
||||
for part in reversed(parts):
|
||||
@@ -564,7 +601,7 @@ class Action:
|
||||
break
|
||||
|
||||
if not long_text_content:
|
||||
long_text_content = messages[-1]["content"].strip()
|
||||
long_text_content = original_content.strip()
|
||||
|
||||
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"Text content is too short ({len(long_text_content)} characters), unable to perform effective analysis. Please provide at least {self.valves.MIN_TEXT_LENGTH} characters of text."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: 智绘心图
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
|
||||
version: 0.7.2
|
||||
version: 0.7.4
|
||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
"""
|
||||
|
||||
@@ -83,15 +83,12 @@ HTML_WRAPPER_TEMPLATE = """
|
||||
.plugin-item {
|
||||
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
|
||||
min-width: 300px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.plugin-item:hover {
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.plugin-item { flex: 1 1 100%; }
|
||||
@@ -128,7 +125,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--card-bg-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
@@ -169,7 +165,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
background-size: 20px 20px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -287,7 +282,8 @@ SCRIPT_TEMPLATE_MINDMAP = """
|
||||
try {
|
||||
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.height = '500px';
|
||||
svgEl.style.height = 'auto';
|
||||
svgEl.style.minHeight = '300px';
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.appendChild(svgEl);
|
||||
|
||||
@@ -409,6 +405,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -452,6 +452,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -541,18 +556,40 @@ class Action:
|
||||
)
|
||||
|
||||
messages = body.get("messages")
|
||||
if (
|
||||
not messages
|
||||
or not isinstance(messages, list)
|
||||
or not messages[-1].get("content")
|
||||
):
|
||||
if not messages or not isinstance(messages, list):
|
||||
error_message = "无法获取有效的用户消息内容。"
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "无法获取有效的用户消息内容。"
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
parts = re.split(r"```html.*?```", original_content, flags=re.DOTALL)
|
||||
long_text_content = ""
|
||||
if parts:
|
||||
for part in reversed(parts):
|
||||
@@ -561,7 +598,7 @@ class Action:
|
||||
break
|
||||
|
||||
if not long_text_content:
|
||||
long_text_content = messages[-1]["content"].strip()
|
||||
long_text_content = original_content.strip()
|
||||
|
||||
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"文本内容过短({len(long_text_content)}字符),无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。"
|
||||
|
||||
@@ -325,6 +325,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -400,6 +404,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -492,10 +511,32 @@ class Action:
|
||||
original_content = ""
|
||||
try:
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(
|
||||
f"[{role_label} Message {i}]\n{text_content}"
|
||||
)
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"Text content too short ({len(original_content)} chars), recommended at least {self.valves.MIN_TEXT_LENGTH} chars for effective deep analysis.\n\n💡 Tip: For short texts, consider using '⚡ Flash Card' for quick refinement."
|
||||
|
||||
@@ -320,6 +320,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -398,6 +402,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -491,10 +510,30 @@ class Action:
|
||||
original_content = ""
|
||||
try:
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"文本内容过短({len(original_content)}字符),建议至少{self.valves.MIN_TEXT_LENGTH}字符以获得有效的深度分析。\n\n💡 提示:对于短文本,建议使用'⚡ 闪记卡'进行快速提炼。"
|
||||
|
||||
301
plugins/filters/context_enhancement_filter/README_CITATIONS.md
Normal file
301
plugins/filters/context_enhancement_filter/README_CITATIONS.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Citations 处理功能文档
|
||||
|
||||
## 概述
|
||||
|
||||
`context_enhancement_filter.py` 现在支持自动处理模型响应中的 `citations` 和 `grounding_metadata`,将其转换为 Open WebUI 的标准引用格式,使搜索来源能够在聊天界面中正确展示。
|
||||
|
||||
## 支持的数据格式
|
||||
|
||||
### 1. Gemini Search 格式
|
||||
|
||||
模型响应包含顶层的 `citations` 数组和 `grounding_metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-xxx",
|
||||
"type": "stream",
|
||||
"model": "gemini-3-flash-preview-search",
|
||||
"content": "回答内容...",
|
||||
"citations": [
|
||||
{
|
||||
"source": {
|
||||
"url": "https://example.com/article",
|
||||
"name": "example.com"
|
||||
},
|
||||
"retrieved_at": "2025-12-27T17:50:03.472550"
|
||||
}
|
||||
],
|
||||
"grounding_metadata": {
|
||||
"web_search_queries": [
|
||||
"搜索查询1",
|
||||
"搜索查询2"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 嵌套在 messages 中的格式
|
||||
|
||||
Citations 数据嵌套在最后一条 assistant 消息中:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "回答内容...",
|
||||
"citations": [...],
|
||||
"grounding_metadata": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 自动引用提取
|
||||
|
||||
插件会自动从以下位置提取 citations:
|
||||
- 响应体顶层的 `citations` 字段
|
||||
- 最后一条 assistant 消息中的 `citations` 字段
|
||||
|
||||
### 2. 引用格式转换
|
||||
|
||||
将模型原始的 citations 格式转换为 Open WebUI 标准格式:
|
||||
|
||||
**输入格式:**
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"url": "https://example.com",
|
||||
"name": "example.com"
|
||||
},
|
||||
"retrieved_at": "2025-12-27T17:50:03.472550"
|
||||
}
|
||||
```
|
||||
|
||||
**输出格式:**
|
||||
```json
|
||||
{
|
||||
"type": "citation",
|
||||
"data": {
|
||||
"document": ["来源:example.com\n检索时间:2025-12-27T17:50:03.472550"],
|
||||
"metadata": [
|
||||
{
|
||||
"source": "example.com",
|
||||
"url": "https://example.com",
|
||||
"date_accessed": "2025-12-27T17:50:03.472550",
|
||||
"type": "web_search_result"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"name": "example.com",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 搜索查询展示
|
||||
|
||||
如果响应包含 `grounding_metadata.web_search_queries`,会在界面上显示使用的搜索查询:
|
||||
|
||||
```
|
||||
🔍 使用了 4 个搜索查询
|
||||
```
|
||||
|
||||
### 4. 处理状态提示
|
||||
|
||||
成功处理 citations 后,会显示状态提示:
|
||||
|
||||
```
|
||||
✓ 已处理 7 个引用来源
|
||||
```
|
||||
|
||||
## 在 Open WebUI 中的展示效果
|
||||
|
||||
### 引用链接
|
||||
每个引用来源会在聊天界面中显示为可点击的链接,用户可以:
|
||||
- 点击查看原始来源
|
||||
- 查看检索时间等元数据
|
||||
- 了解信息的出处
|
||||
|
||||
### 搜索查询历史
|
||||
显示模型使用的搜索查询列表,帮助用户了解:
|
||||
- 模型如何理解和分解查询
|
||||
- 执行了哪些搜索操作
|
||||
- 搜索策略是否合理
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1:Gemini Search 模型
|
||||
|
||||
当使用支持搜索的 Gemini 模型时:
|
||||
|
||||
```python
|
||||
# 用户提问
|
||||
"MiniMax 最新的模型是什么?"
|
||||
|
||||
# 模型响应会包含 citations
|
||||
{
|
||||
"content": "MiniMax 最新的模型是 M2.1...",
|
||||
"citations": [
|
||||
{"source": {"url": "https://qbitai.com/...", "name": "qbitai.com"}, ...},
|
||||
{"source": {"url": "https://minimax.io/...", "name": "minimax.io"}, ...}
|
||||
]
|
||||
}
|
||||
|
||||
# Open WebUI 界面显示:
|
||||
# 1. 模型的回答内容
|
||||
# 2. 可点击的引用链接:qbitai.com, minimax.io 等
|
||||
# 3. 状态提示:✓ 已处理 7 个引用来源
|
||||
```
|
||||
|
||||
### 示例 2:自定义 Pipe/Filter
|
||||
|
||||
如果你的自定义 Pipe 或 Filter 返回包含 citations 的数据:
|
||||
|
||||
```python
|
||||
class CustomPipe:
|
||||
def pipe(self, body: dict) -> dict:
|
||||
# 执行搜索或检索操作
|
||||
search_results = self.perform_search(query)
|
||||
|
||||
# 构建响应,包含 citations
|
||||
return {
|
||||
"messages": [{
|
||||
"role": "assistant",
|
||||
"content": "基于搜索结果...",
|
||||
"citations": [
|
||||
{
|
||||
"source": {"url": url, "name": domain},
|
||||
"retrieved_at": datetime.now().isoformat()
|
||||
}
|
||||
for url, domain in search_results
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`context_enhancement_filter` 会自动处理这些 citations 并在界面中展示。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 启用 Filter
|
||||
|
||||
1. 在 Open WebUI 中安装 `context_enhancement_filter.py`
|
||||
2. 确保 Filter 已启用
|
||||
3. Citations 处理功能自动生效,无需额外配置
|
||||
|
||||
### 与其他功能的集成
|
||||
|
||||
Citations 处理与 Filter 的其他功能(环境变量注入、内容规范化等)无缝集成:
|
||||
|
||||
- **环境变量注入**:在 inlet 阶段处理
|
||||
- **内容规范化**:在 outlet 阶段处理
|
||||
- **Citations 处理**:在 outlet 阶段处理(与内容规范化并行)
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 异步处理
|
||||
|
||||
Citations 处理使用异步方式,不会阻塞主响应流:
|
||||
|
||||
```python
|
||||
asyncio.create_task(self._process_citations(body, __event_emitter__))
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 完善的异常捕获,确保 citations 处理失败不影响正常响应
|
||||
- 详细的日志记录,便于问题排查
|
||||
- 优雅降级:如果处理失败,用户仍能看到模型回答
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 支持多种 citations 数据格式
|
||||
- 向后兼容:不包含 citations 的响应不受影响
|
||||
- 与 Open WebUI 原生 citations 系统完全兼容
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Citations 没有显示?
|
||||
|
||||
**A:** 检查以下几点:
|
||||
1. 模型响应是否包含 `citations` 字段
|
||||
2. Filter 是否已启用
|
||||
3. `__event_emitter__` 是否正常工作
|
||||
4. 浏览器控制台是否有错误信息
|
||||
|
||||
### Q: 如何自定义 citations 展示格式?
|
||||
|
||||
**A:** 修改 `_process_citations` 方法中的 `document_text` 构建逻辑:
|
||||
|
||||
```python
|
||||
# 自定义文档文本格式
|
||||
document_text = f"""
|
||||
📄 **{source_name}**
|
||||
🔗 {source_url}
|
||||
⏰ {retrieved_at}
|
||||
"""
|
||||
```
|
||||
|
||||
### Q: 可以禁用 citations 处理吗?
|
||||
|
||||
**A:** 可以通过以下方式禁用:
|
||||
|
||||
1. **临时禁用**:在 outlet 方法中注释掉 citations 处理代码
|
||||
2. **条件禁用**:添加配置选项到 Valves:
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
enable_citations_processing: bool = Field(
|
||||
default=True,
|
||||
description="启用 citations 自动处理"
|
||||
)
|
||||
```
|
||||
|
||||
## 与 websearch.py 的对比
|
||||
|
||||
| 特性 | websearch.py | context_enhancement_filter.py |
|
||||
|------|--------------|-------------------------------|
|
||||
| 功能 | 主动执行搜索 | 被动处理已有 citations |
|
||||
| 时机 | inlet (请求前) | outlet (响应后) |
|
||||
| 搜索引擎 | SearxNG | 依赖模型自身 |
|
||||
| 内容注入 | 直接修改用户消息 | 不修改消息 |
|
||||
| 引用展示 | ✅ 支持 | ✅ 支持 |
|
||||
|
||||
**推荐使用场景:**
|
||||
- 使用支持搜索的模型(如 Gemini Search):启用 `context_enhancement_filter`
|
||||
- 使用不支持搜索的模型:启用 `websearch.py`
|
||||
- 需要自定义搜索源:使用 `websearch.py` 配置 SearxNG
|
||||
|
||||
## 未来改进方向
|
||||
|
||||
1. **更丰富的元数据展示**
|
||||
- 显示网页标题、摘要
|
||||
- 展示相关性评分
|
||||
- 支持缩略图预览
|
||||
|
||||
2. **智能内容提取**
|
||||
- 从响应内容中提取被引用的具体段落
|
||||
- 高亮显示引用来源对应的文本
|
||||
|
||||
3. **引用验证**
|
||||
- 检查引用链接的有效性
|
||||
- 提供存档链接备份
|
||||
|
||||
4. **用户配置选项**
|
||||
- 自定义引用展示格式
|
||||
- 选择性显示/隐藏某些元数据
|
||||
- 引用分组和排序选项
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Open WebUI Plugin 开发文档](../../docs/features/plugin/tools/development.mdx)
|
||||
- [Event Emitter 使用指南](../../docs/features/plugin/development/events.mdx)
|
||||
- [WebSearch Filter 实现](../websearch/websearch.py)
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个功能!
|
||||
Reference in New Issue
Block a user