refactor: 优化插件HTML输出结构和样式,引入通用HTML包装器实现模块化。

This commit is contained in:
fujie
2025-12-20 15:43:58 +08:00
parent 533a6dee5d
commit afaa25d441
8 changed files with 1348 additions and 510 deletions

View File

@@ -48,26 +48,52 @@ Content to process:
{content} {content}
""" """
# HTML Template for rendering the result in the chat # HTML Wrapper Template (supports multiple plugins and grid layout)
HTML_TEMPLATE = """ HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT --> <!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{user_language}"> <html lang="{user_language}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[Plugin Title]</title>
<style> <style>
/* Add your CSS styles here */ body {
body { font-family: sans-serif; padding: 20px; } font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.container { border: 1px solid #ccc; padding: 20px; border-radius: 8px; } margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* Default width, allows shrinking/growing */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div id="main-container">
<h1>[Result Title]</h1> <!-- CONTENT_INSERTION_POINT -->
<div id="content">{result_content}</div>
</div> </div>
<!-- SCRIPTS_INSERTION_POINT -->
</body> </body>
</html> </html>
""" """
@@ -89,7 +115,7 @@ class Action:
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
) )
# Add other configuration fields as needed # Add other configuration fields as needed
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...") # MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
@@ -122,7 +148,7 @@ class Action:
now = datetime.now() now = datetime.now()
return { return {
"current_date_time_str": now.strftime("%Y-%m-%d %H:%M:%S"), "current_date_time_str": now.strftime("%B %d, %Y %H:%M:%S"),
"current_weekday": now.strftime("%A"), "current_weekday": now.strftime("%A"),
"current_year": now.strftime("%Y"), "current_year": now.strftime("%Y"),
"current_timezone_str": str(now.tzinfo) if now.tzinfo else "Unknown", "current_timezone_str": str(now.tzinfo) if now.tzinfo else "Unknown",
@@ -149,6 +175,55 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "en-US",
) -> str:
"""
Merges new content into an existing HTML container, or creates a new one.
"""
# Check for compatible container marker
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" in existing_html_code
):
base_html = existing_html_code
# Remove code block markers ```html ... ``` for processing
base_html = re.sub(r"^```html\s*", "", base_html)
base_html = re.sub(r"\s*```$", "", base_html)
else:
# Initialize new container
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
# Wrap new content
wrapped_content = f'<div class="plugin-item">\n{new_content}\n</div>'
# Inject Styles
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
# Inject Content
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
# Inject Scripts
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
async def _emit_status( async def _emit_status(
self, self,
emitter: Optional[Callable[[Any], Awaitable[None]]], emitter: Optional[Callable[[Any], Awaitable[None]]],

View File

@@ -48,26 +48,52 @@ USER_PROMPT_TEMPLATE = """
{content} {content}
""" """
# 用于在聊天中渲染结果的 HTML 模板 # HTML 容器模板 (支持多插件共存与网格布局)
HTML_TEMPLATE = """ HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT --> <!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{user_language}"> <html lang="{user_language}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[插件标题]</title>
<style> <style>
/* 在此处添加 CSS 样式 */ body {
body { font-family: sans-serif; padding: 20px; } font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.container { border: 1px solid #ccc; padding: 20px; border-radius: 8px; } margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div id="main-container">
<h1>[结果标题]</h1> <!-- CONTENT_INSERTION_POINT -->
<div id="content">{result_content}</div>
</div> </div>
<!-- SCRIPTS_INSERTION_POINT -->
</body> </body>
</html> </html>
""" """
@@ -89,7 +115,7 @@ class Action:
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)", description="是否强制清除旧的插件结果(如果为 True则不合并直接覆盖",
) )
# 根据需要添加其他配置字段 # 根据需要添加其他配置字段
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...") # MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
@@ -121,11 +147,21 @@ class Action:
except Exception: except Exception:
now = datetime.now() now = datetime.now()
weekday_map = {
"Monday": "星期一",
"Tuesday": "星期二",
"Wednesday": "星期三",
"Thursday": "星期四",
"Friday": "星期五",
"Saturday": "星期六",
"Sunday": "星期日",
}
weekday_en = now.strftime("%A")
return { return {
"current_date_time_str": now.strftime("%Y-%m-%d %H:%M:%S"), "current_date_time_str": now.strftime("%Y%m%d %H:%M:%S"),
"current_weekday": now.strftime("%A"), "current_weekday": weekday_map.get(weekday_en, weekday_en),
"current_year": now.strftime("%Y"), "current_year": now.strftime("%Y"),
"current_timezone_str": str(now.tzinfo) if now.tzinfo else "Unknown", "current_timezone_str": str(now.tzinfo) if now.tzinfo else "未知时区",
} }
def _process_llm_output(self, llm_output: str) -> Any: def _process_llm_output(self, llm_output: str) -> Any:
@@ -150,6 +186,55 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "zh-CN",
) -> str:
"""
将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。
"""
# 检查是否存在兼容的容器标记
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" in existing_html_code
):
base_html = existing_html_code
# 移除代码块标记 ```html ... ``` 以便处理
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'<div class="plugin-item">\n{new_content}\n</div>'
# 注入样式
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
# 注入内容
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
# 注入脚本
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
async def _emit_status( async def _emit_status(
self, self,
emitter: Optional[Callable[[Any], Awaitable[None]]], emitter: Optional[Callable[[Any], Awaitable[None]]],

View File

@@ -19,6 +19,55 @@ from open_webui.models.users import Users
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<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: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* Default width, allows shrinking/growing */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
class Action: class Action:
class Valves(BaseModel): class Valves(BaseModel):
@@ -44,7 +93,7 @@ class Action:
) )
clear_previous_html: bool = Field( clear_previous_html: bool = Field(
default=False, default=False,
description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
) )
def __init__(self): def __init__(self):
@@ -176,16 +225,44 @@ Important Principles:
) )
return body return body
# 2. Generate HTML # 2. Generate HTML components
html_card = self.generate_html_card(card_data) card_content, card_style = self.generate_html_card_components(card_data)
# 3. Append to message # 3. Append to message
# Extract existing HTML if any
existing_html_block = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
body["messages"][-1]["content"],
)
if match:
existing_html_block = match.group(1)
if self.valves.clear_previous_html: if self.valves.clear_previous_html:
body["messages"][-1]["content"] = self._remove_existing_html( body["messages"][-1]["content"] = self._remove_existing_html(
body["messages"][-1]["content"] body["messages"][-1]["content"]
) )
final_html = self._merge_html(
"", card_content, card_style, "", self.valves.language
)
else:
if existing_html_block:
body["messages"][-1]["content"] = self._remove_existing_html(
body["messages"][-1]["content"]
)
final_html = self._merge_html(
existing_html_block,
card_content,
card_style,
"",
self.valves.language,
)
else:
final_html = self._merge_html(
"", card_content, card_style, "", self.valves.language
)
html_embed_tag = f"```html\n{html_card}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
if self.valves.show_status: if self.valves.show_status:
@@ -220,11 +297,51 @@ Important Principles:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def generate_html_card(self, data): def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "en-US",
) -> str:
"""
Merges new content into an existing HTML container, or creates a new one.
"""
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
def generate_html_card_components(self, data):
# Enhanced CSS with premium styling # Enhanced CSS with premium styling
style = """ style = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
.knowledge-card-container { .knowledge-card-container {
@@ -527,41 +644,45 @@ Important Principles:
padding: 16px 20px; padding: 16px 20px;
} }
} }
</style>
""" """
# Enhanced HTML structure # Generate tags HTML
html = f"""<!DOCTYPE html> tags_html = ""
<html lang="en"> if "tags" in data and data["tags"]:
<head> for tag in data["tags"]:
<meta charset="UTF-8"> tags_html += f'<div class="card-tag"><span class="card-tag-label">#</span>{tag}</div>'
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{style} # Generate key points HTML
</head> points_html = ""
<body> if "key_points" in data and data["key_points"]:
<div class="knowledge-card-container"> for point in data["key_points"]:
<div class="knowledge-card"> points_html += f"<li>{point}</li>"
<div class="card-inner">
<div class="card-header"> # Build the card HTML structure
<div class="card-category">{data.get('category', 'General Knowledge')}</div> content = f"""
<h2 class="card-title">{data.get('title', 'Flash Card')}</h2> <div class="knowledge-card-container">
</div> <div class="knowledge-card">
<div class="card-body"> <div class="card-inner">
<div class="card-summary"> <div class="card-header">
{data.get('summary', '')} <div class="card-category">{data.get('category', 'Knowledge')}</div>
<h2 class="card-title">{data.get('title', 'Flash Card')}</h2>
</div>
<div class="card-body">
<div class="card-summary">
{data.get('summary', '')}
</div>
<div class="card-section-title">KEY POINTS</div>
<ul class="card-points">
{points_html}
</ul>
</div>
<div class="card-footer">
{tags_html}
</div> </div>
<div class="card-section-title">Key Points</div>
<ul class="card-points">
{''.join([f'<li>{point}</li>' for point in data.get('key_points', [])])}
</ul>
</div>
<div class="card-footer">
<span class="card-tag-label">Tags</span>
{''.join([f'<span class="card-tag">#{tag}</span>' for tag in data.get('tags', [])])}
</div> </div>
</div> </div>
</div> </div>
</div> """
</body>
</html>""" return content, style
return html

