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[^\]]*)\]\((?P[^)]+)\)") _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 """
Dialogue With Deepy Ask for an image or video idea, then inspect the assistant's reasoning and tool usage without losing the live transcript.
""".strip() def render_shell_html() -> str: return f"
{_shell_markup()}
" def render_stats_html() -> str: return f"" def render_launcher_html() -> str: return ( f"" ) def render_settings_launcher_html() -> str: return ( f"" ) 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) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[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, '
'); const html = [ `
`, "
You
", "
", "
", `
${WAC.escapeHtml(WAC.timeLabel())}
`, `

${contentHtml}

`, "
", ].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 = `
Dialogue With Deepy Ask for an image or video idea, then inspect the assistant's reasoning and tool usage without losing the live transcript.
`; 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"{html.escape(badge_text)}" body_html = f"{blocks_html}{attachments_html}" if len(body_html) == 0 and role == "assistant": body_html = "

Working through the request.

" card_html = ( f"
" f"
{html.escape('You' if role == 'user' else 'Deepy')}
" f"
" f"
" f"
{badge_html}
" f"
{html.escape(str(record.get('created_at', '')))}
" f"
" f"
{body_html}
" f"
" f"
" ) 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"
" f"Thought{html.escape(label)}" f"
{_markdown_to_html(block.get('text', ''))}
" "
" ) 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"
" f"Tool{html.escape(label)}{html.escape(status_label)}" "
" "
" f"
{html.escape(label)} Arguments
{arguments_text}
" f"
Result
{result_text or html.escape('Pending...')}
" "
" "
" "
" ) 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"{subtitle}" if len(subtitle) > 0 else "" thumb_html = ( f"{label}" if len(thumb_url) > 0 else "
" ) cards.append( f"" f"{thumb_html}" "" f"{label}" f"{subtitle_html}" "" "" ) if len(cards) == 0: return "" return f"
{''.join(cards)}
"