ColabWan / shared /gradio /assistant_chat.py
1ripon1's picture
Upload folder using huggingface_hub
7344bef verified
Raw
History Blame Contribute Delete
120 kB
from __future__ import annotations
import html
import json
import os
import re
import time
import urllib.parse
import uuid
from typing import Any
import markdown
from shared.deepy import video_tools as deepy_video_tools
CHAT_HOST_ID = "assistant_chat_html"
CHAT_EVENT_ID = "assistant_chat_event"
DOCK_ID = "assistant_chat_dock"
LAUNCHER_HOST_ID = "assistant_chat_launcher_host"
LAUNCHER_BUTTON_ID = "assistant_chat_toggle"
PANEL_ID = "assistant_chat_panel"
SETTINGS_LAUNCHER_HOST_ID = "assistant_chat_settings_launcher_host"
SETTINGS_TOGGLE_ID = "assistant_chat_settings_toggle"
SETTINGS_PANEL_ID = "assistant_chat_settings_panel"
CHAT_BLOCK_ID = "assistant_chat_shell_block"
STATS_BLOCK_ID = "assistant_chat_stats_block"
STATS_ID = "assistant_chat_stats"
CONTROLS_ID = "assistant_chat_controls"
REQUEST_ID = "assistant_chat_request"
ASK_BUTTON_ID = "assistant_chat_ask_button"
RESET_BUTTON_ID = "assistant_chat_reset_button"
STOP_BRIDGE_ID = "assistant_chat_stop_bridge"
BUSY_QUEUE_INPUT_ID = "assistant_chat_busy_queue_input"
BUSY_QUEUE_BUTTON_ID = "assistant_chat_busy_queue_button"
SAVE_SETTINGS_BUTTON_ID = "assistant_chat_save_settings_button"
_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".jfif", ".pjpeg"}
_VIDEO_EXTENSIONS = deepy_video_tools.VIDEO_EXTENSIONS
_AUDIO_EXTENSIONS = {".wav", ".mp3", ".aac", ".m4a", ".flac", ".ogg", ".opus"}
_MARKDOWN_EXTENSIONS = ["extra", "nl2br", "sane_lists", "fenced_code", "tables"]
_MARKDOWN_IMAGE_RE = re.compile(r"!\[(?P<alt>[^\]]*)\]\((?P<path>[^)]+)\)")
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
_AUDIO_THUMBNAIL_PATH = os.path.join(_REPO_ROOT, "icons", "soundwave.jpg")
SERVER_INSTANCE_ID = uuid.uuid4().hex
_UNSET = object()
def _shell_markup() -> str:
return """
<section class="wangp-assistant-chat">
<div class="wangp-assistant-chat__scroll">
<div class="wangp-assistant-chat__empty">
<div>
<strong>Dialogue With Deepy</strong>
Ask for an image or video idea, then inspect the assistant's reasoning and tool usage without losing the live transcript.
</div>
</div>
<div class="wangp-assistant-chat__transcript"></div>
</div>
<div class="wangp-assistant-chat__status" aria-live="polite">
<div class="wangp-assistant-chat__status-dots" aria-hidden="true"><span></span><span></span><span></span></div>
<div class="wangp-assistant-chat__status-text"></div>
<button class="wangp-assistant-chat__status-stop" type="button" aria-label="Stop Deepy" disabled>Stop</button>
</div>
<button class="wangp-assistant-chat__jump-bottom" type="button" aria-label="Jump to latest messages" aria-hidden="true" tabindex="-1">
<span aria-hidden="true"></span>
</button>
</section>
""".strip()
def render_shell_html() -> str:
return f"<div id='{CHAT_HOST_ID}' data-wangp-assistant-chat-mounted='true'>{_shell_markup()}</div>"
def render_stats_html() -> str:
return f"<div id='{STATS_ID}' class='wangp-assistant-chat__stats' aria-hidden='true'></div>"
def render_launcher_html() -> str:
return (
f"<button id='{LAUNCHER_BUTTON_ID}' class='wangp-assistant-chat__toggle' type='button' "
"aria-label='Toggle Deepy assistant' aria-expanded='false'>"
"<span class='wangp-assistant-chat__toggle-text'>Ask Deepy</span>"
"</button>"
)
def render_settings_launcher_html() -> str:
return (
f"<button id='{SETTINGS_TOGGLE_ID}' class='wangp-assistant-chat__settings-toggle' type='button' "
"aria-label='Toggle Deepy settings' aria-expanded='false'>"
"<span class='wangp-assistant-chat__settings-toggle-text'>Settings</span>"
"</button>"
)
def get_css() -> str:
return r"""
#assistant_chat_dock {
--dock-gap: 14px;
--dock-launcher-width: 41px;
--dock-panel-width: 548px;
--dock-settings-panel-width: 660px;
--dock-settings-panel-offset: 44px;
--dock-font-scale: 0.9;
position: fixed !important;
top: 50%;
left: 0;
z-index: 1500;
width: var(--dock-launcher-width);
transform: translateY(-50%);
pointer-events: none;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
#assistant_chat_dock:not(:has(#assistant_chat_toggle)) {
display: none !important;
}
#assistant_chat_dock > * {
flex: 0 0 auto !important;
}
#assistant_chat_launcher_host,
#assistant_chat_panel,
#assistant_chat_settings_launcher_host,
#assistant_chat_settings_panel {
pointer-events: auto;
}
#assistant_chat_launcher_host {
flex: 0 0 var(--dock-launcher-width) !important;
position: relative;
width: var(--dock-launcher-width) !important;
min-width: var(--dock-launcher-width) !important;
max-width: var(--dock-launcher-width) !important;
min-height: 188px !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
overflow: visible !important;
min-width: 0 !important;
}
#assistant_chat_launcher_host .html-container,
#assistant_chat_shell_block .html-container,
#assistant_chat_stats_block .html-container {
padding: 0 !important;
}
#assistant_chat_launcher_host .prose,
#assistant_chat_shell_block .prose,
#assistant_chat_stats_block .prose {
max-width: none !important;
margin: 0 !important;
}
#assistant_chat_toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--dock-launcher-width);
min-width: var(--dock-launcher-width);
min-height: 188px;
padding: 18px 6px;
border: 1px solid rgba(73, 87, 99, 0.18);
border-left: 0;
border-radius: 0 22px 22px 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 242, 246, 0.98) 100%);
box-shadow: 0 18px 34px rgba(8, 33, 49, 0.16);
cursor: pointer;
transform: translateX(-4px);
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
#assistant_chat_toggle:hover {
transform: translateX(0);
box-shadow: 0 22px 38px rgba(8, 33, 49, 0.2);
}
#assistant_chat_dock.is-open #assistant_chat_toggle {
background: linear-gradient(180deg, rgba(13, 79, 113, 0.98) 0%, rgba(7, 50, 72, 0.98) 100%);
}
#assistant_chat_dock.is-open #assistant_chat_toggle .wangp-assistant-chat__toggle-text {
color: #f4fbff;
}
.wangp-assistant-chat__toggle-text {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
color: #4d6070;
font-size: calc(0.76rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
writing-mode: vertical-rl;
transform: rotate(180deg);
}
#assistant_chat_panel {
position: absolute !important;
top: 50%;
left: calc(var(--dock-launcher-width) + var(--dock-gap));
flex: 0 0 auto !important;
width: min(var(--dock-panel-width), calc(100vw - 92px));
padding: 12px;
border: 1px solid rgba(16, 78, 109, 0.16);
border-radius: 24px;
background: #ffffff;
box-shadow: 0 30px 60px rgba(8, 34, 50, 0.2);
opacity: 0;
visibility: hidden;
transform: translateY(-50%) translateX(-30px) scale(0.98);
transform-origin: left center !important;
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s step-end;
pointer-events: none;
overflow: visible !important;
}
#assistant_chat_dock:not(.is-open) #assistant_chat_panel {
display: none;
}
#assistant_chat_dock.is-open #assistant_chat_panel {
display: block;
opacity: 1;
visibility: visible;
transform: translateY(-50%) translateX(0) scale(1);
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s step-start;
pointer-events: auto;
}
#assistant_chat_settings_launcher_host {
position: absolute !important;
top: 20px;
right: -30px;
z-index: 3;
width: 30px !important;
min-width: 30px !important;
max-width: 30px !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
overflow: visible !important;
}
#assistant_chat_settings_launcher_host .html-container,
#assistant_chat_settings_launcher_host .prose {
padding: 0 !important;
margin: 0 !important;
max-width: none !important;
}
#assistant_chat_settings_toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
min-width: 30px;
min-height: 156px;
padding: 14px 4px;
border: 1px solid rgba(16, 78, 109, 0.18);
border-left: 0;
border-radius: 0 18px 18px 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(237, 245, 250, 0.98) 100%);
box-shadow: 0 16px 28px rgba(8, 34, 50, 0.12);
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
#assistant_chat_settings_toggle:hover {
box-shadow: 0 18px 30px rgba(8, 34, 50, 0.16);
}
#assistant_chat_panel.is-settings-open #assistant_chat_settings_toggle {
background: linear-gradient(180deg, rgba(13, 79, 113, 0.98) 0%, rgba(7, 50, 72, 0.98) 100%);
}
#assistant_chat_panel.is-settings-open #assistant_chat_settings_toggle .wangp-assistant-chat__settings-toggle-text {
color: #f4fbff;
}
.wangp-assistant-chat__settings-toggle-text {
display: inline-flex;
align-items: center;
justify-content: center;
color: #0f5375;
font-size: calc(0.68rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
writing-mode: vertical-rl;
transform: rotate(180deg);
}
#assistant_chat_settings_panel {
position: absolute !important;
top: 0;
left: calc(100% + var(--dock-settings-panel-offset));
z-index: 2;
width: min(var(--dock-settings-panel-width), calc(100vw - 150px));
height: 100%;
padding: 0;
display: block;
border: 0;
border-radius: 24px;
background: transparent;
box-shadow: none;
opacity: 0;
visibility: hidden;
transform: translateX(-24px) scale(0.98);
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s step-end;
pointer-events: none;
overflow: visible !important;
}
#assistant_chat_panel.is-settings-open #assistant_chat_settings_panel {
opacity: 1;
visibility: visible;
transform: translateX(0) scale(1);
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s step-start;
pointer-events: auto;
}
#assistant_chat_settings_panel .form,
#assistant_chat_settings_panel .wrap,
#assistant_chat_settings_panel .block,
#assistant_chat_settings_panel .gradio-container,
#assistant_chat_settings_panel .accordion {
min-width: 0 !important;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card {
width: 100% !important;
height: 100% !important;
max-width: none !important;
display: flex !important;
flex-direction: column !important;
min-width: 0 !important;
padding: 0 !important;
gap: 0 !important;
border: 1px solid rgba(14, 71, 99, 0.18) !important;
border-radius: 22px !important;
background: #ffffff !important;
box-shadow: 0 28px 56px rgba(8, 33, 49, 0.2) !important;
overflow: hidden !important;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .form {
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll {
display: block !important;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto !important;
overflow-x: hidden !important;
padding: 12px 12px 12px;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block {
display: block !important;
margin: 0 0 12px !important;
overflow: visible;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > .label-wrap {
align-items: center;
padding: 10px 14px;
border: 1px solid rgba(23, 90, 125, 0.16);
border-radius: 16px;
background: linear-gradient(180deg, rgba(236, 244, 249, 0.98) 0%, rgba(224, 237, 245, 0.98) 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > .label-wrap.open {
margin-bottom: 8px;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > .label-wrap span {
color: #174a67;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > div:last-child {
overflow: visible;
}
#assistant_chat_settings_panel .label-wrap {
gap: 6px;
}
#assistant_chat_shell_block,
#assistant_chat_stats_block,
#assistant_chat_controls {
margin: 0 !important;
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
padding: 0 !important;
}
#assistant_chat_shell_block {
margin-bottom: 8px !important;
}
#assistant_chat_stats_block {
margin-top: 2px !important;
margin-bottom: 4px !important;
}
#assistant_chat_controls,
#assistant_chat_controls > .form,
#assistant_chat_request {
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_controls > .form {
padding: 0 !important;
border: 0 !important;
min-width: 0 !important;
}
#assistant_chat_controls {
display: flex;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 10px;
}
#assistant_chat_request {
order: 0;
flex: 1 1 auto !important;
width: auto !important;
min-width: 0;
padding: 0 !important;
}
#assistant_chat_request span[data-testid="block-info"],
#assistant_chat_controls span[data-testid="block-info"] {
display: none !important;
}
#assistant_chat_request > .form,
#assistant_chat_request > .wrap {
width: 100% !important;
min-width: 0 !important;
height: 100% !important;
padding: 0 !important;
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
#assistant_chat_request textarea,
#assistant_chat_request input {
width: 100% !important;
min-height: 48px !important;
font-size: calc(0.92rem * var(--dock-font-scale)) !important;
line-height: 1.45;
border: 1px solid rgba(23, 90, 125, 0.18) !important;
border-radius: 15px !important;
background: linear-gradient(180deg, rgba(248, 252, 255, 0.94) 0%, rgba(239, 246, 251, 0.95) 100%) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 8px 18px rgba(14, 53, 75, 0.04) !important;
}
#assistant_chat_request textarea:focus,
#assistant_chat_request input:focus {
border-color: rgba(23, 110, 154, 0.34) !important;
box-shadow: 0 0 0 3px rgba(57, 145, 189, 0.16), 0 10px 20px rgba(14, 53, 75, 0.09) !important;
}
#assistant_chat_request label,
#assistant_chat_request .input-container {
width: 100% !important;
min-height: 52px !important;
display: flex !important;
align-items: center !important;
}
#assistant_chat_ask_button,
#assistant_chat_reset_button {
order: 0;
flex: 0 0 auto !important;
align-self: center;
min-width: 0 !important;
height: 48px;
min-height: 48px;
padding: 0 14px;
border-radius: 15px;
font-size: calc(1.12rem * var(--dock-font-scale));
font-weight: 700;
box-shadow: 0 12px 22px rgba(11, 43, 63, 0.12);
border: 0;
}
#assistant_chat_ask_button {
width: 86px;
background: linear-gradient(180deg, #0e5b81 0%, #0a415e 100%);
color: #f3fbff;
}
#assistant_chat_reset_button {
width: 82px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 246, 250, 0.98) 100%);
color: #164f70;
border: 1px solid rgba(20, 82, 113, 0.14);
}
#assistant_chat_stop_bridge {
display: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__settings-actions {
margin-top: 10px;
}
#assistant_chat_settings_panel .wangp-assistant-chat__settings-actions > .form {
width: 100%;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_save_settings_button {
width: 100%;
min-height: 42px;
border-radius: 14px;
background: linear-gradient(180deg, #0e5b81 0%, #0a415e 100%);
color: #f3fbff;
border: 0;
box-shadow: 0 12px 22px rgba(11, 43, 63, 0.12);
}
#assistant_chat_html {
min-height: 430px;
}
.wangp-assistant-chat {
--chat-border: transparent;
--chat-shadow: none;
--chat-surface: #ffffff;
--chat-status-offset: 18px;
--chat-status-reserved-height: 0px;
--chat-status-gap: 7px;
--assistant-bg: linear-gradient(180deg, #145171 0%, #0c3954 100%);
--assistant-border: rgba(8, 40, 57, 0.42);
--assistant-text: #f2fbff;
--user-bg: linear-gradient(180deg, #ffffff 0%, #f5fbff 100%);
--user-border: rgba(55, 131, 180, 0.18);
--user-text: #163f58;
--muted-text: #5b7282;
--soft-text: #6d8090;
--tool-bg: rgba(234, 245, 251, 0.92);
--tool-border: rgba(40, 108, 153, 0.16);
--status-bg: linear-gradient(180deg, rgba(19, 51, 71, 0.95) 0%, rgba(10, 31, 47, 0.94) 100%);
--status-text: #fbfeff;
--empty-border: rgba(31, 94, 132, 0.12);
position: relative;
display: flex;
flex-direction: column;
height: 430px;
overflow: hidden;
border: 1px solid var(--chat-border);
border-radius: 26px;
background: var(--chat-surface);
box-shadow: var(--chat-shadow);
isolation: isolate;
}
.wangp-assistant-chat:has(.wangp-assistant-chat__status.is-visible) {
--chat-status-reserved-height: 58px;
}
.wangp-assistant-chat::before {
content: "";
position: absolute;
inset: 0;
background: none;
pointer-events: none;
}
.wangp-assistant-chat__scroll {
position: relative;
flex: 1;
overflow-y: auto;
background: transparent;
}
.wangp-assistant-chat__scroll::-webkit-scrollbar {
width: 10px;
}
.wangp-assistant-chat__scroll::-webkit-scrollbar-thumb {
border-radius: 999px;
border: 2px solid transparent;
background: rgba(29, 92, 128, 0.2);
background-clip: padding-box;
}
.wangp-assistant-chat__empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
box-sizing: border-box;
padding: 36px 34px 102px;
border: 0;
border-radius: 0;
color: var(--muted-text);
text-align: center;
font-size: calc(0.98rem * var(--dock-font-scale));
line-height: 1.6;
background: transparent;
backdrop-filter: none;
}
.wangp-assistant-chat__empty strong {
display: block;
margin-bottom: 6px;
color: #194d70;
font-size: calc(1rem * var(--dock-font-scale));
}
.wangp-assistant-chat__transcript {
display: flex;
flex-direction: column;
gap: 16px;
padding: 22px 18px calc(var(--chat-status-offset) + var(--chat-status-reserved-height) + var(--chat-status-gap));
}
.wangp-assistant-chat__stats {
min-height: calc(0.78rem * var(--dock-font-scale));
padding: 0 2px;
font-size: calc(0.64rem * var(--dock-font-scale));
line-height: 1.15;
white-space: nowrap;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
color: #8d9aa5;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.wangp-assistant-chat__stats.is-visible {
opacity: 0.96;
}
.wangp-assistant-chat__message {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.wangp-assistant-chat__message--user {
flex-direction: row-reverse;
}
.wangp-assistant-chat__avatar {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 54px;
height: 54px;
border-radius: 50%;
font-size: calc(0.8rem * var(--dock-font-scale));
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
box-shadow: 0 12px 22px rgba(18, 61, 88, 0.12);
margin-top: 10px;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__avatar {
color: #eefbff;
background: linear-gradient(180deg, rgba(11, 72, 103, 0.96) 0%, rgba(7, 48, 70, 0.96) 100%);
border: 1px solid rgba(7, 39, 57, 0.35);
}
.wangp-assistant-chat__message--user .wangp-assistant-chat__avatar {
color: #0e4564;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(245, 251, 255, 0.99) 100%);
border: 1px solid rgba(47, 124, 170, 0.14);
}
.wangp-assistant-chat__message-card {
width: min(82%, 860px);
border-radius: 22px;
padding: 16px 16px 14px;
box-shadow: 0 18px 34px rgba(11, 36, 54, 0.08);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__message-card {
border: 1px solid var(--assistant-border);
background: var(--assistant-bg);
color: var(--assistant-text);
}
.wangp-assistant-chat__message--user .wangp-assistant-chat__message-card {
border: 1px solid var(--user-border);
background: var(--user-bg);
color: var(--user-text);
}
.wangp-assistant-chat__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
font-size: calc(0.82rem * var(--dock-font-scale));
color: var(--soft-text);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__meta {
color: rgba(242, 251, 255, 0.74);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__time {
color: #f4fbff;
}
.wangp-assistant-chat__meta-left {
display: inline-flex;
align-items: center;
min-height: 1em;
}
.wangp-assistant-chat__author {
font-weight: 700;
letter-spacing: 0.03em;
}
.wangp-assistant-chat__time {
opacity: 0.9;
white-space: nowrap;
}
.wangp-assistant-chat__badge {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 8px;
padding: 4px 10px;
border-radius: 999px;
font-size: calc(0.72rem * var(--dock-font-scale));
font-weight: 700;
letter-spacing: 0.02em;
background: rgba(31, 110, 154, 0.1);
color: #20658f;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__badge {
background: rgba(255, 255, 255, 0.12);
color: #eff9ff;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__tool-title,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__disclosure summary {
color: var(--assistant-text);
}
.wangp-assistant-chat__body {
font-size: calc(0.97rem * var(--dock-font-scale));
line-height: 1.68;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body p,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body li,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body strong,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body em,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body blockquote,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body h1,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body h2,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body h3,
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body h4 {
color: var(--assistant-text);
}
.wangp-assistant-chat__body > :first-child {
margin-top: 0;
}
.wangp-assistant-chat__body > :last-child {
margin-bottom: 0;
}
.wangp-assistant-chat__body p,
.wangp-assistant-chat__body ul,
.wangp-assistant-chat__body ol,
.wangp-assistant-chat__body pre,
.wangp-assistant-chat__body blockquote {
margin: 0 0 0.85em;
}
.wangp-assistant-chat__body ul,
.wangp-assistant-chat__body ol {
padding-left: 1.2em;
}
.wangp-assistant-chat__body code {
padding: 0.12em 0.34em;
border-radius: 8px;
font-size: 0.92em;
background: rgba(16, 73, 104, 0.08);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body code {
color: var(--assistant-text);
background: rgba(255, 255, 255, 0.12);
}
.wangp-assistant-chat__body pre {
overflow-x: auto;
padding: 12px 13px;
border-radius: 14px;
border: 1px solid rgba(26, 84, 117, 0.12);
background: rgba(239, 247, 251, 0.96);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__body pre {
color: var(--assistant-text);
border-color: rgba(255, 255, 255, 0.12);
background: rgba(7, 33, 48, 0.38);
}
.wangp-assistant-chat__body a {
color: inherit;
font-weight: 600;
}
.wangp-assistant-chat__disclosure {
margin-top: 12px;
border: 1px solid var(--tool-border);
border-radius: 16px;
background: var(--tool-bg);
overflow: hidden;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__disclosure {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.08);
}
.wangp-assistant-chat__disclosure summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 12px;
cursor: pointer;
list-style: none;
font-weight: 700;
font-size: calc(0.7rem * var(--dock-font-scale));
line-height: 1.3;
}
.wangp-assistant-chat__disclosure > summary {
display: flex;
}
.wangp-assistant-chat__disclosure summary::-webkit-details-marker {
display: none;
}
.wangp-assistant-chat__disclosure summary::after {
content: "\25B8";
font-size: calc(0.78rem * var(--dock-font-scale));
transition: color 0.18s ease;
color: #2f769f;
}
.wangp-assistant-chat__disclosure[open] summary::after {
content: "\25BE";
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__disclosure summary::after {
color: rgba(245, 251, 255, 0.86);
}
.wangp-assistant-chat__disclosure-body {
padding: 0 14px 14px;
font-size: calc(0.84rem * var(--dock-font-scale));
line-height: 1.52;
color: #385363;
}
.wangp-assistant-chat__disclosure:not([open]) > .wangp-assistant-chat__disclosure-body {
display: none;
}
.wangp-assistant-chat__disclosure[open] > .wangp-assistant-chat__disclosure-body {
display: block;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__disclosure-body {
color: var(--assistant-text);
}
.wangp-assistant-chat__tool-title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: calc(0.72rem * var(--dock-font-scale));
}
.wangp-assistant-chat__tool-chip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: calc(0.54rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.03em;
text-transform: uppercase;
color: #205f86;
background: rgba(33, 109, 153, 0.12);
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__tool-chip {
color: #eff9ff;
background: rgba(255, 255, 255, 0.14);
}
.wangp-assistant-chat__tool-status {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 999px;
font-size: calc(0.55rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.02em;
}
.wangp-assistant-chat__tool-status--running {
background: rgba(229, 160, 38, 0.14);
color: #90600f;
}
.wangp-assistant-chat__tool-status--done {
background: rgba(72, 208, 128, 0.16);
color: #5df0a0;
}
.wangp-assistant-chat__tool-status--error {
background: rgba(183, 62, 62, 0.12);
color: #973232;
}
.wangp-assistant-chat__pre {
margin: 10px 0 0;
padding: 12px 13px;
border-radius: 14px;
overflow-x: auto;
background: rgba(247, 251, 253, 0.95);
border: 1px solid rgba(30, 92, 127, 0.1);
font-size: calc(0.72rem * var(--dock-font-scale));
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__pre {
color: var(--assistant-text);
background: rgba(7, 33, 48, 0.38);
border-color: rgba(255, 255, 255, 0.12);
}
.wangp-assistant-chat__tool-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.wangp-assistant-chat__tool-section-title {
margin-bottom: 6px;
font-size: calc(0.67rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #557385;
}
.wangp-assistant-chat__message--assistant .wangp-assistant-chat__tool-section-title {
color: rgba(233, 246, 255, 0.76);
}
.wangp-assistant-chat__attachments {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 12px;
margin-top: 12px;
}
.wangp-assistant-chat__attachment {
display: flex;
gap: 12px;
align-items: center;
min-width: 0;
padding: 12px;
border: 1px solid rgba(31, 101, 141, 0.12);
border-radius: 16px;
color: inherit;
text-decoration: none;
background: rgba(255, 255, 255, 0.78);
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.wangp-assistant-chat__attachment:hover {
transform: translateY(-1px);
border-color: rgba(31, 101, 141, 0.22);
box-shadow: 0 14px 28px rgba(12, 45, 67, 0.1);
}
.wangp-assistant-chat__attachment-thumb {
flex: 0 0 88px;
width: 88px;
height: 88px;
object-fit: cover;
border-radius: 14px;
border: 1px solid rgba(26, 82, 114, 0.12);
background: rgba(234, 245, 251, 0.9);
}
.wangp-assistant-chat__attachment-meta {
min-width: 0;
}
.wangp-assistant-chat__attachment-title {
display: block;
font-weight: 700;
color: #1b587e;
}
.wangp-assistant-chat__attachment-subtitle {
display: block;
margin-top: 4px;
color: #667d8c;
font-size: calc(0.84rem * var(--dock-font-scale));
line-height: 1.45;
word-break: break-word;
}
.wangp-assistant-chat__status {
position: absolute;
left: 18px;
right: 18px;
bottom: var(--chat-status-offset);
z-index: 3;
display: flex;
align-items: center;
gap: 12px;
margin: 0;
padding: 12px 14px;
border-radius: 18px;
background: var(--status-bg);
color: var(--status-text);
box-shadow: 0 16px 34px rgba(10, 30, 46, 0.18);
transform: translateY(8px);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.wangp-assistant-chat__status,
.wangp-assistant-chat__status-text,
.wangp-assistant-chat__status-stop {
color: var(--status-text);
}
.wangp-assistant-chat__status.is-visible {
opacity: 1;
transform: translateY(0);
}
.wangp-assistant-chat__status-text {
flex: 1;
min-width: 0;
font-size: calc(0.92rem * var(--dock-font-scale));
line-height: 1.45;
font-weight: 600;
pointer-events: none;
}
.wangp-assistant-chat__status-dots {
display: inline-flex;
align-items: center;
gap: 5px;
pointer-events: none;
}
.wangp-assistant-chat__status-dots span {
width: 7px;
height: 7px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
animation: wangp-assistant-chat-pulse 1.18s infinite ease-in-out;
}
.wangp-assistant-chat__status-dots span:nth-child(2) {
animation-delay: 0.15s;
}
.wangp-assistant-chat__status-dots span:nth-child(3) {
animation-delay: 0.3s;
}
.wangp-assistant-chat__status-stop {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 62px;
min-height: 34px;
padding: 0 12px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
background: rgba(179, 58, 58, 0.9);
box-shadow: 0 10px 18px rgba(6, 18, 28, 0.16);
font-size: calc(0.74rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
pointer-events: auto;
transition: transform 0.16s ease, background 0.16s ease, opacity 0.16s ease;
}
.wangp-assistant-chat__status-stop:hover:not(:disabled) {
transform: translateY(-1px);
background: rgba(197, 72, 72, 0.96);
}
.wangp-assistant-chat__status-stop:disabled {
opacity: 0.55;
cursor: default;
}
.wangp-assistant-chat__jump-bottom {
position: absolute;
left: 50%;
bottom: calc(var(--chat-status-offset) + 8px);
z-index: 4;
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(251, 254, 255, 0.88);
border-radius: 999px;
background: transparent;
color: transparent;
box-shadow: none;
backdrop-filter: none;
transform: translate(-50%, 10px);
opacity: 0;
pointer-events: none;
filter: drop-shadow(0 2px 4px rgba(9, 31, 46, 0.28));
transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
}
.wangp-assistant-chat__jump-bottom.is-visible {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, 0);
}
.wangp-assistant-chat__jump-bottom span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
box-sizing: border-box;
border-right: 3px solid rgba(251, 254, 255, 0.98);
border-bottom: 3px solid rgba(251, 254, 255, 0.98);
transform: translateY(-2px) rotate(45deg);
}
.wangp-assistant-chat__jump-bottom:hover {
border-color: rgba(251, 254, 255, 1);
}
.wangp-assistant-chat__jump-bottom:hover span {
border-right-color: rgba(251, 254, 255, 1);
border-bottom-color: rgba(251, 254, 255, 1);
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-grid {
position: relative;
gap: 12px;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-grid-row {
gap: 12px;
align-items: stretch;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-card {
min-width: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-card > .form {
min-width: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-row {
gap: 10px;
align-items: flex-end;
flex-wrap: nowrap;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-dropdown {
flex: 1 1 auto !important;
min-width: 0 !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-row > .form,
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-dropdown {
min-width: 0 !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-row > .form,
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-dropdown,
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-dropdown .wrap {
overflow: visible !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-dropdown .wrap > ul.options[role="listbox"] {
position: absolute !important;
inset: calc(100% - 8px) auto auto 0 !important;
width: 100% !important;
max-height: min(280px, 40vh) !important;
z-index: 2147483647 !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-actions {
flex: 0 0 auto !important;
gap: 4px;
width: 34px !important;
min-width: 34px !important;
max-width: 34px !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-actions > .form {
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn {
width: 34px !important;
min-width: 34px !important;
max-width: 34px !important;
height: 34px;
min-height: 34px;
padding: 0 !important;
border: 1px solid rgba(17, 84, 118, 0.14);
border-radius: 12px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(236, 244, 249, 0.99) 100%);
color: #155574;
box-shadow: 0 10px 18px rgba(11, 44, 63, 0.08);
font-size: calc(0.88rem * var(--dock-font-scale));
line-height: 1;
font-weight: 700;
transition: transform 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 24px rgba(11, 44, 63, 0.12);
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn--danger {
color: #8b2d2d;
background: linear-gradient(180deg, rgba(255, 252, 252, 0.99) 0%, rgba(249, 239, 239, 0.99) 100%);
border-color: rgba(156, 62, 62, 0.16);
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap.hide {
display: none !important;
pointer-events: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap:not(.hide) {
position: absolute !important;
inset: 0;
z-index: 40;
display: flex !important;
align-items: center;
justify-content: center;
margin: 0 !important;
padding: 12px !important;
border: 0 !important;
background: rgba(10, 38, 53, 0.18) !important;
backdrop-filter: blur(3px);
overflow: hidden !important;
box-sizing: border-box;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap > .form {
width: 100% !important;
height: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap > .styler {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: min(100%, 450px) !important;
max-width: 450px !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
overflow: visible !important;
flex: 0 1 auto !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card {
width: 100% !important;
max-width: 450px !important;
min-width: 0 !important;
flex: 0 1 auto !important;
padding: 0 !important;
gap: 0 !important;
border: 1px solid rgba(14, 71, 99, 0.18) !important;
border-radius: 22px !important;
background: #ffffff !important;
box-shadow: 0 28px 56px rgba(8, 33, 49, 0.2) !important;
overflow: hidden !important;
}
#assistant_chat_settings_panel > .wangp-assistant-chat__settings-card.wangp-assistant-chat__template-modal-card {
width: 100% !important;
max-width: none !important;
flex: 1 1 auto !important;
}
#assistant_chat_settings_panel .tab-nav button,
#assistant_chat_settings_panel button[role="tab"] {
font-size: calc(0.82rem * var(--dock-font-scale));
padding-top: 6px !important;
padding-bottom: 6px !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card > .form {
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card .html-container {
padding: 0 !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card .prose {
margin: 0 !important;
max-width: none !important;
}
.wangp-assistant-chat__template-modal-titlebar {
padding: 10px 16px 9px;
background: linear-gradient(180deg, rgba(16, 86, 121, 0.98) 0%, rgba(10, 59, 84, 0.98) 100%);
color: #f3fbff;
}
.wangp-assistant-chat__template-modal-kicker {
font-size: calc(0.66rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
opacity: 0.78;
}
.wangp-assistant-chat__template-modal-heading {
font-size: calc(0.9rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.02em;
color: #f3fbff !important;
}
.wangp-assistant-chat__template-modal-context {
margin: 16px 18px 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.wangp-assistant-chat__template-modal-context-label {
font-size: calc(0.7rem * var(--dock-font-scale));
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #5b7282;
}
.wangp-assistant-chat__template-modal-context-value {
margin-top: 5px;
color: #174a67;
font-size: calc(0.95rem * var(--dock-font-scale));
font-weight: 700;
word-break: break-word;
}
.wangp-assistant-chat__template-modal-message {
margin: 14px 18px 0;
padding: 0;
border-radius: 0;
font-size: calc(0.9rem * var(--dock-font-scale));
line-height: 1.5;
font-weight: 600;
background: transparent !important;
}
.wangp-assistant-chat__template-modal-message.is-info {
color: #164f70;
}
.wangp-assistant-chat__template-modal-message.is-warning {
color: #7a5415;
}
.wangp-assistant-chat__template-modal-message.is-error {
color: #b33434;
}
.wangp-assistant-chat__template-modal-actions {
justify-content: flex-end;
gap: 10px;
padding: 18px;
}
.wangp-assistant-chat__template-modal-btn {
min-width: 92px;
height: 40px;
min-height: 40px;
border-radius: 14px;
border: 1px solid rgba(17, 84, 118, 0.14);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(237, 245, 250, 0.99) 100%);
color: #155574;
box-shadow: 0 10px 18px rgba(11, 44, 63, 0.08);
font-weight: 700;
}
.wangp-assistant-chat__template-modal-btn--primary {
color: #f4fbff;
border-color: rgba(10, 59, 84, 0.12);
background: linear-gradient(180deg, rgba(16, 86, 121, 0.98) 0%, rgba(10, 59, 84, 0.98) 100%);
}
#assistant_chat_dock.is-dark #assistant_chat_toggle {
border-color: rgba(28, 104, 145, 0.28);
background: linear-gradient(180deg, rgba(13, 79, 113, 0.98) 0%, rgba(7, 50, 72, 0.98) 100%);
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.34);
}
#assistant_chat_dock.is-dark #assistant_chat_toggle .wangp-assistant-chat__toggle-text {
color: #f4fbff;
}
#assistant_chat_dock.is-dark.is-open #assistant_chat_toggle {
border-color: rgba(115, 120, 126, 0.6);
background: linear-gradient(180deg, rgba(92, 96, 102, 0.98) 0%, rgba(58, 61, 66, 0.98) 100%);
}
#assistant_chat_dock.is-dark.is-open #assistant_chat_toggle .wangp-assistant-chat__toggle-text {
color: #f4fbff;
}
#assistant_chat_dock.is-dark #assistant_chat_settings_toggle {
border-color: rgba(28, 104, 145, 0.28);
background: linear-gradient(180deg, rgba(13, 79, 113, 0.98) 0%, rgba(7, 50, 72, 0.98) 100%);
box-shadow: 0 16px 28px rgba(0, 0, 0, 0.3);
}
#assistant_chat_dock.is-dark #assistant_chat_settings_toggle .wangp-assistant-chat__settings-toggle-text {
color: #f4fbff;
}
#assistant_chat_dock.is-dark #assistant_chat_panel.is-settings-open #assistant_chat_settings_toggle {
border-color: rgba(115, 120, 126, 0.6);
background: linear-gradient(180deg, rgba(92, 96, 102, 0.98) 0%, rgba(58, 61, 66, 0.98) 100%);
}
#assistant_chat_dock.is-dark #assistant_chat_panel.is-settings-open #assistant_chat_settings_toggle .wangp-assistant-chat__settings-toggle-text {
color: #f4fbff;
}
#assistant_chat_dock.is-dark #assistant_chat_panel,
#assistant_chat_dock.is-dark #assistant_chat_settings_panel {
border-color: rgba(92, 96, 102, 0.78);
background: #000000;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.46), inset 0 0 0 1px rgba(70, 73, 78, 0.42);
color: #eaf2f7;
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > .label-wrap {
border-color: rgba(112, 138, 156, 0.18);
background: linear-gradient(180deg, rgba(9, 9, 9, 0.98) 0%, rgba(20, 20, 20, 0.98) 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel > .wangp-assistant-chat__settings-card > .wangp-assistant-chat__settings-scroll > .block > .label-wrap span {
color: #e6eef4;
}
#assistant_chat_dock.is-dark #assistant_chat_request textarea,
#assistant_chat_dock.is-dark #assistant_chat_request input {
color: #eef6fb !important;
caret-color: #eef6fb !important;
border-color: rgba(103, 132, 151, 0.24) !important;
background: linear-gradient(180deg, rgba(10, 10, 10, 0.96) 0%, rgba(19, 19, 19, 0.96) 100%) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 8px 18px rgba(0, 0, 0, 0.22) !important;
}
#assistant_chat_dock.is-dark #assistant_chat_request textarea::placeholder,
#assistant_chat_dock.is-dark #assistant_chat_request input::placeholder {
color: #93a6b4 !important;
}
#assistant_chat_dock.is-dark #assistant_chat_reset_button {
color: #e8f1f6;
border-color: rgba(103, 132, 151, 0.22);
background: linear-gradient(180deg, rgba(12, 12, 12, 0.98) 0%, rgba(22, 22, 22, 0.98) 100%);
box-shadow: 0 12px 22px rgba(0, 0, 0, 0.22);
}
#assistant_chat_dock.is-dark .wangp-assistant-chat {
--chat-surface: #000000;
--assistant-bg: linear-gradient(180deg, #0f4a69 0%, #082f45 100%);
--assistant-border: rgba(67, 114, 143, 0.34);
--assistant-text: #f2fbff;
--user-bg: linear-gradient(180deg, #12181d 0%, #090d10 100%);
--user-border: rgba(101, 127, 145, 0.2);
--user-text: #edf4f9;
--muted-text: #b3c1cb;
--soft-text: #98a9b5;
--tool-bg: rgba(17, 24, 30, 0.96);
--tool-border: rgba(103, 132, 151, 0.18);
--empty-border: rgba(103, 132, 151, 0.16);
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__empty strong,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body p,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body li,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body strong,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body em,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body blockquote,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body h1,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body h2,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body h3,
#assistant_chat_dock.is-dark .wangp-assistant-chat__body h4 {
color: #edf4f9;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__stats {
color: #9eb0bd;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__message--user .wangp-assistant-chat__avatar {
color: #eef6fb;
background: linear-gradient(180deg, rgba(24, 31, 37, 0.99) 0%, rgba(10, 12, 14, 0.99) 100%);
border-color: rgba(103, 132, 151, 0.2);
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__body code {
background: rgba(130, 162, 183, 0.12);
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__body pre {
color: #eaf2f7;
border-color: rgba(103, 132, 151, 0.16);
background: rgba(10, 14, 17, 0.96);
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn,
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-btn {
color: #ecf4f9;
border-color: rgba(103, 132, 151, 0.22);
background: linear-gradient(180deg, rgba(10, 10, 10, 0.99) 0%, rgba(21, 21, 21, 0.99) 100%);
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.22);
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn--danger {
color: #ffb1b1;
border-color: rgba(173, 84, 84, 0.24);
background: linear-gradient(180deg, rgba(22, 10, 10, 0.99) 0%, rgba(32, 14, 14, 0.99) 100%);
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap:not(.hide) {
background: rgba(0, 0, 0, 0.52) !important;
}
#assistant_chat_dock.is-dark #assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card {
border-color: rgba(92, 96, 102, 0.82) !important;
background: #000000 !important;
box-shadow: 0 28px 56px rgba(0, 0, 0, 0.42), inset 0 0 0 1px rgba(70, 73, 78, 0.44) !important;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-context-label {
color: #9fb1be;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-context-value,
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-message.is-info {
color: #ecf4f9;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-message.is-warning {
color: #f3d189;
}
#assistant_chat_dock.is-dark .wangp-assistant-chat__template-modal-message.is-error {
color: #ff9e9e;
}
@keyframes wangp-assistant-chat-pulse {
0%, 80%, 100% { transform: scale(0.66); opacity: 0.46; }
40% { transform: scale(1); opacity: 1; }
}
@media (max-width: 900px) {
#assistant_chat_dock {
top: auto;
bottom: 18px;
width: 36px;
transform: none;
}
#assistant_chat_toggle {
min-height: 152px;
width: 36px;
min-width: 36px;
padding: 14px 5px;
border-radius: 0 18px 18px 0;
}
#assistant_chat_panel {
top: auto;
bottom: 0;
left: calc(36px + var(--dock-gap));
width: min(360px, calc(100vw - 72px));
padding: 12px;
transform: translateX(-20px) scale(0.98);
}
.wangp-assistant-chat {
height: 390px;
border-radius: 20px;
}
.wangp-assistant-chat__scroll {
padding: 0;
}
.wangp-assistant-chat__message-card {
width: min(92%, 100%);
padding: 14px 14px 12px;
}
.wangp-assistant-chat__avatar {
width: 46px;
height: 46px;
margin-top: 9px;
}
.wangp-assistant-chat__empty {
padding: 28px 20px 88px;
}
.wangp-assistant-chat__transcript {
padding: 16px 12px calc(var(--chat-status-offset) + var(--chat-status-reserved-height) + var(--chat-status-gap));
}
.wangp-assistant-chat__attachments {
grid-template-columns: 1fr;
}
.wangp-assistant-chat__attachment-thumb {
width: 72px;
height: 72px;
flex-basis: 72px;
}
#assistant_chat_controls {
flex-wrap: wrap;
justify-content: flex-end;
}
#assistant_chat_request {
flex: 1 1 100% !important;
width: 100% !important;
order: 1;
}
#assistant_chat_ask_button,
#assistant_chat_reset_button {
order: 2;
flex: 1 1 calc(50% - 5px) !important;
width: auto;
}
#assistant_chat_dock.is-open #assistant_chat_panel {
transform: translateX(0) scale(1);
}
#assistant_chat_settings_launcher_host {
top: 14px;
right: 12px;
width: auto !important;
min-width: 0 !important;
max-width: none !important;
}
#assistant_chat_settings_toggle {
min-height: 30px;
width: auto;
min-width: 30px;
padding: 8px 12px;
border-radius: 14px;
border-left: 1px solid rgba(16, 78, 109, 0.18);
}
.wangp-assistant-chat__settings-toggle-text {
writing-mode: horizontal-tb;
transform: none;
letter-spacing: 0.08em;
}
#assistant_chat_settings_panel {
top: 12px;
left: 12px;
width: calc(100% - 24px);
height: calc(100% - 24px);
transform: translateY(10px) scale(0.98);
}
#assistant_chat_panel.is-settings-open #assistant_chat_settings_panel {
transform: translateY(0) scale(1);
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-grid-row,
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-row {
flex-wrap: wrap;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-actions {
width: 100%;
min-width: 0 !important;
max-width: none !important;
flex-direction: row;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-actions > .form {
width: 100%;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-tool-icon-btn {
flex: 1 1 calc(50% - 4px);
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap {
inset: 0;
padding: 8px !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-wrap > .styler {
width: 100% !important;
max-width: none !important;
}
#assistant_chat_settings_panel .wangp-assistant-chat__template-modal-card {
width: 100% !important;
max-width: none !important;
}
}
"""
def get_javascript() -> str:
return r"""
window.__wangpAssistantChatNS = window.__wangpAssistantChatNS || {};
window.__wangpAssistantChatPending = window.__wangpAssistantChatPending || [];
const WAC = window.__wangpAssistantChatNS;
window.WAC = WAC;
WAC.state = WAC.state || { order: [], messages: {}, status: null, stats: null };
WAC.init = WAC.init || false;
WAC.observer = WAC.observer || null;
WAC.eventNode = WAC.eventNode || null;
WAC.pollTimer = WAC.pollTimer || null;
WAC.lastPayloadId = WAC.lastPayloadId || '';
WAC.lastPayloadText = WAC.lastPayloadText || '';
WAC.dockBridgeInstalled = WAC.dockBridgeInstalled || false;
WAC.dockOpen = typeof WAC.dockOpen === 'boolean' ? WAC.dockOpen : false;
WAC.settingsOpen = typeof WAC.settingsOpen === 'boolean' ? WAC.settingsOpen : false;
WAC.disclosureNode = WAC.disclosureNode || null;
WAC.disclosureState = WAC.disclosureState || {};
WAC.dock = function () {
return document.querySelector('#assistant_chat_dock');
};
WAC.panel = function () {
return document.querySelector('#assistant_chat_panel');
};
WAC.launcher = function () {
return document.querySelector('#assistant_chat_toggle');
};
WAC.settingsPanel = function () {
return document.querySelector('#assistant_chat_settings_panel');
};
WAC.settingsLauncher = function () {
return document.querySelector('#assistant_chat_settings_toggle');
};
WAC.requestInput = function () {
return document.querySelector('#assistant_chat_request textarea, #assistant_chat_request input');
};
WAC.escapeHtml = function (value) {
return String(value || '').replace(/[&<>\"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[char] || char));
};
WAC.timeLabel = function () {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
WAC.bottomThreshold = function () {
return 1;
};
WAC.captureAutoscrollState = function () {
const scroll = WAC.scroll();
if (!scroll) return { atBottom: true, top: 0 };
return {
atBottom: WAC.isNearBottom(),
top: Math.max(0, scroll.scrollTop),
};
};
WAC.applyAutoscrollState = function (state) {
const scroll = WAC.scroll();
if (!scroll) return;
if (state && state.atBottom) {
scroll.scrollTop = scroll.scrollHeight;
WAC.syncJumpToBottom();
return;
}
if (!state) {
WAC.syncJumpToBottom();
return;
}
scroll.scrollTop = Math.max(0, Number(state.top || 0));
WAC.syncJumpToBottom();
};
WAC.optimisticSubmits = Array.isArray(WAC.optimisticSubmits) ? WAC.optimisticSubmits : [];
WAC.serverInstanceId = WAC.serverInstanceId || '';
WAC.normalizeText = function (value) {
return String(value || '').replace(/\r\n?/g, '\n').replace(/\u00a0/g, ' ').trim();
};
WAC.splitRequestBlocks = function (value) {
const normalized = String(value || '').replace(/\r\n?/g, '\n').trim();
if (!normalized) return [];
const blocks = [];
let current = [];
for (const rawLine of normalized.split('\n')) {
if (!String(rawLine).trim()) {
if (current.length > 0) {
const block = current.join('\n').trim();
if (block) blocks.push(block);
current = [];
}
continue;
}
current.push(String(rawLine).replace(/\s+$/, ''));
}
if (current.length > 0) {
const block = current.join('\n').trim();
if (block) blocks.push(block);
}
return blocks;
};
WAC.gradioConfig = function () {
return window.gradio_config || window.__gradio_config__ || null;
};
WAC.componentNode = function (id) {
if (id === null || typeof id === 'undefined') return null;
return document.getElementById(`component-${id}`);
};
WAC.isVisibleNode = function (node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
WAC.dropdownChoiceTexts = function (component) {
const rawChoices = component && component.props ? component.props.choices : [];
if (!Array.isArray(rawChoices)) return [];
const texts = [];
for (const choice of rawChoices) {
if (Array.isArray(choice)) {
texts.push(String(choice[0] || '').toLowerCase());
texts.push(String(choice[1] || '').toLowerCase());
continue;
}
if (choice && typeof choice === 'object') {
texts.push(String(choice.label || choice.name || '').toLowerCase());
texts.push(String(choice.value || '').toLowerCase());
continue;
}
texts.push(String(choice || '').toLowerCase());
}
return texts;
};
WAC.findWanGpSettingsDropdown = function () {
const cfg = WAC.gradioConfig();
const components = cfg && Array.isArray(cfg.components) ? cfg.components : [];
let fallback = null;
for (const component of components) {
if (!component || String(component.type || '').toLowerCase() !== 'dropdown') continue;
const texts = WAC.dropdownChoiceTexts(component);
const hasSettings = texts.some((text) => text.includes('>settings'));
const hasProfiles = texts.some((text) => text.includes('>profiles'));
const hasLoraPresetHint = texts.some((text) => text.includes('lora preset'));
if (!hasSettings || (!hasProfiles && !hasLoraPresetHint)) continue;
const node = WAC.componentNode(component.id);
if (node && WAC.isVisibleNode(node)) return { component, node };
if (!fallback) fallback = { component, node };
}
return fallback;
};
WAC.getWanGpSettingsSelection = function () {
const located = WAC.findWanGpSettingsDropdown();
if (!located || !located.component) return { value: '', label: '' };
const component = located.component;
const node = located.node || WAC.componentNode(component.id);
const input = node ? node.querySelector('input[role="listbox"], input, textarea') : null;
const label = WAC.normalizeText(input ? (input.value || input.getAttribute('value') || '') : '');
const value = WAC.normalizeText(component && component.props ? component.props.value : '');
return { value, label };
};
WAC.buildOptimisticUserMessage = function (optimisticId, content) {
const contentHtml = WAC.escapeHtml(content).replace(/\n/g, '<br>');
const html = [
`<article class='wangp-assistant-chat__message wangp-assistant-chat__message--user' data-message-id='${optimisticId}'>`,
"<div class='wangp-assistant-chat__avatar'>You</div>",
"<div class='wangp-assistant-chat__message-card'>",
"<div class='wangp-assistant-chat__meta'><div class='wangp-assistant-chat__meta-left'></div>",
`<div class='wangp-assistant-chat__time'>${WAC.escapeHtml(WAC.timeLabel())}</div></div>`,
`<div class='wangp-assistant-chat__body'><p>${contentHtml}</p></div>`,
"</div></article>",
].join('');
return { id: optimisticId, role: 'user', html };
};
WAC.dropOptimisticSubmit = function (optimisticId) {
const targetId = String(optimisticId || '');
WAC.optimisticSubmits = (WAC.optimisticSubmits || []).filter((item) => String(item && item.id || '') !== targetId);
};
WAC.clearRequestInput = function (expectedText) {
const input = WAC.requestInput();
if (!input) return;
const current = WAC.normalizeText(input.value || '');
const expected = WAC.normalizeText(expectedText || '');
if (expected && current && current !== expected) return;
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
};
WAC.reconcileOptimisticSubmits = function () {
const optimistic = Array.isArray(WAC.optimisticSubmits) ? WAC.optimisticSubmits.slice() : [];
if (optimistic.length === 0) return;
const serverUserTexts = [];
for (const messageId of WAC.state.order) {
const message = WAC.state.messages[messageId];
if (!message || message.role !== 'user' || String(message.id || '').startsWith('optimistic_')) continue;
const node = WAC.createMessageNode(message);
serverUserTexts.push(WAC.messageBodyText(node));
}
let matchedPrefix = 0;
const maxMatch = Math.min(serverUserTexts.length, optimistic.length);
for (let count = maxMatch; count > 0; count -= 1) {
const serverSuffix = serverUserTexts.slice(serverUserTexts.length - count);
const optimisticPrefix = optimistic.slice(0, count).map((item) => WAC.normalizeText(item && item.text || ''));
if (serverSuffix.length === optimisticPrefix.length && serverSuffix.every((text, index) => text === optimisticPrefix[index])) {
matchedPrefix = count;
break;
}
const flattenedPrefix = [];
for (const item of optimistic.slice(0, count)) {
const content = WAC.normalizeText(item && item.text || '');
if (!content) continue;
const blocks = WAC.splitRequestBlocks(content);
flattenedPrefix.push(...(blocks.length > 1 ? blocks : [content]).map((block) => WAC.normalizeText(block)));
}
const splitServerSuffix = flattenedPrefix.length > count ? serverUserTexts.slice(serverUserTexts.length - flattenedPrefix.length) : [];
if (splitServerSuffix.length === flattenedPrefix.length && splitServerSuffix.every((text, index) => text === flattenedPrefix[index])) {
matchedPrefix = count;
break;
}
}
WAC.optimisticSubmits = optimistic.slice(matchedPrefix);
for (const item of WAC.optimisticSubmits) {
const optimisticId = String(item && item.id || '').trim();
const content = WAC.normalizeText(item && item.text || '');
if (!optimisticId || !content || WAC.state.messages[optimisticId]) continue;
WAC.state.order.push(optimisticId);
WAC.state.messages[optimisticId] = WAC.buildOptimisticUserMessage(optimisticId, content);
}
};
WAC.pushOptimisticUserMessage = function (text) {
const content = WAC.normalizeText(text);
if (!content) return;
const now = Date.now();
const lastOptimistic = (WAC.optimisticSubmits || [])[WAC.optimisticSubmits.length - 1] || { text: '', ts: 0 };
if (WAC.normalizeText(lastOptimistic.text || '') === content && (now - Number(lastOptimistic.ts || 0)) < 900) return;
const optimisticId = `optimistic_${now}`;
WAC.optimisticSubmits.push({ id: optimisticId, text: content, ts: now });
WAC.upsertMessage(WAC.buildOptimisticUserMessage(optimisticId, content));
};
WAC.host = function () {
return document.querySelector('#assistant_chat_html');
};
WAC.shell = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat');
};
WAC.scroll = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat__scroll');
};
WAC.transcript = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat__transcript');
};
WAC.empty = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat__empty');
};
WAC.statusNode = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat__status');
};
WAC.jumpBottomNode = function () {
return document.querySelector('#assistant_chat_html .wangp-assistant-chat__jump-bottom');
};
WAC.statsNode = function () {
return document.getElementById('assistant_chat_stats');
};
WAC.disclosureKey = function (node) {
if (!node || !node.getAttribute) return '';
const reasoningId = String(node.getAttribute('data-reasoning-id') || '').trim();
if (reasoningId) return `reasoning:${reasoningId}`;
const toolId = String(node.getAttribute('data-tool-id') || '').trim();
if (toolId) return `tool:${toolId}`;
return '';
};
WAC.captureDisclosureState = function (root) {
const scope = root || WAC.transcript();
if (!scope || !scope.querySelectorAll) return;
scope.querySelectorAll('.wangp-assistant-chat__disclosure').forEach((node) => {
const key = WAC.disclosureKey(node);
if (!key) return;
WAC.disclosureState[key] = !!node.open;
});
};
WAC.applyDisclosureState = function (root) {
const scope = root || WAC.transcript();
if (!scope || !scope.querySelectorAll) return;
scope.querySelectorAll('.wangp-assistant-chat__disclosure').forEach((node) => {
const key = WAC.disclosureKey(node);
if (!key || !(key in WAC.disclosureState)) return;
node.open = !!WAC.disclosureState[key];
});
};
WAC.handleDisclosureToggle = function (event) {
const node = event && event.target;
if (!node || !node.classList || !node.classList.contains('wangp-assistant-chat__disclosure')) return;
const key = WAC.disclosureKey(node);
if (!key) return;
WAC.disclosureState[key] = !!node.open;
};
WAC.toggleDisclosure = function (node) {
if (!node || !node.classList || !node.classList.contains('wangp-assistant-chat__disclosure')) return;
const scrollState = WAC.captureAutoscrollState();
node.open = !node.open;
const key = WAC.disclosureKey(node);
if (key) WAC.disclosureState[key] = !!node.open;
WAC.applyAutoscrollState(scrollState);
};
WAC.handleDisclosurePointerDown = function (event) {
const summary = event && event.target && event.target.closest ? event.target.closest('summary') : null;
if (!summary) return false;
const disclosureNode = summary.parentElement;
if (!disclosureNode || !disclosureNode.classList || !disclosureNode.classList.contains('wangp-assistant-chat__disclosure')) return false;
event.preventDefault();
event.stopPropagation();
WAC.toggleDisclosure(disclosureNode);
return true;
};
WAC.handleAttachmentPointerDown = function (event) {
const link = event && event.target && event.target.closest ? event.target.closest('a.wangp-assistant-chat__attachment') : null;
if (!link) return false;
const isPrimaryPointer = event.button === 0 || event.pointerType === 'touch' || event.pointerType === 'pen';
if (!isPrimaryPointer || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return false;
const href = String(link.href || '').trim();
if (!href) return false;
event.preventDefault();
event.stopPropagation();
const target = String(link.target || '_blank').trim() || '_blank';
if (target === '_blank') {
const opened = window.open(href, '_blank', 'noopener');
if (opened) opened.opener = null;
return true;
}
window.location.assign(href);
return true;
};
WAC.stopBridgeTargets = function () {
const wrapper = document.querySelector('#assistant_chat_stop_bridge');
if (!wrapper) return [];
const targets = [wrapper];
const button = wrapper.querySelector('button');
if (button) targets.unshift(button);
return targets.filter((target, index, items) => !!target && items.indexOf(target) === index);
};
WAC.queueBusyRequest = function (text) {
const input = document.querySelector('#assistant_chat_busy_queue_input textarea, #assistant_chat_busy_queue_input input');
const button = document.querySelector('#assistant_chat_busy_queue_button button, #assistant_chat_busy_queue_button');
if (!input || !button) return false;
input.value = String(text || '');
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof button.click === 'function') button.click();
return true;
};
WAC.isAssistantBusy = function () {
if (WAC.state && WAC.state.status && WAC.state.status.visible && WAC.state.status.text) return true;
const stopButton = document.querySelector('#assistant_chat_html .wangp-assistant-chat__status-stop');
return !!(stopButton && !stopButton.disabled);
};
WAC.eventSource = function () {
return document.querySelector('#assistant_chat_event textarea, #assistant_chat_event input');
};
WAC.consumePayload = function (payload) {
if (!payload) return [];
let envelope = payload;
if (typeof payload === 'string') {
try {
envelope = JSON.parse(payload);
} catch (_error) {
return [];
}
}
const payloadId = envelope && typeof envelope.event_id === 'string' ? envelope.event_id : '';
const payloadText = typeof payload === 'string' ? payload : JSON.stringify(envelope);
if ((payloadId && payloadId === WAC.lastPayloadId) || (!payloadId && payloadText === WAC.lastPayloadText)) return [];
WAC.lastPayloadId = payloadId;
WAC.lastPayloadText = payloadText;
if (Array.isArray(envelope.batch)) {
for (const item of envelope.batch) WAC.consumePayload(item);
WAC.lastPayloadId = payloadId;
WAC.lastPayloadText = payloadText;
return [];
}
const instanceId = envelope && typeof envelope.instance_id === 'string' ? envelope.instance_id : '';
if (instanceId) {
if (WAC.serverInstanceId && WAC.serverInstanceId !== instanceId) {
WAC.reset();
}
WAC.serverInstanceId = instanceId;
}
const event = envelope && envelope.event ? envelope.event : envelope;
if (!event || typeof event !== 'object') return [];
if (event.type === 'reset') {
WAC.reset();
return [];
}
if (event.type === 'upsert_message') {
WAC.upsertMessage(event.message || {});
return [];
}
if (event.type === 'remove_message') {
WAC.removeMessage(event.message_id);
return [];
}
if (event.type === 'status') {
WAC.setStatus(event.status || null);
if (Object.prototype.hasOwnProperty.call(event, 'stats')) WAC.setStats(event.stats || null);
return [];
}
if (event.type === 'stats') {
WAC.setStats(event.stats || null);
return [];
}
if (event.type === 'sync') {
WAC.sync(event.messages || [], event.status || null, Object.prototype.hasOwnProperty.call(event, 'stats') ? (event.stats || null) : WAC.state.stats);
return [];
}
return [];
};
WAC.readEventSource = function () {
const node = WAC.eventSource();
if (!node) return;
const value = typeof node.value === 'string' ? node.value.trim() : '';
if (!value) return;
WAC.consumePayload(value);
};
WAC.handleEventNodeMutation = function () {
const node = WAC.eventSource();
if (!node || node === WAC.eventNode) return;
WAC.eventNode = node;
const handler = function () { WAC.readEventSource(); };
node.addEventListener('input', handler, true);
node.addEventListener('change', handler, true);
setTimeout(handler, 0);
};
WAC.replaceState = function (messages, status, stats) {
const nextState = { order: [], messages: {}, status: status || null, stats: typeof stats === 'undefined' ? (WAC.state ? (WAC.state.stats || null) : null) : (stats || null) };
const items = Array.isArray(messages) ? messages : [];
for (const message of items) {
if (!message || !message.id) continue;
const key = String(message.id);
nextState.order.push(key);
nextState.messages[key] = message;
}
WAC.state = nextState;
};
WAC.syncDockVisibility = function () {
document.querySelectorAll('#assistant_chat_dock').forEach((dock) => {
const hasLauncher = !!dock.querySelector('#assistant_chat_toggle');
dock.style.display = hasLauncher ? 'flex' : 'none';
});
};
WAC.parseThemeColor = function (value) {
const match = String(value || '').trim().match(/^rgba?\(([^)]+)\)$/i);
if (!match) return null;
const parts = match[1].split(',').map((part) => parseFloat(part.trim()));
if (parts.length < 3 || parts.slice(0, 3).some((part) => !Number.isFinite(part))) return null;
const alpha = Number.isFinite(parts[3]) ? parts[3] : 1;
if (alpha <= 0.01) return null;
return { r: parts[0], g: parts[1], b: parts[2], a: alpha };
};
WAC.resolveThemeBackground = function (node) {
let current = node;
while (current) {
const resolved = WAC.parseThemeColor(window.getComputedStyle(current).backgroundColor);
if (resolved) return resolved;
current = current.parentElement;
}
return WAC.parseThemeColor(window.getComputedStyle(document.body).backgroundColor);
};
WAC.relativeLuminance = function (rgb) {
if (!rgb) return 1;
const normalize = function (value) {
const channel = Math.max(0, Math.min(255, Number(value || 0))) / 255;
return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
};
return 0.2126 * normalize(rgb.r) + 0.7152 * normalize(rgb.g) + 0.0722 * normalize(rgb.b);
};
WAC.isDarkTheme = function () {
const nodes = [
document.querySelector('.gradio-container'),
document.body,
document.documentElement,
document.querySelector('gradio-app'),
].filter(Boolean);
if (nodes.some((node) => node.classList && node.classList.contains('dark'))) return true;
if (nodes.some((node) => String(node.getAttribute('data-theme') || node.getAttribute('theme') || '').toLowerCase().includes('dark'))) return true;
const sample = document.querySelector('.gradio-container') || document.body;
const background = WAC.resolveThemeBackground(sample);
const foreground = WAC.parseThemeColor(window.getComputedStyle(sample).color) || WAC.parseThemeColor(window.getComputedStyle(document.body).color);
const backgroundLuminance = WAC.relativeLuminance(background);
const foregroundLuminance = WAC.relativeLuminance(foreground);
return backgroundLuminance < 0.18 || (foreground && backgroundLuminance < foregroundLuminance);
};
WAC.syncThemeState = function () {
const dock = WAC.dock();
if (!dock) return;
dock.classList.toggle('is-dark', !!WAC.isDarkTheme());
};
WAC.syncDockState = function () {
WAC.syncDockVisibility();
WAC.syncThemeState();
const dock = WAC.dock();
const launcher = WAC.launcher();
if (dock) dock.classList.toggle('is-open', !!WAC.dockOpen);
if (launcher) launcher.setAttribute('aria-expanded', WAC.dockOpen ? 'true' : 'false');
WAC.syncSettingsState();
};
WAC.syncSettingsState = function () {
const panel = WAC.panel();
const launcher = WAC.settingsLauncher();
const open = !!WAC.dockOpen && !!WAC.settingsOpen;
if (panel) panel.classList.toggle('is-settings-open', open);
if (launcher) launcher.setAttribute('aria-expanded', open ? 'true' : 'false');
};
WAC.syncDockLayout = function () {
const dock = WAC.dock();
if (!dock) return;
if (window.innerWidth <= 900) {
dock.style.removeProperty('--dock-panel-width');
dock.style.removeProperty('--dock-settings-panel-width');
return;
}
const candidates = [
dock.parentElement,
dock.parentElement ? dock.parentElement.closest('.column') : null,
dock.parentElement && dock.parentElement.parentElement ? dock.parentElement.parentElement.closest('.column') : null,
].filter((node) => node && node !== dock);
const flowColumn = candidates
.map((node) => ({ node, rect: node.getBoundingClientRect() }))
.filter((entry) => entry.rect.width > 180)
.sort((a, b) => a.rect.width - b.rect.width)[0];
const flowRect = flowColumn ? flowColumn.rect : null;
const dockStyle = window.getComputedStyle(dock);
const launcherWidth = parseFloat(dockStyle.getPropertyValue('--dock-launcher-width')) || 41;
const dockGap = parseFloat(dockStyle.getPropertyValue('--dock-gap')) || 14;
const panelLeft = launcherWidth + dockGap;
const measuredWidth = flowRect ? Math.round(flowRect.width) : 0;
const columnBoundWidth = flowRect ? Math.round(flowRect.right - panelLeft - 12) : 0;
const maxWidth = Math.max(320, window.innerWidth - panelLeft - 28);
const panelWidth = Math.max(Math.min(320, maxWidth), Math.min(measuredWidth || 548, columnBoundWidth || measuredWidth || 548, maxWidth));
const maxSettingsWidth = Math.max(panelWidth, window.innerWidth - panelLeft - 44);
const settingsWidth = Math.min(maxSettingsWidth, Math.max(panelWidth, Math.min(panelWidth + 112, 660)));
dock.style.setProperty('--dock-panel-width', `${panelWidth}px`);
dock.style.setProperty('--dock-settings-panel-width', `${settingsWidth}px`);
};
WAC.setDockOpen = function (open) {
WAC.dockOpen = !!open;
WAC.syncDockState();
WAC.syncDockLayout();
if (WAC.dockOpen) {
window.setTimeout(() => {
const input = WAC.requestInput();
if (input) input.focus();
}, 140);
}
};
WAC.toggleDock = function (forceOpen) {
const nextOpen = typeof forceOpen === 'boolean' ? forceOpen : !WAC.dockOpen;
WAC.setDockOpen(nextOpen);
};
WAC.setSettingsOpen = function (open) {
WAC.settingsOpen = !!open;
if (WAC.settingsOpen && !WAC.dockOpen) WAC.dockOpen = true;
WAC.syncDockState();
WAC.syncDockLayout();
};
WAC.toggleSettings = function (forceOpen) {
const nextOpen = typeof forceOpen === 'boolean' ? forceOpen : !WAC.settingsOpen;
WAC.setSettingsOpen(nextOpen);
};
WAC.ensureShell = function () {
const host = WAC.host();
if (!host) return false;
if (host.dataset.wangpAssistantChatMounted === 'true' && WAC.shell()) {
WAC.showEmptyIfNeeded();
WAC.syncDockState();
WAC.syncDockLayout();
return true;
}
host.innerHTML = `
<section class="wangp-assistant-chat">
<div class="wangp-assistant-chat__scroll">
<div class="wangp-assistant-chat__empty">
<div>
<strong>Dialogue With Deepy</strong>
Ask for an image or video idea, then inspect the assistant's reasoning and tool usage without losing the live transcript.
</div>
</div>
<div class="wangp-assistant-chat__transcript"></div>
</div>
<div class="wangp-assistant-chat__status" aria-live="polite">
<div class="wangp-assistant-chat__status-dots" aria-hidden="true"><span></span><span></span><span></span></div>
<div class="wangp-assistant-chat__status-text"></div>
<button class="wangp-assistant-chat__status-stop" type="button" aria-label="Stop Deepy" disabled>Stop</button>
</div>
<button class="wangp-assistant-chat__jump-bottom" type="button" aria-label="Jump to latest messages" aria-hidden="true" tabindex="-1">
<span aria-hidden="true"></span>
</button>
</section>
`;
host.dataset.wangpAssistantChatMounted = 'true';
WAC.hydrate();
WAC.syncDockVisibility();
WAC.syncDockState();
WAC.syncDockLayout();
WAC.syncDisclosureBridge();
WAC.syncScrollBridge();
return true;
};
WAC.isNearBottom = function () {
const scroll = WAC.scroll();
if (!scroll) return true;
return (scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight) <= WAC.bottomThreshold();
};
WAC.syncJumpToBottom = function () {
const node = WAC.jumpBottomNode();
if (!node) return;
const show = WAC.state.order.length > 0 && !WAC.isNearBottom();
node.classList.toggle('is-visible', show);
node.setAttribute('aria-hidden', show ? 'false' : 'true');
node.tabIndex = show ? 0 : -1;
};
WAC.scrollToBottom = function () {
const scroll = WAC.scroll();
if (!scroll) return;
scroll.scrollTop = scroll.scrollHeight;
WAC.syncJumpToBottom();
};
WAC.hideEmpty = function () {
const empty = WAC.empty();
if (empty) empty.style.display = 'none';
};
WAC.showEmptyIfNeeded = function () {
const empty = WAC.empty();
const transcript = WAC.transcript();
const isEmpty = WAC.state.order.length === 0;
if (empty) empty.style.display = isEmpty ? 'flex' : 'none';
if (transcript) transcript.style.display = isEmpty ? 'none' : 'flex';
WAC.syncJumpToBottom();
};
WAC.createMessageNode = function (message) {
const tpl = document.createElement('template');
tpl.innerHTML = (message && message.html) ? String(message.html).trim() : '';
return tpl.content.firstElementChild;
};
WAC.syncAttributes = function (target, source) {
if (!target || !source || !target.getAttributeNames || !source.getAttributeNames) return;
const sourceNames = new Set(source.getAttributeNames());
for (const name of target.getAttributeNames()) {
if (!sourceNames.has(name)) target.removeAttribute(name);
}
for (const name of sourceNames) {
const nextValue = source.getAttribute(name);
if (target.getAttribute(name) !== nextValue) target.setAttribute(name, nextValue);
}
};
WAC.patchDisclosureNode = function (current, next) {
if (!current || !next) return;
WAC.syncAttributes(current, next);
current.className = next.className;
const currentSummary = current.querySelector(':scope > summary');
const nextSummary = next.querySelector(':scope > summary');
if (currentSummary && nextSummary && currentSummary.innerHTML !== nextSummary.innerHTML) currentSummary.innerHTML = nextSummary.innerHTML;
const currentBody = current.querySelector(':scope > .wangp-assistant-chat__disclosure-body');
const nextBody = next.querySelector(':scope > .wangp-assistant-chat__disclosure-body');
if (currentBody && nextBody && currentBody.innerHTML !== nextBody.innerHTML) currentBody.innerHTML = nextBody.innerHTML;
};
WAC.reuseDisclosureNodes = function (currentBody, nextBody) {
if (!currentBody || !nextBody) return;
const existingByKey = new Map();
currentBody.querySelectorAll(':scope > .wangp-assistant-chat__disclosure').forEach((node) => {
const key = WAC.disclosureKey(node);
if (key) existingByKey.set(key, node);
});
nextBody.querySelectorAll(':scope > .wangp-assistant-chat__disclosure').forEach((node) => {
const key = WAC.disclosureKey(node);
if (!key) return;
const current = existingByKey.get(key);
if (!current) return;
WAC.patchDisclosureNode(current, node);
node.replaceWith(current);
});
};
WAC.patchMessageNode = function (current, next) {
if (!current || !next) return;
WAC.syncAttributes(current, next);
current.className = next.className;
const currentAvatar = current.querySelector(':scope > .wangp-assistant-chat__avatar');
const nextAvatar = next.querySelector(':scope > .wangp-assistant-chat__avatar');
if (currentAvatar && nextAvatar) {
WAC.syncAttributes(currentAvatar, nextAvatar);
if (currentAvatar.innerHTML !== nextAvatar.innerHTML) currentAvatar.innerHTML = nextAvatar.innerHTML;
}
const currentCard = current.querySelector(':scope > .wangp-assistant-chat__message-card');
const nextCard = next.querySelector(':scope > .wangp-assistant-chat__message-card');
if (!currentCard || !nextCard) {
current.replaceChildren(...Array.from(next.childNodes));
return;
}
WAC.syncAttributes(currentCard, nextCard);
currentCard.className = nextCard.className;
const currentMeta = currentCard.querySelector(':scope > .wangp-assistant-chat__meta');
const nextMeta = nextCard.querySelector(':scope > .wangp-assistant-chat__meta');
if (currentMeta && nextMeta) {
WAC.syncAttributes(currentMeta, nextMeta);
currentMeta.className = nextMeta.className;
if (currentMeta.innerHTML !== nextMeta.innerHTML) currentMeta.innerHTML = nextMeta.innerHTML;
}
const currentBody = currentCard.querySelector(':scope > .wangp-assistant-chat__body');
const nextBody = nextCard.querySelector(':scope > .wangp-assistant-chat__body');
if (currentBody && nextBody) {
WAC.syncAttributes(currentBody, nextBody);
currentBody.className = nextBody.className;
WAC.reuseDisclosureNodes(currentBody, nextBody);
currentBody.replaceChildren(...Array.from(nextBody.childNodes));
}
};
WAC.messageBodyText = function (node) {
const body = node && node.querySelector ? node.querySelector('.wangp-assistant-chat__body') : null;
return body ? WAC.normalizeText(body.innerText || body.textContent || '') : '';
};
WAC.upsertMessage = function (message) {
if (!message || !message.id) return;
WAC.ensureShell();
const transcript = WAC.transcript();
if (!transcript) return;
WAC.captureDisclosureState(transcript);
const scrollState = WAC.captureAutoscrollState();
const node = WAC.createMessageNode(message);
if (!node) return;
const existing = transcript.querySelector(`[data-message-id="${CSS.escape(String(message.id))}"]`);
const incomingId = String(message.id);
if (!existing && message.role === 'user' && !incomingId.startsWith('optimistic_') && Array.isArray(WAC.optimisticSubmits) && WAC.optimisticSubmits.length > 0) {
const incomingText = WAC.messageBodyText(node);
const optimistic = WAC.optimisticSubmits.find((item) => WAC.normalizeText(item && item.text || '') === incomingText);
const optimisticId = String(optimistic && optimistic.id || '');
const optimisticNode = optimisticId ? transcript.querySelector(`[data-message-id="${CSS.escape(optimisticId)}"]`) : null;
if (optimisticNode && optimisticId && incomingText) {
optimisticNode.replaceWith(node);
delete WAC.state.messages[optimisticId];
WAC.state.order = WAC.state.order.map((id) => id === optimisticId ? incomingId : id);
WAC.state.messages[incomingId] = message;
WAC.dropOptimisticSubmit(optimisticId);
WAC.hideEmpty();
WAC.applyDisclosureState(transcript);
WAC.applyAutoscrollState(scrollState);
return;
}
}
if (existing) {
WAC.patchMessageNode(existing, node);
} else {
WAC.state.order.push(incomingId);
transcript.appendChild(node);
}
WAC.state.messages[incomingId] = message;
WAC.hideEmpty();
WAC.applyDisclosureState(transcript);
WAC.applyAutoscrollState(scrollState);
};
WAC.removeMessage = function (messageId) {
const transcript = WAC.transcript();
if (!transcript) return;
const scrollState = WAC.captureAutoscrollState();
const existing = transcript.querySelector(`[data-message-id="${CSS.escape(String(messageId))}"]`);
if (existing) existing.remove();
delete WAC.state.messages[String(messageId)];
WAC.state.order = WAC.state.order.filter(id => id !== String(messageId));
WAC.showEmptyIfNeeded();
WAC.applyAutoscrollState(scrollState);
};
WAC.setStatus = function (status, restoreAnchor) {
WAC.ensureShell();
const scrollState = WAC.captureAutoscrollState();
WAC.state.status = status || null;
const node = WAC.statusNode();
if (!node) return;
const textNode = node.querySelector('.wangp-assistant-chat__status-text');
const stopNode = node.querySelector('.wangp-assistant-chat__status-stop');
if (!status || !status.visible || !status.text) {
node.classList.remove('is-visible');
node.removeAttribute('data-kind');
if (textNode) textNode.textContent = '';
if (stopNode) stopNode.disabled = true;
WAC.applyAutoscrollState(scrollState);
return;
}
if (textNode) textNode.textContent = String(status.text);
node.dataset.kind = String(status.kind || 'status');
if (stopNode) stopNode.disabled = false;
node.classList.add('is-visible');
WAC.applyAutoscrollState(scrollState);
};
WAC.setStats = function (stats) {
WAC.ensureShell();
WAC.state.stats = stats || null;
const node = WAC.statsNode();
if (!node) return;
if (!stats || stats.visible === false || !stats.text) {
node.classList.remove('is-visible');
node.textContent = '';
return;
}
node.textContent = String(stats.text);
node.classList.add('is-visible');
};
WAC.sync = function (messages, status, stats) {
WAC.ensureShell();
WAC.captureDisclosureState(WAC.transcript());
const scrollState = WAC.captureAutoscrollState();
WAC.replaceState(messages, status, stats);
WAC.reconcileOptimisticSubmits();
WAC.hydrate(scrollState);
};
WAC.reset = function () {
WAC.state = { order: [], messages: {}, status: null, stats: null };
WAC.optimisticSubmits = [];
WAC.disclosureState = {};
WAC.ensureShell();
const transcript = WAC.transcript();
if (transcript) transcript.innerHTML = '';
WAC.showEmptyIfNeeded();
WAC.setStatus(null);
WAC.setStats(null);
};
WAC.hydrate = function (scrollState) {
const transcript = WAC.transcript();
if (!transcript) return;
const existingById = new Map();
transcript.querySelectorAll(':scope > [data-message-id]').forEach((node) => {
const messageId = String(node.getAttribute('data-message-id') || '');
if (messageId) existingById.set(messageId, node);
});
const fragment = document.createDocumentFragment();
for (const messageId of WAC.state.order) {
const message = WAC.state.messages[messageId];
if (!message) continue;
const node = WAC.createMessageNode(message);
if (!node) continue;
const existing = existingById.get(String(messageId));
if (existing) {
WAC.patchMessageNode(existing, node);
fragment.appendChild(existing);
existingById.delete(String(messageId));
continue;
}
fragment.appendChild(node);
}
transcript.replaceChildren(fragment);
WAC.applyDisclosureState(transcript);
WAC.showEmptyIfNeeded();
WAC.setStatus(WAC.state.status, null);
WAC.setStats(WAC.state.stats);
WAC.applyAutoscrollState(scrollState);
};
WAC.applyEvent = function (payload) {
return WAC.consumePayload(payload);
};
WAC.syncDisclosureBridge = function () {
const transcript = WAC.transcript();
if (!transcript || transcript === WAC.disclosureNode) return;
if (WAC.disclosureNode) WAC.disclosureNode.removeEventListener('toggle', WAC.handleDisclosureToggle, true);
WAC.disclosureNode = transcript;
WAC.disclosureNode.addEventListener('toggle', WAC.handleDisclosureToggle, true);
};
WAC.handleScroll = function () {
WAC.syncJumpToBottom();
};
WAC.syncScrollBridge = function () {
const scroll = WAC.scroll();
if (!scroll || scroll === WAC.scrollNode) {
WAC.syncJumpToBottom();
return;
}
if (WAC.scrollNode) WAC.scrollNode.removeEventListener('scroll', WAC.handleScroll, { passive: true });
WAC.scrollNode = scroll;
WAC.scrollNode.addEventListener('scroll', WAC.handleScroll, { passive: true });
WAC.syncJumpToBottom();
};
WAC.installObserver = function () {
if (WAC.observer) return;
const target = document.querySelector('gradio-app') || document.body;
if (!target) return;
WAC.observer = new MutationObserver(() => {
if (WAC.observerScheduled) return;
WAC.observerScheduled = true;
window.requestAnimationFrame(() => {
WAC.observerScheduled = false;
if (WAC.host()) WAC.ensureShell();
WAC.syncScrollBridge();
WAC.syncThemeState();
WAC.syncDockLayout();
WAC.handleEventNodeMutation();
WAC.readEventSource();
});
});
WAC.observer.observe(target, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-theme', 'theme', 'style'] });
};
WAC.installEventBridge = function () {
WAC.handleEventNodeMutation();
WAC.syncDisclosureBridge();
WAC.syncScrollBridge();
if (!WAC.pollTimer) WAC.pollTimer = window.setInterval(() => { WAC.readEventSource(); }, 250);
window.addEventListener('focus', () => { WAC.readEventSource(); }, { passive: true });
document.addEventListener('visibilitychange', () => {
if (!document.hidden) WAC.readEventSource();
});
window.addEventListener('resize', () => { WAC.syncDockLayout(); }, { passive: true });
};
WAC.installDockBridge = function () {
if (WAC.dockBridgeInstalled) return;
WAC.dockBridgeInstalled = true;
WAC.dockOpen = false;
try { window.localStorage.removeItem('wangp-assistant-chat-open'); } catch (_error) {}
document.addEventListener('pointerdown', (event) => {
if (WAC.handleDisclosurePointerDown(event)) return;
if (WAC.handleAttachmentPointerDown(event)) return;
}, true);
document.addEventListener('click', (event) => {
const attachmentLink = event.target && event.target.closest ? event.target.closest('.wangp-assistant-chat__attachment, .wangp-assistant-chat__body a') : null;
if (attachmentLink) return;
const disclosureSummary = event.target && event.target.closest ? event.target.closest('summary') : null;
if (disclosureSummary) {
const disclosureNode = disclosureSummary.parentElement;
if (disclosureNode && disclosureNode.classList && disclosureNode.classList.contains('wangp-assistant-chat__disclosure')) {
event.preventDefault();
event.stopPropagation();
return;
}
}
const toggle = event.target && event.target.closest ? event.target.closest('#assistant_chat_toggle') : null;
if (toggle) {
event.preventDefault();
WAC.toggleDock();
return;
}
const settingsToggle = event.target && event.target.closest ? event.target.closest('#assistant_chat_settings_toggle') : null;
if (settingsToggle) {
event.preventDefault();
WAC.toggleSettings();
return;
}
const stopButton = event.target && event.target.closest ? event.target.closest('.wangp-assistant-chat__status-stop') : null;
if (stopButton) {
event.preventDefault();
for (const target of WAC.stopBridgeTargets()) {
target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
if (typeof target.click === 'function') target.click();
}
return;
}
const jumpBottomButton = event.target && event.target.closest ? event.target.closest('.wangp-assistant-chat__jump-bottom') : null;
if (jumpBottomButton) {
event.preventDefault();
WAC.scrollToBottom();
return;
}
const askButton = event.target && event.target.closest ? event.target.closest('#assistant_chat_ask_button') : null;
if (!askButton) return;
const input = WAC.requestInput();
const text = input ? String(input.value || '').trim() : '';
if (!text) return;
WAC.setDockOpen(true);
WAC.pushOptimisticUserMessage(text);
window.setTimeout(() => { WAC.clearRequestInput(text); }, 0);
if (WAC.isAssistantBusy()) {
event.preventDefault();
event.stopPropagation();
WAC.queueBusyRequest(text);
return;
}
}, true);
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (WAC.settingsOpen) {
WAC.setSettingsOpen(false);
return;
}
if (!WAC.dockOpen) return;
WAC.setDockOpen(false);
}, true);
document.addEventListener('keydown', (event) => {
const input = WAC.requestInput();
if (!input || event.target !== input || event.key !== 'Enter' || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return;
const text = String(input.value || '').trim();
if (!text) return;
event.preventDefault();
event.stopPropagation();
WAC.setDockOpen(true);
WAC.pushOptimisticUserMessage(text);
window.setTimeout(() => { WAC.clearRequestInput(text); }, 0);
if (WAC.isAssistantBusy()) {
WAC.queueBusyRequest(text);
return;
}
const askButton = document.querySelector('#assistant_chat_ask_button button, #assistant_chat_ask_button');
if (askButton && typeof askButton.click === 'function') askButton.click();
}, true);
WAC.syncDockState();
WAC.syncDockLayout();
};
if (!WAC.init) {
WAC.installObserver();
WAC.installEventBridge();
WAC.installDockBridge();
WAC.init = true;
}
setTimeout(() => { WAC.ensureShell(); WAC.handleEventNodeMutation(); WAC.readEventSource(); WAC.syncDockState(); WAC.syncDockLayout(); }, 50);
if (window.__wangpAssistantChatPending.length > 0) {
const pending = window.__wangpAssistantChatPending.slice();
window.__wangpAssistantChatPending.length = 0;
for (const payload of pending) WAC.consumePayload(payload);
}
window.applyAssistantChatEvent = function (payload) {
return WAC.consumePayload(payload);
};
"""
def reset_session_chat(session) -> None:
session.chat_transcript.clear()
session.chat_transcript_counter = 0
def build_reset_event() -> str:
return _event_payload({"type": "reset"})
def build_status_event(text: str | None, kind: str = "status", visible: bool = True, stats: dict[str, Any] | None = None) -> str:
status = None if not visible or not text else {"visible": True, "kind": str(kind or "status"), "text": str(text or "").strip()}
event = {"type": "status", "status": status}
if stats is not None:
event["stats"] = stats
return _event_payload(event)
def build_stats_event(stats: dict[str, Any] | None = None) -> str:
return _event_payload({"type": "stats", "stats": stats})
def build_event_batch(payloads: list[str]) -> str:
envelopes = []
for payload in payloads or []:
payload_text = str(payload or "").strip()
if len(payload_text) == 0:
continue
try:
envelope = json.loads(payload_text)
except Exception:
continue
if isinstance(envelope, dict):
envelopes.append(envelope)
if len(envelopes) == 0:
return ""
if len(envelopes) == 1:
return json.dumps(envelopes[0], ensure_ascii=False)
return json.dumps({"event_id": uuid.uuid4().hex, "instance_id": SERVER_INSTANCE_ID, "batch": envelopes}, ensure_ascii=False)
def build_sync_event(session, status: dict[str, Any] | None = None, stats: dict[str, Any] | None = None) -> str:
messages = [_render_message_payload(record) for record in session.chat_transcript]
event = {"type": "sync", "messages": messages, "status": status}
if stats is not None:
event["stats"] = stats
return _event_payload(event)
def _queued_tail_insert_index(session) -> int:
records = list(session.chat_transcript or [])
insert_index = len(records)
while insert_index > 0:
record = records[insert_index - 1]
if not isinstance(record, dict):
break
if str(record.get("role", "")).strip() != "user":
break
if str(record.get("badge", "")).strip() != "Queued":
break
insert_index -= 1
return insert_index
def add_user_message(session, text: str, queued: bool = False) -> tuple[str, str]:
record = {
"id": _next_message_id(session, "user"),
"role": "user",
"author": "You",
"created_at": _time_label(),
"blocks": [],
"attachments": [],
"badge": "Queued" if queued else "",
}
content = str(text or "").strip()
if len(content) > 0:
record["blocks"].append({"id": _next_block_id("content"), "type": "markdown", "text": content})
session.chat_transcript.append(record)
return record["id"], _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def create_assistant_turn(session) -> str:
record = {
"id": _next_message_id(session, "assistant"),
"role": "assistant",
"author": "Deepy",
"created_at": _time_label(),
"blocks": [],
"attachments": [],
"badge": "",
}
session.chat_transcript.insert(_queued_tail_insert_index(session), record)
return record["id"]
def add_assistant_note(session, text: str, badge: str | None = None, author: str = "System") -> tuple[str, str | None]:
content = str(text or "").strip()
if len(content) == 0:
return "", None
record = {
"id": _next_message_id(session, "assistant"),
"role": "assistant",
"author": str(author or "").strip() or "System",
"created_at": _time_label(),
"blocks": [{"id": _next_block_id("content"), "type": "markdown", "text": content}],
"attachments": [],
"badge": str(badge or "").strip(),
}
session.chat_transcript.insert(_queued_tail_insert_index(session), record)
return record["id"], _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def get_message_content(session, message_id: str) -> str:
record = _find_message(session, message_id)
if record is None:
return ""
parts = [str(block.get("text", "")).strip() for block in _ensure_message_blocks(record) if isinstance(block, dict) and block.get("type") == "markdown" and len(str(block.get("text", "")).strip()) > 0]
return "\n\n".join(parts)
def get_message_reasoning_content(session, message_id: str) -> str:
record = _find_message(session, message_id)
if record is None:
return ""
parts = [str(block.get("text", "")).strip() for block in _ensure_message_blocks(record) if isinstance(block, dict) and block.get("type") == "reasoning" and len(str(block.get("text", "")).strip()) > 0]
return "\n\n".join(parts)
def set_message_badge(session, message_id: str, badge: str | None) -> str | None:
record = _find_message(session, message_id)
if record is None:
return None
record["badge"] = str(badge or "").strip()
return _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def clear_message_blocks(session, message_id: str) -> str | None:
record = _find_message(session, message_id)
if record is None:
return None
record["blocks"] = []
record["attachments"] = []
return _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def clear_assistant_content(session, message_id: str) -> str | None:
record = _find_message(session, message_id)
if record is None:
return None
blocks = _ensure_message_blocks(record)
kept_blocks = [block for block in blocks if not (isinstance(block, dict) and block.get("type") == "markdown")]
if len(kept_blocks) == len(blocks):
return None
record["blocks"] = kept_blocks
record["content"] = ""
return _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def remove_message(session, message_id: str) -> str | None:
target_id = str(message_id or "").strip()
if len(target_id) == 0:
return None
original_len = len(session.chat_transcript)
session.chat_transcript[:] = [record for record in session.chat_transcript if str(record.get("id", "")) != target_id]
if len(session.chat_transcript) == original_len:
return None
return _event_payload({"type": "remove_message", "message_id": target_id})
def append_reasoning(session, message_id: str, text: str) -> str | None:
_reasoning_id, payload = upsert_reasoning_block(session, message_id, None, text)
return payload
def upsert_reasoning_block(session, message_id: str, reasoning_id: str | None, text: str) -> tuple[str, str | None]:
reasoning_text = str(text or "").strip()
if len(reasoning_text) == 0:
return "", None
record = _find_message(session, message_id)
if record is None:
return "", None
blocks = _ensure_message_blocks(record)
target_id = str(reasoning_id or "").strip()
for block in blocks:
if not isinstance(block, dict) or block.get("type") != "reasoning" or block.get("id", "") != target_id:
continue
if str(block.get("text", "")).strip() == reasoning_text:
return target_id, None
block["text"] = reasoning_text
return target_id, _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
target_id = target_id or f"reasoning_{uuid.uuid4().hex[:10]}"
blocks.append({"id": target_id, "type": "reasoning", "text": reasoning_text})
return target_id, _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def add_tool_call(session, message_id: str, tool_name: str, arguments: dict[str, Any], tool_label: str | None = None) -> tuple[str, str | None]:
record = _find_message(session, message_id)
if record is None:
return "", None
tool_record = {
"id": _next_tool_id(),
"type": "tool",
"name": str(tool_name or "").strip(),
"label": str(tool_label or "").strip() or _friendly_tool_label(tool_name),
"arguments": dict(arguments or {}),
"result": None,
"status": "running",
"status_text": "Running",
"attachment": None,
}
_ensure_message_blocks(record).append(tool_record)
return tool_record["id"], _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def update_tool_call(session, message_id: str, tool_id: str, status: str | None = None, result: dict[str, Any] | object = _UNSET, status_text: str | None = None) -> str | None:
record = _find_message(session, message_id)
if record is None:
return None
for tool_record in _ensure_message_blocks(record):
if not isinstance(tool_record, dict) or tool_record.get("type") != "tool" or tool_record.get("id") != tool_id:
continue
if status is not None:
tool_record["status"] = str(status or "").strip().lower() or "running"
if status_text is not None:
tool_record["status_text"] = str(status_text or "").strip()
if result is not _UNSET:
tool_record["result"] = None if result is None else dict(result or {})
tool_record["attachment"] = _attachment_from_tool_result(tool_record.get("result"))
return _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
return None
def complete_tool_call(session, message_id: str, tool_id: str, result: dict[str, Any]) -> str | None:
status = str((result or {}).get("status", "")).strip().lower()
failed = status in {"error", "failed", "interrupted"}
return update_tool_call(session, message_id, tool_id, status="error" if failed else "done", result=result, status_text="Interrupted" if status == "interrupted" else ("Error" if failed else "Done"))
def set_assistant_content(session, message_id: str, text: str) -> str | None:
record = _find_message(session, message_id)
if record is None:
return None
content_text = str(text or "").strip()
if len(content_text) == 0:
return None
blocks = _ensure_message_blocks(record)
if len(blocks) > 0 and isinstance(blocks[-1], dict) and blocks[-1].get("type") == "markdown":
if str(blocks[-1].get("text", "")).strip() == content_text:
return None
blocks[-1]["text"] = content_text
else:
blocks.append({"id": _next_block_id("content"), "type": "markdown", "text": content_text})
return _event_payload({"type": "upsert_message", "message": _render_message_payload(record)})
def _next_message_id(session, prefix: str) -> str:
session.chat_transcript_counter += 1
return f"{prefix}_{session.chat_transcript_counter}"
def _next_tool_id() -> str:
return f"tool_{uuid.uuid4().hex[:10]}"
def _next_block_id(prefix: str) -> str:
return f"{prefix}_{uuid.uuid4().hex[:10]}"
def _friendly_tool_label(tool_name: str | None) -> str:
name = str(tool_name or "").strip()
if len(name) == 0:
return "Tool"
return name.replace("_", " ").replace("-", " ").strip().title()
def _find_message(session, message_id: str) -> dict[str, Any] | None:
target_id = str(message_id or "")
for record in session.chat_transcript:
if record.get("id") == target_id:
return record
return None
def _ensure_message_blocks(record: dict[str, Any]) -> list[dict[str, Any]]:
blocks = record.get("blocks", None)
if isinstance(blocks, list):
return blocks
blocks = []
content = str(record.get("content", "") or "").strip()
if len(content) > 0:
blocks.append({"id": _next_block_id("content"), "type": "markdown", "text": content})
for reasoning_block in record.get("reasoning", []) or []:
if isinstance(reasoning_block, dict):
reasoning_id = str(reasoning_block.get("id", "")).strip() or _next_block_id("reasoning")
reasoning_text = str(reasoning_block.get("text", "")).strip()
else:
reasoning_id = _next_block_id("reasoning")
reasoning_text = str(reasoning_block or "").strip()
if len(reasoning_text) > 0:
blocks.append({"id": reasoning_id, "type": "reasoning", "text": reasoning_text})
for tool_block in record.get("tools", []) or []:
if not isinstance(tool_block, dict):
continue
migrated_block = dict(tool_block)
migrated_block["type"] = "tool"
migrated_block["id"] = str(migrated_block.get("id", "")).strip() or _next_tool_id()
blocks.append(migrated_block)
record["blocks"] = blocks
return blocks
def _time_label() -> str:
return time.strftime("%H:%M")
def _event_payload(event: dict[str, Any]) -> str:
return json.dumps({"event_id": uuid.uuid4().hex, "instance_id": SERVER_INSTANCE_ID, "event": event}, ensure_ascii=False)
def _markdown_to_html(text: str) -> str:
text = str(text or "").strip()
if len(text) == 0:
return ""
text = html.escape(text, quote=False)
return markdown.markdown(text, extensions=_MARKDOWN_EXTENSIONS, output_format="html5")
def _extract_attachments_from_markdown(text: str) -> tuple[str, list[dict[str, Any]]]:
attachments = []
def replace_match(match: re.Match[str]) -> str:
attachment = _attachment_from_path(match.group("path"), match.group("alt"))
if attachment is not None:
attachments.append(attachment)
return ""
stripped = _MARKDOWN_IMAGE_RE.sub(replace_match, str(text or ""))
stripped = re.sub(r"\n{3,}", "\n\n", stripped).strip()
return stripped, attachments
def _attachment_from_tool_result(result: dict[str, Any] | None) -> dict[str, Any] | None:
if not isinstance(result, dict):
return None
output_file = str(result.get("output_file", "")).strip()
if len(output_file) == 0:
return None
ext = os.path.splitext(output_file)[1].lower()
label = "Generated image" if ext in _IMAGE_EXTENSIONS else ("Generated video" if ext in _VIDEO_EXTENSIONS else ("Generated audio" if ext in _AUDIO_EXTENSIONS else "Generated file"))
return _attachment_from_path(output_file, label)
def _attachment_from_path(path: str, label: str | None = None) -> dict[str, Any] | None:
clean_path = str(path or "").strip()
if len(clean_path) == 0:
return None
normalized_path = clean_path
if normalized_path.startswith("/gradio_api/file="):
normalized_path = normalized_path.split("=", 1)[1]
normalized_path = urllib.parse.unquote(normalized_path).replace("\\", "/")
normalized_path = os.path.normpath(normalized_path).replace("\\", "/")
path_key = normalized_path.lower()
href = f"/gradio_api/file={urllib.parse.quote(normalized_path, safe='/')}"
ext = os.path.splitext(normalized_path)[1].lower()
resolved_label = str(label or os.path.basename(normalized_path) or "Open file").strip()
subtitle = os.path.basename(normalized_path)
if resolved_label == subtitle:
subtitle = ""
thumb_url = ""
kind = "file"
if ext in _IMAGE_EXTENSIONS:
kind = "image"
thumb_url = href
elif ext in _VIDEO_EXTENSIONS:
kind = "video"
try:
thumb_url = deepy_video_tools.get_video_thumbnail_data_url(normalized_path)
except Exception:
thumb_url = ""
elif ext in _AUDIO_EXTENSIONS:
kind = "audio"
if os.path.isfile(_AUDIO_THUMBNAIL_PATH):
audio_thumb_path = os.path.normpath(_AUDIO_THUMBNAIL_PATH).replace("\\", "/")
thumb_url = f"/gradio_api/file={urllib.parse.quote(audio_thumb_path, safe='/')}"
return {
"path_key": path_key,
"href": href,
"label": resolved_label,
"subtitle": subtitle,
"kind": kind,
"thumb_url": thumb_url,
}
def _render_message_payload(record: dict[str, Any]) -> dict[str, Any]:
role = str(record.get("role", "assistant"))
badge_text = str(record.get("badge", "")).strip()
blocks_html, rendered_attachment_keys = _render_message_blocks(record)
attachments_html = _render_attachments(
[
attachment
for attachment in list(record.get("attachments", []))
if isinstance(attachment, dict) and (attachment.get("path_key", "") or attachment.get("href", "")) not in rendered_attachment_keys
]
)
badge_html = "" if len(badge_text) == 0 else f"<span class='wangp-assistant-chat__badge'>{html.escape(badge_text)}</span>"
body_html = f"{blocks_html}{attachments_html}"
if len(body_html) == 0 and role == "assistant":
body_html = "<p>Working through the request.</p>"
card_html = (
f"<article class='wangp-assistant-chat__message wangp-assistant-chat__message--{html.escape(role)}' data-message-id='{html.escape(str(record.get('id', '')))}'>"
f"<div class='wangp-assistant-chat__avatar'>{html.escape('You' if role == 'user' else 'Deepy')}</div>"
f"<div class='wangp-assistant-chat__message-card'>"
f"<div class='wangp-assistant-chat__meta'>"
f"<div class='wangp-assistant-chat__meta-left'>{badge_html}</div>"
f"<div class='wangp-assistant-chat__time'>{html.escape(str(record.get('created_at', '')))}</div>"
f"</div>"
f"<div class='wangp-assistant-chat__body'>{body_html}</div>"
f"</div>"
f"</article>"
)
return {"id": record.get("id", ""), "role": role, "html": card_html}
def _render_message_blocks(record: dict[str, Any]) -> tuple[str, set[str]]:
blocks = _ensure_message_blocks(record)
if len(blocks) == 0:
return "", set()
rendered = []
rendered_attachment_keys = set()
reasoning_total = sum(1 for block in blocks if isinstance(block, dict) and block.get("type") == "reasoning" and len(str(block.get("text", "")).strip()) > 0)
reasoning_no = 0
for block in blocks:
if not isinstance(block, dict):
continue
block_type = str(block.get("type", "markdown")).strip().lower() or "markdown"
if block_type == "markdown":
content_source, attachments = _extract_attachments_from_markdown(block.get("text", ""))
content_html = _markdown_to_html(content_source)
if len(content_html) > 0:
rendered.append(content_html)
attachment_html = _render_attachments(_dedupe_attachments(attachments, rendered_attachment_keys))
if len(attachment_html) > 0:
rendered.append(attachment_html)
continue
if block_type == "reasoning":
reasoning_text = str(block.get("text", "")).strip()
if len(reasoning_text) == 0:
continue
reasoning_no += 1
rendered.append(_render_reasoning_block(block, reasoning_no, reasoning_total))
continue
if block_type == "tool":
rendered.append(_render_tool_block(block))
attachment_html = _render_attachments(_dedupe_attachments([block.get("attachment")] if isinstance(block.get("attachment"), dict) else [], rendered_attachment_keys))
if len(attachment_html) > 0:
rendered.append(attachment_html)
return "".join(rendered), rendered_attachment_keys
def _dedupe_attachments(attachments: list[dict[str, Any]], rendered_attachment_keys: set[str]) -> list[dict[str, Any]]:
unique = []
for attachment in attachments:
if not isinstance(attachment, dict):
continue
dedupe_key = attachment.get("path_key", "") or attachment.get("href", "")
if len(dedupe_key) == 0 or dedupe_key in rendered_attachment_keys:
continue
rendered_attachment_keys.add(dedupe_key)
unique.append(attachment)
return unique
def _render_reasoning_block(block: dict[str, Any], block_no: int, total_blocks: int) -> str:
label = "Thought process"
return (
f"<details class='wangp-assistant-chat__disclosure wangp-assistant-chat__disclosure--reasoning' data-reasoning-id='{html.escape(str(block.get('id', '')))}'>"
f"<summary><span class='wangp-assistant-chat__tool-title'><span class='wangp-assistant-chat__tool-chip'>Thought</span>{html.escape(label)}</span></summary>"
f"<div class='wangp-assistant-chat__disclosure-body'><div class='wangp-assistant-chat__reasoning-block'>{_markdown_to_html(block.get('text', ''))}</div></div>"
"</details>"
)
def _render_tool_block(tool_record: dict[str, Any]) -> str:
name = str(tool_record.get("name", "tool")).strip() or "tool"
label = str(tool_record.get("label", "")).strip() or _friendly_tool_label(name)
status = str(tool_record.get("status", "running")).strip().lower()
status_label = str(tool_record.get("status_text", "")).strip() or {"running": "Running", "done": "Done", "error": "Error"}.get(status, status.title() or "Running")
status_class = {"running": "running", "done": "done", "error": "error"}.get(status, "running")
arguments_text = html.escape(json.dumps(tool_record.get("arguments", {}), ensure_ascii=False, indent=2, sort_keys=True))
result_payload = tool_record.get("result", {})
result_text = html.escape(json.dumps(result_payload, ensure_ascii=False, indent=2, sort_keys=True)) if result_payload is not None else ""
return (
f"<details class='wangp-assistant-chat__disclosure wangp-assistant-chat__disclosure--tool' data-tool-id='{html.escape(str(tool_record.get('id', '')))}'>"
f"<summary><span class='wangp-assistant-chat__tool-title'><span class='wangp-assistant-chat__tool-chip'>Tool</span>{html.escape(label)}</span><span class='wangp-assistant-chat__tool-status wangp-assistant-chat__tool-status--{status_class}'>{html.escape(status_label)}</span></summary>"
"<div class='wangp-assistant-chat__disclosure-body'>"
"<div class='wangp-assistant-chat__tool-grid'>"
f"<div><div class='wangp-assistant-chat__tool-section-title'>{html.escape(label)} Arguments</div><pre class='wangp-assistant-chat__pre'>{arguments_text}</pre></div>"
f"<div><div class='wangp-assistant-chat__tool-section-title'>Result</div><pre class='wangp-assistant-chat__pre'>{result_text or html.escape('Pending...')}</pre></div>"
"</div>"
"</div>"
"</details>"
)
def _render_attachments(attachments: list[dict[str, Any]]) -> str:
if len(attachments) == 0:
return ""
cards = []
for attachment in attachments:
href = str(attachment.get("href", "")).strip()
if len(href) == 0:
continue
label = html.escape(str(attachment.get("label", "Open file")))
subtitle = html.escape(str(attachment.get("subtitle", "")))
thumb_url = str(attachment.get("thumb_url", "")).strip()
subtitle_html = f"<span class='wangp-assistant-chat__attachment-subtitle'>{subtitle}</span>" if len(subtitle) > 0 else ""
thumb_html = (
f"<img class='wangp-assistant-chat__attachment-thumb' loading='lazy' src='{html.escape(thumb_url)}' alt='{label}'>"
if len(thumb_url) > 0
else "<div class='wangp-assistant-chat__attachment-thumb'></div>"
)
cards.append(
f"<a class='wangp-assistant-chat__attachment' href='{html.escape(href)}' target='_blank' rel='noopener'>"
f"{thumb_html}"
"<span class='wangp-assistant-chat__attachment-meta'>"
f"<span class='wangp-assistant-chat__attachment-title'>{label}</span>"
f"{subtitle_html}"
"</span>"
"</a>"
)
if len(cards) == 0:
return ""
return f"<div class='wangp-assistant-chat__attachments'>{''.join(cards)}</div>"