View File

@@ -19,6 +19,55 @@ from open_webui.models.users import Users
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<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: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
class Action: class Action:
class Valves(BaseModel): class Valves(BaseModel):
@@ -41,7 +90,7 @@ class Action:
) )
clear_previous_html: bool = Field( clear_previous_html: bool = Field(
default=False, default=False,
description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)", description="是否强制清除旧的插件结果(如果为 True则不合并直接覆盖",
) )
def __init__(self): def __init__(self):
@@ -173,21 +222,44 @@ class Action:
) )
return body return body
# 2. Generate HTML # 2. Generate HTML components
html_card = self.generate_html_card(card_data) card_content, card_style = self.generate_html_card_components(card_data)
# 3. Append to message # 3. Append to message
# We append it to the user message so it shows up as part of the interaction # Extract existing HTML if any
# Or we can append it to the assistant response if we were a Pipe, but this is an Action. existing_html_block = ""
# Actions usually modify the input or trigger a side effect. match = re.search(
# To show the card, we can append it to the message content. r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
body["messages"][-1]["content"],
)
if match:
existing_html_block = match.group(1)
if self.valves.clear_previous_html: if self.valves.clear_previous_html:
body["messages"][-1]["content"] = self._remove_existing_html( body["messages"][-1]["content"] = self._remove_existing_html(
body["messages"][-1]["content"] body["messages"][-1]["content"]
) )
final_html = self._merge_html(
"", card_content, card_style, "", self.valves.language
)
else:
if existing_html_block:
body["messages"][-1]["content"] = self._remove_existing_html(
body["messages"][-1]["content"]
)
final_html = self._merge_html(
existing_html_block,
card_content,
card_style,
"",
self.valves.language,
)
else:
final_html = self._merge_html(
"", card_content, card_style, "", self.valves.language
)
html_embed_tag = f"```html\n{html_card}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
if self.valves.show_status: if self.valves.show_status:
@@ -222,11 +294,51 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def generate_html_card(self, data): def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "zh-CN",
) -> str:
"""
将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。
"""
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
def generate_html_card_components(self, data):
# Enhanced CSS with premium styling # Enhanced CSS with premium styling
style = """ style = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
.knowledge-card-container { .knowledge-card-container {
@@ -529,41 +641,46 @@ class Action:
padding: 16px 20px; padding: 16px 20px;
} }
} }
</style>
""" """
# Enhanced HTML structure # Generate tags HTML
html = f"""<!DOCTYPE html> tags_html = ""
<html lang="zh-CN"> if "tags" in data and data["tags"]:
<head> for tag in data["tags"]:
<meta charset="UTF-8"> tags_html += f'<span class="card-tag">#{tag}</span>'
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{style} # Generate key points HTML
</head> points_html = ""
<body> if "key_points" in data and data["key_points"]:
<div class="knowledge-card-container"> for point in data["key_points"]:
<div class="knowledge-card"> points_html += f"<li>{point}</li>"
<div class="card-inner">
<div class="card-header"> # Build the card HTML structure
<div class="card-category">{data.get('category', '通用知识')}</div> content = f"""
<h2 class="card-title">{data.get('title', '知识卡片')}</h2> <div class="knowledge-card-container">
</div> <div class="knowledge-card">
<div class="card-body"> <div class="card-inner">
<div class="card-summary"> <div class="card-header">
{data.get('summary', '')} <div class="card-category">{data.get('category', 'Knowledge')}</div>
<h2 class="card-title">{data.get('title', 'Flash Card')}</h2>
</div>
<div class="card-body">
<div class="card-summary">
{data.get('summary', '')}
</div>
<div class="card-section-title">KEY POINTS</div>
<ul class="card-points">
{points_html}
</ul>
</div>
<div class="card-footer">
<span class="card-tag-label">标签</span>
{tags_html}
</div> </div>
<div class="card-section-title">核心要点</div>
<ul class="card-points">
{''.join([f'<li>{point}</li>' for point in data.get('key_points', [])])}
</ul>
</div>
<div class="card-footer">
<span class="card-tag-label">标签</span>
{''.join([f'<span class="card-tag">#{tag}</span>' for tag in data.get('tags', [])])}
</div> </div>
</div> </div>
</div> </div>
</div> """
</body>
</html>""" return content, style
return html

View File

@@ -59,18 +59,56 @@ User Language: {user_language}
{long_text_content} {long_text_content}
""" """
HTML_TEMPLATE_MINDMAP = """ HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT --> <!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{user_language}"> <html lang="{user_language}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Mind Map: Mind Map Visualization</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<style> <style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* Default width, allows shrinking/growing */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
CSS_TEMPLATE_MINDMAP = """
:root { :root {
--primary-color: #1e88e5; --primary-color: #1e88e5;
--secondary-color: #43a047; --secondary-color: #43a047;
@@ -84,101 +122,98 @@ HTML_TEMPLATE_MINDMAP = """
--border-radius: 12px; --border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
} }
body { .mindmap-container-wrapper {
font-family: var(--font-family); font-family: var(--font-family);
line-height: 1.7; line-height: 1.7;
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
padding: 24px; padding: 0;
background-color: var(--background-color); background-color: var(--card-bg-color);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} height: 100%;
.container { display: flex;
max-width: 1280px; flex-direction: column;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
} }
.header { .header {
background: var(--header-gradient); background: var(--header-gradient);
color: white; color: white;
padding: 32px 40px; padding: 20px 24px;
text-align: center; text-align: center;
} }
.header h1 { .header h1 {
margin: 0; margin: 0;
font-size: 2em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2); text-shadow: 0 1px 3px rgba(0,0,0,0.2);
} }
.user-context { .user-context {
font-size: 0.85em; font-size: 0.8em;
color: var(--muted-text-color); color: var(--muted-text-color);
background-color: #eceff1; background-color: #eceff1;
padding: 12px 20px; padding: 8px 16px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
} }
.user-context span { margin: 4px 10px; } .user-context span { margin: 2px 8px; }
.content-area { .content-area {
padding: 30px 40px; padding: 20px;
flex-grow: 1;
} }
.markmap-container { .markmap-container {
position: relative; position: relative;
background-color: #fff; background-color: #fff;
background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px); background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px);
background-size: 20px 20px; background-size: 20px 20px;
border-radius: var(--border-radius); border-radius: 8px;
padding: 24px; padding: 16px;
min-height: 700px; min-height: 500px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.05); box-shadow: inset 0 2px 6px rgba(0,0,0,0.03);
} }
.download-area { .download-area {
text-align: center; text-align: center;
padding-top: 30px; padding-top: 20px;
margin-top: 30px; margin-top: 20px;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
} }
.download-btn { .download-btn {
background-color: var(--primary-color); background-color: var(--primary-color);
color: white; color: white;
border: none; border: none;
padding: 12px 24px; padding: 8px 16px;
border-radius: 8px; border-radius: 6px;
font-size: 1em; font-size: 0.9em;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
margin: 0 10px; margin: 0 6px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
} }
.download-btn.secondary { .download-btn.secondary {
background-color: var(--secondary-color); background-color: var(--secondary-color);
} }
.download-btn:hover { .download-btn:hover {
transform: translateY(-2px); transform: translateY(-1px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15); box-shadow: 0 4px 8px rgba(0,0,0,0.1);
} }
.download-btn.copied { .download-btn.copied {
background-color: #2e7d32; /* A darker green for success */ background-color: #2e7d32;
} }
.footer { .footer {
text-align: center; text-align: center;
padding: 24px; padding: 16px;
font-size: 0.85em; font-size: 0.8em;
color: #90a4ae; color: #90a4ae;
background-color: #eceff1; background-color: #eceff1;
border-top: 1px solid var(--border-color);
} }
.footer a { .footer a {
color: var(--primary-color); color: var(--primary-color);
@@ -192,43 +227,47 @@ HTML_TEMPLATE_MINDMAP = """
color: #c62828; color: #c62828;
background-color: #ffcdd2; background-color: #ffcdd2;
border: 1px solid #ef9a9a; border: 1px solid #ef9a9a;
padding: 20px; padding: 16px;
border-radius: var(--border-radius); border-radius: 8px;
font-weight: 500; font-weight: 500;
font-size: 1.1em; font-size: 1em;
} }
</style> """
</head>
<body> CONTENT_TEMPLATE_MINDMAP = """
<div class="container"> <div class="mindmap-container-wrapper">
<div class="header"> <div class="header">
<h1>🧠 Smart Mind Map</h1> <h1>🧠 Smart Mind Map</h1>
</div> </div>
<div class="user-context"> <div class="user-context">
<span><strong>User:</strong> {user_name}</span> <span><strong>User:</strong> {user_name}</span>
<span><strong>Analysis Time:</strong> {current_date_time_str}</span> <span><strong>Time:</strong> {current_date_time_str}</span>
<span><strong>Weekday:</strong> {current_weekday_zh}</span> </div>
</div> <div class="content-area">
<div class="content-area"> <div class="markmap-container" id="markmap-container-{unique_id}"></div>
<div class="markmap-container" id="markmap-container-{unique_id}"></div> <div class="download-area">
<div class="download-area"> <button id="download-svg-btn-{unique_id}" class="download-btn">
<button id="download-svg-btn-{unique_id}" class="download-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <span class="btn-text">SVG</span>
<span class="btn-text">Copy SVG Code</span> </button>
</button> <button id="download-md-btn-{unique_id}" class="download-btn secondary">
<button id="download-md-btn-{unique_id}" class="download-btn secondary"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> <span class="btn-text">Markdown</span>
<span class="btn-text">Copy Markdown</span> </button>
</button> </div>
</div>
<div class="footer">
<p>© {current_year} Smart Mind Map • <a href="https://markmap.js.org/" target="_blank">Markmap</a></p>
</div> </div>
</div> </div>
<div class="footer">
<p>© {current_year} Smart Mind Map • Rendering engine powered by <a href="https://markmap.js.org/" target="_blank">Markmap</a></p>
</div>
</div>
<script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script> <script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script>
"""
SCRIPT_TEMPLATE_MINDMAP = """
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<script> <script>
(function() { (function() {
const renderMindmap = () => { const renderMindmap = () => {
@@ -248,7 +287,7 @@ HTML_TEMPLATE_MINDMAP = """
try { try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.style.width = '100%'; svgEl.style.width = '100%';
svgEl.style.height = '700px'; svgEl.style.height = '500px';
containerEl.innerHTML = ''; containerEl.innerHTML = '';
containerEl.appendChild(svgEl); containerEl.appendChild(svgEl);
@@ -256,7 +295,7 @@ HTML_TEMPLATE_MINDMAP = """
const transformer = new Transformer(); const transformer = new Transformer();
const { root } = transformer.transform(markdownContent); const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 16px !important; }`; const style = (id) => `${id} text { font-size: 14px !important; }`;
const options = { const options = {
autoFit: true, autoFit: true,
@@ -285,10 +324,10 @@ HTML_TEMPLATE_MINDMAP = """
button.disabled = true; button.disabled = true;
if (isSuccess) { if (isSuccess) {
buttonText.textContent = ' Copied!'; buttonText.textContent = '';
button.classList.add('copied'); button.classList.add('copied');
} else { } else {
buttonText.textContent = ' Copy Failed'; buttonText.textContent = '';
} }
setTimeout(() => { setTimeout(() => {
@@ -306,7 +345,6 @@ HTML_TEMPLATE_MINDMAP = """
showFeedback(button, false); showFeedback(button, false);
}); });
} else { } else {
// Fallback for older/insecure contexts
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = content; textArea.value = content;
textArea.style.position = 'fixed'; textArea.style.position = 'fixed';
@@ -351,8 +389,6 @@ HTML_TEMPLATE_MINDMAP = """
} }
})(); })();
</script> </script>
</body>
</html>
""" """
@@ -372,7 +408,7 @@ class Action:
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
) )
def __init__(self): def __init__(self):
@@ -403,6 +439,48 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "en-US",
) -> str:
"""
Merges new content into an existing HTML container, or creates a new one.
"""
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
async def action( async def action(
self, self,
body: dict, body: dict,
@@ -431,7 +509,7 @@ class Action:
shanghai_tz = pytz.timezone("Asia/Shanghai") shanghai_tz = pytz.timezone("Asia/Shanghai")
current_datetime_shanghai = datetime.now(shanghai_tz) current_datetime_shanghai = datetime.now(shanghai_tz)
current_date_time_str = current_datetime_shanghai.strftime( current_date_time_str = current_datetime_shanghai.strftime(
"%Y-%m-%d %H:%M:%S" "%B %d, %Y %H:%M:%S"
) )
current_weekday_en = current_datetime_shanghai.strftime("%A") current_weekday_en = current_datetime_shanghai.strftime("%A")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown") current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown")
@@ -440,7 +518,7 @@ class Action:
except Exception as e: except Exception as e:
logger.warning(f"Failed to get timezone info: {e}, using default values.") logger.warning(f"Failed to get timezone info: {e}, using default values.")
now = datetime.now() now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") current_date_time_str = now.strftime("%B %d, %Y %H:%M:%S")
current_weekday_zh = "Unknown" current_weekday_zh = "Unknown"
current_year = now.strftime("%Y") current_year = now.strftime("%Y")
current_timezone_str = "Unknown" current_timezone_str = "Unknown"
@@ -558,20 +636,52 @@ class Action:
] ]
markdown_syntax = self._extract_markdown_syntax(assistant_response_content) markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
final_html_content = ( # Prepare content components
HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id) content_html = (
.replace("{user_language}", user_language) CONTENT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
.replace("{user_name}", user_name) .replace("{user_name}", user_name)
.replace("{current_date_time_str}", current_date_time_str) .replace("{current_date_time_str}", current_date_time_str)
.replace("{current_weekday_zh}", current_weekday_zh)
.replace("{current_year}", current_year) .replace("{current_year}", current_year)
.replace("{markdown_syntax}", markdown_syntax) .replace("{markdown_syntax}", markdown_syntax)
) )
script_html = SCRIPT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
# Extract existing HTML if any
existing_html_block = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
long_text_content,
)
if match:
existing_html_block = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML: if self.valves.CLEAR_PREVIOUS_HTML:
long_text_content = self._remove_existing_html(long_text_content) long_text_content = self._remove_existing_html(long_text_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_MINDMAP, script_html, user_language
)
else:
# If we found existing HTML, we remove the old block from text and merge into it
if existing_html_block:
long_text_content = self._remove_existing_html(long_text_content)
final_html = self._merge_html(
existing_html_block,
content_html,
CSS_TEMPLATE_MINDMAP,
script_html,
user_language,
)
else:
final_html = self._merge_html(
"",
content_html,
CSS_TEMPLATE_MINDMAP,
script_html,
user_language,
)
html_embed_tag = f"```html\n{final_html_content}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__: if self.valves.show_status and __event_emitter__:

