""" title: Infographic icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIj48cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHJ4PSIyIi8+PHBhdGggZD0iTTcgOGg1Ii8+PHBhdGggZD0iTTcgMTJoNyIvPjxwYXRoIGQ9Ik03IDE2aDkiLz48L3N2Zz4= version: 1.1.2 description: Transform text content into beautiful infographics with multiple templates and automatic icon search. """ from pydantic import BaseModel, Field from typing import Optional, Dict, Any import logging import time import re from fastapi import Request from datetime import datetime from open_webui.utils.chat import generate_chat_completion from open_webui.models.users import Users logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger(__name__) # ================================================================= # LLM Prompts # ================================================================= SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """ You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format. ## Infographic Syntax Specification Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes. ### Syntax Rules - Entry uses `infographic ` - Key-value pairs are separated by spaces, **absolutely NO colons allowed** - Use two spaces for indentation - Object arrays use `-` with line breaks āš ļø **IMPORTANT WARNING: This is NOT YAML format!** - āŒ Wrong: `children:` `items:` `data:` (with colons) - āœ… Correct: `children` `items` `data` (without colons) ### Template Library & Selection Guide Choose the most appropriate template based on the content structure: #### 1. List & Hierarchy - **List**: `list-grid` (Grid Cards), `list-vertical` (Vertical List) - **Tree**: `tree-vertical` (Vertical Tree), `tree-horizontal` (Horizontal Tree) - **Mindmap**: `mindmap` (Mind Map) #### 2. Sequence & Relationship - **Process**: `sequence-roadmap` (Roadmap), `sequence-zigzag` (Zigzag Process), `sequence-horizontal` (Horizontal Process) - **Relationship**: `relation-sankey` (Sankey Diagram), `relation-circle` (Circular Relationship) #### 3. Comparison & Analysis - **Comparison**: `compare-binary` (Binary Comparison), `list-grid` (Multi-item Grid Comparison) - **Analysis**: `compare-swot` (SWOT Analysis), `quadrant-quarter` (Quadrant Chart) #### 4. Charts & Data - **Statistics**: `statistic-card` (Statistic Cards) - **Charts**: `chart-bar` (Bar Chart), `chart-column` (Column Chart), `chart-line` (Line Chart), `chart-pie` (Pie Chart), `chart-doughnut` (Doughnut Chart), `chart-area` (Area Chart) ### Data Structure Examples #### A. Standard List/Tree (Default) Use `items` and `children` structure. ```infographic infographic list-grid data title Project Modules items - label Module A desc Description of A - label Module B desc Description of B ``` #### B. Binary Comparison Use `items` for two sides and `children` for comparison points. ```infographic infographic compare-binary data title Advantages vs Disadvantages desc Compare two aspects side by side items - label Advantages children - label Strong R&D desc Leading technology and innovation capability - label High customer loyalty desc Repurchase rate over 60% - label Disadvantages children - label Weak brand exposure desc Insufficient marketing, low awareness - label Narrow channel coverage desc Limited online channels ``` #### C. SWOT Analysis Use `children` to define the 4 quadrants (Strengths, Weaknesses, Opportunities, Threats). ```infographic infographic compare-swot data title Product SWOT Analysis items - label Internal Factors children - label Strengths children - label High Performance - label Low Cost - label Weaknesses children - label Limited Features - label External Factors children - label Opportunities children - label Growing Market - label Threats children - label New Competitors ``` #### D. Quadrant Chart Use `items` for quadrants and `illus` for icons. ```infographic infographic quadrant-quarter data title Priority Matrix items - label High Importance children - label Urgent desc Do it now illus mdi/alert - label Not Urgent desc Schedule it illus mdi/calendar - label Low Importance children - label Urgent desc Delegate it illus mdi/account-arrow-right - label Not Urgent desc Delete it illus mdi/delete ``` #### D. Charts (Bar/Column/Line/Pie) Use `items` with `label` and `value`. ```infographic infographic chart-bar data title Quarterly Revenue items - label Q1 value 120 - label Q2 value 150 - label Q3 value 180 - label Q4 value 220 ``` ### Common Data Fields - `label`: Main title/label (Required) - `desc`: Description text - `value`: Numeric value (for charts) - `icon`: Icon name (e.g., `mdi/home`, `mdi/account`) or `ref:search:` - `children`: Nested items (for trees, SWOT, etc.) - `illus`: Illustration icon (specific to some templates like Quadrant) ## Output Requirements 1. **Language**: Output content in the user's language. 2. **Format**: Wrap output in ```infographic ... ```. 3. **No Colons**: Do NOT use colons after keys. 4. **Indentation**: Use 2 spaces. """ USER_PROMPT_GENERATE_INFOGRAPHIC = """ Please analyze the following text content and convert its core information into AntV Infographic syntax format. --- **User Context:** User Name: {user_name} Current Date/Time: {current_date_time_str} User Language: {user_language} --- **Text Content:** {long_text_content} Please select the most appropriate infographic template based on text characteristics and output standard infographic syntax. Pay attention to correct indentation format (two spaces). **Important Note:** - If using `list-grid` format, ensure each card's `desc` description is limited to **maximum 30 Chinese characters** (or **approximately 60 English characters**) to maintain visual consistency with all descriptions fitting in 2 lines. - Descriptions should be concise and highlight key points. """ # ================================================================= # HTML Container Template # ================================================================= HTML_WRAPPER_TEMPLATE = """
""" # ================================================================= # CSS Style Template # ================================================================= CSS_TEMPLATE_INFOGRAPHIC = """ :root { --ig-primary-color: #6366f1; --ig-secondary-color: #8b5cf6; --ig-tertiary-color: #10b981; --ig-background-color: #f8fafc; --ig-card-bg-color: #ffffff; --ig-text-color: #1e293b; --ig-muted-text-color: #64748b; --ig-border-color: #e2e8f0; --ig-header-gradient: linear-gradient(135deg, #6366f1, #8b5cf6); } .infographic-container-wrapper { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: var(--ig-text-color); height: 100%; display: flex; flex-direction: column; } .infographic-container-wrapper .header { background: var(--ig-header-gradient); color: white; padding: 20px 24px; text-align: center; } .infographic-container-wrapper .header h1 { margin: 0; font-size: 1.5em; font-weight: 600; } .infographic-container-wrapper .user-context { font-size: 0.8em; color: var(--ig-muted-text-color); background-color: #f1f5f9; padding: 8px 16px; display: flex; justify-content: space-around; flex-wrap: wrap; border-bottom: 1px solid var(--ig-border-color); } .infographic-container-wrapper .content-area { padding: 20px; flex-grow: 1; } .infographic-container-wrapper .infographic-render-container { border-radius: 8px; padding: 16px; background: #fff; overflow: visible; /* Ensure content is visible */ transition: height 0.3s ease; } .infographic-render-container svg text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important; } .infographic-render-container svg foreignObject { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important; line-height: 1.4 !important; } /* Main title styles */ .infographic-render-container svg foreignObject[data-element-type="title"] > * { font-size: 1.5em !important; font-weight: bold !important; line-height: 1.4 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* Page subtitle and card title styles */ .infographic-render-container svg foreignObject[data-element-type="desc"] > *, .infographic-render-container svg foreignObject[data-element-type="item-label"] > * { font-size: 0.6em !important; line-height: 1.4 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* Card title with extra bottom spacing */ .infographic-render-container svg foreignObject[data-element-type="item-label"] > * { padding-bottom: 8px !important; display: block !important; } /* Card description text keeps normal wrapping */ .infographic-render-container svg foreignObject[data-element-type="item-desc"] > * { line-height: 1.4 !important; white-space: normal !important; } .infographic-container-wrapper .download-area { text-align: center; padding-top: 20px; margin-top: 20px; border-top: 1px solid var(--ig-border-color); } .infographic-container-wrapper .download-btn { background-color: var(--ig-primary-color); color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 0.9em; cursor: pointer; transition: all 0.2s; margin: 4px 6px; display: inline-flex; align-items: center; gap: 6px; } .infographic-container-wrapper .download-btn.secondary { background-color: var(--ig-secondary-color); } .infographic-container-wrapper .download-btn.tertiary { background-color: var(--ig-tertiary-color); } .infographic-container-wrapper .download-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .infographic-container-wrapper .footer { text-align: center; padding: 16px; font-size: 0.8em; color: var(--ig-muted-text-color); background-color: #f8fafc; border-top: 1px solid var(--ig-border-color); } .infographic-container-wrapper .error-message { color: #dc2626; background-color: #fef2f2; border: 1px solid #fecaca; padding: 16px; border-radius: 8px; text-align: center; } """ # ================================================================= # HTML Content Template # ================================================================= CONTENT_TEMPLATE_INFOGRAPHIC = """

šŸ“Š Smart Infographic

User: {user_name} Time: {current_date_time_str}
""" # ================================================================= # JavaScript Rendering Script # ================================================================= SCRIPT_TEMPLATE_INFOGRAPHIC = """ """ class Action: class Valves(BaseModel): SHOW_STATUS: bool = Field( default=True, description="Show operation status updates in chat interface." ) MODEL_ID: str = Field( default="", description="Built-in LLM model ID for text analysis. If empty, uses current conversation model.", ) MIN_TEXT_LENGTH: int = Field( default=100, description="Minimum text length (characters) required for infographic analysis.", ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, description="Force clear old plugin results (if True, overwrite instead of merge).", ) MESSAGE_COUNT: int = Field( default=1, description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.", ) def __init__(self): self.valves = self.Valves() def _extract_infographic_syntax(self, llm_output: str) -> str: """Extract infographic syntax from LLM output""" match = re.search(r"```infographic\s*(.*?)\s*```", llm_output, re.DOTALL) if match: extracted_content = match.group(1).strip() else: logger.warning( "LLM output did not follow expected format, treating entire output as syntax." ) extracted_content = llm_output.strip() return extracted_content.replace("", "<\\/script>") async def _emit_status(self, emitter, description: str, done: bool = False): """Send status update event""" if self.valves.SHOW_STATUS and emitter: await emitter( {"type": "status", "data": {"description": description, "done": done}} ) async def _emit_notification(self, emitter, content: str, ntype: str = "info"): """Send notification event (info/success/warning/error)""" if emitter: await emitter( {"type": "notification", "data": {"type": ntype, "content": content}} ) def _remove_existing_html(self, content: str) -> str: """Remove existing plugin-generated HTML code blocks from content""" pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() def _extract_text_content(self, content) -> str: """Extract text from message content, supporting multimodal message formats""" if isinstance(content, str): return content elif isinstance(content, list): # Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}] text_parts = [] for item in content: if isinstance(item, dict) and item.get("type") == "text": text_parts.append(item.get("text", "")) elif isinstance(item, str): text_parts.append(item) return "\n".join(text_parts) return str(content) if content else "" def _merge_html( self, existing_html_code: str, new_content: str, new_styles: str = "", new_scripts: str = "", user_language: str = "en", ) -> str: """Merge new content into existing HTML container or create a new one""" if ( "" in existing_html_code and "" in existing_html_code ): base_html = existing_html_code base_html = re.sub(r"^```html\s*", "", base_html) base_html = re.sub(r"\s*```$", "", base_html) else: base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language) wrapped_content = f'
\n{new_content}\n
' if new_styles: base_html = base_html.replace( "/* STYLES_INSERTION_POINT */", f"{new_styles}\n/* STYLES_INSERTION_POINT */", ) base_html = base_html.replace( "", f"{wrapped_content}\n", ) if new_scripts: base_html = base_html.replace( "", f"{new_scripts}\n", ) return base_html.strip() async def action( self, body: dict, __user__: Optional[Dict[str, Any]] = None, __event_emitter__: Optional[Any] = None, __request__: Optional[Request] = None, ) -> Optional[dict]: logger.info("Action: Infographic started (v1.0.0)") # Get user information if isinstance(__user__, (list, tuple)): user_language = __user__[0].get("language", "en") if __user__ else "en" 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") user_name = __user__.get("name", "User") user_id = __user__.get("id", "unknown_user") # Get current time now = datetime.now() current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") current_year = now.strftime("%Y") original_content = "" try: messages = body.get("messages", []) if not messages: raise ValueError("Unable to get valid user message content.") # Get last N messages based on MESSAGE_COUNT message_count = min(self.valves.MESSAGE_COUNT, len(messages)) recent_messages = messages[-message_count:] # Aggregate content from selected messages with labels aggregated_parts = [] for i, msg in enumerate(recent_messages, 1): text_content = self._extract_text_content(msg.get("content")) if text_content: role = msg.get("role", "unknown") role_label = ( "User" if role == "user" else "Assistant" if role == "assistant" else role ) aggregated_parts.append( f"[{role_label} Message {i}]\n{text_content}" ) if not aggregated_parts: raise ValueError("Unable to get valid user message content.") original_content = "\n\n---\n\n".join(aggregated_parts) # Extract non-HTML text parts = re.split(r"```html.*?```", original_content, flags=re.DOTALL) long_text_content = "" if parts: for part in reversed(parts): if part.strip(): long_text_content = part.strip() break if not long_text_content: long_text_content = original_content.strip() # Check text length if len(long_text_content) < self.valves.MIN_TEXT_LENGTH: short_text_message = f"Text content too short ({len(long_text_content)} characters). Please provide at least {self.valves.MIN_TEXT_LENGTH} characters for effective analysis." await self._emit_notification( __event_emitter__, short_text_message, "warning" ) return { "messages": [ {"role": "assistant", "content": f"āš ļø {short_text_message}"} ] } await self._emit_notification( __event_emitter__, "šŸ“Š Infographic started, generating...", "info" ) await self._emit_status( __event_emitter__, "šŸ“Š Infographic: Starting generation...", False, ) # Generate unique ID unique_id = f"id_{int(time.time() * 1000)}" # Build prompt await self._emit_status( __event_emitter__, "šŸ“Š Infographic: Calling AI model to analyze content...", False, ) formatted_user_prompt = USER_PROMPT_GENERATE_INFOGRAPHIC.format( user_name=user_name, current_date_time_str=current_date_time_str, user_language=user_language, long_text_content=long_text_content, ) # Determine model to use target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") llm_payload = { "model": target_model, "messages": [ {"role": "system", "content": SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT}, {"role": "user", "content": formatted_user_prompt}, ], "stream": False, } user_obj = Users.get_user_by_id(user_id) if not user_obj: raise ValueError(f"Unable to get user object, user ID: {user_id}") llm_response = await generate_chat_completion( __request__, llm_payload, user_obj ) if ( not llm_response or "choices" not in llm_response or not llm_response["choices"] ): raise ValueError("Invalid LLM response format or empty.") await self._emit_status( __event_emitter__, "šŸ“Š Infographic: AI analysis complete, parsing syntax...", False, ) assistant_response_content = llm_response["choices"][0]["message"][ "content" ] infographic_syntax = self._extract_infographic_syntax( assistant_response_content ) # Prepare content components await self._emit_status( __event_emitter__, "šŸ“Š Infographic: Rendering chart...", False, ) content_html = ( CONTENT_TEMPLATE_INFOGRAPHIC.replace("{unique_id}", unique_id) .replace("{user_name}", user_name) .replace("{current_date_time_str}", current_date_time_str) .replace("{current_year}", current_year) .replace("{infographic_syntax}", infographic_syntax) ) # Replace placeholder first, then convert {{ to { and }} to } script_html = SCRIPT_TEMPLATE_INFOGRAPHIC.replace("{unique_id}", unique_id) script_html = script_html.replace("{{", "{").replace("}}", "}") # Extract existing HTML if any existing_html_block = "" match = re.search( r"```html\s*([\s\S]*?)```", original_content, ) if match: existing_html_block = match.group(1) if self.valves.CLEAR_PREVIOUS_HTML: original_content = self._remove_existing_html(original_content) final_html = self._merge_html( "", content_html, CSS_TEMPLATE_INFOGRAPHIC, script_html, user_language, ) else: if existing_html_block: original_content = self._remove_existing_html(original_content) final_html = self._merge_html( existing_html_block, content_html, CSS_TEMPLATE_INFOGRAPHIC, script_html, user_language, ) else: final_html = self._merge_html( "", content_html, CSS_TEMPLATE_INFOGRAPHIC, script_html, user_language, ) html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" await self._emit_status( __event_emitter__, "āœ… Infographic: Generation complete!", True ) await self._emit_notification( __event_emitter__, f"šŸ“Š Infographic generated, {user_name}!", "success", ) logger.info("Infographic generation completed") except Exception as e: error_message = f"Infographic processing failed: {str(e)}" logger.error(f"Infographic error: {error_message}", exc_info=True) user_facing_error = f"Sorry, infographic encountered an error during processing: {str(e)}.\nPlease check the Open WebUI backend logs for more details." body["messages"][-1][ "content" ] = f"{original_content}\n\nāŒ **Error:** {user_facing_error}" await self._emit_status( __event_emitter__, "āŒ Infographic: Generation failed", True ) await self._emit_notification( __event_emitter__, f"āŒ Infographic generation failed, {user_name}!", "error", ) return body