2026-03-04 00:11:28 +08:00
"""
title : Smart Mind Map Tool
author : Fu - Jie
author_url : https : / / github . com / Fu - Jie / openwebui - extensions
funding_url : https : / / github . com / open - webui
2026-03-04 23:14:06 +08:00
version : 1.0 .0
required_open_webui_version : 0.8 .0
description : Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge .
2026-03-04 00:11:28 +08:00
"""
import asyncio
import logging
2026-03-04 23:14:06 +08:00
import os
2026-03-04 00:11:28 +08:00
import re
import time
import json
from datetime import datetime , timezone
from typing import Any , Callable , Awaitable , Dict , Optional
2026-03-04 23:14:06 +08:00
from zoneinfo import ZoneInfo
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
from fastapi import Request , Response
from fastapi . responses import HTMLResponse
2026-03-04 00:11:28 +08:00
from pydantic import BaseModel , Field
from open_webui . utils . chat import generate_chat_completion
from open_webui . models . users import Users
2026-03-04 23:14:06 +08:00
logging . basicConfig ( level = logging . INFO )
2026-03-04 00:11:28 +08:00
logger = logging . getLogger ( __name__ )
2026-03-04 23:14:06 +08:00
TRANSLATIONS = {
" en-US " : {
" status_starting " : " Smart Mind Map is starting, generating mind map for you... " ,
" error_no_content " : " Unable to retrieve valid user message content. " ,
" error_text_too_short " : " Text content is too short ( {len} characters), unable to perform effective analysis. Please provide at least {min_len} characters of text. " ,
" status_analyzing " : " Smart Mind Map: Analyzing text structure in depth... " ,
" status_drawing " : " Smart Mind Map: Drawing completed! " ,
" notification_success " : " Mind map has been generated, {user_name} ! " ,
" error_processing " : " Smart Mind Map processing failed: {error} " ,
" error_user_facing " : " Sorry, Smart Mind Map encountered an error during processing: {error} . \n Please check the Open WebUI backend logs for more details. " ,
" status_failed " : " Smart Mind Map: Processing failed. " ,
" notification_failed " : " Smart Mind Map generation failed, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Rendering image... " ,
" status_image_generated " : " Smart Mind Map: Image generated! " ,
" notification_image_success " : " Mind map image has been generated, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " User: " ,
" ui_time " : " Time: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Reset " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Expand Level " ,
" ui_depth_all " : " Expand All " ,
" ui_depth_2 " : " Level 2 " ,
" ui_depth_3 " : " Level 3 " ,
" ui_fullscreen " : " Fullscreen " ,
" ui_theme " : " Theme " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Unable to load mind map: Missing valid content. " ,
" html_error_load_failed " : " ⚠️ Resource loading failed, please try again later. " ,
" js_done " : " Done " ,
" js_failed " : " Failed " ,
" js_generating " : " Generating... " ,
" js_filename " : " mindmap.png " ,
" js_upload_failed " : " Upload failed: " ,
" md_image_alt " : " 🧠 Mind Map " ,
" notification_waiting " : " Analysis is taking longer than expected. Please wait, we ' re still working on your mind map... " ,
} ,
" zh-CN " : {
" status_starting " : " 思维导图已启动,正在为您生成思维导图... " ,
" error_no_content " : " 无法获取有效的用户消息内容。 " ,
" error_text_too_short " : " 文本内容过短( {len} 字符),无法进行有效分析。请提供至少 {min_len} 字符的文本。 " ,
" status_analyzing " : " 思维导图:深入分析文本结构... " ,
" status_drawing " : " 思维导图:绘制完成! " ,
" notification_success " : " 思维导图已生成, {user_name} ! " ,
" error_processing " : " 思维导图处理失败: {error} " ,
" error_user_facing " : " 抱歉,思维导图在处理时遇到错误: {error} 。 \n 请检查Open WebUI后端日志获取更多详情。 " ,
" status_failed " : " 思维导图:处理失败。 " ,
" notification_failed " : " 思维导图生成失败, {user_name} ! " ,
" status_rendering_image " : " 思维导图:正在渲染图片... " ,
" status_image_generated " : " 思维导图:图片已生成! " ,
" notification_image_success " : " 思维导图图片已生成, {user_name} ! " ,
" ui_title " : " 🧠 智能思维导图 " ,
" ui_user " : " 用户: " ,
" ui_time " : " 时间: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " 缩小 " ,
" ui_zoom_reset " : " 重置 " ,
" ui_zoom_in " : " 放大 " ,
" ui_depth_select " : " 展开层级 " ,
" ui_depth_all " : " 全部展开 " ,
" ui_depth_2 " : " 展开 2 级 " ,
" ui_depth_3 " : " 展开 3 级 " ,
" ui_fullscreen " : " 全屏 " ,
" ui_theme " : " 主题 " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ 无法加载思维导图:缺少有效内容。 " ,
" html_error_load_failed " : " ⚠️ 资源加载失败,请稍后重试。 " ,
" js_done " : " 完成 " ,
" js_failed " : " 失败 " ,
" js_generating " : " 生成中... " ,
" js_filename " : " 思维导图.png " ,
" js_upload_failed " : " 上传失败: " ,
" md_image_alt " : " 🧠 思维导图 " ,
" notification_waiting " : " 分析时间可能比预期稍长,请稍等,我们正在为您拼命绘图中... " ,
} ,
" zh-HK " : {
" status_starting " : " 思維導圖已啟動,正在為您生成思維導圖... " ,
" error_no_content " : " 無法獲取有效的用戶消息內容。 " ,
" error_text_too_short " : " 文本內容過短( {len} 字元),無法進行有效分析。請提供至少 {min_len} 字元的文本。 " ,
" status_analyzing " : " 思維導圖:深入分析文本結構... " ,
" status_drawing " : " 思維導圖:繪製完成! " ,
" notification_success " : " 思維導圖已生成, {user_name} ! " ,
" error_processing " : " 思維導圖處理失敗: {error} " ,
" error_user_facing " : " 抱歉,思維導圖在處理時遇到錯誤: {error} 。 \n 請檢查Open WebUI後端日誌獲取更多詳情。 " ,
" status_failed " : " 思維導圖:處理失敗。 " ,
" notification_failed " : " 思維導圖生成失敗, {user_name} ! " ,
" status_rendering_image " : " 思維導圖:正在渲染圖片... " ,
" status_image_generated " : " 思維導圖:圖片已生成! " ,
" notification_image_success " : " 思維導圖圖片已生成, {user_name} ! " ,
" ui_title " : " 🧠 智能思維導圖 " ,
" ui_user " : " 用戶: " ,
" ui_time " : " 時間: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " 縮小 " ,
" ui_zoom_reset " : " 重置 " ,
" ui_zoom_in " : " 放大 " ,
" ui_depth_select " : " 展開層級 " ,
" ui_depth_all " : " 全部展開 " ,
" ui_depth_2 " : " 展開 2 級 " ,
" ui_depth_3 " : " 展開 3 級 " ,
" ui_fullscreen " : " 全屏 " ,
" ui_theme " : " 主題 " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ 無法加載思維導圖:缺少有效內容。 " ,
" html_error_load_failed " : " ⚠️ 資源加載失敗,請稍後重試。 " ,
" js_done " : " 完成 " ,
" js_failed " : " 失敗 " ,
" js_generating " : " 生成中... " ,
" js_filename " : " 思維導圖.png " ,
" js_upload_failed " : " 上傳失敗: " ,
" md_image_alt " : " 🧠 思維導圖 " ,
" notification_waiting " : " 分析時間可能比預期稍長,請稍等,我們正在為您拼命繪圖中... " ,
} ,
" zh-TW " : {
" status_starting " : " 思維導圖已啟動,正在為您生成思維導圖... " ,
" error_no_content " : " 無法獲取有效的用戶消息內容。 " ,
" error_text_too_short " : " 文本內容過短( {len} 字元),無法進行有效分析。請提供至少 {min_len} 字元的文本。 " ,
" status_analyzing " : " 思維導圖:深入分析文本結構... " ,
" status_drawing " : " 思維導圖:繪製完成! " ,
" notification_success " : " 思維導圖已生成, {user_name} ! " ,
" error_processing " : " 思維導圖處理失敗: {error} " ,
" error_user_facing " : " 抱歉,思維導圖在處理時遇到錯誤: {error} 。 \n 請檢查Open WebUI後端日誌獲取更多詳情。 " ,
" status_failed " : " 思維導圖:處理失敗。 " ,
" notification_failed " : " 思維導圖生成失敗, {user_name} ! " ,
" status_rendering_image " : " 思維導圖:正在渲染圖片... " ,
" status_image_generated " : " 思維導圖:圖片已生成! " ,
" notification_image_success " : " 思維導圖圖片已生成, {user_name} ! " ,
" ui_title " : " 🧠 智能思維導圖 " ,
" ui_user " : " 用戶: " ,
" ui_time " : " 時間: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " 縮小 " ,
" ui_zoom_reset " : " 重置 " ,
" ui_zoom_in " : " 放大 " ,
" ui_depth_select " : " 展開層級 " ,
" ui_depth_all " : " 全部展開 " ,
" ui_depth_2 " : " 展開 2 級 " ,
" ui_depth_3 " : " 展開 3 級 " ,
" ui_fullscreen " : " 全屏 " ,
" ui_theme " : " 主題 " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ 無法加載思維導圖:缺少有效內容。 " ,
" html_error_load_failed " : " ⚠️ 資源加載失敗,請稍后重試。 " ,
" js_done " : " 完成 " ,
" js_failed " : " 失敗 " ,
" js_generating " : " 生成中... " ,
" js_filename " : " 思維導圖.png " ,
" js_upload_failed " : " 上傳失敗: " ,
" md_image_alt " : " 🧠 思維導圖 " ,
" notification_waiting " : " 分析時間可能比預期稍長,請稍等,我們正在為您拼命繪圖中... " ,
} ,
" ko-KR " : {
" status_starting " : " 스마트 마인드맵이 시작되었습니다, 마인드맵을 생성 중입니다... " ,
" error_no_content " : " 유효한 사용자 메시지 내용을 가져올 수 없습니다. " ,
" error_text_too_short " : " 텍스트 내용이 너무 짧아( {len} 자), 효과적인 분석을 수행할 수 없습니다. 최소 {min_len} 자 이상의 텍스트를 제공해 주세요. " ,
" status_analyzing " : " 스마트 마인드맵: 텍스트 구조 심층 분석 중... " ,
" status_drawing " : " 스마트 마인드맵: 그리기 완료! " ,
" notification_success " : " 마인드맵이 생성되었습니다, {user_name} 님! " ,
" error_processing " : " 스마트 마인드맵 처리 실패: {error} " ,
" error_user_facing " : " 죄송합니다, 스마트 마인드맵 처리 중 오류가 발생했습니다: {error} . \n 자세한 내용은 Open WebUI 백엔드 로그를 확인해 주세요. " ,
" status_failed " : " 스마트 마인드맵: 처리 실패. " ,
" notification_failed " : " 스마트 마인드맵 생성 실패, {user_name} 님! " ,
" status_rendering_image " : " 스마트 마인드맵: 이미지 렌더링 중... " ,
" status_image_generated " : " 스마트 마인드맵: 이미지 생성됨! " ,
" notification_image_success " : " 마인드맵 이미지가 생성되었습니다, {user_name} 님! " ,
" ui_title " : " 🧠 스마트 마인드맵 " ,
" ui_user " : " 사용자: " ,
" ui_time " : " 시간: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " 초기화 " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " 레벨 확장 " ,
" ui_depth_all " : " 모두 확장 " ,
" ui_depth_2 " : " 레벨 2 " ,
" ui_depth_3 " : " 레벨 3 " ,
" ui_fullscreen " : " 전체 화면 " ,
" ui_theme " : " 테마 " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ 마인드맵을 로드할 수 없습니다: 유효한 내용이 없습니다. " ,
" html_error_load_failed " : " ⚠️ 리소스 로드 실패, 나중에 다시 시도해 주세요. " ,
" js_done " : " 완료 " ,
" js_failed " : " 실패 " ,
" js_generating " : " 생성 중... " ,
" js_filename " : " mindmap.png " ,
" js_upload_failed " : " 업로드 실패: " ,
" md_image_alt " : " 🧠 마인드맵 " ,
" notification_waiting " : " 분석 시간이 예상보다 오래 걸리고 있습니다. 잠시만 기다려 주세요, 마인드맵을 생성 중입니다... " ,
} ,
" ja-JP " : {
" status_starting " : " スマートマインドマップが起動しました。マインドマップを生成しています... " ,
" error_no_content " : " 有効なユーザーメッセージの内容を取得できませんでした。 " ,
" error_text_too_short " : " 텍스트 내용이 너무 짧아( {len} 자), 효과적인 분석을 수행할 수 없습니다. 최소 {min_len} 자 이상의 텍스트를 제공해 주세요. " ,
" status_analyzing " : " スマートマインドマップ:テキスト構造を詳細に分析中... " ,
" status_drawing " : " スマートマインドマップ:描画完了! " ,
" notification_success " : " マインドマップが生成されました、 {user_name} さん! " ,
" error_processing " : " スマートマインドマップ処理失敗: {error} " ,
" error_user_facing " : " 申し訳ありません、スマートマインドマップの処理中にエラーが発生しました: {error} 。 \n 詳細については、Open WebUIバックエンドログを確認してください。 " ,
" status_failed " : " スマートマインドマップ:処理失敗。 " ,
" notification_failed " : " スマートマインドマップ生成失敗、 {user_name} さん! " ,
" status_rendering_image " : " スマートマインドマップ:画像レンダ링中... " ,
" status_image_generated " : " スマートマインドマップ:画像生成完了! " ,
" notification_image_success " : " マインドマップ画像が生成されました、 {user_name} さん! " ,
" ui_title " : " 🧠 スマートマインドマップ " ,
" ui_user " : " ユーザー: " ,
" ui_time " : " 時間: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " リセット " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " レベル展開 " ,
" ui_depth_all " : " すべて展開 " ,
" ui_depth_2 " : " レベル2 " ,
" ui_depth_3 " : " レベル3 " ,
" ui_fullscreen " : " 全画面 " ,
" ui_theme " : " テーマ " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ マインドマップを読み込めません:有効なコンテンツがありません。 " ,
" html_error_load_failed " : " ⚠️ リソースの読み込みに失敗しました。後でもう一度お試しください。 " ,
" js_done " : " 完了 " ,
" js_failed " : " 失敗 " ,
" js_generating " : " 生成中... " ,
" js_filename " : " mindmap.png " ,
" js_upload_failed " : " アップロード失敗: " ,
" md_image_alt " : " 🧠 マインドマップ " ,
" notification_waiting " : " 分析に時間がかかっています。マインドマップを生成中ですので、もうしばらくお待ちください... " ,
} ,
" fr-FR " : {
" status_starting " : " Smart Mind Map démarre, génération de la carte heuristique en cours... " ,
" error_no_content " : " Impossible de récupérer le contenu valide du message utilisateur. " ,
" error_text_too_short " : " Le contenu du texte est trop court ( {len} caractères), impossible d ' effectuer une analyse efficace. Veuillez fournir au moins {min_len} caractères de texte. " ,
" status_analyzing " : " Smart Mind Map : Analyse approfondie de la structure du texte... " ,
" status_drawing " : " Smart Mind Map : Dessin terminé ! " ,
" notification_success " : " La carte heuristique a été générée, {user_name} ! " ,
" error_processing " : " Échec du traitement de Smart Mind Map : {error} " ,
" error_user_facing " : " Désolé, Smart Mind Map a rencontré une erreur lors du traitement : {error} . \n Veuillez vérifier les journaux backend d ' Open WebUI pour plus de détails. " ,
" status_failed " : " Smart Mind Map : Échec du traitement. " ,
" notification_failed " : " Échec de la génération de la carte heuristique, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map : Rendu de l ' image... " ,
" status_image_generated " : " Smart Mind Map : Image générée ! " ,
" notification_image_success " : " L ' image de la carte heuristique a été générée, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Utilisateur : " ,
" ui_time " : " Heure : " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Rénitialiser " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Niveau d ' expansion " ,
" ui_depth_all " : " Tout développer " ,
" ui_depth_2 " : " Niveau 2 " ,
" ui_depth_3 " : " Niveau 3 " ,
" ui_fullscreen " : " Plein écran " ,
" ui_theme " : " Thème " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Impossible de charger la carte heuristique : contenu valide manquant. " ,
" html_error_load_failed " : " ⚠️ Échec du chargement des ressources, veuillez réessayer plus tard. " ,
" js_done " : " Terminé " ,
" js_failed " : " Échec " ,
" js_generating " : " Génération... " ,
" js_filename " : " carte_heuristique.png " ,
" js_upload_failed " : " Échec du téléchargement : " ,
" md_image_alt " : " 🧠 Carte Heuristique " ,
} ,
" de-DE " : {
" status_starting " : " Smart Mind Map startet, Mindmap wird für Sie erstellt... " ,
" error_no_content " : " Gültiger Inhalt der Benutzernachricht konnte nicht abgerufen werden. " ,
" error_text_too_short " : " Der Textinhalt ist zu kurz ( {len} Zeichen), eine effektive Analyse ist nicht möglich. Bitte geben Sie mindestens {min_len} Zeichen Text an. " ,
" status_analyzing " : " Smart Mind Map: Detaillierte Analyse der Textstruktur... " ,
" status_drawing " : " Smart Mind Map: Zeichnen abgeschlossen! " ,
" notification_success " : " Mindmap wurde erstellt, {user_name} ! " ,
" error_processing " : " Smart Mind Map Verarbeitung fehlgeschlagen: {error} " ,
" error_user_facing " : " Entschuldigung, bei der Verarbeitung von Smart Mind Map ist ein Fehler aufgetreten: {error} . \n Bitte überprüfen Sie die Open WebUI Backend-Protokolle für weitere Details. " ,
" status_failed " : " Smart Mind Map: Verarbeitung fehlgeschlagen. " ,
" notification_failed " : " Erstellung der Mindmap fehlgeschlagen, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Bild wird gerendert... " ,
" status_image_generated " : " Smart Mind Map: Bild erstellt! " ,
" notification_image_success " : " Mindmap-Bild wurde erstellt, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Benutzer: " ,
" ui_time " : " Zeit: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Zurücksetzen " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Ebene erweitern " ,
" ui_depth_all " : " Alles erweitern " ,
" ui_depth_2 " : " Ebene 2 " ,
" ui_depth_3 " : " Ebene 3 " ,
" ui_fullscreen " : " Vollbild " ,
" ui_theme " : " Thema " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Mindmap kann nicht geladen werden: Gültiger Inhalt fehlt. " ,
" html_error_load_failed " : " ⚠️ Ressourcenladen fehlgeschlagen, bitte versuchen Sie es später erneut. " ,
" js_done " : " Fertig " ,
" js_failed " : " Fehlgeschlagen " ,
" js_generating " : " Generiere... " ,
" js_filename " : " mindmap.png " ,
" js_upload_failed " : " Upload fehlgeschlagen: " ,
" md_image_alt " : " 🧠 Mindmap " ,
} ,
" es-ES " : {
" status_starting " : " Smart Mind Map se está iniciando, generando mapa mental para usted... " ,
" error_no_content " : " No se puede recuperar el contenido válido del mensaje del usuario. " ,
" error_text_too_short " : " El contenido del texto es demasiado corto ( {len} caracteres), no se puede realizar un análisis efectivo. Proporcione al menos {min_len} caracteres de texto. " ,
" status_analyzing " : " Smart Mind Map: Analizando la estructura del texto en profundidad... " ,
" status_drawing " : " Smart Mind Map: ¡Dibujo completado! " ,
" notification_success " : " ¡El mapa mental ha sido generado, {user_name} ! " ,
" error_processing " : " Falló el procesamiento de Smart Mind Map: {error} " ,
" error_user_facing " : " Lo sentimos, Smart Mind Map encontró un error durante el procesamiento: {error} . \n Consulte los registros del backend de Open WebUI para más detalles. " ,
" status_failed " : " Smart Mind Map: Procesamiento fallido. " ,
" notification_failed " : " ¡La generación del mapa mental falló, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Renderizando imagen... " ,
" status_image_generated " : " Smart Mind Map: ¡Imagen generada! " ,
" notification_image_success " : " ¡La imagen del mapa mental ha sido generada, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Usuario: " ,
" ui_time " : " Hora: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Restablecer " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Expandir Nivel " ,
" ui_depth_all " : " Expandir Todo " ,
" ui_depth_2 " : " Nivel 2 " ,
" ui_depth_3 " : " Nivel 3 " ,
" ui_fullscreen " : " Pantalla completa " ,
" ui_theme " : " Tema " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ No se puede cargar el mapa mental: Falta contenido válido. " ,
" html_error_load_failed " : " ⚠️ Falló la carga de recursos, inténtelo de nuevo más tarde. " ,
" js_done " : " Hecho " ,
" js_failed " : " Fallido " ,
" js_generating " : " Generando... " ,
" js_filename " : " mapa_mental.png " ,
" js_upload_failed " : " Carga fallida: " ,
" md_image_alt " : " 🧠 Mapa Mental " ,
} ,
" it-IT " : {
" status_starting " : " Smart Mind Map si sta avviando, generazione mappa mentale in corso... " ,
" error_no_content " : " Impossibile recuperare il contenuto valido del messaggio utente. " ,
" error_text_too_short " : " Il testo è troppo breve ( {len} caratteri), impossibile eseguire un ' analisi efficace. Fornire almeno {min_len} caratteri di testo. " ,
" status_analyzing " : " Smart Mind Map: Analisi approfondita della struttura del testo... " ,
" status_drawing " : " Smart Mind Map: Disegno completato! " ,
" notification_success " : " La mappa mentale è stata generata, {user_name} ! " ,
" error_processing " : " Elaborazione Smart Mind Map fallita: {error} " ,
" error_user_facing " : " Spiacenti, Smart Mind Map ha riscontrato un errore durante l ' elaborazione: {error} . \n Controllare i log del backend di Open WebUI per ulteriori dettagli. " ,
" status_failed " : " Smart Mind Map: Elaborazione fallita. " ,
" notification_failed " : " Generazione mappa mentale fallita, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Rendering immagine... " ,
" status_image_generated " : " Smart Mind Map: Immagine generata! " ,
" notification_image_success " : " L ' immagine della mappa mentale è stata generata, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Utente: " ,
" ui_time " : " Ora: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Reimposta " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Espandi Livello " ,
" ui_depth_all " : " Espandi Tutto " ,
" ui_depth_2 " : " Livello 2 " ,
" ui_depth_3 " : " Livello 3 " ,
" ui_fullscreen " : " Schermo intero " ,
" ui_theme " : " Tema " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Impossibile caricare la mappa mentale: Contenuto valido mancante. " ,
" html_error_load_failed " : " ⚠️ Caricamento risorse fallito, riprovare più tardi. " ,
" js_done " : " Fatto " ,
" js_failed " : " Fallito " ,
" js_generating " : " Generazione... " ,
" js_filename " : " mappa_mentale.png " ,
" js_upload_failed " : " Caricamento fallito: " ,
" md_image_alt " : " 🧠 Mappa Mentale " ,
} ,
" vi-VN " : {
" status_starting " : " Smart Mind Map đang khởi động, đang tạo sơ đồ tư duy cho bạn... " ,
" error_no_content " : " Không thể lấy nội dung tin nhắn người dùng hợp lệ. " ,
" error_text_too_short " : " Nội dung văn bản quá ngắn ( {len} ký tự), không thể thực hiện phân tích hiệu quả. Vui lòng cung cấp ít nhất {min_len} ký tự văn bản. " ,
" status_analyzing " : " Smart Mind Map: Phân tích sâu cấu trúc văn bản... " ,
" status_drawing " : " Smart Mind Map: Vẽ hoàn tất! " ,
" notification_success " : " Sơ đồ tư duy đã được tạo, {user_name} ! " ,
" error_processing " : " Xử lý Smart Mind Map thất bại: {error} " ,
" error_user_facing " : " Xin lỗi, Smart Mind Map đã gặp lỗi trong quá trình xử lý: {error} . \n Vui lòng kiểm tra nhật ký backend Open WebUI để biết thêm chi tiết. " ,
" status_failed " : " Smart Mind Map: Xử lý thất bại. " ,
" notification_failed " : " Tạo sơ đồ tư duy thất bại, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Đang render hình ảnh... " ,
" status_image_generated " : " Smart Mind Map: Hình ảnh đã tạo! " ,
" notification_image_success " : " Hình ảnh sơ đồ tư duy đã được tạo, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Người dùng: " ,
" ui_time " : " Thời gian: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Đặt lại " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Mở rộng Cấp độ " ,
" ui_depth_all " : " Mở rộng Tất cả " ,
" ui_depth_2 " : " Cấp độ 2 " ,
" ui_depth_3 " : " Cấp độ 3 " ,
" ui_fullscreen " : " Toàn màn hình " ,
" ui_theme " : " Chủ đề " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Không thể tải sơ đồ tư duy: Thiếu nội dung hợp lệ. " ,
" html_error_load_failed " : " ⚠️ Tải tài nguyên thất bại, vui lòng thử lại sau. " ,
" js_done " : " Xong " ,
" js_failed " : " Thất bại " ,
" js_generating " : " Đang tạo... " ,
" js_filename " : " sodo_tuduy.png " ,
" js_upload_failed " : " Tải lên thất bại: " ,
" md_image_alt " : " 🧠 Sơ đồ Tư duy " ,
} ,
" id-ID " : {
" status_starting " : " Smart Mind Map sedang dimulai, membuat peta pikiran untuk Anda... " ,
" error_no_content " : " Tidak dapat mengambil konten pesan pengguna yang valid. " ,
" error_text_too_short " : " Konten teks terlalu pendek ( {len} karakter), tidak dapat melakukan analisis efektif. Harap berikan setidaknya {min_len} karakter teks. " ,
" status_analyzing " : " Smart Mind Map: Menganalisis struktur teks secara mendalam... " ,
" status_drawing " : " Smart Mind Map: Menggambar selesai! " ,
" notification_success " : " Peta pikiran telah dibuat, {user_name} ! " ,
" error_processing " : " Pemrosesan Smart Mind Map gagal: {error} " ,
" error_user_facing " : " Maaf, Smart Mind Map mengalami kesalahan saat memproses: {error} . \n Silakan periksa log backend Open WebUI untuk detail lebih lanjut. " ,
" status_failed " : " Smart Mind Map: Pemrosesan gagal. " ,
" notification_failed " : " Pembuatan peta pikiran gagal, {user_name} ! " ,
" status_rendering_image " : " Smart Mind Map: Merender gambar... " ,
" status_image_generated " : " Smart Mind Map: Gambar dibuat! " ,
" notification_image_success " : " Gambar peta pikiran telah dibuat, {user_name} ! " ,
" ui_title " : " 🧠 Smart Mind Map " ,
" ui_user " : " Pengguna: " ,
" ui_time " : " Waktu: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " Atur Ulang " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Perluas Level " ,
" ui_depth_all " : " Perluas Semua " ,
" ui_depth_2 " : " Level 2 " ,
" ui_depth_3 " : " Level 3 " ,
" ui_fullscreen " : " Layar Penuh " ,
" ui_theme " : " Tema " ,
" ui_footer " : " <b>Powered by</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Tidak dapat memuat peta pikiran: Konten valid hilang. " ,
" html_error_load_failed " : " ⚠️ Gagal memuat sumber daya, silakan coba lagi nanti. " ,
" js_done " : " Selesai " ,
" js_failed " : " Gagal " ,
" js_generating " : " Membuat... " ,
" js_filename " : " peta_pikiran.png " ,
" js_upload_failed " : " Unggah gagal: " ,
" md_image_alt " : " 🧠 Peta Pikiran " ,
} ,
" ru-RU " : {
" status_starting " : " Умная интеллект-карта запускается, создаем карту для вас... " ,
" error_no_content " : " Н е удалось получить допустимое содержимое сообщения пользователя." ,
" error_text_too_short " : " Текст слишком короткий ( {len} символов), невозможно провести эффективный анализ. Пожалуйста, предоставьте текст длиной не менее {min_len} символов. " ,
" status_analyzing " : " Умная интеллект-карта: Глубокий анализ структуры текста... " ,
" status_drawing " : " Умная интеллект-карта: Отрисовка завершена! " ,
" notification_success " : " Интеллект-карта создана, {user_name} ! " ,
" error_processing " : " Ошибка обработки умной интеллект-карты: {error} " ,
" error_user_facing " : " Извините, при обработке умной интеллект-карты произошла ошибка: {error} . \n Пожалуйста, проверьте логи бэкенда Open WebUI для получения подробной информации. " ,
" status_failed " : " Умная интеллект-карта: Обработка не удалась. " ,
" notification_failed " : " Н е удалось создать интеллект-карту, {user_name} ! " ,
" status_rendering_image " : " Умная интеллект-карта: Рендеринг изображения... " ,
" status_image_generated " : " Умная интеллект-карта: Изображение создано! " ,
" notification_image_success " : " Изображение интеллект-карты создано, {user_name} ! " ,
" ui_title " : " 🧠 Умная интеллект-карта " ,
" ui_user " : " Пользователь: " ,
" ui_time " : " Время: " ,
" ui_download_png " : " PNG " ,
" ui_download_svg " : " SVG " ,
" ui_download_md " : " Markdown " ,
" ui_zoom_out " : " - " ,
" ui_zoom_reset " : " С б р о с " ,
" ui_zoom_in " : " + " ,
" ui_depth_select " : " Развернуть уровни " ,
" ui_depth_all " : " Развернуть все " ,
" ui_depth_2 " : " Уровень 2 " ,
" ui_depth_3 " : " Уровень 3 " ,
" ui_fullscreen " : " Полный экран " ,
" ui_theme " : " Тема " ,
" ui_footer " : " <b>Работает на</b> <a href= ' https://markmap.js.org/ ' target= ' _blank ' rel= ' noopener noreferrer ' >Markmap</a> " ,
" html_error_missing_content " : " ⚠️ Н е удалось загрузить карту: Отсутствует допустимое содержимое. " ,
" html_error_load_failed " : " ⚠️ Ошибка загрузки ресурсов, пожалуйста, попробуйте позже. " ,
" js_done " : " Готово " ,
" js_failed " : " Ошибка " ,
" js_generating " : " Генерация... " ,
" js_filename " : " mindmap.png " ,
" js_upload_failed " : " Ошибка загрузки: " ,
" md_image_alt " : " 🧠 Интеллект-карта " ,
} ,
}
SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
You are a professional mind map generation assistant , capable of efficiently analyzing long - form text provided by users and structuring its core themes , key concepts , branches , and sub - branches into standard Markdown list syntax for rendering by Markmap . js .
Please strictly follow these guidelines :
- * * Language * * : All output must be in the exact same language as the input text ( the text you are analyzing ) .
- * * Format Consistency * * : Even if this system prompt is in English , if the user input is in Chinese , the mind map content must be in Chinese . If input is Russian , output Russian .
- * * Format * * : Your output must strictly be in Markdown list format , wrapped with ` ` ` markdown and ` ` ` .
- Use ` #` to define the central theme (root node).
- Use ` - ` with two - space indentation to represent branches and sub - branches .
- * * Root Node ( Central Theme ) — Strict Length Limits * * :
- The ` #` root node must be an ultra-compact title, like a newspaper headline. It should be a keyword or short phrase, NEVER a full sentence.
- * * CJK scripts ( Chinese , Japanese , Korean ) * * : Maximum * * 10 characters * * ( e . g . , ` # 老人缓解呼吸困难方法` ✓ / `# 老人在家时感到呼吸困难的缓解方法` ✗)
- * * Latin - script languages ( English , Spanish , French , Italian , Portuguese , Russian ) * * : Maximum * * 5 words or 35 characters * * ( e . g . , ` # Methods to Relieve Dyspnea` ✓ / `# How Elderly People Can Relieve Breathing Difficulty at Home` ✗)
- * * German , Dutch or languages with long compound words * * : Maximum * * 4 words or 30 characters * *
- * * Arabic , Hebrew and other RTL scripts * * : Maximum * * 5 words or 25 characters * *
- * * All other languages * * : Maximum * * 5 words or 30 characters * *
- If the identified theme would exceed the limit , distill it further into the single most essential keyword or 2 - 3 word phrase .
- * * Branch Node Content * * :
- Identify main concepts as first - level list items .
- Identify supporting details or sub - concepts as nested list items .
- Node content should be concise and clear , avoiding verbosity .
- * * Output Markdown syntax only * * : Do not include any additional greetings , explanations , or guiding text .
- * * If text is too short or cannot generate a valid mind map * * : Output a simple Markdown list indicating inability to generate , for example :
` ` ` markdown
# Unable to Generate Mind Map
- Reason : Insufficient or unclear text content
` ` `
- * * Awareness of Target Audience Layout * * : You will be provided ` Target Rendering Mode ` .
- If ` Target Rendering Mode ` is ` direct ` : The client has massive horizontal space but limited scrolling vertically . Extract more first - level concepts to make the mind map spread wide like a sprawling fan , rather than deep single columns .
- If ` Target Rendering Mode ` is ` legacy ` : The client uses a narrow , portrait sidebar . Extract fewer top - level nodes , and break points into deeper , tighter sub - branches so the map grows vertically downwards .
"""
USER_PROMPT_GENERATE_MINDMAP = """
Please analyze the following long - form text and structure its core themes , key concepts , branches , and sub - branches into standard Markdown list syntax for Markmap . js rendering .
- - -
* * User Context Information : * *
User Name : { user_name }
Current Date & Time : { current_date_time_str }
Current Weekday : { current_weekday }
Current Timezone : { current_timezone_str }
User Language : { user_language }
Target Rendering Mode : Auto - adapting ( Dynamic width based on viewport )
- - -
* * Long - form Text Content : * *
{ long_text_content }
"""
def _resolve_language ( valves , lang : str ) - > str :
""" Resolve the best matching language code from the TRANSLATIONS dict. """
target_lang = lang
# 0. Basic base language match (e.g. 'en', 'zh', 'ja')
if len ( lang ) == 2 :
for supported_lang in TRANSLATIONS :
if supported_lang . startswith ( lang ) :
return supported_lang
# 1. Direct match
if target_lang in TRANSLATIONS :
return target_lang
# 2. Variant fallback (explicit mapping)
# Mapping regional variants to their most complete translation set
fallback_map = {
" zh " : " zh-CN " ,
" en " : " en-US " ,
" ja " : " ja-JP " ,
" ko " : " ko-KR " ,
" zh-CN " : " zh-CN " ,
" zh-HK " : " zh-HK " ,
" zh-TW " : " zh-TW " ,
" es-AR " : " es-ES " ,
" es-MX " : " es-ES " ,
" fr-CA " : " fr-FR " ,
" en-CA " : " en-US " ,
" en-GB " : " en-US " ,
" en-AU " : " en-US " ,
" de-AT " : " de-DE " ,
}
if target_lang in fallback_map :
target_lang = fallback_map [ target_lang ]
if target_lang in TRANSLATIONS :
return target_lang
# 3. Base language fallback (e.g. fr-BE -> fr-FR)
if " - " in lang :
base_lang = lang . split ( " - " ) [ 0 ]
# Check if base lang matches any supported translation
for supported_lang in TRANSLATIONS :
if supported_lang . startswith ( base_lang ) :
return supported_lang
return " en-US "
def _extract_text_content ( content : Any ) - > str :
""" Normalize message content to a plain text string.
Handles both simple string content and OpenAI - style multi - part
content arrays ( e . g . [ { " type " : " text " , " text " : " hello " } ] ) .
"""
if isinstance ( content , str ) :
return content
if isinstance ( content , list ) :
return " " . join (
part . get ( " text " , " " )
for part in content
if isinstance ( part , dict ) and part . get ( " type " ) == " text "
) . strip ( )
return str ( content )
def _get_translation ( valves , lang : str , key : str , * * kwargs ) - > str :
""" Retrieve a localized string by key, falling back to en-US on miss.
Args :
valves : Plugin Valves instance ( used by _resolve_language ) .
lang : BCP - 47 language tag resolved from user context .
key : Translation key defined in the TRANSLATIONS dict .
* * kwargs : Optional format arguments interpolated into the string .
Returns :
Fully formatted localized string .
"""
target = _resolve_language ( valves , lang )
trans_set = TRANSLATIONS . get ( target , TRANSLATIONS [ " en-US " ] )
text = trans_set . get ( key , TRANSLATIONS [ " en-US " ] . get ( key , key ) )
return text . format ( * * kwargs ) if kwargs else text
def _extract_markdown_syntax ( content : str ) - > str :
""" Strip wrapping fenced code block markers from LLM output.
Extracts the inner Markdown text from a ` ` ` markdown . . . ` ` ` block .
If no code fence is found , the raw content is returned as - is .
Also escapes ' </script> ' tags to prevent XSS when embedding in HTML .
"""
match = re . search (
r " ```(?:markdown|md)? \ s*(.*?) \ s*``` " , content , re . DOTALL | re . IGNORECASE
)
extracted = match . group ( 1 ) . strip ( ) if match else content . strip ( )
return extracted . replace ( " </script> " , " < \\ /script> " )
HTML_WRAPPER_TEMPLATE = """
< ! - - OPENWEBUI_PLUGIN_OUTPUT - - >
< ! DOCTYPE html >
< html lang = " {lang} " >
< head >
< meta charset = " UTF-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1.0 " >
< style >
body {
font - family : - apple - system , BlinkMacSystemFont , " Segoe UI " , Roboto , Helvetica , Arial , sans - serif ;
margin : 0 ;
padding : 12 px ;
background - color : transparent ;
width : 100 % ;
box - sizing : border - box ;
}
#main-container {
display : flex ;
flex - direction : column ;
align - items : stretch ;
width : 100 % ;
}
. plugin - item {
width : 100 % ;
border - radius : 12 px ;
overflow : visible ;
transition : all 0.3 s ease ;
}
. plugin - item : hover {
transform : translateY ( - 2 px ) ;
}
/ * STYLES_INSERTION_POINT * /
< / style >
< / head >
< body >
< div id = " main-container " >
< ! - - CONTENT_INSERTION_POINT - - >
< / div >
< ! - - SCRIPTS_INSERTION_POINT - - >
< / body >
< / html >
"""
async def _emit_status ( emitter , description : str , done : bool = False ) :
""" Emit a status event to the OpenWebUI frontend.
Args :
emitter : The __event_emitter__ callable injected by OpenWebUI .
description : Human - readable status message to display .
done : True marks the status as terminal ( spinner stops ) .
"""
if emitter :
await emitter (
{ " type " : " status " , " data " : { " description " : description , " done " : done } }
)
async def _emit_notification ( emitter , content : str , ntype : str = " info " ) :
""" Emit a toast notification event to the OpenWebUI frontend.
Args :
emitter : The __event_emitter__ callable injected by OpenWebUI .
content : Notification body text .
ntype : Severity level — one of ' info ' , ' warning ' , ' error ' , ' success ' .
"""
if emitter :
await emitter (
{ " type " : " notification " , " data " : { " type " : ntype , " content " : content } }
)
async def _get_user_context (
__user__ : dict ,
__request__ : Request ,
valves : Any = None ,
__event_call__ : Callable = None ,
) - > dict :
""" Extract basic user context with safe fallbacks, matching Action logic perfectly. """
# 1. Base identity
if isinstance ( __user__ , ( list , tuple ) ) :
user_data = __user__ [ 0 ] if __user__ else { }
elif isinstance ( __user__ , dict ) :
user_data = __user__
else :
user_data = { }
user_id = user_data . get ( " id " , " unknown " )
user_name = user_data . get ( " name " , " User " )
# Priority 4 (Lowest): User Profile language setting
user_language = user_data . get ( " language " , " en-US " )
# Priority 3: Accept-Language from __request__ headers
if (
__request__
and hasattr ( __request__ , " headers " )
and " accept-language " in __request__ . headers
) :
raw_lang = __request__ . headers . get ( " accept-language " , " " )
if raw_lang :
user_language = raw_lang . split ( " , " ) [ 0 ] . split ( " ; " ) [ 0 ]
# Priority 2: Browser/Frontend Detection (via JS)
if __event_call__ :
try :
js_code = """
try {
return (
document . documentElement . lang | |
localStorage . getItem ( ' locale ' ) | |
localStorage . getItem ( ' language ' ) | |
navigator . language | |
' en-US '
) ;
} catch ( e ) {
return ' en-US ' ;
}
"""
frontend_lang = await asyncio . wait_for (
__event_call__ ( { " type " : " execute " , " data " : { " code " : js_code } } ) ,
timeout = 2.0 ,
)
if frontend_lang and isinstance ( frontend_lang , str ) :
logger . info ( f " Frontend language detected via JS: { frontend_lang } " )
user_language = frontend_lang
except Exception as e :
logger . warning (
f " Failed to retrieve frontend language via __event_call__: { e } "
)
return { " user_id " : user_id , " user_name " : user_name , " user_language " : user_language }
SCRIPT_TEMPLATE_MINDMAP = """
< script >
( function ( ) {
const uniqueId = { unique_id_json } ;
const i18n = { i18n_json } ;
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 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 parentClassTheme = getThemeFromParentClass ( ) ;
const prefersDark = window . matchMedia & & window . matchMedia ( ' (prefers-color-scheme: dark) ' ) . matches ;
const chosen = explicitTheme | | parentClassTheme | | ( prefersDark ? ' dark ' : ' light ' ) ;
wrapperEl . classList . toggle ( ' theme-dark ' , chosen == = ' dark ' ) ;
return chosen ;
} ;
const renderMindmap = ( ) = > {
const containerEl = document . getElementById ( ' markmap-container- ' + uniqueId ) ;
if ( ! containerEl | | containerEl . dataset . markmapRendered ) return ;
const sourceEl = document . getElementById ( ' markdown-source- ' + uniqueId ) ;
if ( ! sourceEl ) return ;
const markdownContent = sourceEl . textContent . trim ( ) ;
if ( ! markdownContent ) {
containerEl . innerHTML = ' <div class= " error-message " > ' + i18n . html_error_missing_content + ' </div> ' ;
return ;
}
ensureMarkmapReady ( ) . then ( ( ) = > {
const svgEl = document . createElementNS ( ' http://www.w3.org/2000/svg ' , ' svg ' ) ;
svgEl . style . width = ' 100 % ' ;
svgEl . style . height = ' 100 % ' ;
containerEl . innerHTML = ' ' ;
containerEl . appendChild ( svgEl ) ;
const { Transformer , Markmap } = window . markmap ;
const transformer = new Transformer ( ) ;
const { root } = transformer . transform ( markdownContent ) ;
const containerWidth = containerEl . clientWidth | | window . innerWidth ;
const containerHeight = containerEl . clientHeight | | window . innerHeight ;
const isPortrait = containerHeight > = containerWidth * 0.8 ;
const style = ( id ) = > `
$ { id } text , $ { id } foreignObject { font - size : 16 px ; }
$ { id } foreignObject { line - height : 1.6 ; }
$ { id } foreignObject div { padding : 2 px 0 ; }
$ { id } foreignObject h1 { font - size : 24 px ; font - weight : 700 ; margin : 0 0 6 px 0 ; border - bottom : 2 px solid currentColor ; padding - bottom : 4 px ; display : inline - block ; }
$ { id } foreignObject h2 { font - size : 18 px ; font - weight : 600 ; margin : 0 0 4 px 0 ; }
$ { id } foreignObject strong { font - weight : 700 ; }
$ { id } foreignObject p { margin : 2 px 0 ; }
` ;
let responsiveMaxWidth = Math . max ( 220 , Math . floor ( containerWidth * 0.35 ) ) ;
let dynamicSpacingVertical = 12 ;
let dynamicSpacingHorizontal = 60 ;
if ( isPortrait ) {
responsiveMaxWidth = Math . max ( 140 , Math . floor ( containerWidth * 0.35 ) ) ;
dynamicSpacingVertical = 20 ;
}
const options = {
autoFit : true ,
style : style ,
initialExpandLevel : 3 ,
zoom : true ,
pan : true ,
fitRatio : 0.95 ,
maxWidth : responsiveMaxWidth ,
spacingVertical : dynamicSpacingVertical ,
spacingHorizontal : dynamicSpacingHorizontal ,
colorFreezeLevel : 2
} ;
const markmapInstance = Markmap . create ( svgEl , options , root ) ;
setTimeout ( ( ) = > markmapInstance . fit ( ) , 300 ) ;
const resizeObserver = new ResizeObserver ( entries = > {
for ( let entry of entries ) {
if ( entry . contentRect . width > 0 & & entry . contentRect . height > 0 ) {
requestAnimationFrame ( ( ) = > markmapInstance . fit ( ) ) ;
}
}
} ) ;
resizeObserver . observe ( containerEl ) ;
window [ ' markmapInstance_ ' + uniqueId ] = markmapInstance ;
containerEl . dataset . markmapRendered = ' true ' ;
setupControls ( {
containerEl ,
svgEl ,
markmapInstance ,
root
} ) ;
} ) . catch ( ( error ) = > {
console . error ( ' Markmap loading error: ' , error ) ;
containerEl . innerHTML = ' <div class= " error-message " > ' + i18n . html_error_load_failed + ' </div> ' ;
} ) ;
} ;
const adjustLayout = ( ) = > {
const wrapper = document . querySelector ( ' .mindmap-container-wrapper ' ) ;
const header = document . querySelector ( ' .header ' ) ;
const contentArea = document . querySelector ( ' .content-area ' ) ;
if ( ! wrapper | | ! header | | ! contentArea ) return ;
const headerH = header . getBoundingClientRect ( ) . height ;
const totalH = wrapper . getBoundingClientRect ( ) . height ;
const contentH = Math . max ( totalH - headerH , 200 ) ;
contentArea . style . height = contentH + ' px ' ;
} ;
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 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 wrapper = containerEl . closest ( ' .mindmap-container-wrapper ' ) ;
let currentTheme = setTheme ( wrapper ) ;
const showFeedback = ( button , textOk = i18n . js_done , textFail = i18n . js_failed ) = > {
if ( ! button ) return ;
const buttonText = button . querySelector ( ' .btn-text ' ) | | button ;
const originalText = buttonText . textContent ;
button . disabled = true ;
buttonText . textContent = textOk ;
setTimeout ( ( ) = > {
buttonText . textContent = originalText ;
button . disabled = false ;
} , 1800 ) ;
} ;
const copyToClipboard = ( content , button ) = > {
if ( navigator . clipboard & & window . isSecureContext ) {
navigator . clipboard . writeText ( content ) . then ( ( ) = > showFeedback ( button ) , ( ) = > showFeedback ( button , i18n . js_failed , i18n . js_failed ) ) ;
} else {
const textArea = document . createElement ( ' textarea ' ) ;
textArea . value = content ;
textArea . style . position = ' fixed ' ;
textArea . style . opacity = ' 0 ' ;
document . body . appendChild ( textArea ) ;
textArea . focus ( ) ;
textArea . select ( ) ;
try {
document . execCommand ( ' copy ' ) ;
showFeedback ( button ) ;
} catch ( err ) {
showFeedback ( button , i18n . js_failed , i18n . js_failed ) ;
}
document . body . removeChild ( textArea ) ;
}
} ;
const handleDownloadSVG = ( ) = > {
const clonedSvg = svgEl . 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 : 14 px ; }
h1 { font - size : 22 px ; font - weight : 700 ; margin : 0 ; }
h2 { font - size : 18 px ; 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 ) ;
} ;
const handleDownloadMD = ( ) = > {
const markdownContent = document . getElementById ( ' markdown-source- ' + uniqueId ) ? . textContent | | ' ' ;
copyToClipboard ( markdownContent , downloadMdBtn ) ;
} ;
const handleDownloadPNG = ( ) = > {
const btn = downloadPngBtn ;
const originalText = btn . textContent ;
btn . textContent = i18n . js_generating ;
btn . disabled = true ;
try {
const clonedSvg = svgEl . cloneNode ( true ) ;
clonedSvg . setAttribute ( ' xmlns ' , ' http://www.w3.org/2000/svg ' ) ;
const rect = svgEl . getBoundingClientRect ( ) ;
const width = rect . width | | 800 ;
const height = rect . height | | 600 ;
clonedSvg . setAttribute ( ' width ' , width ) ;
clonedSvg . setAttribute ( ' height ' , height ) ;
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 ) ;
} ) ;
const style = document . createElementNS ( ' http://www.w3.org/2000/svg ' , ' style ' ) ;
style . textContent = `
text { font - family : sans - serif ; font - size : 14 px ; 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 ) ;
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 img = new Image ( ) ;
img . onload = ( ) = > {
const canvas = document . createElement ( ' canvas ' ) ;
const scale = 3 ;
canvas . width = width * scale ;
canvas . height = height * scale ;
const ctx = canvas . getContext ( ' 2d ' ) ;
ctx . scale ( scale , scale ) ;
ctx . drawImage ( img , 0 , 0 , width , height ) ;
canvas . toBlob ( ( blob ) = > {
if ( ! blob ) return ;
const a = document . createElement ( ' a ' ) ;
a . download = i18n . js_filename ;
a . href = URL . createObjectURL ( blob ) ;
a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
btn . textContent = originalText ;
btn . disabled = false ;
showFeedback ( btn ) ;
} , ' image/png ' ) ;
} ;
img . src = ' data:image/svg+xml;base64, ' + svgBase64 ;
} catch ( err ) {
btn . textContent = originalText ;
btn . disabled = false ;
showFeedback ( btn , i18n . js_failed , i18n . js_failed ) ;
}
} ;
const handleZoom = ( dir ) = > {
if ( dir == = ' reset ' ) markmapInstance . fit ( ) ;
else if ( markmapInstance . rescale ) {
markmapInstance . rescale ( dir == = ' in ' ? 1.25 : 0.8 ) ;
}
} ;
const handleDepthChange = ( e ) = > {
const level = parseInt ( e . target . value , 10 ) ;
const expandLevel = level == = 0 ? Infinity : level ;
const applyFold = ( node , currentDepth ) = > {
if ( ! node ) return ;
if ( ! node . payload ) node . payload = { } ;
node . payload . fold = currentDepth > = expandLevel ? 1 : 0 ;
if ( node . children ) node . children . forEach ( c = > applyFold ( c , currentDepth + 1 ) ) ;
} ;
const cleanRoot = JSON . parse ( JSON . stringify ( root ) ) ;
applyFold ( cleanRoot , 0 ) ;
markmapInstance . setOptions ( { initialExpandLevel : expandLevel } ) ;
markmapInstance . setData ( cleanRoot ) ;
setTimeout ( ( ) = > markmapInstance . fit ( ) , 50 ) ;
} ;
const handleFullscreen = ( ) = > {
const el = wrapper | | containerEl ;
if ( ! document . fullscreenElement ) {
el . requestFullscreen ( ) . catch ( ( ) = > containerEl . requestFullscreen ( ) ) ;
} else {
document . exitFullscreen ( ) ;
}
} ;
downloadSvgBtn ? . addEventListener ( ' click ' , ( ) = > handleDownloadSVG ( ) ) ;
downloadMdBtn ? . addEventListener ( ' click ' , ( ) = > handleDownloadMD ( ) ) ;
downloadPngBtn ? . addEventListener ( ' click ' , ( ) = > handleDownloadPNG ( ) ) ;
zoomInBtn ? . addEventListener ( ' click ' , ( ) = > handleZoom ( ' in ' ) ) ;
zoomOutBtn ? . addEventListener ( ' click ' , ( ) = > handleZoom ( ' out ' ) ) ;
zoomResetBtn ? . addEventListener ( ' click ' , ( ) = > handleZoom ( ' reset ' ) ) ;
depthSelect ? . addEventListener ( ' change ' , handleDepthChange ) ;
fullscreenBtn ? . addEventListener ( ' click ' , handleFullscreen ) ;
themeToggleBtn ? . addEventListener ( ' click ' , ( ) = > {
currentTheme = currentTheme == = ' dark ' ? ' light ' : ' dark ' ;
setTheme ( wrapper , currentTheme ) ;
} ) ;
} ;
if ( document . readyState == = ' loading ' ) {
document . addEventListener ( ' DOMContentLoaded ' , renderMindmap ) ;
} else {
renderMindmap ( ) ;
}
} ) ( ) ;
< / script >
"""
2026-03-04 00:11:28 +08:00
class Tools :
2026-03-04 23:14:06 +08:00
""" Smart Mind Map Tool — OpenWebUI Tool plugin.
Proactively transforms multi - turn conversation content into an
interactive , browser - rendered mind map powered by Markmap .
The AI decides when a mind map would be beneficial and invokes
this tool automatically , unlike the Action variant which requires
manual user initiation .
Key features :
- Full UI parity with the Smart Mind Map Action .
- Multi - layer language detection ( JS frontend > HTTP header > profile ) .
- 5 - second waiting notification to reassure users during slow LLM calls .
- Configurable via Valves : model , min text length , message count ,
and fallback language .
"""
2026-03-04 00:11:28 +08:00
class Valves ( BaseModel ) :
2026-03-04 23:14:06 +08:00
MODEL_ID : str = Field (
default = " " ,
description = " Specific model ID to use for mind map analysis (e.g., ' gpt-4o ' ). If empty, uses the current conversation model. " ,
)
MIN_TEXT_LENGTH : int = Field (
default = 100 ,
description = " Minimum text length (character count) required for mind map analysis. " ,
)
MESSAGE_COUNT : int = Field (
default = 12 ,
description = " Number of recent messages to use for generation (0 for all messages). " ,
)
2026-03-04 00:11:28 +08:00
def __init__ ( self ) :
2026-03-04 23:14:06 +08:00
""" Initialise plugin state: Valves config and internal lookup maps. """
2026-03-04 00:11:28 +08:00
self . valves = self . Valves ( )
2026-03-04 23:14:06 +08:00
self . weekday_map = {
" Monday " : " Monday " ,
" Tuesday " : " Tuesday " ,
" Wednesday " : " Wednesday " ,
" Thursday " : " Thursday " ,
" Friday " : " Friday " ,
" Saturday " : " Saturday " ,
" Sunday " : " Sunday " ,
}
self . fallback_map = {
" es-AR " : " es-ES " ,
" es-MX " : " es-ES " ,
" fr-CA " : " fr-FR " ,
" en-CA " : " en-US " ,
" en-GB " : " en-US " ,
" en-AU " : " en-US " ,
" de-AT " : " de-DE " ,
2026-03-04 00:11:28 +08:00
}
2026-03-04 23:14:06 +08:00
CSS_TEMPLATE = """
2026-03-04 00:11:28 +08:00
: root {
2026-03-04 23:14:06 +08:00
- - primary - color : #1e88e5;
- - secondary - color : #43a047;
- - background - color : #f4f6f8;
- - card - bg - color : #ffffff;
- - text - color : #000000;
- - link - color : #546e7a;
- - node - stroke - color : #90a4ae;
- - muted - text - color : #546e7a;
- - border - color : #e0e0e0;
- - header - gradient : linear - gradient ( 135 deg , var ( - - secondary - color ) , var ( - - primary - color ) ) ;
- - shadow : 0 10 px 20 px rgba ( 0 , 0 , 0 , 0.06 ) ;
- - border - radius : 12 px ;
- - font - family : - apple - system , BlinkMacSystemFont , " Segoe UI " , Roboto , " Helvetica Neue " , Arial , sans - serif ;
2026-03-04 00:11:28 +08:00
}
. theme - dark {
2026-03-04 23:14:06 +08:00
- - 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 ( 135 deg , #0ea5e9, #22c55e);
- - shadow : 0 10 px 20 px rgba ( 0 , 0 , 0 , 0.3 ) ;
}
html , body {
margin : 0 ;
padding : 0 ;
width : 100 % ;
height : 100 % ;
background - color : transparent ! important ;
overflow : hidden ;
}
. mindmap - container - wrapper {
font - family : var ( - - font - family ) ;
line - height : 1.6 ;
color : var ( - - text - color ) ;
margin : 0 ;
padding : 0 ;
- webkit - font - smoothing : antialiased ;
- moz - osx - font - smoothing : grayscale ;
display : flex ;
flex - direction : column ;
background : var ( - - card - bg - color ) ;
width : 100 % ;
aspect - ratio : 16 / 9 ;
min - height : 300 px ;
max - height : 800 px ;
box - sizing : border - box ;
overflow : hidden ;
border : 1 px solid var ( - - border - color ) ;
border - radius : var ( - - border - radius ) ;
box - shadow : var ( - - shadow ) ;
}
. header {
background : var ( - - card - bg - color ) ;
color : var ( - - text - color ) ;
padding : 8 px 16 px ;
display : flex ;
flex - direction : column ;
gap : 8 px ;
flex - shrink : 0 ;
border - bottom : 1 px solid var ( - - border - color ) ;
z - index : 10 ;
}
. header - top {
display : flex ;
align - items : center ;
gap : 12 px ;
}
. header h1 {
margin : 0 ;
font - size : 1.1 em ;
font - weight : 600 ;
letter - spacing : 0.3 px ;
display : flex ;
align - items : center ;
gap : 8 px ;
}
. header - credits {
font - size : 0.8 em ;
color : var ( - - muted - text - color ) ;
opacity : 0.8 ;
white - space : nowrap ;
}
. header - credits a {
color : var ( - - primary - color ) ;
text - decoration : none ;
border - bottom : 1 px dotted var ( - - link - color ) ;
}
. content - area {
padding : 0 ;
flex : 1 1 0 ;
background : var ( - - card - bg - color ) ;
position : relative ;
overflow : hidden ;
width : 100 % ;
min - height : 0 ;
}
. markmap - container {
position : absolute ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
background - color : var ( - - card - bg - color ) ;
}
. markmap - container svg {
width : 100 % ;
height : 100 % ;
display : block ;
}
. 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 ;
stroke - opacity : 0.6 ;
}
. theme - dark . markmap - node circle {
fill : var ( - - card - bg - color ) ! important ;
}
. markmap - container svg . markmap - node circle ,
. markmap - container svg . markmap - node rect {
stroke : var ( - - node - stroke - color ) ! important ;
}
. control - rows {
display : flex ;
align - items : center ;
flex - wrap : wrap ;
gap : 12 px ;
margin - left : auto ;
}
. btn - group {
display : inline - flex ;
gap : 4 px ;
align - items : center ;
border : 1 px solid var ( - - border - color ) ;
border - radius : 6 px ;
padding : 2 px ;
background : var ( - - background - color ) ;
}
. control - btn {
background - color : transparent ;
color : var ( - - text - color ) ;
border : none ;
padding : 4 px 10 px ;
border - radius : 4 px ;
font - size : 0.85 em ;
font - weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ease ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
height : 28 px ;
box - sizing : border - box ;
opacity : 0.8 ;
}
. control - btn : hover {
background - color : var ( - - card - bg - color ) ;
opacity : 1 ;
box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. control - btn . primary {
background - color : var ( - - primary - color ) ;
color : white ;
opacity : 1 ;
}
. control - btn . primary : hover {
box - shadow : 0 2 px 5 px rgba ( 30 , 136 , 229 , 0.3 ) ;
}
select . control - btn {
appearance : none ;
padding - right : 28 px ;
background - image : url ( " data:image/svg+xml;charset=US-ASCII, % 3Csvg %20x mlns % 3D % 22http % 3A %2F %2F www.w3.org %2F 2000 %2F svg %22% 20width % 3D %22292.4% 22 %20he ight % 3D %22292.4% 22 %3E % 3Cpath %20f ill % 3D %22% 23FFFFFF %22% 20d % 3D % 22M287 %2069.4a 17.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.9c 3.6 %203.6% 207.8 %205.4% 2012.8 %205.4s 9.2-1.8 % 2012.8-5.4L287 %2095c 3.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 %2F svg %3E " ) ;
background - repeat : no - repeat ;
background - position : right 8 px center ;
background - size : 10 px ;
}
. control - btn option {
background - color : var ( - - card - bg - color ) ;
color : var ( - - text - color ) ;
}
@media screen and ( max - width : 768 px ) {
. mindmap - container - wrapper {
aspect - ratio : 4 / 5 ;
min - height : 480 px ;
max - height : 85 vh ;
}
. header { flex - direction : column ; gap : 10 px ; }
. btn - group { padding : 2 px ; }
. control - btn { padding : 4 px 6 px ; font - size : 0.75 em ; height : 28 px ; }
select . control - btn { padding - right : 20 px ; background - position : right 4 px center ; }
}
"""
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
CONTENT_TEMPLATE = """
< div class = " mindmap-container-wrapper " >
2026-03-04 00:11:28 +08:00
< div class = " header " >
2026-03-04 23:14:06 +08:00
< div class = " header-top " >
< h1 > { t_ui_title } < / h1 >
< div class = " header-credits " >
< span > { t_ui_footer } < / span >
< / div >
< div class = " control-rows " >
< div class = " btn-group " >
< button id = " download-png-btn- {unique_id} " class = " control-btn primary " title = " {t_ui_download_png} " > PNG < / button >
< button id = " download-svg-btn- {unique_id} " class = " control-btn " title = " {t_ui_download_svg} " > SVG < / button >
< button id = " download-md-btn- {unique_id} " class = " control-btn " title = " {t_ui_download_md} " > MD < / button >
< / div >
< div class = " btn-group " >
< button id = " zoom-out-btn- {unique_id} " class = " control-btn " title = " {t_ui_zoom_out} " > - < / button >
< button id = " zoom-reset-btn- {unique_id} " class = " control-btn " title = " {t_ui_zoom_reset} " > ↺ < / button >
< button id = " zoom-in-btn- {unique_id} " class = " control-btn " title = " {t_ui_zoom_in} " > + < / button >
< / div >
< div class = " btn-group " >
< select id = " depth-select- {unique_id} " class = " control-btn " title = " {t_ui_depth_select} " >
< option value = " 0 " > { t_ui_depth_all } < / option >
< option value = " 2 " > { t_ui_depth_2 } < / option >
< option value = " 3 " selected > { t_ui_depth_3 } < / option >
< / select >
< button id = " fullscreen-btn- {unique_id} " class = " control-btn " title = " {t_ui_fullscreen} " > ⛶ < / button >
< button id = " theme-toggle-btn- {unique_id} " class = " control-btn " title = " {t_ui_theme} " > ◑ < / button >
< / div >
< / div >
2026-03-04 00:11:28 +08:00
< / div >
< / div >
2026-03-04 23:14:06 +08:00
< div class = " content-area " >
< div class = " markmap-container " id = " markmap-container- {unique_id} " > < / div >
< / div >
2026-03-04 00:11:28 +08:00
< / div >
2026-03-04 23:14:06 +08:00
< script type = " text/template " id = " markdown-source- {unique_id} " > { markdown_syntax } < / script >
"""
2026-03-04 00:11:28 +08:00
async def generate_mind_map (
self ,
2026-03-04 23:14:06 +08:00
__user__ : dict = { } ,
__event_emitter__ : Callable = None ,
__event_call__ : Callable = None ,
__request__ : Request = None ,
__messages__ : list = [ ] ,
__metadata__ : dict = { } ,
2026-03-04 00:11:28 +08:00
) - > Any :
2026-03-04 23:14:06 +08:00
""" Entry point invoked by the AI to generate an interactive mind map.
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
Aggregates conversation messages from the __messages__ injection ,
calls the configured LLM to produce a Markmap - compatible Markdown
outline , then renders it as a self - contained HTML response embedded
directly into the chat .
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
OpenWebUI injects __messages__ directly with the full conversation
history . The MESSAGE_COUNT valve controls how many recent messages
are included ( 0 = all ) .
A background timer fires a user - visible notification if LLM analysis
exceeds 5 seconds , ensuring the user is informed during slow responses .
Args :
__user__ : OpenWebUI user context dict .
__event_emitter__ : Async callable for status / notification events .
__event_call__ : Async callable for JS execution ( language detection ) .
__request__ : Starlette Request — used for Accept - Language header .
__messages__ : Full conversation history injected by OpenWebUI .
__metadata__ : OpenWebUI metadata bag containing model ID , etc .
Returns :
HTMLResponse with the full mind map page on success , or an error string .
"""
user_ctx = await _get_user_context (
__user__ , __request__ , self . valves , __event_call__
)
user_lang = user_ctx [ " user_language " ]
user_name = user_ctx [ " user_name " ]
# Aggregate conversation messages from __messages__ (OpenWebUI direct injection)
target_text = " "
all_msgs = __messages__ or [ ]
if all_msgs :
count = self . valves . MESSAGE_COUNT
if count > 1 :
recent = all_msgs [ - count : ]
else :
# 0: all messages
recent = all_msgs
aggregated = [ ]
for msg in recent :
# Filter out messages that don't have user-visible content
# or are internal tool calls to avoid noise
role = msg . get ( " role " )
content = _extract_text_content ( msg . get ( " content " , " " ) )
if content and role in [ " user " , " assistant " ] :
prefix = " User: " if role == " user " else " Assistant: "
aggregated . append ( f " { prefix } { content } " )
if aggregated :
target_text = " \n \n " . join ( aggregated )
logger . info ( f " Aggregated { len ( aggregated ) } messages for mind map. " )
await _emit_status (
__event_emitter__ ,
_get_translation ( self . valves , user_lang , " status_starting " ) ,
False ,
)
if not target_text or len ( target_text ) < self . valves . MIN_TEXT_LENGTH :
msg = _get_translation (
self . valves ,
user_lang ,
" error_text_too_short " ,
len = len ( target_text ) ,
min_len = self . valves . MIN_TEXT_LENGTH ,
)
await _emit_notification ( __event_emitter__ , msg , " warning " )
return f " ⚠️ { msg } "
await _emit_status (
__event_emitter__ ,
_get_translation ( self . valves , user_lang , " status_analyzing " ) ,
False ,
)
async def _notify_waiting ( ) :
try :
await asyncio . sleep ( 5.0 )
await _emit_notification (
__event_emitter__ ,
_get_translation ( self . valves , user_lang , " notification_waiting " ) ,
" info " ,
)
except asyncio . CancelledError :
pass
waiting_task = asyncio . create_task ( _notify_waiting ( ) )
2026-03-04 00:11:28 +08:00
try :
2026-03-04 23:14:06 +08:00
target_model = self . valves . MODEL_ID
if not target_model :
meta_model = __metadata__ . get ( " model " , " " )
if isinstance ( meta_model , dict ) :
target_model = meta_model . get ( " id " , " gpt-4o " )
elif isinstance ( meta_model , str ) and meta_model . strip ( ) :
target_model = meta_model
target_model = target_model or " gpt-4o "
# Prepare prompt context
tz_str = os . environ . get ( " TZ " , " UTC " )
now = datetime . now ( ZoneInfo ( tz_str ) )
current_date_time_str = now . strftime ( " % Y- % m- %d % H: % M: % S " )
current_weekday = now . strftime ( " % A " )
resolved_lang = _resolve_language ( self . valves , user_lang )
prompt = (
USER_PROMPT_GENERATE_MINDMAP . replace ( " {user_name} " , user_name )
. replace ( " {current_date_time_str} " , current_date_time_str )
. replace ( " {current_weekday} " , current_weekday )
. replace ( " {current_timezone_str} " , tz_str )
. replace ( " {user_language} " , resolved_lang )
. replace ( " {long_text_content} " , target_text )
)
payload = {
2026-03-04 00:11:28 +08:00
" model " : target_model ,
" messages " : [
2026-03-04 23:14:06 +08:00
{ " role " : " system " , " content " : SYSTEM_PROMPT_MINDMAP_ASSISTANT } ,
{ " role " : " user " , " content " : prompt } ,
2026-03-04 00:11:28 +08:00
] ,
" temperature " : 0.5 ,
2026-03-04 23:14:06 +08:00
" stream " : False ,
2026-03-04 00:11:28 +08:00
}
2026-03-04 23:14:06 +08:00
user_obj = Users . get_user_by_id ( user_ctx [ " user_id " ] )
response = await generate_chat_completion ( __request__ , payload , user_obj )
assistant_content = response [ " choices " ] [ 0 ] [ " message " ] [ " content " ]
markdown_syntax = _extract_markdown_syntax ( assistant_content )
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
unique_id = f " mm_ { int ( time . time ( ) ) } "
ui_trans = {
f " t_ { k } " : _get_translation ( self . valves , user_lang , k )
for k in TRANSLATIONS [ " en-US " ]
if k . startswith ( " ui_ " )
}
html_body = self . CONTENT_TEMPLATE . replace ( " {unique_id} " , unique_id ) . replace (
" {markdown_syntax} " , markdown_syntax
)
for k , v in ui_trans . items ( ) :
html_body = html_body . replace ( f " {{ { k } }} " , v )
js_trans = {
k : v
for k , v in TRANSLATIONS . get (
_resolve_language ( self . valves , user_lang ) , TRANSLATIONS [ " en-US " ]
) . items ( )
if k . startswith ( " js_ " ) or k . startswith ( " html_ " )
}
js_code = SCRIPT_TEMPLATE_MINDMAP . replace (
" {unique_id_json} " , json . dumps ( unique_id )
) . replace ( " {i18n_json} " , json . dumps ( js_trans , ensure_ascii = False ) )
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
full_html = (
HTML_WRAPPER_TEMPLATE . replace ( " {lang} " , user_lang [ 0 : 2 ] )
. replace ( " /* STYLES_INSERTION_POINT */ " , self . CSS_TEMPLATE )
. replace ( " <!-- CONTENT_INSERTION_POINT --> " , html_body )
. replace ( " <!-- SCRIPTS_INSERTION_POINT --> " , js_code )
)
2026-03-04 00:11:28 +08:00
2026-03-04 23:14:06 +08:00
waiting_task . cancel ( )
await _emit_status (
__event_emitter__ ,
_get_translation ( self . valves , user_lang , " status_drawing " ) ,
True ,
)
return HTMLResponse (
content = full_html , headers = { " Content-Disposition " : " inline " }
)
2026-03-04 00:11:28 +08:00
except Exception as e :
2026-03-04 23:14:06 +08:00
waiting_task . cancel ( )
logger . error ( f " Generate Mind Map failed: { e } " )
await _emit_status ( __event_emitter__ , f " Error: { e } " , True )
return f " ❌ { e } "