View File

@@ -56,23 +56,59 @@ USER_PROMPT_GENERATE_MINDMAP = """
--- ---
**长篇文本内容:** **长篇文本内容:**
Use code with caution.
Python
{long_text_content} {long_text_content}
""" """
HTML_TEMPLATE_MINDMAP = """ HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT --> <!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{user_language}"> <html lang="{user_language}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智绘心图: 思维导图</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<style> <style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
CSS_TEMPLATE_MINDMAP = """
:root { :root {
--primary-color: #1e88e5; --primary-color: #1e88e5;
--secondary-color: #43a047; --secondary-color: #43a047;
@@ -86,101 +122,98 @@ HTML_TEMPLATE_MINDMAP = """
--border-radius: 12px; --border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
} }
body { .mindmap-container-wrapper {
font-family: var(--font-family); font-family: var(--font-family);
line-height: 1.7; line-height: 1.7;
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
padding: 24px; padding: 0;
background-color: var(--background-color); background-color: var(--card-bg-color);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} height: 100%;
.container { display: flex;
max-width: 1280px; flex-direction: column;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
} }
.header { .header {
background: var(--header-gradient); background: var(--header-gradient);
color: white; color: white;
padding: 32px 40px; padding: 20px 24px;
text-align: center; text-align: center;
} }
.header h1 { .header h1 {
margin: 0; margin: 0;
font-size: 2em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2); text-shadow: 0 1px 3px rgba(0,0,0,0.2);
} }
.user-context { .user-context {
font-size: 0.85em; font-size: 0.8em;
color: var(--muted-text-color); color: var(--muted-text-color);
background-color: #eceff1; background-color: #eceff1;
padding: 12px 20px; padding: 8px 16px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
} }
.user-context span { margin: 4px 10px; } .user-context span { margin: 2px 8px; }
.content-area { .content-area {
padding: 30px 40px; padding: 20px;
flex-grow: 1;
} }
.markmap-container { .markmap-container {
position: relative; position: relative;
background-color: #fff; background-color: #fff;
background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px); background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px);
background-size: 20px 20px; background-size: 20px 20px;
border-radius: var(--border-radius); border-radius: 8px;
padding: 24px; padding: 16px;
min-height: 700px; min-height: 500px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.05); box-shadow: inset 0 2px 6px rgba(0,0,0,0.03);
} }
.download-area { .download-area {
text-align: center; text-align: center;
padding-top: 30px; padding-top: 20px;
margin-top: 30px; margin-top: 20px;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
} }
.download-btn { .download-btn {
background-color: var(--primary-color); background-color: var(--primary-color);
color: white; color: white;
border: none; border: none;
padding: 12px 24px; padding: 8px 16px;
border-radius: 8px; border-radius: 6px;
font-size: 1em; font-size: 0.9em;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
margin: 0 10px; margin: 0 6px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
} }
.download-btn.secondary { .download-btn.secondary {
background-color: var(--secondary-color); background-color: var(--secondary-color);
} }
.download-btn:hover { .download-btn:hover {
transform: translateY(-2px); transform: translateY(-1px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15); box-shadow: 0 4px 8px rgba(0,0,0,0.1);
} }
.download-btn.copied { .download-btn.copied {
background-color: #2e7d32; /* A darker green for success */ background-color: #2e7d32;
} }
.footer { .footer {
text-align: center; text-align: center;
padding: 24px; padding: 16px;
font-size: 0.85em; font-size: 0.8em;
color: #90a4ae; color: #90a4ae;
background-color: #eceff1; background-color: #eceff1;
border-top: 1px solid var(--border-color);
} }
.footer a { .footer a {
color: var(--primary-color); color: var(--primary-color);
@@ -194,43 +227,47 @@ HTML_TEMPLATE_MINDMAP = """
color: #c62828; color: #c62828;
background-color: #ffcdd2; background-color: #ffcdd2;
border: 1px solid #ef9a9a; border: 1px solid #ef9a9a;
padding: 20px; padding: 16px;
border-radius: var(--border-radius); border-radius: 8px;
font-weight: 500; font-weight: 500;
font-size: 1.1em; font-size: 1em;
} }
</style> """
</head>
<body> CONTENT_TEMPLATE_MINDMAP = """
<div class="container"> <div class="mindmap-container-wrapper">
<div class="header"> <div class="header">
<h1>🧠 智绘心图</h1> <h1>🧠 智能思维导图</h1>
</div> </div>
<div class="user-context"> <div class="user-context">
<span><strong>用户:</strong> {user_name}</span> <span><strong>用户:</strong> {user_name}</span>
<span><strong>分析时间:</strong> {current_date_time_str}</span> <span><strong>时间:</strong> {current_date_time_str}</span>
<span><strong>星期:</strong> {current_weekday_zh}</span> </div>
</div> <div class="content-area">
<div class="content-area"> <div class="markmap-container" id="markmap-container-{unique_id}"></div>
<div class="markmap-container" id="markmap-container-{unique_id}"></div> <div class="download-area">
<div class="download-area"> <button id="download-svg-btn-{unique_id}" class="download-btn">
<button id="download-svg-btn-{unique_id}" class="download-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <span class="btn-text">SVG</span>
<span class="btn-text">复制 SVG 代码</span> </button>
</button> <button id="download-md-btn-{unique_id}" class="download-btn secondary">
<button id="download-md-btn-{unique_id}" class="download-btn secondary"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> <span class="btn-text">Markdown</span>
<span class="btn-text">复制 Markdown</span> </button>
</button> </div>
</div>
<div class="footer">
<p>© {current_year} 智能思维导图 • <a href="https://markmap.js.org/" target="_blank">Markmap</a></p>
</div> </div>
</div> </div>
<div class="footer">
<p>© {current_year} 智绘心图 • 渲染引擎由 <a href="https://markmap.js.org/" target="_blank">Markmap</a> 提供</p>
</div>
</div>
<script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script> <script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script>
"""
SCRIPT_TEMPLATE_MINDMAP = """
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<script> <script>
(function() { (function() {
const renderMindmap = () => { const renderMindmap = () => {
@@ -243,14 +280,14 @@ HTML_TEMPLATE_MINDMAP = """
const markdownContent = sourceEl.textContent.trim(); const markdownContent = sourceEl.textContent.trim();
if (!markdownContent) { if (!markdownContent) {
containerEl.innerHTML = '<div class="error-message">⚠️ 无法加载思维导图: 缺少有效内容。</div>'; containerEl.innerHTML = '<div class="error-message">⚠️ 无法加载思维导图缺少有效内容。</div>';
return; return;
} }
try { try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.style.width = '100%'; svgEl.style.width = '100%';
svgEl.style.height = '700px'; svgEl.style.height = '500px';
containerEl.innerHTML = ''; containerEl.innerHTML = '';
containerEl.appendChild(svgEl); containerEl.appendChild(svgEl);
@@ -258,7 +295,7 @@ HTML_TEMPLATE_MINDMAP = """
const transformer = new Transformer(); const transformer = new Transformer();
const { root } = transformer.transform(markdownContent); const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 16px !important; }`; const style = (id) => `${id} text { font-size: 14px !important; }`;
const options = { const options = {
autoFit: true, autoFit: true,
@@ -272,7 +309,7 @@ HTML_TEMPLATE_MINDMAP = """
} catch (error) { } catch (error) {
console.error('Markmap rendering error:', error); console.error('Markmap rendering error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败!<br>原因: ' + error.message + '</div>'; containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败<br>原因' + error.message + '</div>';
} }
}; };
@@ -287,10 +324,10 @@ HTML_TEMPLATE_MINDMAP = """
button.disabled = true; button.disabled = true;
if (isSuccess) { if (isSuccess) {
buttonText.textContent = ' 已复制!'; buttonText.textContent = '';
button.classList.add('copied'); button.classList.add('copied');
} else { } else {
buttonText.textContent = ' 复制失败'; buttonText.textContent = '';
} }
setTimeout(() => { setTimeout(() => {
@@ -308,7 +345,6 @@ HTML_TEMPLATE_MINDMAP = """
showFeedback(button, false); showFeedback(button, false);
}); });
} else { } else {
// Fallback for older/insecure contexts
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = content; textArea.value = content;
textArea.style.position = 'fixed'; textArea.style.position = 'fixed';
@@ -353,8 +389,6 @@ HTML_TEMPLATE_MINDMAP = """
} }
})(); })();
</script> </script>
</body>
</html>
""" """
@@ -369,11 +403,11 @@ class Action:
) )
MIN_TEXT_LENGTH: int = Field( MIN_TEXT_LENGTH: int = Field(
default=100, default=100,
description="进行思维导图分析所需的最小文本长度(字符数)", description="进行思维导图分析所需的最小文本长度字符数",
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)", description="是否强制清除旧的插件结果(如果为 True则不合并直接覆盖",
) )
def __init__(self): def __init__(self):
@@ -404,6 +438,48 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "zh-CN",
) -> str:
"""
将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。
"""
if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
async def action( async def action(
self, self,
body: dict, body: dict,
@@ -432,7 +508,7 @@ class Action:
shanghai_tz = pytz.timezone("Asia/Shanghai") shanghai_tz = pytz.timezone("Asia/Shanghai")
current_datetime_shanghai = datetime.now(shanghai_tz) current_datetime_shanghai = datetime.now(shanghai_tz)
current_date_time_str = current_datetime_shanghai.strftime( current_date_time_str = current_datetime_shanghai.strftime(
"%Y-%m-%d %H:%M:%S" "%Y%m%d %H:%M:%S"
) )
current_weekday_en = current_datetime_shanghai.strftime("%A") current_weekday_en = current_datetime_shanghai.strftime("%A")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期") current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期")
@@ -441,7 +517,7 @@ class Action:
except Exception as e: except Exception as e:
logger.warning(f"获取时区信息失败: {e},使用默认值。") logger.warning(f"获取时区信息失败: {e},使用默认值。")
now = datetime.now() now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") current_date_time_str = now.strftime("%Y%m%d %H:%M:%S")
current_weekday_zh = "未知星期" current_weekday_zh = "未知星期"
current_year = now.strftime("%Y") current_year = now.strftime("%Y")
current_timezone_str = "未知时区" current_timezone_str = "未知时区"
@@ -558,20 +634,52 @@ class Action:
] ]
markdown_syntax = self._extract_markdown_syntax(assistant_response_content) markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
final_html_content = ( # Prepare content components
HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id) content_html = (
.replace("{user_language}", user_language) CONTENT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
.replace("{user_name}", user_name) .replace("{user_name}", user_name)
.replace("{current_date_time_str}", current_date_time_str) .replace("{current_date_time_str}", current_date_time_str)
.replace("{current_weekday_zh}", current_weekday_zh)
.replace("{current_year}", current_year) .replace("{current_year}", current_year)
.replace("{markdown_syntax}", markdown_syntax) .replace("{markdown_syntax}", markdown_syntax)
) )
script_html = SCRIPT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
# Extract existing HTML if any
existing_html_block = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
long_text_content,
)
if match:
existing_html_block = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML: if self.valves.CLEAR_PREVIOUS_HTML:
long_text_content = self._remove_existing_html(long_text_content) long_text_content = self._remove_existing_html(long_text_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_MINDMAP, script_html, user_language
)
else:
# If we found existing HTML, we remove the old block from text and merge into it
if existing_html_block:
long_text_content = self._remove_existing_html(long_text_content)
final_html = self._merge_html(
existing_html_block,
content_html,
CSS_TEMPLATE_MINDMAP,
script_html,
user_language,
)
else:
final_html = self._merge_html(
"",
content_html,
CSS_TEMPLATE_MINDMAP,
script_html,
user_language,
)
html_embed_tag = f"```html\n{final_html_content}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__: if self.valves.show_status and __event_emitter__:

View File

@@ -27,6 +27,58 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# =================================================================
# HTML Wrapper Template (supports multiple plugins and grid layout)
# =================================================================
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<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: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* Default width, allows shrinking/growing */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
# ================================================================= # =================================================================
# Internal LLM Prompts # Internal LLM Prompts
# ================================================================= # =================================================================
@@ -93,15 +145,7 @@ Please conduct a deep and comprehensive analysis, focusing on actionable advice.
# Frontend HTML Template (Jinja2 Syntax) # Frontend HTML Template (Jinja2 Syntax)
# ================================================================= # =================================================================
HTML_TEMPLATE = """ CSS_TEMPLATE_SUMMARY = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{{ user_language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deep Reading: Deep Analysis Report</title>
<style>
:root { :root {
--primary-color: #4285f4; --primary-color: #4285f4;
--secondary-color: #1e88e5; --secondary-color: #1e88e5;
@@ -116,163 +160,147 @@ HTML_TEMPLATE = """
--border-radius: 8px; --border-radius: 8px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
} }
body { .summary-container-wrapper {
font-family: var(--font-family); font-family: var(--font-family);
line-height: 1.8; line-height: 1.8;
color: var(--text-color); color: var(--text-color);
margin: 0; height: 100%;
padding: 24px; display: flex;
background-color: var(--background-color); flex-direction: column;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
.container { .summary-container-wrapper .header {
max-width: 900px;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
}
.header {
background: var(--header-gradient); background: var(--header-gradient);
color: white; color: white;
padding: 40px; padding: 20px 24px;
text-align: center; text-align: center;
} }
.header h1 { .summary-container-wrapper .header h1 {
margin: 0; margin: 0;
font-size: 2.2em; font-size: 1.5em;
font-weight: 500; font-weight: 500;
letter-spacing: -0.5px; letter-spacing: -0.5px;
} }
.user-context { .summary-container-wrapper .user-context {
font-size: 0.9em; font-size: 0.8em;
color: var(--muted-text-color); color: var(--muted-text-color);
background-color: #f1f3f4; background-color: #f1f3f4;
padding: 16px 40px; padding: 8px 16px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.user-context span { margin: 4px 12px; } .summary-container-wrapper .user-context span { margin: 2px 8px; }
.content { padding: 40px; } .summary-container-wrapper .content { padding: 20px; flex-grow: 1; }
.section { .summary-container-wrapper .section {
margin-bottom: 32px; margin-bottom: 16px;
padding-bottom: 32px; padding-bottom: 16px;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid #e8eaed;
} }
.section:last-child { .summary-container-wrapper .section:last-child {
border-bottom: none; border-bottom: none;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
} }
.section h2 { .summary-container-wrapper .section h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 12px;
font-size: 1.5em; font-size: 1.2em;
font-weight: 500; font-weight: 500;
color: var(--text-color); color: var(--text-color);
display: flex; display: flex;
align-items: center; align-items: center;
padding-bottom: 12px; padding-bottom: 8px;
border-bottom: 2px solid var(--primary-color); border-bottom: 2px solid var(--primary-color);
} }
.section h2 .icon { .summary-container-wrapper .section h2 .icon {
margin-right: 12px; margin-right: 8px;
font-size: 1.3em; font-size: 1.1em;
line-height: 1; line-height: 1;
} }
.summary-section h2 { border-bottom-color: var(--primary-color); } .summary-container-wrapper .summary-section h2 { border-bottom-color: var(--primary-color); }
.keypoints-section h2 { border-bottom-color: var(--secondary-color); } .summary-container-wrapper .keypoints-section h2 { border-bottom-color: var(--secondary-color); }
.actions-section h2 { border-bottom-color: var(--action-color); } .summary-container-wrapper .actions-section h2 { border-bottom-color: var(--action-color); }
.summary-container-wrapper .html-content {
.html-content { font-size: 0.95em;
font-size: 1.05em;
line-height: 1.8;
}
.html-content p:first-child { margin-top: 0; }
.html-content p:last-child { margin-bottom: 0; }
.html-content ul {
list-style: none;
padding-left: 0;
margin: 16px 0;
}
.html-content li {
padding: 12px 0 12px 32px;
position: relative;
margin-bottom: 8px;
line-height: 1.7; line-height: 1.7;
} }
.html-content li::before { .summary-container-wrapper .html-content p:first-child { margin-top: 0; }
.summary-container-wrapper .html-content p:last-child { margin-bottom: 0; }
.summary-container-wrapper .html-content ul {
list-style: none;
padding-left: 0;
margin: 12px 0;
}
.summary-container-wrapper .html-content li {
padding: 8px 0 8px 24px;
position: relative;
margin-bottom: 6px;
line-height: 1.6;
}
.summary-container-wrapper .html-content li::before {
position: absolute; position: absolute;
left: 0; left: 0;
top: 12px; top: 8px;
font-family: 'Arial'; font-family: 'Arial';
font-weight: bold; font-weight: bold;
font-size: 1.1em; font-size: 1em;
} }
.keypoints-section .html-content li::before { .summary-container-wrapper .keypoints-section .html-content li::before {
content: ''; content: '';
color: var(--secondary-color); color: var(--secondary-color);
font-size: 1.5em; font-size: 1.3em;
top: 8px; top: 5px;
} }
.actions-section .html-content li::before { .summary-container-wrapper .actions-section .html-content li::before {
content: ''; content: '';
color: var(--action-color); color: var(--action-color);
} }
.summary-container-wrapper .no-content {
.no-content {
color: var(--muted-text-color); color: var(--muted-text-color);
font-style: italic; font-style: italic;
padding: 20px; padding: 12px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 4px; border-radius: 4px;
} }
.summary-container-wrapper .footer {
.footer {
text-align: center; text-align: center;
padding: 24px; padding: 16px;
font-size: 0.85em; font-size: 0.8em;
color: #5f6368; color: #5f6368;
background-color: #f8f9fa; background-color: #f8f9fa;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
} }
</style> """
</head>
<body> CONTENT_TEMPLATE_SUMMARY = """
<div class="container"> <div class="summary-container-wrapper">
<div class="header"> <div class="header">
<h1>📖 Deep Reading: Deep Analysis Report</h1> <h1>📖 Deep Reading: Analysis Report</h1>
</div>
<div class="user-context">
<span><strong>User:</strong> {{ user_name }}</span>
<span><strong>Analysis Time:</strong> {{ current_date_time_str }}</span>
<span><strong>Weekday:</strong> {{ current_weekday }}</span>
</div>
<div class="content">
<div class="section summary-section">
<h2><span class="icon">📝</span>Detailed Summary</h2>
<div class="html-content">{{ summary_html | safe }}</div>
</div> </div>
<div class="section keypoints-section"> <div class="user-context">
<h2><span class="icon">💡</span>Key Information Points</h2> <span><strong>User:</strong> {user_name}</span>
<div class="html-content">{{ keypoints_html | safe }}</div> <span><strong>Time:</strong> {current_date_time_str}</span>
</div> </div>
<div class="section actions-section"> <div class="content">
<h2><span class="icon">🎯</span>Actionable Advice</h2> <div class="section summary-section">
<div class="html-content">{{ actions_html | safe }}</div> <h2><span class="icon">📝</span>Detailed Summary</h2>
<div class="html-content">{summary_html}</div>
</div>
<div class="section keypoints-section">
<h2><span class="icon">💡</span>Key Information Points</h2>
<div class="html-content">{keypoints_html}</div>
</div>
<div class="section actions-section">
<h2><span class="icon">🎯</span>Actionable Advice</h2>
<div class="html-content">{actions_html}</div>
</div>
</div>
<div class="footer">
<p>&copy; {current_year} Deep Reading - Text Analysis Service</p>
</div> </div>
</div> </div>
<div class="footer"> """
<p>&copy; {{ current_year }} Deep Reading - Deep Text Analysis Service</p>
</div>
</div>
</body>
</html>"""
class Action: class Action:
@@ -295,7 +323,7 @@ class Action:
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
) )
def __init__(self): def __init__(self):
@@ -358,12 +386,64 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _build_html(self, context: dict) -> str: def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "en-US",
) -> str:
""" """
Build final HTML content using Jinja2 template and context data. Merges new content into an existing HTML container, or creates a new one.
""" """
template = Template(HTML_TEMPLATE) if (
return template.render(context) "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
def _build_content_html(self, context: dict) -> str:
"""
Build content HTML using context data.
"""
return (
CONTENT_TEMPLATE_SUMMARY.replace(
"{user_name}", context.get("user_name", "User")
)
.replace(
"{current_date_time_str}", context.get("current_date_time_str", "")
)
.replace("{current_year}", context.get("current_year", ""))
.replace("{summary_html}", context.get("summary_html", ""))
.replace("{keypoints_html}", context.get("keypoints_html", ""))
.replace("{actions_html}", context.get("actions_html", ""))
)
async def action( async def action(
self, self,
@@ -390,7 +470,7 @@ class Action:
user_id = __user__.get("id", "unknown_user") user_id = __user__.get("id", "unknown_user")
now = datetime.now() now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") current_date_time_str = now.strftime("%B %d, %Y %H:%M:%S")
current_weekday = now.strftime("%A") current_weekday = now.strftime("%A")
current_year = now.strftime("%Y") current_year = now.strftime("%Y")
current_timezone_str = "Unknown Timezone" current_timezone_str = "Unknown Timezone"
@@ -497,12 +577,38 @@ class Action:
**processed_content, **processed_content,
} }
final_html_content = self._build_html(context) content_html = self._build_content_html(context)
# Extract existing HTML if any
existing_html_block = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
original_content,
)
if match:
existing_html_block = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML: if self.valves.CLEAR_PREVIOUS_HTML:
original_content = self._remove_existing_html(original_content) original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_SUMMARY, "", 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_SUMMARY,
"",
user_language,
)
else:
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_SUMMARY, "", user_language
)
html_embed_tag = f"```html\n{final_html_content}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__: if self.valves.show_status and __event_emitter__:

View File

@@ -24,6 +24,58 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# =================================================================
# HTML 容器模板 (支持多插件共存与网格布局)
# =================================================================
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<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: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
min-width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
}
.plugin-item:hover {
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
</body>
</html>
"""
# ================================================================= # =================================================================
# 内部 LLM 提示词设计 # 内部 LLM 提示词设计
# ================================================================= # =================================================================
@@ -90,15 +142,7 @@ USER_PROMPT_GENERATE_SUMMARY = """
# 前端 HTML 模板 (Jinja2 语法) # 前端 HTML 模板 (Jinja2 语法)
# ================================================================= # =================================================================
HTML_TEMPLATE = """ CSS_TEMPLATE_SUMMARY = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{{ user_language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>精读:深度分析报告</title>
<style>
:root { :root {
--primary-color: #4285f4; --primary-color: #4285f4;
--secondary-color: #1e88e5; --secondary-color: #1e88e5;
@@ -113,163 +157,147 @@ HTML_TEMPLATE = """
--border-radius: 8px; --border-radius: 8px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
} }
body { .summary-container-wrapper {
font-family: var(--font-family); font-family: var(--font-family);
line-height: 1.8; line-height: 1.8;
color: var(--text-color); color: var(--text-color);
margin: 0; height: 100%;
padding: 24px; display: flex;
background-color: var(--background-color); flex-direction: column;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
.container { .summary-container-wrapper .header {
max-width: 900px;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
}
.header {
background: var(--header-gradient); background: var(--header-gradient);
color: white; color: white;
padding: 40px; padding: 20px 24px;
text-align: center; text-align: center;
} }
.header h1 { .summary-container-wrapper .header h1 {
margin: 0; margin: 0;
font-size: 2.2em; font-size: 1.5em;
font-weight: 500; font-weight: 500;
letter-spacing: -0.5px; letter-spacing: -0.5px;
} }
.user-context { .summary-container-wrapper .user-context {
font-size: 0.9em; font-size: 0.8em;
color: var(--muted-text-color); color: var(--muted-text-color);
background-color: #f1f3f4; background-color: #f1f3f4;
padding: 16px 40px; padding: 8px 16px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.user-context span { margin: 4px 12px; } .summary-container-wrapper .user-context span { margin: 2px 8px; }
.content { padding: 40px; } .summary-container-wrapper .content { padding: 20px; flex-grow: 1; }
.section { .summary-container-wrapper .section {
margin-bottom: 32px; margin-bottom: 16px;
padding-bottom: 32px; padding-bottom: 16px;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid #e8eaed;
} }
.section:last-child { .summary-container-wrapper .section:last-child {
border-bottom: none; border-bottom: none;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
} }
.section h2 { .summary-container-wrapper .section h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 12px;
font-size: 1.5em; font-size: 1.2em;
font-weight: 500; font-weight: 500;
color: var(--text-color); color: var(--text-color);
display: flex; display: flex;
align-items: center; align-items: center;
padding-bottom: 12px; padding-bottom: 8px;
border-bottom: 2px solid var(--primary-color); border-bottom: 2px solid var(--primary-color);
} }
.section h2 .icon { .summary-container-wrapper .section h2 .icon {
margin-right: 12px; margin-right: 8px;
font-size: 1.3em; font-size: 1.1em;
line-height: 1; line-height: 1;
} }
.summary-section h2 { border-bottom-color: var(--primary-color); } .summary-container-wrapper .summary-section h2 { border-bottom-color: var(--primary-color); }
.keypoints-section h2 { border-bottom-color: var(--secondary-color); } .summary-container-wrapper .keypoints-section h2 { border-bottom-color: var(--secondary-color); }
.actions-section h2 { border-bottom-color: var(--action-color); } .summary-container-wrapper .actions-section h2 { border-bottom-color: var(--action-color); }
.summary-container-wrapper .html-content {
.html-content { font-size: 0.95em;
font-size: 1.05em;
line-height: 1.8;
}
.html-content p:first-child { margin-top: 0; }
.html-content p:last-child { margin-bottom: 0; }
.html-content ul {
list-style: none;
padding-left: 0;
margin: 16px 0;
}
.html-content li {
padding: 12px 0 12px 32px;
position: relative;
margin-bottom: 8px;
line-height: 1.7; line-height: 1.7;
} }
.html-content li::before { .summary-container-wrapper .html-content p:first-child { margin-top: 0; }
.summary-container-wrapper .html-content p:last-child { margin-bottom: 0; }
.summary-container-wrapper .html-content ul {
list-style: none;
padding-left: 0;
margin: 12px 0;
}
.summary-container-wrapper .html-content li {
padding: 8px 0 8px 24px;
position: relative;
margin-bottom: 6px;
line-height: 1.6;
}
.summary-container-wrapper .html-content li::before {
position: absolute; position: absolute;
left: 0; left: 0;
top: 12px; top: 8px;
font-family: 'Arial'; font-family: 'Arial';
font-weight: bold; font-weight: bold;
font-size: 1.1em; font-size: 1em;
} }
.keypoints-section .html-content li::before { .summary-container-wrapper .keypoints-section .html-content li::before {
content: ''; content: '';
color: var(--secondary-color); color: var(--secondary-color);
font-size: 1.5em; font-size: 1.3em;
top: 8px; top: 5px;
} }
.actions-section .html-content li::before { .summary-container-wrapper .actions-section .html-content li::before {
content: ''; content: '';
color: var(--action-color); color: var(--action-color);
} }
.summary-container-wrapper .no-content {
.no-content {
color: var(--muted-text-color); color: var(--muted-text-color);
font-style: italic; font-style: italic;
padding: 20px; padding: 12px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 4px; border-radius: 4px;
} }
.summary-container-wrapper .footer {
.footer {
text-align: center; text-align: center;
padding: 24px; padding: 16px;
font-size: 0.85em; font-size: 0.8em;
color: #5f6368; color: #5f6368;
background-color: #f8f9fa; background-color: #f8f9fa;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
} }
</style> """
</head>
<body> CONTENT_TEMPLATE_SUMMARY = """
<div class="container"> <div class="summary-container-wrapper">
<div class="header"> <div class="header">
<h1>📖 精读:深度分析报告</h1> <h1>📖 精读:深度分析报告</h1>
</div>
<div class="user-context">
<span><strong>用户:</strong> {{ user_name }}</span>
<span><strong>分析时间:</strong> {{ current_date_time_str }}</span>
<span><strong>星期:</strong> {{ current_weekday }}</span>
</div>
<div class="content">
<div class="section summary-section">
<h2><span class="icon">📝</span>详细摘要</h2>
<div class="html-content">{{ summary_html | safe }}</div>
</div> </div>
<div class="section keypoints-section"> <div class="user-context">
<h2><span class="icon">💡</span>关键信息点</h2> <span><strong>用户:</strong> {user_name}</span>
<div class="html-content">{{ keypoints_html | safe }}</div> <span><strong>时间:</strong> {current_date_time_str}</span>
</div> </div>
<div class="section actions-section"> <div class="content">
<h2><span class="icon">🎯</span>行动建议</h2> <div class="section summary-section">
<div class="html-content">{{ actions_html | safe }}</div> <h2><span class="icon">📝</span>详细摘要</h2>
<div class="html-content">{summary_html}</div>
</div>
<div class="section keypoints-section">
<h2><span class="icon">💡</span>关键信息点</h2>
<div class="html-content">{keypoints_html}</div>
</div>
<div class="section actions-section">
<h2><span class="icon">🎯</span>行动建议</h2>
<div class="html-content">{actions_html}</div>
</div>
</div>
<div class="footer">
<p>&copy; {current_year} 精读 - 深度文本分析服务</p>
</div> </div>
</div> </div>
<div class="footer"> """
<p>&copy; {{ current_year }} 精读 - 深度文本分析服务</p>
</div>
</div>
</body>
</html>"""
class Action: class Action:
@@ -290,11 +318,20 @@ class Action:
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)", description="是否强制清除旧的插件结果(如果为 True则不合并直接覆盖",
) )
def __init__(self): def __init__(self):
self.valves = self.Valves() self.valves = self.Valves()
self.weekday_map = {
"Monday": "星期一",
"Tuesday": "星期二",
"Wednesday": "星期三",
"Thursday": "星期四",
"Friday": "星期五",
"Saturday": "星期六",
"Sunday": "星期日",
}
def _process_llm_output(self, llm_output: str) -> Dict[str, str]: def _process_llm_output(self, llm_output: str) -> Dict[str, str]:
""" """
@@ -347,12 +384,64 @@ class Action:
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```" pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _build_html(self, context: dict) -> str: def _merge_html(
self,
existing_html_code: str,
new_content: str,
new_styles: str = "",
new_scripts: str = "",
user_language: str = "zh-CN",
) -> str:
""" """
使用 Jinja2 模板和上下文数据构建最终的HTML内容 将新内容合并到现有的 HTML 容器中,或者创建一个新的容器
""" """
template = Template(HTML_TEMPLATE) if (
return template.render(context) "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
and "<!-- CONTENT_INSERTION_POINT -->" 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'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped_content}\n<!-- CONTENT_INSERTION_POINT -->",
)
if new_scripts:
base_html = base_html.replace(
"<!-- SCRIPTS_INSERTION_POINT -->",
f"{new_scripts}\n<!-- SCRIPTS_INSERTION_POINT -->",
)
return base_html.strip()
def _build_content_html(self, context: dict) -> str:
"""
使用上下文数据构建内容 HTML。
"""
return (
CONTENT_TEMPLATE_SUMMARY.replace(
"{user_name}", context.get("user_name", "用户")
)
.replace(
"{current_date_time_str}", context.get("current_date_time_str", "")
)
.replace("{current_year}", context.get("current_year", ""))
.replace("{summary_html}", context.get("summary_html", ""))
.replace("{keypoints_html}", context.get("keypoints_html", ""))
.replace("{actions_html}", context.get("actions_html", ""))
)
async def action( async def action(
self, self,
@@ -379,8 +468,9 @@ class Action:
user_id = __user__.get("id", "unknown_user") user_id = __user__.get("id", "unknown_user")
now = datetime.now() now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") current_date_time_str = now.strftime("%Y%m%d %H:%M:%S")
current_weekday = now.strftime("%A") current_weekday_en = now.strftime("%A")
current_weekday = self.weekday_map.get(current_weekday_en, current_weekday_en)
current_year = now.strftime("%Y") current_year = now.strftime("%Y")
current_timezone_str = "未知时区" current_timezone_str = "未知时区"
@@ -486,12 +576,38 @@ class Action:
**processed_content, **processed_content,
} }
final_html_content = self._build_html(context) content_html = self._build_content_html(context)
# Extract existing HTML if any
existing_html_block = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
original_content,
)
if match:
existing_html_block = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML: if self.valves.CLEAR_PREVIOUS_HTML:
original_content = self._remove_existing_html(original_content) original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_SUMMARY, "", 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_SUMMARY,
"",
user_language,
)
else:
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE_SUMMARY, "", user_language
)
html_embed_tag = f"```html\n{final_html_content}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__: if self.valves.show_status and __event_emitter__: