feat: Update Smart Mind Map plugin to version 0.8.0 with enhanced features and improved styling

- Added author information and funding URL to the plugin metadata.
- Improved description for better clarity on functionality.
- Refactored CSS for better responsiveness and visual appeal, including dark theme support.
- Enhanced JavaScript for improved user interaction, including new controls for zooming and theme toggling.
- Updated download functionality to support PNG format with improved scaling and styling.
- Added user context extraction for better user information handling.
- Updated error handling and logging for better debugging and user feedback.
- Translated plugin title and description to Chinese for localization.
This commit is contained in:
fujie
2025-12-31 02:00:10 +08:00
parent 048997d37e
commit 6bae24ab74
4 changed files with 776 additions and 192 deletions

View File

@@ -1,6 +1,6 @@
# Smart Mind Map - Mind Mapping Generation Plugin
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.7.2 | **License:** MIT
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.8.0 | **License:** MIT
> **Important**: To ensure the maintainability and usability of all plugins, each plugin should be accompanied by clear and comprehensive documentation to ensure its functionality, configuration, and usage are well explained.
@@ -12,9 +12,13 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
-**Intelligent Text Analysis**: Automatically identifies core themes, key concepts, and hierarchical structures
-**Interactive Visualization**: Generates beautiful interactive mind maps based on Markmap.js
-**High-Resolution PNG Export**: Export mind maps as high-quality PNG images (9x scale, ~1-2MB file size)
-**Complete Control Panel**: Zoom controls (+/-/reset), expand level selection (All/2/3 levels), and fullscreen mode
-**Theme Switching**: Manual theme toggle button (light/dark) with automatic theme detection
-**Dark Mode Support**: Full dark mode support with automatic detection and manual override
-**Multi-language Support**: Automatically adjusts output based on user language
-**Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
-**Export Capabilities**: Supports copying SVG code and Markdown source
-**Export Capabilities**: Supports PNG, SVG code, and Markdown source export
-**Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
---
@@ -24,8 +28,10 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
1. **Text Extraction**: Extracts text content from user messages (automatically filters HTML code blocks)
2. **Intelligent Analysis**: Analyzes text structure using the configured LLM model
3. **Markdown Generation**: Converts analysis results to Markmap-compatible Markdown format
4. **Visual Rendering**: Renders the mind map using Markmap.js in an HTML template
5. **Interactive Display**: Presents the mind map to users in an interactive format within the chat interface
4. **Visual Rendering**: Renders the mind map using Markmap.js in an HTML template with optimized font hierarchy (H1: 22px bold, H2: 18px bold)
5. **Interactive Display**: Presents the mind map to users in an interactive format with complete control panel
6. **Theme Detection**: Automatically detects and applies the current OpenWebUI theme (light/dark mode)
7. **Export Options**: Provides PNG (high-resolution), SVG, and Markdown export functionality
---
@@ -72,6 +78,8 @@ You can adjust the following parameters in the plugin's settings (Valves):
| `show_status` | `true` | Whether to display operation status updates in the chat interface (e.g., "Analyzing..."). |
| `LLM_MODEL_ID` | `gemini-2.5-flash` | LLM model ID for text analysis. Recommended to use fast and economical models. |
| `MIN_TEXT_LENGTH` | `100` | Minimum text length (in characters) required for mind map analysis. Text that's too short cannot generate valid mind maps. |
| `CLEAR_PREVIOUS_HTML` | `false` | Whether to clear previous plugin-generated HTML content when generating a new mind map. |
| `MESSAGE_COUNT` | `1` | Number of recent messages to use for mind map generation (1-5). |
---
@@ -102,10 +110,20 @@ The plugin will generate an interactive mind map centered on "Artificial Intelli
### Export Features
Generated mind maps support two export methods:
Generated mind maps support three export methods:
1. **Copy SVG Code**: Click the "Copy SVG Code" button to copy the mind map in SVG format to the clipboard
2. **Copy Markdown**: Click the "Copy Markdown" button to copy the raw Markdown format to the clipboard
1. **Download PNG**: Click the "📥 Download PNG" button to export the mind map as a high-resolution PNG image (9x scale, ~1-2MB file size)
2. **Copy SVG Code**: Click the "Copy SVG Code" button to copy the mind map in SVG format to the clipboard
3. **Copy Markdown**: Click the "Copy Markdown" button to copy the raw Markdown format to the clipboard
### Control Panel
The interactive mind map includes a comprehensive control panel:
- **Zoom Controls**: `+` (zoom in), `-` (zoom out), `↻` (reset view)
- **Expand Level**: Switch between "All", "2 Levels", "3 Levels" to control node expansion depth
- **Fullscreen**: Enter fullscreen mode for better viewing experience
- **Theme Toggle**: Manually switch between light and dark themes
---
@@ -116,6 +134,23 @@ Generated mind maps support two export methods:
- **Markmap.js**: Open-source mind mapping rendering engine
- **D3.js**: Data visualization foundation library
- **Responsive Design**: Adapts to different screen sizes
- **Font Hierarchy**: Optimized typography with H1 (22px bold) and H2 (18px bold) for better readability
### PNG Export Technology
- **SVG to Canvas Conversion**: Converts mind map SVG to canvas for PNG export
- **ForeignObject Handling**: Properly processes HTML content within SVG elements
- **High Resolution**: 9x scale factor for print-quality output (~1-2MB file size)
- **Theme Preservation**: Maintains current theme (light/dark) in exported PNG
### Theme Detection Mechanism
Automatically detects and applies themes with a 4-level priority:
1. **Explicit Toggle**: User manually clicks theme toggle button (highest priority)
2. **Meta Tag**: Reads `<meta name="theme-color">` from parent document
3. **Class/Data-Theme**: Checks `class` or `data-theme` attributes on parent HTML/body
4. **System Preference**: Falls back to `prefers-color-scheme` media query
### Backend Processing
@@ -123,10 +158,11 @@ Generated mind maps support two export methods:
- **Text Preprocessing**: Automatically filters HTML code blocks, extracts plain text content
- **Format Conversion**: Converts LLM output to Markmap-compatible Markdown format
### Security
### Security Enhancements
- **XSS Protection**: Automatically escapes `</script>` tags to prevent script injection
- **Input Validation**: Checks text length to avoid invalid requests
- **Non-Bubbling Events**: Button clicks use `stopPropagation()` to prevent navigation interception
---
@@ -170,6 +206,30 @@ Generated mind maps support two export methods:
- Verify generated Markdown format conforms to Markmap specifications
- Try refreshing the page to re-render
### Issue: PNG Export Not Working
**Symptom:** PNG download button doesn't work or produces blank/corrupted images
**Solution:**
- Ensure browser supports HTML5 Canvas API (all modern browsers do)
- Check browser console for errors related to `toDataURL()` or canvas rendering
- Verify the mind map is fully rendered before clicking export
- Try refreshing the page and re-generating the mind map
- Use Chrome or Firefox for best PNG export compatibility
### Issue: Theme Not Auto-Detected
**Symptom:** Mind map doesn't match OpenWebUI theme colors
**Solution:**
- Enable "iframe Sandbox Allow Same Origin" in OpenWebUI Settings → Interface → Artifacts
- Verify the iframe's sandbox attribute includes both `allow-same-origin` and `allow-scripts`
- Ensure parent document has `<meta name="theme-color">` tag or theme class/attribute
- Use the manual theme toggle button to override automatic detection
- Check browser console for cross-origin errors
### Issue: Export Function Not Working
**Solution:**
@@ -197,11 +257,52 @@ Generated mind maps support two export methods:
- For particularly long texts, consider summarizing before generating mind maps
- Disable `show_status` in production environments to reduce interface updates
4. **Export Quality**
- **PNG Export**: Best for presentations, documents, and sharing (9x resolution suitable for printing)
- **SVG Export**: Best for further editing in vector graphics tools (infinite scalability)
- **Markdown Export**: Best for version control, collaboration, and regeneration
5. **Theme Consistency**
- Enable same-origin access for automatic theme detection
- Use manual theme toggle if automatic detection fails
- Export PNG after switching to desired theme for consistent visuals
---
## Requirements
This plugin uses only OpenWebUI's built-in dependencies. **No additional packages need to be installed.**
---
## Changelog
### v0.7.2 (Current Version)
### v0.8.0 (Current Version)
**Major Features:**
- Added high-resolution PNG export (9x scale, ~1-2MB file size)
- Implemented complete control panel with zoom controls (+/-/reset)
- Added expand level selection (All/2/3 levels)
- Integrated fullscreen mode with auto-fit
- Added manual theme toggle button (light/dark)
- Implemented automatic theme detection with 4-level priority
**Improvements:**
- Optimized font hierarchy (H1: 22px bold, H2: 18px bold)
- Enhanced dark mode with full theme support
- Improved PNG export technology (SVG to Canvas with foreignObject handling)
- Added theme preservation in exported PNG images
- Enhanced security with non-bubbling button events
**Bug Fixes:**
- Fixed theme detection in cross-origin iframes
- Resolved PNG export issues with HTML content in SVG
- Improved compatibility with OpenWebUI theme system
### v0.7.2
- Optimized text extraction logic, automatically filters HTML code blocks
- Improved error handling and user feedback

