feat(pipes): automate artifacts html generation via emitter message append

This commit is contained in:
fujie
2026-02-28 13:48:53 +08:00
parent 8001ab18ee
commit f32e90e182

View File

@@ -153,9 +153,9 @@ BASE_GUIDELINES = (
"3. **Interactive Artifacts (HTML)**: **Premium Delivery Protocol**: For web applications, you MUST perform two actions:\n" "3. **Interactive Artifacts (HTML)**: **Premium Delivery Protocol**: For web applications, you MUST perform two actions:\n"
" - 1. **Persist**: Create the file in the workspace (e.g., `index.html`) for project structure.\n" " - 1. **Persist**: Create the file in the workspace (e.g., `index.html`) for project structure.\n"
" - 2. **Publish & Embed**: Call `publish_file_from_workspace(filename='your_file.html')`. This will automatically trigger the **Premium Experience** by directly embedding the interactive component using the action-style return.\n" " - 2. **Publish & Embed**: Call `publish_file_from_workspace(filename='your_file.html')`. This will automatically trigger the **Premium Experience** by directly embedding the interactive component using the action-style return.\n"
" - **CRITICAL ANTI-INLINE RULE**: Never output your *own* raw HTML source code directly in the chat. You MUST ALWAYS persist the HTML to a file and call `publish_file_from_workspace`. The ONLY HTML code block you are allowed to output is the `html_embed` iframe returned by the publish tool.\n" " - **CRITICAL ANTI-INLINE RULE**: Never output your *own* raw HTML source code directly in the chat. You MUST ALWAYS persist the HTML to a file and call `publish_file_from_workspace`.\n"
" - **CRITICAL**: When using this protocol in **Rich UI mode** (`embed_type='richui'`), **DO NOT** output the raw HTML code in a code block. Provide ONLY the **[Preview]** and **[Download]** links returned by the tool. The interactive embed will appear automatically after your message finishes.\n" " - **CRITICAL**: When using this protocol in **Rich UI mode** (`embed_type='richui'`), **DO NOT** output the raw HTML code in a code block. Provide ONLY the **[Preview]** and **[Download]** links returned by the tool. The interactive embed will appear automatically after your message finishes.\n"
" - **Artifacts mode** (`embed_type='artifacts'`): You MUST provide the **[Preview]** and **[Download]** links, AND then you MUST output the provided `html_embed` EXACTLY as returned, wrapped within a ```html code block to enable the interactive preview.\n" " - **Artifacts mode** (`embed_type='artifacts'`): You MUST provide the **[Preview]** and **[Download]** links. DO NOT output HTML code block. The system will automatically append the HTML visualization to the chat string.\n"
" - **Process Visibility**: While raw code is often replaced by links/frames, you SHOULD provide a **very brief Markdown summary** of the component's structure or key features (e.g., 'Generated login form with validation') before publishing. This keeps the user informed of the 'processing' progress.\n" " - **Process Visibility**: While raw code is often replaced by links/frames, you SHOULD provide a **very brief Markdown summary** of the component's structure or key features (e.g., 'Generated login form with validation') before publishing. This keeps the user informed of the 'processing' progress.\n"
" - **Game/App Controls**: If your HTML includes keyboard controls (e.g., arrow keys, spacebar for games), you MUST include `event.preventDefault()` in your `keydown` listeners to prevent the parent browser page from scrolling.\n" " - **Game/App Controls**: If your HTML includes keyboard controls (e.g., arrow keys, spacebar for games), you MUST include `event.preventDefault()` in your `keydown` listeners to prevent the parent browser page from scrolling.\n"
"4. **Media & Files**: ALWAYS embed generated images, GIFs, and videos directly using `![caption](url)`. Supported formats like PNG, JPG, GIF, MOV, and MP4 should be shown as visual assets. Never provide plain text links for visual media.\n" "4. **Media & Files**: ALWAYS embed generated images, GIFs, and videos directly using `![caption](url)`. Supported formats like PNG, JPG, GIF, MOV, and MP4 should be shown as visual assets. Never provide plain text links for visual media.\n"
@@ -166,7 +166,7 @@ BASE_GUIDELINES = (
" - **Implicit Requests**: If asked to 'export', 'get link', or 'save', automatically trigger this sequence.\n" " - **Implicit Requests**: If asked to 'export', 'get link', or 'save', automatically trigger this sequence.\n"
" - **Execution Sequence**: 1. **Write Local**: Create file. 2. **Publish**: Call `publish_file_from_workspace`. 3. **Response Structure**:\n" " - **Execution Sequence**: 1. **Write Local**: Create file. 2. **Publish**: Call `publish_file_from_workspace`. 3. **Response Structure**:\n"
" - **For PDF files**: You MUST output ONLY Markdown links from the tool output (preview + download). **CRITICAL: NEVER output iframe/html_embed for PDF.**\n" " - **For PDF files**: You MUST output ONLY Markdown links from the tool output (preview + download). **CRITICAL: NEVER output iframe/html_embed for PDF.**\n"
" - **For HTML files**: Choose mode by complexity. **Artifacts mode** (`embed_type='artifacts'`): REQUIRED for dashboards, reports, and large/long UI since it has unlimited height. Output [Preview]/[Download], then MISSION-CRITICAL: output the provided `html_embed` value wrapped EXACTLY within a ```html code block. **Rich UI mode** (`embed_type='richui'`): For small widgets ONLY. If you MUST use Rich UI for long content, you MUST add a clickable 'Full Screen' button inside your HTML design to allow expanding. Output ONLY [Preview]/[Download]; do NOT output iframe/html block because Rich UI will render automatically via emitter.\n" " - **For HTML files**: Choose mode by complexity. **Artifacts mode** (`embed_type='artifacts'`): REQUIRED for dashboards, reports, and large/long UI since it has unlimited height. Output ONLY [Preview]/[Download]; do NOT output any iframe/html block because the protocol will automatically append the html code block via emitter. **Rich UI mode** (`embed_type='richui'`): For small widgets ONLY. If you MUST use Rich UI for long content, you MUST add a clickable 'Full Screen' button inside your HTML design to allow expanding. Output ONLY [Preview]/[Download]; do NOT output HTML block because Rich UI will render automatically via emitter.\n"
" - **URL Format**: You MUST use the **ABSOLUTE URLs** provided in the tool output. NEVER modify them.\n" " - **URL Format**: You MUST use the **ABSOLUTE URLs** provided in the tool output. NEVER modify them.\n"
" - **Bypass RAG**: This protocol automatically handles S3 storage and bypasses RAG, ensuring 100% accurate data delivery.\n" " - **Bypass RAG**: This protocol automatically handles S3 storage and bypasses RAG, ensuring 100% accurate data delivery.\n"
"6. **TODO Visibility**: Every time you call the `update_todo` tool, you **MUST** immediately follow up with a beautifully formatted **Markdown summary** of the current TODO list. Use task checkboxes (`- [ ]`), progress indicators, and clear headings so the user can see the status directly in the chat.\n" "6. **TODO Visibility**: Every time you call the `update_todo` tool, you **MUST** immediately follow up with a beautifully formatted **Markdown summary** of the current TODO list. Use task checkboxes (`- [ ]`), progress indicators, and clear headings so the user can see the status directly in the chat.\n"
@@ -1191,7 +1191,7 @@ class Pipe:
if embed_type == "richui": if embed_type == "richui":
hint += "\n\nCRITICAL: You are in 'richui' mode. DO NOT output an HTML code block or iframe in your message. Just output the links above." hint += "\n\nCRITICAL: You are in 'richui' mode. DO NOT output an HTML code block or iframe in your message. Just output the links above."
elif embed_type == "artifacts": elif embed_type == "artifacts":
hint += "\n\nIMPORTANT: You are in 'artifacts' mode. You MUST wrap the provided 'html_embed' (the iframe) inside a ```html code block in your response to enable the interactive preview." hint += "\n\nIMPORTANT: You are in 'artifacts' mode. DO NOT output an HTML code block in your message. The system will automatically inject it after you finish."
elif has_preview: elif has_preview:
hint = self._get_translation( hint = self._get_translation(
user_lang, user_lang,
@@ -1224,23 +1224,12 @@ class Pipe:
} }
if has_preview and view_url: if has_preview and view_url:
result_dict["view_url"] = view_url result_dict["view_url"] = view_url
if is_html and embed_type == "artifacts":
# Artifacts mode: standard barebone iframe (OpenWebUI might strip complex styles or sandbox attrs)
iframe_html = f'<iframe src="{view_url}" style="width:100%; min-height:600px; border:none;"></iframe>'
result_dict["html_embed"] = iframe_html
# Note: We do NOT add to pending_embeds. The AI will output this in the message.
elif embed_type == "richui":
# In richui mode, we physically remove html_embed to prevent the AI from outputting it
# The system will handle the rendering via emitter
pass
# 6. Premium Rich UI Experience for HTML only (Direct Embed via emitter) # Premium Experience for HTML only (Direct Embed via emitter)
# We emit events directly ONLY IF embed_type is 'richui'. # Emission is delayed until session.idle to avoid UI flicker and ensure reliability.
# Note: Emission is now delayed until session.idle to avoid UI flicker and ensure reliability. if is_html and rich_ui_supported:
if is_html and embed_type == "richui" and rich_ui_supported:
try: try:
# For Rich UI Mode, OpenWebUI expects the raw HTML of the component itself # For BOTH Rich UI and Artifacts Mode, OpenWebUI expects the raw HTML of the component itself
# The system will wrap this in its own managed iframe.
embed_content = await asyncio.to_thread( embed_content = await asyncio.to_thread(
lambda: target_path.read_text( lambda: target_path.read_text(
encoding="utf-8", errors="replace" encoding="utf-8", errors="replace"
@@ -1248,15 +1237,25 @@ class Pipe:
) )
if pending_embeds is not None: if pending_embeds is not None:
pending_embeds.append( if embed_type == "richui":
{ pending_embeds.append(
"filename": safe_filename, {
"content": embed_content, "filename": safe_filename,
"type": "richui", "content": embed_content,
} "type": "richui",
) }
)
elif embed_type == "artifacts":
artifacts_content = f"\n```html\n{embed_content}\n```\n"
pending_embeds.append(
{
"filename": safe_filename,
"content": artifacts_content,
"type": "message",
}
)
except Exception as e: except Exception as e:
logger.error(f"Failed to prepare Rich UI embed: {e}") logger.error(f"Failed to prepare HTML embed: {e}")
return result_dict return result_dict
@@ -5669,10 +5668,10 @@ class Pipe:
except Exception: except Exception:
pass pass
# 2. Emit Rich UI components (richui type) # 2. Emit UI components (richui or message type)
if pending_embeds: if pending_embeds:
for embed in pending_embeds: for embed in pending_embeds:
if embed.get("type") == "richui": if embed.get("type") in ["richui", "message"]:
# Status update # Status update
await __event_emitter__( await __event_emitter__(
{ {
@@ -5699,15 +5698,27 @@ class Pipe:
}, },
} }
) )
# Standard OpenWebUI Embed Structure: type: "embeds", data: {"embeds": [content]}
await __event_emitter__( if embed.get("type") == "richui":
{ # Standard OpenWebUI Embed Structure: type: "embeds", data: {"embeds": [content]}
"type": "embeds", await __event_emitter__(
"data": { {
"embeds": [embed["content"]] "type": "embeds",
}, "data": {
} "embeds": [embed["content"]]
) },
}
)
elif embed.get("type") == "message":
# Artifacts mode appends as raw message
await __event_emitter__(
{
"type": "message",
"data": {
"content": embed["content"]
},
}
)
# 3. LOCK internal status emission for background tasks # 3. LOCK internal status emission for background tasks
# (Stray Task A from tool.execution_complete will now be discarded) # (Stray Task A from tool.execution_complete will now be discarded)