""" title: šŸ“Š Smart Infographic (AntV) author: jeff author_url: https://github.com/Fu-Jie/awesome-openwebui icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPHBhdGggZD0iTTMuNSAyMC41aDE3IiAvPgogIDxwYXRoIGQ9Ik04IDIwLjV2LTguNSIgLz4KICA8cGF0aCBkPSJNMTIgMjAuNXYtMTMiIC8+CiAgPHBhdGggZD0iTTE2IDIwLjV2LTYiIC8+Cjwvc3ZnPg== version: 1.3.0 description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads. """ 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 ``` #### E. Stylize Configuration You can apply specific visual styles using the `theme` block. **Supported Styles (`stylize`):** - `rough`: Hand-drawn style - `pattern`: Pattern fill - `linear-gradient`: Linear gradient fill - `radial-gradient`: Radial gradient fill **Example (Rough Style):** ```infographic infographic list-row-simple-horizontal-arrow theme stylize rough data ... ``` **Example (Gradient Style):** ```infographic infographic chart-bar theme stylize linear-gradient data ... ``` #### F. 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