View File

@@ -1,6 +1,6 @@
# 思维导图 - 思维导图生成插件
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.2 | **许可证:** MIT
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.8.0 | **许可证:** MIT
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
@@ -10,22 +10,28 @@
## 核心特性
-**智能文本分析**: 自动识别文本的核心主题、关键概念和层次结构
-**交互式可视化**: 基于 Markmap.js 生成美观的交互式思维导图
-**多语言支持**: 根据用户语言自动调整输出
-**实时渲染**: 在聊天界面中直接渲染思维导图,无需跳转
-**导出功能**: 支持复制 SVG 代码和 Markdown 源码
-**自定义配置**: 可配置 LLM 模型、最小文本长度等参数
-**智能文本分析**自动识别文本的核心主题、关键概念和层次结构
-**交互式可视化**基于 Markmap.js 生成美观的交互式思维导图
-**高分辨率 PNG 导出**:导出高质量的 PNG 图片9 倍分辨率,约 1-2MB 文件大小)
-**完整控制面板**:缩放控制(+/-/重置)、展开层级选择(全部/2级/3级、全屏模式
-**主题切换**:手动主题切换按钮(亮色/暗色)与自动主题检测
-**深色模式支持**:完整的深色模式支持,自动检测与手动覆盖
-**多语言支持**:根据用户语言自动调整输出
-**实时渲染**:在聊天界面中直接渲染思维导图,无需跳转
-**导出功能**:支持 PNG、SVG 代码和 Markdown 源码导出
-**自定义配置**:可配置 LLM 模型、最小文本长度等参数
---
## 工作原理
1. **文本提取**: 从用户消息中提取文本内容(自动过滤 HTML 代码块)
2. **智能分析**: 使用配置的 LLM 模型分析文本结构
3. **Markdown 生成**: 将分析结果转换为 Markmap 兼容的 Markdown 格式
4. **可视化渲染**: 在 HTML 模板中使用 Markmap.js 渲染思维导图
5. **交互展示**: 在聊天界面中以可交互的形式展示给用户
1. **文本提取**从用户消息中提取文本内容(自动过滤 HTML 代码块)
2. **智能分析**使用配置的 LLM 模型分析文本结构
3. **Markdown 生成**将分析结果转换为 Markmap 兼容的 Markdown 格式
4. **可视化渲染**在 HTML 模板中使用 Markmap.js 渲染思维导图并优化字体层级H122px 粗体H218px 粗体)
5. **交互展示**以可交互的形式展示给用户,并提供完整的控制面板
6. **主题检测**:自动检测并应用当前 OpenWebUI 的主题(亮色/暗色模式)
7. **导出选项**:提供 PNG高分辨率、SVG 和 Markdown 导出功能
---
@@ -72,6 +78,8 @@
| `show_status` | `true` | 是否在聊天界面显示操作状态更新(如"正在分析...")。 |
| `LLM_MODEL_ID` | `gemini-2.5-flash` | 用于文本分析的 LLM 模型 ID。推荐使用快速且经济的模型。 |
| `MIN_TEXT_LENGTH` | `100` | 进行思维导图分析所需的最小文本长度(字符数)。文本过短将无法生成有效的导图。 |
| `CLEAR_PREVIOUS_HTML` | `false` | 在生成新的思维导图时,是否清除之前由插件生成的 HTML 内容。 |
| `MESSAGE_COUNT` | `1` | 用于生成思维导图的最近消息数量1-5。 |
---
@@ -102,10 +110,20 @@
### 导出功能
生成的思维导图支持种导出方式:
生成的思维导图支持种导出方式:
1. **复制 SVG 代码**: 点击"复制 SVG 代码"按钮,可将思维导图的 SVG 格式复制到剪贴板
2. **复制 Markdown**: 点击"复制 Markdown"按钮,可将原始 Markdown 格式复制到剪贴板
1. **下载 PNG**:点击“📥 下载 PNG”按钮可将思维导图导出为高分辨率 PNG 图片9 倍分辨率,约 1-2MB 文件大小)
2. **复制 SVG 代码**点击复制 SVG 代码”按钮,可将思维导图的 SVG 格式复制到剪贴板
3. **复制 Markdown**:点击“复制 Markdown”按钮可将原始 Markdown 格式复制到剪贴板
### 控制面板
交互式思维导图包含完整的控制面板:
- **缩放控制**`+`(放大)、`-`(缩小)、`↻`(重置视图)
- **展开层级**在“全部”、“2 级”、“3 级”之间切换,控制节点展开深度
- **全屏模式**:进入全屏模式,获得更好的查看体验
- **主题切换**:手动在亮色和暗色主题之间切换
---
@@ -113,20 +131,38 @@
### 前端渲染
- **Markmap.js**: 开源的思维导图渲染引擎
- **D3.js**: 数据可视化基础库
- **响应式设计**: 适配不同屏幕尺寸
- **Markmap.js**开源的思维导图渲染引擎
- **D3.js**数据可视化基础库
- **响应式设计**适配不同屏幕尺寸
- **字体层级**优化的字体排版H122px 粗体)和 H218px 粗体),提供更好的可读性
### PNG 导出技术
- **SVG 转 Canvas**:将思维导图 SVG 转换为 Canvas 以导出 PNG
- **ForeignObject 处理**:正确处理 SVG 元素中的 HTML 内容
- **高分辨率**9 倍缩放因子,输出打印级质量(约 1-2MB 文件大小)
- **主题保持**:在导出的 PNG 中保持当前主题(亮色/暗色)
### 主题检测机制
自动检测并应用主题,具有 4 级优先级:
1. **显式切换**:用户手动点击主题切换按钮(最高优先级)
2. **Meta 标签**:从父文档读取 `<meta name="theme-color">`
3. **Class/Data-Theme**:检查父文档 HTML/body 的 `class``data-theme` 属性
4. **系统偏好**:回退到 `prefers-color-scheme` 媒体查询
### 后端处理
- **LLM 集成**: 通过 `generate_chat_completion` 调用配置的模型
- **文本预处理**: 自动过滤 HTML 代码块,提取纯文本内容
- **格式转换**: 将 LLM 输出转换为 Markmap 兼容的 Markdown 格式
- **LLM 集成**通过 `generate_chat_completion` 调用配置的模型
- **文本预处理**自动过滤 HTML 代码块,提取纯文本内容
- **格式转换**将 LLM 输出转换为 Markmap 兼容的 Markdown 格式
### 安全性
### 安全性增强
- **XSS 防护**: 自动转义 `</script>` 标签,防止脚本注入
- **输入验证**: 检查文本长度,避免无效请求
- **XSS 防护**自动转义 `</script>` 标签,防止脚本注入
- **输入验证**检查文本长度,避免无效请求
- **非冒泡事件**:按钮点击使用 `stopPropagation()` 防止导航拦截
---
@@ -170,6 +206,30 @@
- 验证生成的 Markdown 格式是否符合 Markmap 规范
- 尝试刷新页面重新渲染
### 问题PNG 导出不工作
**现象:**PNG 下载按钮不工作或生成空白/损坏的图片
**解决方案:**
- 确保浏览器支持 HTML5 Canvas API所有现代浏览器都支持
- 检查浏览器控制台是否有与 `toDataURL()` 或 Canvas 渲染相关的错误
- 确保思维导图在点击导出前已完全渲染
- 尝试刷新页面并重新生成思维导图
- 使用 Chrome 或 Firefox获得最佳 PNG 导出兼容性
### 问题:主题未自动检测
**现象:**思维导图不匹配 OpenWebUI 主题颜色
**解决方案:**
- 在 OpenWebUI 设置 → 界面 → 产物中启用“iframe 沙盒允许同源访问”
- 验证 iframe 的 sandbox 属性包含 `allow-same-origin``allow-scripts`
- 确保父文档有 `<meta name="theme-color">` 标签或主题 class/属性
- 使用手动主题切换按钮覆盖自动检测
- 检查浏览器控制台是否有跨域错误
### 问题:导出功能不工作
**解决方案:**
@@ -197,11 +257,52 @@
- 对于特别长的文本,考虑先进行摘要再生成思维导图
- 在生产环境中关闭 `show_status` 以减少界面更新
4. **导出质量**
- **PNG 导出**最适合演示、文档和分享9 倍分辨率适合打印)
- **SVG 导出**:最适合在矢量图形工具中进一步编辑(无限缩放)
- **Markdown 导出**:最适合版本控制、协作和重新生成
5. **主题一致性**
- 启用同源访问以实现自动主题检测
- 如果自动检测失败,使用手动主题切换
- 在切换到所需主题后导出 PNG以保持视觉一致性
---
## 依赖要求
本插件仅使用 OpenWebUI 的内置依赖,**无需安装额外的软件包。**
---
## 更新日志
### v0.7.2 (当前版本)
### v0.8.0当前版本
**主要功能:**
- 添加高分辨率 PNG 导出9 倍分辨率,约 1-2MB 文件大小)
- 实现完整的控制面板,包含缩放控制(+/-/重置)
- 添加展开层级选择(全部/2级/3级
- 集成全屏模式,自动适应
- 添加手动主题切换按钮(亮色/暗色)
- 实现 4 级优先级的自动主题检测
**改进项:**
- 优化字体层级H122px 粗体H218px 粗体)
- 增强深色模式,完整的主题支持
- 改进 PNG 导出技术SVG 转 Canvas处理 foreignObject
- 在导出的 PNG 图片中保持主题
- 增强安全性,按钮事件使用非冒泡机制
**Bug 修复:**
- 修复跨域 iframe 中的主题检测问题
- 解决 SVG 中 HTML 内容的 PNG 导出问题
- 改进与 OpenWebUI 主题系统的兼容性
### v0.7.2
- 优化文本提取逻辑,自动过滤 HTML 代码块
- 改进错误处理和用户反馈

View File

@@ -1,18 +1,23 @@
"""
title: Smart Mind Map
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.8.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
version: 0.7.4
description: Intelligently analyzes long texts and generates interactive mind maps, supporting SVG/Markdown export.
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging
import time
import os
import re
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from zoneinfo import ZoneInfo
from fastapi import Request
from datetime import datetime
import pytz
from pydantic import BaseModel, Field
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
@@ -75,24 +80,20 @@ HTML_WRAPPER_TEMPLATE = """
}
#main-container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 20px;
align-items: flex-start;
align-items: stretch;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* Default width, allows stretching */
min-width: 300px;
width: 100%;
border-radius: 12px;
overflow: hidden;
overflow: visible;
transition: all 0.3s ease;
}
.plugin-item:hover {
transform: translateY(-2px);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
@@ -111,17 +112,32 @@ CSS_TEMPLATE_MINDMAP = """
--secondary-color: #43a047;
--background-color: #f4f6f8;
--card-bg-color: #ffffff;
--text-color: #263238;
--text-color: #000000;
--link-color: #546e7a;
--node-stroke-color: #90a4ae;
--muted-text-color: #546e7a;
--border-color: #e0e0e0;
--header-gradient: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
--shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
--shadow: 0 10px 20px rgba(0, 0, 0, 0.06);
--border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.theme-dark {
--primary-color: #64b5f6;
--secondary-color: #81c784;
--background-color: #111827;
--card-bg-color: #1f2937;
--text-color: #ffffff;
--link-color: #cbd5e1;
--node-stroke-color: #94a3b8;
--muted-text-color: #9ca3af;
--border-color: #374151;
--header-gradient: linear-gradient(135deg, #0ea5e9, #22c55e);
--shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.mindmap-container-wrapper {
font-family: var(--font-family);
line-height: 1.7;
line-height: 1.6;
color: var(--text-color);
margin: 0;
padding: 0;
@@ -130,99 +146,142 @@ CSS_TEMPLATE_MINDMAP = """
height: 100%;
display: flex;
flex-direction: column;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.header {
background: var(--header-gradient);
color: white;
padding: 20px 24px;
padding: 18px 20px;
text-align: center;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.header h1 {
margin: 0;
font-size: 1.5em;
font-size: 1.4em;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
letter-spacing: 0.3px;
}
.user-context {
font-size: 0.8em;
font-size: 0.85em;
color: var(--muted-text-color);
background-color: #eceff1;
padding: 8px 16px;
background-color: rgba(255, 255, 255, 0.6);
padding: 8px 14px;
display: flex;
justify-content: space-around;
justify-content: space-between;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
gap: 6px;
}
.user-context span { margin: 2px 8px; }
.content-area {
padding: 20px;
.theme-dark .user-context {
background-color: rgba(31, 41, 55, 0.7);
}
.user-context span { margin: 2px 6px; }
.content-area {
padding: 16px;
flex-grow: 1;
background: var(--card-bg-color);
}
.markmap-container {
position: relative;
background-color: #fff;
background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px);
background-size: 20px 20px;
border-radius: 8px;
padding: 16px;
background-color: var(--card-bg-color);
border-radius: 10px;
padding: 12px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: inset 0 2px 6px rgba(0,0,0,0.03);
width: 100%;
min-height: 60vh;
overflow: visible;
}
.download-area {
text-align: center;
padding-top: 20px;
margin-top: 20px;
border-top: 1px solid var(--border-color);
.markmap-container svg {
width: 100%;
height: 100%;
}
.download-btn {
.markmap-container svg text {
fill: var(--text-color) !important;
font-family: var(--font-family);
}
.markmap-container svg foreignObject,
.markmap-container svg .markmap-foreign,
.markmap-container svg .markmap-foreign div {
color: var(--text-color) !important;
font-family: var(--font-family);
}
.markmap-container svg .markmap-link {
stroke: var(--link-color) !important;
}
.markmap-container svg .markmap-node circle,
.markmap-container svg .markmap-node rect {
stroke: var(--node-stroke-color) !important;
}
.control-rows {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 12px;
}
.btn-group {
display: inline-flex;
gap: 6px;
align-items: center;
}
.control-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
padding: 8px 12px;
border-radius: 8px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
margin: 0 6px;
transition: background-color 0.15s ease, transform 0.15s ease;
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
box-sizing: border-box;
}
.download-btn.secondary {
background-color: var(--secondary-color);
}
.download-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.download-btn.copied {
background-color: #2e7d32;
select.control-btn {
appearance: none;
padding-right: 28px;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 10px;
}
.control-btn.secondary { background-color: var(--secondary-color); }
.control-btn.neutral { background-color: #64748b; }
.control-btn:hover { transform: translateY(-1px); }
.control-btn.copied { background-color: #2e7d32; }
.control-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.footer {
text-align: center;
padding: 16px;
font-size: 0.8em;
color: #90a4ae;
background-color: #eceff1;
padding: 12px;
font-size: 0.85em;
color: var(--muted-text-color);
background-color: var(--card-bg-color);
border-top: 1px solid var(--border-color);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.footer a {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.footer a:hover { text-decoration: underline; }
.error-message {
color: #c62828;
background-color: #ffcdd2;
border: 1px solid #ef9a9a;
padding: 16px;
padding: 14px;
border-radius: 8px;
font-weight: 500;
font-size: 1em;
@@ -240,15 +299,32 @@ CONTENT_TEMPLATE_MINDMAP = """
</div>
<div class="content-area">
<div class="markmap-container" id="markmap-container-{unique_id}"></div>
<div class="download-area">
<button id="download-svg-btn-{unique_id}" class="download-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<span class="btn-text">SVG</span>
</button>
<button id="download-md-btn-{unique_id}" class="download-btn secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span class="btn-text">Markdown</span>
</button>
<div class="control-rows">
<div class="btn-group">
<button id="download-png-btn-{unique_id}" class="control-btn secondary">
<span class="btn-text">PNG</span>
</button>
<button id="download-svg-btn-{unique_id}" class="control-btn">
<span class="btn-text">SVG</span>
</button>
<button id="download-md-btn-{unique_id}" class="control-btn neutral">
<span class="btn-text">Markdown</span>
</button>
</div>
<div class="btn-group">
<button id="zoom-out-btn-{unique_id}" class="control-btn neutral" title="Zoom Out">-</button>
<button id="zoom-reset-btn-{unique_id}" class="control-btn neutral" title="Reset">Reset</button>
<button id="zoom-in-btn-{unique_id}" class="control-btn neutral" title="Zoom In">+</button>
</div>
<div class="btn-group">
<select id="depth-select-{unique_id}" class="control-btn secondary" title="Expand Level">
<option value="0" selected>Expand All</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
</select>
<button id="fullscreen-btn-{unique_id}" class="control-btn">Fullscreen</button>
<button id="theme-toggle-btn-{unique_id}" class="control-btn neutral">Theme</button>
</div>
</div>
</div>
<div class="footer">
@@ -260,13 +336,105 @@ CONTENT_TEMPLATE_MINDMAP = """
"""
SCRIPT_TEMPLATE_MINDMAP = """
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<script>
(function() {
const uniqueId = "{unique_id}";
const loadScriptOnce = (src, checkFn) => {
if (checkFn()) return Promise.resolve();
return new Promise((resolve, reject) => {
const existing = document.querySelector(`script[data-src="${src}"]`);
if (existing) {
existing.addEventListener('load', () => resolve());
existing.addEventListener('error', () => reject(new Error('Loading failed: ' + src)));
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.dataset.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Loading failed: ' + src));
document.head.appendChild(script);
});
};
const ensureMarkmapReady = () =>
loadScriptOnce('https://cdn.jsdelivr.net/npm/d3@7', () => window.d3)
.then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-lib@0.17', () => window.markmap && window.markmap.Transformer))
.then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-view@0.17', () => window.markmap && window.markmap.Markmap));
const parseColorLuma = (colorStr) => {
if (!colorStr) return null;
// hex #rrggbb or rrggbb
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
if (m) {
const hex = m[1];
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
// rgb(r, g, b) or rgba(r, g, b, a)
m = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
return null;
};
const getThemeFromMeta = (doc, scope = 'self') => {
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
if (!metas.length) return null;
const color = metas[metas.length - 1].content.trim();
const luma = parseColorLuma(color);
if (luma === null) return null;
return luma < 0.5 ? 'dark' : 'light';
};
const getParentDocumentSafe = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
void pDoc.title;
return pDoc;
} catch (err) {
return null;
}
};
const getThemeFromParentClass = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
const html = pDoc.documentElement;
const body = pDoc.body;
const htmlClass = html ? html.className : '';
const bodyClass = body ? body.className : '';
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
return null;
} catch (err) {
return null;
}
};
const setTheme = (wrapperEl, explicitTheme) => {
const parentDoc = getParentDocumentSafe();
const metaThemeParent = parentDoc ? getThemeFromMeta(parentDoc, 'parent') : null;
const parentClassTheme = getThemeFromParentClass();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const chosen = explicitTheme || metaThemeParent || parentClassTheme || (prefersDark ? 'dark' : 'light');
wrapperEl.classList.toggle('theme-dark', chosen === 'dark');
return chosen;
};
const renderMindmap = () => {
const uniqueId = "{unique_id}";
const containerEl = document.getElementById('markmap-container-' + uniqueId);
if (!containerEl || containerEl.dataset.markmapRendered) return;
@@ -275,71 +443,83 @@ SCRIPT_TEMPLATE_MINDMAP = """
const markdownContent = sourceEl.textContent.trim();
if (!markdownContent) {
containerEl.innerHTML = '<div class="error-message">⚠️ Unable to load mind map: Missing valid content.</div>';
containerEl.innerHTML = '<div class=\"error-message\">⚠️ Unable to load mind map: Missing valid content.</div>';
return;
}
try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
ensureMarkmapReady().then(() => {
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.style.width = '100%';
svgEl.style.height = 'auto';
svgEl.style.minHeight = '300px';
containerEl.innerHTML = '';
svgEl.style.height = '100%';
svgEl.style.minHeight = '60vh';
containerEl.innerHTML = '';
containerEl.appendChild(svgEl);
const { Transformer, Markmap } = window.markmap;
const transformer = new Transformer();
const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 14px !important; }`;
const options = {
const style = (id) => `
${id} text, ${id} foreignObject { font-size: 14px; }
${id} foreignObject h1 { font-size: 22px; font-weight: 700; margin: 0; }
${id} foreignObject h2 { font-size: 18px; font-weight: 600; margin: 0; }
${id} foreignObject strong { font-weight: 700; }
`;
const options = {
autoFit: true,
style: style
style: style,
initialExpandLevel: Infinity,
zoom: true,
pan: true
};
Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
attachDownloadHandlers(uniqueId);
} catch (error) {
console.error('Markmap rendering error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ Mind map rendering failed!<br>Reason: ' + error.message + '</div>';
}
const markmapInstance = Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
setupControls({
containerEl,
svgEl,
markmapInstance,
root,
});
}).catch((error) => {
console.error('Markmap loading error:', error);
containerEl.innerHTML = '<div class=\"error-message\">⚠️ Resource loading failed, please try again later.</div>';
});
};
const attachDownloadHandlers = (uniqueId) => {
const setupControls = ({ containerEl, svgEl, markmapInstance, root }) => {
const downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId);
const downloadPngBtn = document.getElementById('download-png-btn-' + uniqueId);
const downloadMdBtn = document.getElementById('download-md-btn-' + uniqueId);
const containerEl = document.getElementById('markmap-container-' + uniqueId);
const zoomInBtn = document.getElementById('zoom-in-btn-' + uniqueId);
const zoomOutBtn = document.getElementById('zoom-out-btn-' + uniqueId);
const zoomResetBtn = document.getElementById('zoom-reset-btn-' + uniqueId);
const depthSelect = document.getElementById('depth-select-' + uniqueId);
const fullscreenBtn = document.getElementById('fullscreen-btn-' + uniqueId);
const themeToggleBtn = document.getElementById('theme-toggle-btn-' + uniqueId);
const showFeedback = (button, isSuccess) => {
const buttonText = button.querySelector('.btn-text');
const wrapper = containerEl.closest('.mindmap-container-wrapper');
let currentTheme = setTheme(wrapper);
const showFeedback = (button, textOk = 'Done', textFail = 'Failed') => {
if (!button) return;
const buttonText = button.querySelector('.btn-text') || button;
const originalText = buttonText.textContent;
button.disabled = true;
if (isSuccess) {
buttonText.textContent = '';
button.classList.add('copied');
} else {
buttonText.textContent = '';
}
buttonText.textContent = textOk;
button.classList.add('copied');
setTimeout(() => {
buttonText.textContent = originalText;
button.disabled = false;
button.classList.remove('copied');
}, 2500);
}, 1800);
};
const copyToClipboard = (content, button) => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
showFeedback(button, true);
}, () => {
showFeedback(button, false);
});
navigator.clipboard.writeText(content).then(() => showFeedback(button), () => showFeedback(button, 'Failed', 'Failed'));
} else {
const textArea = document.createElement('textarea');
textArea.value = content;
@@ -350,32 +530,228 @@ SCRIPT_TEMPLATE_MINDMAP = """
textArea.select();
try {
document.execCommand('copy');
showFeedback(button, true);
showFeedback(button);
} catch (err) {
showFeedback(button, false);
showFeedback(button, 'Failed', 'Failed');
}
document.body.removeChild(textArea);
}
};
if (downloadSvgBtn) {
downloadSvgBtn.addEventListener('click', (event) => {
event.stopPropagation();
const svgEl = containerEl.querySelector('svg');
if (svgEl) {
const svgData = new XMLSerializer().serializeToString(svgEl);
copyToClipboard(svgData, downloadSvgBtn);
}
});
}
const handleDownloadSVG = () => {
const svg = containerEl.querySelector('svg');
if (!svg) return;
// Inline styles before export
const clonedSvg = svg.cloneNode(true);
const style = document.createElement('style');
style.textContent = `
text { font-family: sans-serif; fill: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; }
foreignObject, .markmap-foreign, .markmap-foreign div { color: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; font-family: sans-serif; font-size: 14px; }
h1 { font-size: 22px; font-weight: 700; margin: 0; }
h2 { font-size: 18px; font-weight: 600; margin: 0; }
strong { font-weight: 700; }
.markmap-link { stroke: ${currentTheme === 'dark' ? '#cbd5e1' : '#546e7a'}; }
.markmap-node circle, .markmap-node rect { stroke: ${currentTheme === 'dark' ? '#94a3b8' : '#94a3b8'}; }
`;
clonedSvg.prepend(style);
const svgData = new XMLSerializer().serializeToString(clonedSvg);
copyToClipboard(svgData, downloadSvgBtn);
};
if (downloadMdBtn) {
downloadMdBtn.addEventListener('click', (event) => {
event.stopPropagation();
const markdownContent = document.getElementById('markdown-source-' + uniqueId).textContent;
copyToClipboard(markdownContent, downloadMdBtn);
});
}
const handleDownloadMD = () => {
const markdownContent = document.getElementById('markdown-source-' + uniqueId)?.textContent || '';
if (!markdownContent) return;
copyToClipboard(markdownContent, downloadMdBtn);
};
const handleDownloadPNG = () => {
const btn = downloadPngBtn;
const originalText = btn.querySelector('.btn-text').textContent;
btn.querySelector('.btn-text').textContent = 'Generating...';
btn.disabled = true;
const svg = containerEl.querySelector('svg');
if (!svg) {
btn.querySelector('.btn-text').textContent = originalText;
btn.disabled = false;
showFeedback(btn, 'Failed', 'Failed');
return;
}
try {
// Clone SVG and inline styles
const clonedSvg = svg.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
const rect = svg.getBoundingClientRect();
const width = rect.width || 800;
const height = rect.height || 600;
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
// Remove foreignObject (HTML content) and replace with text
const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
foreignObjects.forEach(fo => {
const text = fo.textContent || '';
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textEl.setAttribute('x', fo.getAttribute('x') || '0');
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
textEl.setAttribute('fill', currentTheme === 'dark' ? '#ffffff' : '#000000');
textEl.setAttribute('font-family', 'sans-serif');
textEl.setAttribute('font-size', '14');
textEl.textContent = text.trim();
g.appendChild(textEl);
fo.parentNode.replaceChild(g, fo);
});
// Inline styles
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `
text { font-family: sans-serif; font-size: 14px; fill: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; }
.markmap-link { fill: none; stroke: ${currentTheme === 'dark' ? '#cbd5e1' : '#546e7a'}; stroke-width: 2; }
.markmap-node circle { stroke: ${currentTheme === 'dark' ? '#94a3b8' : '#94a3b8'}; stroke-width: 2; }
`;
clonedSvg.insertBefore(style, clonedSvg.firstChild);
// Add background rect
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', currentTheme === 'dark' ? '#1f2937' : '#ffffff');
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const scale = 9;
canvas.width = width * scale;
canvas.height = height * scale;
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.fillStyle = currentTheme === 'dark' ? '#1f2937' : '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
if (!blob) {
btn.querySelector('.btn-text').textContent = originalText;
btn.disabled = false;
showFeedback(btn, 'Failed', 'Failed');
return;
}
// Use non-bubbling MouseEvent to avoid router interception
const a = document.createElement('a');
a.download = 'mindmap.png';
a.href = URL.createObjectURL(blob);
a.style.display = 'none';
document.body.appendChild(a);
const evt = new MouseEvent('click', {
view: window,
bubbles: false,
cancelable: false
});
a.dispatchEvent(evt);
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}, 100);
btn.querySelector('.btn-text').textContent = originalText;
btn.disabled = false;
showFeedback(btn);
}, 'image/png');
};
img.onerror = (e) => {
console.error('PNG image load error:', e);
btn.querySelector('.btn-text').textContent = originalText;
btn.disabled = false;
showFeedback(btn, 'Failed', 'Failed');
};
img.src = dataUrl;
} catch (err) {
console.error('PNG export error:', err);
btn.querySelector('.btn-text').textContent = originalText;
btn.disabled = false;
showFeedback(btn, 'Failed', 'Failed');
}
};
const handleZoom = (direction) => {
if (direction === 'reset') {
markmapInstance.fit();
return;
}
// Simple zoom simulation if d3 zoom instance is not accessible
// Markmap uses d3-zoom, so we can try to select the svg and transition
const svg = d3.select(svgEl);
// We can't easily access the internal zoom behavior object created by markmap
// So we rely on fit() for reset, and maybe just let user scroll/pinch for zoom
// Or we can try to rescale if supported
if (markmapInstance.rescale) {
const scale = direction === 'in' ? 1.25 : 0.8;
markmapInstance.rescale(scale);
} else {
// Fallback: just fit, as manual transform manipulation conflicts with d3
// Or we could try to find the zoom behavior attached to the node
// const zoom = d3.zoomTransform(svgEl);
// But we need the zoom behavior function to call scaleBy
}
};
const handleDepthChange = (e) => {
const level = parseInt(e.target.value, 10);
const expandLevel = level === 0 ? Infinity : level;
// Deep clone root to reset internal state (payload.fold) added by markmap
const cleanRoot = JSON.parse(JSON.stringify(root));
markmapInstance.setOptions({ initialExpandLevel: expandLevel });
markmapInstance.setData(cleanRoot);
markmapInstance.fit();
};
const handleFullscreen = () => {
const el = containerEl;
if (!document.fullscreenElement) {
el.requestFullscreen().then(() => {
setTimeout(() => markmapInstance.fit(), 200);
});
} else {
document.exitFullscreen();
}
};
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement === containerEl) {
setTimeout(() => markmapInstance.fit(), 200);
}
});
const handleThemeToggle = () => {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(wrapper, currentTheme);
};
downloadSvgBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadSVG(); });
downloadMdBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadMD(); });
downloadPngBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadPNG(); });
zoomInBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('in'); });
zoomOutBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('out'); });
zoomResetBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('reset'); });
depthSelect?.addEventListener('change', (e) => { e.stopPropagation(); handleDepthChange(e); });
fullscreenBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleFullscreen(); });
themeToggleBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleThemeToggle(); });
};
if (document.readyState === 'loading') {
@@ -423,6 +799,21 @@ class Action:
"Sunday": "Sunday",
}
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""Extract basic user context with safe fallbacks."""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "User"),
"user_language": user_data.get("language", "en-US"),
}
def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match:
@@ -517,33 +908,21 @@ class Action:
__event_emitter__: Optional[Any] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: Smart Mind Map (v0.7.2) started")
if isinstance(__user__, (list, tuple)):
user_language = (
__user__[0].get("language", "en-US") if __user__ else "en-US"
)
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
user_id = (
__user__[0]["id"]
if __user__ and "id" in __user__[0]
else "unknown_user"
)
elif isinstance(__user__, dict):
user_language = __user__.get("language", "en-US")
user_name = __user__.get("name", "User")
user_id = __user__.get("id", "unknown_user")
logger.info("Action: Smart Mind Map (v0.8.0) started")
user_ctx = self._get_user_context(__user__)
user_language = user_ctx["user_language"]
user_name = user_ctx["user_name"]
user_id = user_ctx["user_id"]
try:
shanghai_tz = pytz.timezone("Asia/Shanghai")
current_datetime_shanghai = datetime.now(shanghai_tz)
current_date_time_str = current_datetime_shanghai.strftime(
"%B %d, %Y %H:%M:%S"
)
current_weekday_en = current_datetime_shanghai.strftime("%A")
tz_env = os.environ.get("TZ")
tzinfo = ZoneInfo(tz_env) if tz_env else None
now_dt = datetime.now(tzinfo or timezone.utc)
current_date_time_str = now_dt.strftime("%B %d, %Y %H:%M:%S")
current_weekday_en = now_dt.strftime("%A")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown")
current_year = current_datetime_shanghai.strftime("%Y")
current_timezone_str = "Asia/Shanghai"
current_year = now_dt.strftime("%Y")
current_timezone_str = tz_env or "UTC"
except Exception as e:
logger.warning(f"Failed to get timezone info: {e}, using default values.")
now = datetime.now()
@@ -722,7 +1101,7 @@ class Action:
f"Mind map has been generated, {user_name}!",
"success",
)
logger.info("Action: Smart Mind Map (v0.7.2) completed successfully")
logger.info("Action: Smart Mind Map (v0.8.0) completed successfully")
except Exception as e:
error_message = f"Smart Mind Map processing failed: {str(e)}"

View File

@@ -1,7 +1,10 @@
"""
title: 思维导图
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.8.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
"""
@@ -626,7 +629,7 @@ SCRIPT_TEMPLATE_MINDMAP = """
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const scale = 2;
const scale = 9;
canvas.width = width * scale;
canvas.height = height * scale;
const ctx = canvas.getContext('2d');
@@ -645,7 +648,7 @@ SCRIPT_TEMPLATE_MINDMAP = """
// Use non-bubbling MouseEvent to avoid router interception
const a = document.createElement('a');
a.download = 'mindmap.png';
a.download = '思维导图.png';
a.href = URL.createObjectURL(blob);
a.style.display = 'none';
document.body.appendChild(a);