<script setup>
import { useNamespace, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { computed, ref, onBeforeUnmount, shallowRef, watch, onMounted, nextTick } from "vue";
import {
  aiBubbleProps
} from "./ai-bubble";
import { YhAvatar } from "../../avatar";
import { YhButton } from "../../button";
import { YhIcon } from "../../icon";
import { YhAiThoughtChain } from "../../ai-thought-chain";
import { loadMarkdown } from "../../markdown-it";
import hljs, { loadHighlightStyle } from "../../highlight";
import { sanitizeHighlightedHtml, sanitizeMarkup, sanitizeSvgMarkup } from "../../sanitize";
defineOptions({
  name: "YhAiBubble"
});
const props = defineProps(aiBubbleProps);
const emit = defineEmits(["copy-fail"]);
const ns = useNamespace("ai-bubble");
const { t } = useLocale();
const hasMounted = ref(false);
const { themeStyle } = useComponentTheme(
  "ai-bubble",
  computed(() => props.themeOverrides)
);
const playingAsset = ref(null);
const audioInstance = ref(null);
const handleAudioToggle = (url) => {
  if (playingAsset.value === url) {
    audioInstance.value?.pause();
    playingAsset.value = null;
  } else {
    if (audioInstance.value) {
      audioInstance.value.pause();
      audioInstance.value.src = "";
    }
    playingAsset.value = url;
    const audio = new Audio(url);
    audioInstance.value = audio;
    audio.play().catch((err) => {
      console.warn("Audio playback failed:", err);
      playingAsset.value = null;
    });
    audio.onended = () => {
      playingAsset.value = null;
    };
  }
};
const handleDownload = (url) => {
  window.open(url, "_blank");
};
const escapeHtml = (str) => {
  const htmlEntities = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;"
  };
  return str.replace(/[&<>"']/g, (char) => htmlEntities[char]);
};
const sanitizeBubbleHtml = (html) => {
  if (!props.enableSanitizer) {
    return html;
  }
  return sanitizeMarkup(html, {
    allowedTags: [.../* @__PURE__ */ new Set([...props.allowedTags, "button", "sub", "sup"])],
    allowedAttributes: [
      .../* @__PURE__ */ new Set([
        ...props.allowedAttributes,
        "aria-label",
        "data-code",
        "data-id",
        "data-lang",
        "data-latex",
        "role",
        "tabindex"
      ])
    ],
    allowedSchemes: props.allowedSchemes,
    sanitizer: props.sanitizer
  });
};
const getFileIcon = (url = "") => {
  const ext = url.split(".").pop()?.toLowerCase() || "";
  switch (ext) {
    case "pdf":
      return "file-pdf";
    case "xlsx":
    case "xls":
    case "csv":
      return "file-excel";
    case "doc":
    case "docx":
      return "file-word";
    case "mp4":
    case "webm":
    case "mov":
      return "file-video";
    case "mp3":
    case "wav":
    case "ogg":
      return "file-audio";
    case "txt":
    case "md":
      return "file-txt";
    default:
      return "document";
  }
};
const _mermaidContainer = ref(null);
const mermaidLoading = ref(false);
const mermaidError = ref(null);
let mermaidModule = null;
const initMermaid = async () => {
  if (mermaidModule) return mermaidModule;
  try {
    const module = await import("../../mermaid");
    mermaidModule = module.default || module;
    mermaidModule.initialize({
      startOnLoad: false,
      theme: "default",
      securityLevel: "strict",
      flowchart: { curve: "basis", padding: 15 },
      sequence: { actorMargin: 50, boxMargin: 10 }
    });
    return mermaidModule;
  } catch (e) {
    console.warn("Mermaid not available:", e);
    return null;
  }
};
const _renderMermaid = async (code) => {
  if (!mermaidModule) {
    await initMermaid();
  }
  if (!mermaidModule) return `<pre class="mermaid-error">${escapeHtml(code)}</pre>`;
  mermaidLoading.value = true;
  mermaidError.value = null;
  try {
    const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const { svg } = await mermaidModule.render(id, code);
    mermaidLoading.value = false;
    return sanitizeSvgMarkup(svg);
  } catch (e) {
    mermaidLoading.value = false;
    mermaidError.value = e instanceof Error ? e.message : "Failed to render mermaid diagram";
    return `<pre class="mermaid-error">${escapeHtml(code)}</pre>`;
  }
};
const expandedCodeBlocks = ref(/* @__PURE__ */ new Set());
const copiedCodeBlocks = ref(/* @__PURE__ */ new Set());
const editingCodeBlock = ref(null);
const editCodeContent = ref("");
const runningCodeBlock = ref(null);
const codeOutput = ref({});
let webContainerInstance = null;
const monaco = shallowRef(null);
const monacoEditor = shallowRef(null);
const monacoContainer = ref(null);
const loadMonaco = async () => {
  if (monaco.value) return monaco.value;
  try {
    const m = await import("monaco-editor");
    monaco.value = m;
    return m;
  } catch (e) {
    console.error(
      '[YhAiBubble] \u65E0\u6CD5\u52A0\u8F7D monaco-editor\u3002\u5982\u679C\u9700\u8981\u4F7F\u7528\u7F16\u8F91\u4EE3\u7801\u5757\u529F\u80FD\uFF0C\u8BF7\u5728\u60A8\u7684\u9879\u76EE\u4E2D\u5B89\u88C5 "monaco-editor" \u4F9D\u8D56\u3002',
      e
    );
    return null;
  }
};
const isWebContainerSupported = () => {
  if (typeof window === "undefined") return false;
  const isSecure = window.isSecureContext;
  const hasSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined";
  const isCrossOriginIsolated = window.crossOriginIsolated;
  if (!isSecure || !hasSharedArrayBuffer || !isCrossOriginIsolated) {
    console.warn(
      "[YhAiBubble] WebContainer requires secure context, SharedArrayBuffer and crossOriginIsolated. Falling back to browser runtime."
    );
    return false;
  }
  return true;
};
const initWebContainer = async () => {
  if (webContainerInstance) return webContainerInstance;
  if (!isWebContainerSupported()) {
    return null;
  }
  try {
    const { WebContainer } = await import("@webcontainer/api");
    webContainerInstance = await WebContainer.boot();
    return webContainerInstance;
  } catch (e) {
    console.warn("WebContainer not available, fallback to browser runtime:", e);
    return null;
  }
};
const getCodeBlockId = (code, lang) => {
  const key = (code || "").slice(0, 50) + (lang || "");
  let hash = 0;
  for (let i = 0; i < key.length; i++) {
    hash = hash * 31 + key.charCodeAt(i) >>> 0;
  }
  return `cb-${hash.toString(16)}`;
};
const toggleCodeBlock = (id) => {
  if (expandedCodeBlocks.value.has(id)) {
    expandedCodeBlocks.value.delete(id);
  } else {
    expandedCodeBlocks.value.add(id);
  }
};
const copyTextToClipboard = async (text) => {
  if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {
    }
  }
  if (typeof document !== "undefined") {
    try {
      const textarea = document.createElement("textarea");
      textarea.value = text;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      const successful = document.execCommand("copy");
      document.body.removeChild(textarea);
      if (successful) return true;
    } catch {
    }
  }
  return false;
};
const copyCode = async (code, id) => {
  const success = await copyTextToClipboard(code);
  if (success) {
    copiedCodeBlocks.value.add(id);
    setTimeout(() => copiedCodeBlocks.value.delete(id), 2e3);
  } else {
    emit("copy-fail", new Error("Copy failed"));
  }
};
const _editCode = (code, id) => {
  editingCodeBlock.value = id;
  editCodeContent.value = code;
};
const openCodeEditor = async (code, id, lang) => {
  editingCodeBlock.value = id;
  editCodeContent.value = code;
  await nextTick();
  const container = monacoContainer.value;
  const m = await loadMonaco();
  if (!container || !m) return;
  if (monacoEditor.value) {
    monacoEditor.value.dispose();
    monacoEditor.value = null;
  }
  monacoEditor.value = m.editor.create(container, {
    value: code,
    language: lang || "typescript",
    automaticLayout: true,
    minimap: { enabled: false },
    fontSize: 13,
    theme: "vs-dark"
  });
};
const saveEditCode = async (id) => {
  if (monacoEditor.value) {
    editCodeContent.value = monacoEditor.value.getValue();
  }
  const success = await copyTextToClipboard(editCodeContent.value);
  if (success) {
    copiedCodeBlocks.value.add(id);
    setTimeout(() => copiedCodeBlocks.value.delete(id), 2e3);
  } else {
    emit("copy-fail", new Error("Copy failed"));
  }
  editingCodeBlock.value = null;
  editCodeContent.value = "";
};
const cancelEditCode = () => {
  editingCodeBlock.value = null;
  editCodeContent.value = "";
  if (monacoEditor.value) {
    monacoEditor.value.dispose();
    monacoEditor.value = null;
  }
};
const runCodeInBrowser = (code, id) => {
  codeOutput.value[id] = [];
  codeOutput.value[id].push("> Running...\n");
  try {
    let hasOutput = false;
    const streamLog = (type, ...args) => {
      const line = args.map(String).join(" ");
      const prefix = type === "error" ? "Error: " : type === "warn" ? "Warn: " : "";
      codeOutput.value[id].push(prefix + line);
      hasOutput = true;
    };
    const streamConsole = {
      log: (...args) => streamLog("log", ...args),
      error: (...args) => streamLog("error", ...args),
      warn: (...args) => streamLog("warn", ...args),
      info: (...args) => streamLog("log", ...args),
      debug: (...args) => streamLog("log", ...args)
    };
    const fn = new Function(
      "console",
      `
      return (function() {
        ${code}
      })()
    `
    );
    const result = fn(streamConsole);
    if (result !== void 0) {
      codeOutput.value[id].push(`\u2190 ${String(result)}`);
      hasOutput = true;
    }
    if (!hasOutput) {
      codeOutput.value[id].push("\n\u2713 Executed successfully (no output)");
    }
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    codeOutput.value[id].push(`
\u2717 Error: ${message}`);
  }
};
const runPythonInBrowser = async (code, id) => {
  codeOutput.value[id].push("> Initializing Pyodide (Python in browser)...");
  try {
    const win = window;
    if (!win.loadPyodide) {
      await import(
        /* @vite-ignore */
        props.pyodideUrl
      );
    }
    if (!win.loadPyodide) {
      throw new Error("Pyodide failed to load.");
    }
    const pyodide = await win.loadPyodide({
      indexURL: props.pyodideUrl.substring(0, props.pyodideUrl.lastIndexOf("/"))
    });
    codeOutput.value[id].push("> Running Python code...\n");
    pyodide.setStdout({
      batched: (text) => {
        if (codeOutput.value[id]) {
          codeOutput.value[id].push(text);
        }
      }
    });
    const result = await pyodide.runPythonAsync(code);
    if (result !== void 0 && result !== null) {
      codeOutput.value[id].push(`
Result: ${result}`);
    }
    codeOutput.value[id].push("\n\u2713 Python execution complete");
  } catch (e) {
    const error = e instanceof Error ? e.message : String(e);
    codeOutput.value[id].push(`
Error: ${error}`);
  }
};
const runPythonRemote = async (code, id) => {
  codeOutput.value[id].push(`> Running Python via remote API: ${props.pythonApiUrl}...`);
  try {
    const response = await fetch(props.pythonApiUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ code, language: "python" })
    });
    const result = await response.json();
    if (result.output) {
      codeOutput.value[id].push(`
${result.output}`);
    }
    if (result.error) {
      codeOutput.value[id].push(`
Error: ${result.error}`);
    }
    codeOutput.value[id].push("\n\u2713 Remote Python execution complete");
  } catch (e) {
    const error = e instanceof Error ? e.message : String(e);
    codeOutput.value[id].push(`
Remote Error: ${error}`);
  }
};
const runCode = async (code, lang, id) => {
  runningCodeBlock.value = id;
  codeOutput.value[id] = [];
  const triggerRender = () => {
    if (props.markdown && mdi.value && props.content) {
      parsedContent.value = sanitizeBubbleHtml(mdi.value.render(props.content));
    }
  };
  triggerRender();
  const renderInterval = setInterval(triggerRender, 100);
  try {
    if (props.onRunCode) {
      try {
        codeOutput.value[id].push("> Running...\n");
        const result = await props.onRunCode(code, lang);
        if (result && Symbol.asyncIterator in result) {
          const asyncResult = result;
          for await (const chunk of asyncResult) {
            codeOutput.value[id].push(chunk.output || chunk.error || "");
          }
        } else {
          const finalResult = result;
          codeOutput.value[id] = finalResult.output ? finalResult.output.split("\n") : finalResult.error ? [`Error: ${finalResult.error}`] : [];
        }
      } catch (e) {
        const message = e instanceof Error ? e.message : String(e);
        codeOutput.value[id].push(`Error: ${message}`);
      }
    } else if (lang === "javascript" || lang === "js") {
      const mdOptions = getMarkdownOptions();
      const runtime = mdOptions.codeBlock?.runtime || "browser";
      if (runtime === "webcontainer") {
        try {
          const wc = await initWebContainer();
          if (!wc) {
            runCodeInBrowser(code, id);
          } else {
            codeOutput.value[id].push("> Running in WebContainer...\n");
            await wc.mount({
              "index.js": {
                file: {
                  contents: code
                }
              }
            });
            const process = await wc.spawn("node", ["index.js"]);
            const reader = process.output.getReader();
            const decoder = new TextDecoder();
            while (true) {
              const { done, value } = await reader.read();
              if (done) break;
              const chunk = decoder.decode(value, { stream: true });
              if (chunk) {
                const lines = chunk.split("\n");
                lines.forEach((line) => {
                  codeOutput.value[id].push(line);
                });
              }
            }
            codeOutput.value[id].push("\n\u2713 WebContainer execution complete");
          }
        } catch (e) {
          console.warn("WebContainer execution failed, fallback to browser runtime:", e);
          codeOutput.value[id].push("\n\u26A0 WebContainer not supported, falling back to browser...");
          runCodeInBrowser(code, id);
        }
      } else {
        runCodeInBrowser(code, id);
      }
    } else if (lang === "python" || lang === "py") {
      if (props.enablePythonRuntime) {
        if (props.pythonRuntime === "remote" && props.pythonApiUrl) {
          await runPythonRemote(code, id);
        } else {
          await runPythonInBrowser(code, id);
        }
      } else {
        codeOutput.value[id].push(
          'Python runtime is disabled. Please enable "enable-python-runtime" prop.'
        );
      }
    } else {
      codeOutput.value[id].push(`Language "${lang}" execution not supported in browser`);
    }
  } finally {
    clearInterval(renderInterval);
    triggerRender();
    runningCodeBlock.value = null;
  }
};
const explainCode = async (code, lang) => {
  if (props.onExplainCode) {
    return await props.onExplainCode(code, lang);
  }
  return "";
};
const parsedContent = ref("");
let renderRafId = null;
let streamTimer = null;
let streamPosition = 0;
let streamBuffer = "";
const streamingCodeBlocks = ref(/* @__PURE__ */ new Set());
const injectCodeStreamStyles = () => {
  if (typeof document === "undefined") return;
  const styleId = "yh-ai-code-stream-styles";
  if (document.getElementById(styleId)) return;
  const style = document.createElement("style");
  style.id = styleId;
  style.textContent = `
    .code-block-wrapper.streaming .code-block-body code {
      display: block;
    }
    .code-block-wrapper.streaming .code-block-body code .line {
      display: block;
      opacity: 0;
      animation: code-line-fade-in 0.15s ease forwards;
    }
    .code-block-wrapper.streaming .code-block-body code .line:last-child {
      animation: none;
    }
    @keyframes code-line-fade-in {
      from { opacity: 0; transform: translateX(-8px); }
      to { opacity: 1; transform: translateX(0); }
    }
    /* \u5149\u6807\u95EA\u70C1\u6548\u679C */
    .code-block-wrapper.streaming .code-block-body code .line:last-child::after {
      content: '';
      display: inline-block;
      width: 2px;
      height: 1.1em;
      background-color: var(--yh-color-primary, #409eff);
      margin-left: 4px;
      vertical-align: -2px;
      animation: cursor-blink 0.8s infinite;
    }
    @keyframes cursor-blink {
      0%, 50% { opacity: 1; }
      51%, 100% { opacity: 0; }
    }
  `;
  document.head.appendChild(style);
};
const _markCodeLinesForStreaming = (codeBlockId) => {
  streamingCodeBlocks.value.add(codeBlockId);
  injectCodeStreamStyles();
};
const bubbleRef = ref(null);
watch(
  () => parsedContent.value,
  (_newContent) => {
    if (props.streaming && props.typing) {
      setTimeout(() => {
        if (!bubbleRef.value) return;
        const codeBlocks = bubbleRef.value.querySelectorAll(".code-block-wrapper");
        codeBlocks.forEach((block) => {
          const id = block.getAttribute("data-id");
          if (id && !streamingCodeBlocks.value.has(id)) {
            const code = block.querySelector("code");
            if (code) {
              const html = code.innerHTML;
              const lines = html.split("\n");
              code.innerHTML = lines.map(
                (line, i) => `<span class="line" style="animation-delay: ${i * 30}ms">${line}</span>`
              ).join("\n");
              block.classList.add("streaming");
              streamingCodeBlocks.value.add(id);
            }
          }
        });
      }, 100);
    }
  }
);
const renderMermaidBlocks = async () => {
  await nextTick();
  if (!bubbleRef.value) return;
  const elements = bubbleRef.value.querySelectorAll("pre.mermaid");
  for (const el of Array.from(elements)) {
    if (el.getAttribute("data-processed")) continue;
    el.setAttribute("data-processed", "true");
    const code = el.textContent || "";
    if (!code.trim()) continue;
    const parent = el.parentElement;
    if (!parent) continue;
    const container = document.createElement("div");
    container.className = "mermaid-rendered";
    container.innerHTML = '<span class="mermaid-loading">Rendering...</span>';
    el.style.display = "none";
    parent.appendChild(container);
    try {
      const svg = await _renderMermaid(code);
      if (parent.contains(container)) {
        container.innerHTML = svg;
      }
    } catch {
      if (parent.contains(container)) {
        container.innerHTML = `<span class="mermaid-error">Failed to render</span>`;
      }
    }
  }
};
watch(
  () => parsedContent.value,
  () => {
    renderMermaidBlocks();
  }
);
const _renderLatex = (text) => {
  text = text.replace(/\$\$([\s\S]+?)\$\$/g, (_, math) => {
    return `<div class="latex-block" data-latex="${encodeURIComponent(math.trim())}">${math.trim()}</div>`;
  });
  text = text.replace(/\$([^\$\n]+?)\$/g, (_, math) => {
    return `<span class="latex-inline" data-latex="${encodeURIComponent(math.trim())}">${math.trim()}</span>`;
  });
  return text;
};
const _structuredDataRef = ref(null);
const _renderStructuredData = computed(() => {
  if (!props.structuredData) return null;
  const { type, data, options } = props.structuredData;
  switch (type) {
    case "json":
      return { type: "json", data, options };
    case "table":
      return { type: "table", data, options };
    case "chart":
      return { type: "chart", data, options };
    case "mindmap":
      return { type: "mindmap", data, options };
    case "thought-chain":
      return { type: "thought-chain", data, options };
    default:
      return null;
  }
});
const jsonHtml = computed(() => {
  if (!props.structuredData || props.structuredData.type !== "json") return "";
  try {
    const rawData = props.structuredData.data;
    const jsonString = typeof rawData === "string" ? rawData : JSON.stringify(rawData, null, 2);
    if (hljs.getLanguage("json")) {
      return sanitizeHighlightedHtml(
        hljs.highlight(jsonString, {
          language: "json",
          ignoreIllegals: true
        }).value
      );
    }
    return sanitizeHighlightedHtml(escapeHtml(jsonString));
  } catch (e) {
    console.warn("Failed to render JSON structured data:", e);
    return "";
  }
});
const hoveredCitation = ref(null);
const citationTooltipStyle = ref({
  top: "0px",
  left: "0px",
  position: "fixed",
  transform: "",
  zIndex: "9999"
});
let citationHoverTimer = null;
const handleCitationHover = (e) => {
  const target = e.target;
  if (target && target.classList && target.classList.contains("yh-ai-citation")) {
    const id = target.getAttribute("data-id");
    const citation = props.citations?.find(
      (c) => String(c.id) === id
    );
    if (citation) {
      if (citationHoverTimer) clearTimeout(citationHoverTimer);
      hoveredCitation.value = citation;
      const rect = target.getBoundingClientRect();
      citationTooltipStyle.value = {
        top: `${rect.top - 10}px`,
        left: `${rect.left + rect.width / 2}px`,
        transform: "translate(-50%, -100%)",
        position: "fixed",
        zIndex: "9999"
      };
    }
  }
};
const handleCitationLeave = (e) => {
  const target = e.target;
  if (target && target.classList && target.classList.contains("yh-ai-citation")) {
    citationHoverTimer = setTimeout(() => {
      hoveredCitation.value = null;
    }, 200);
  }
};
const handleCitationClick = (citation) => {
  if (props.onCitationClick) {
    props.onCitationClick(citation);
  } else if (citation.url) {
    window.open(citation.url, "_blank");
  }
};
const handleTooltipEnter = () => {
  if (citationHoverTimer) clearTimeout(citationHoverTimer);
};
const handleTooltipLeave = () => {
  citationHoverTimer = setTimeout(() => {
    hoveredCitation.value = null;
  }, 200);
};
const getMarkdownOptions = () => {
  return {
    mermaid: true,
    latex: true,
    filePreview: true,
    linkify: true,
    typographer: true,
    ...props.markdownOptions,
    codeBlock: {
      copyable: true,
      languageTag: true,
      lineNumbers: false,
      editable: false,
      runnable: true,
      explainable: true,
      collapsible: true,
      collapseLinesThreshold: 10,
      ...props.markdownOptions?.codeBlock || {}
    }
  };
};
let cachedMarkdownItClass = null;
const getMarkdownInstance = (MarkdownItClass) => {
  const Cls = MarkdownItClass || cachedMarkdownItClass || mdi.value?.constructor;
  if (!Cls) {
    throw new Error(
      "[YhAiBubble] markdown-it \u5C1A\u672A\u52A0\u8F7D\u3002\u5982\u679C\u9700\u8981\u540C\u6B65\u83B7\u53D6 markdown \u5B9E\u4F8B\uFF0C\u8BF7\u7B49\u5F85\u5F02\u6B65\u52A0\u8F7D\u5B8C\u6210\uFF0C\u6216\u5728\u8C03\u7528 getMarkdownInstance \u65F6\u4F20\u5165\u6784\u9020\u5668\u53C2\u6570\u3002"
    );
  }
  const mdOptions = getMarkdownOptions();
  const md = new Cls({
    html: mdOptions.html ?? false,
    linkify: mdOptions.linkify ?? true,
    typographer: mdOptions.typographer ?? true,
    highlight: function(str, lang) {
      const codeBlockId = getCodeBlockId(str, lang);
      const codeBlockOptions = mdOptions.codeBlock;
      let result = "";
      const hlLang = lang === "vue" ? "xml" : lang;
      if (hlLang && hljs.getLanguage(hlLang)) {
        try {
          result = hljs.highlight(str, { language: hlLang, ignoreIllegals: true }).value;
        } catch {
        }
      } else {
        result = md.utils.escapeHtml(str);
      }
      if (codeBlockOptions?.lineNumbers) {
        const lines = result.split("\n");
        const numberedLines = lines.map((line, i) => `<span class="line-number">${i + 1}</span>${line}`).join("\n");
        result = numberedLines;
      }
      const lineCount = str.split("\n").length;
      const shouldCollapse = codeBlockOptions?.collapsible && lineCount > (codeBlockOptions?.collapseLinesThreshold ?? 10);
      const isExpanded = expandedCodeBlocks.value.has(codeBlockId);
      let wrapperStart = `<div class="code-block-wrapper" data-lang="${lang}" data-id="${codeBlockId}">`;
      wrapperStart += `<div class="code-block-header">`;
      if (codeBlockOptions?.languageTag) {
        wrapperStart += `<span class="code-lang">${lang || "text"}</span>`;
      }
      wrapperStart += `<div class="code-actions">`;
      if (codeBlockOptions?.copyable) {
        const copyText = copiedCodeBlocks.value.has(codeBlockId) ? "Copied!" : "Copy";
        wrapperStart += `<button class="code-action-btn copy-btn" data-code="${encodeURIComponent(str)}" data-id="${codeBlockId}">${copyText}</button>`;
      }
      if (codeBlockOptions?.editable) {
        wrapperStart += `<button class="code-action-btn edit-btn" data-id="${codeBlockId}">Edit</button>`;
      }
      const isRunnableLang = ["javascript", "js"].includes(lang) || props.enablePythonRuntime && ["python", "py"].includes(lang);
      if (codeBlockOptions?.runnable && isRunnableLang) {
        wrapperStart += `<button class="code-action-btn run-btn" data-code="${encodeURIComponent(str)}" data-lang="${lang}" data-id="${codeBlockId}">Run</button>`;
      }
      if (codeBlockOptions?.explainable) {
        wrapperStart += `<button class="code-action-btn explain-btn" data-code="${encodeURIComponent(str)}" data-lang="${lang}" data-id="${codeBlockId}">Explain</button>`;
      }
      if (shouldCollapse) {
        wrapperStart += `<button class="code-action-btn collapse-btn" data-id="${codeBlockId}">${isExpanded ? "Collapse" : "Expand"}</button>`;
      }
      wrapperStart += `</div></div>`;
      const _bodyClass = shouldCollapse ? isExpanded ? "code-block-body expanded" : "code-block-body collapsed" : "code-block-body";
      let wrapperEnd = `</div>`;
      if (codeOutput.value[codeBlockId]) {
        const outputLines = codeOutput.value[codeBlockId];
        const outputHtml = Array.isArray(outputLines) ? outputLines.map((line) => {
          if (line.startsWith("> "))
            return `<span class="output-prefix">${escapeHtml(line)}</span>`;
          if (line.startsWith("\u2190 "))
            return `<span class="output-return">${escapeHtml(line)}</span>`;
          if (line.startsWith("\u2713"))
            return `<span class="output-success">${escapeHtml(line)}</span>`;
          if (line.startsWith("\u2717"))
            return `<span class="output-error">${escapeHtml(line)}</span>`;
          if (line.startsWith("\u26A0"))
            return `<span class="output-warning">${escapeHtml(line)}</span>`;
          if (line.startsWith("Error:"))
            return `<span class="output-error">${escapeHtml(line)}</span>`;
          return escapeHtml(line);
        }).join("\n") : escapeHtml(outputLines);
        wrapperEnd += `<div class="code-output"><pre>${outputHtml}</pre></div>`;
      }
      wrapperEnd += `</div>`;
      return wrapperStart + `<pre class="hljs ${lang || ""}"><code>${result}</code></pre>` + wrapperEnd;
    }
  });
  md.block.ruler.before(
    "code",
    "mermaid",
    (state, startLine, _endLine, silent) => {
      const start = state.bMarks[startLine];
      const max = state.eMarks[startLine];
      const line = state.src.slice(start, max);
      if (!line.trim().startsWith("```mermaid")) return false;
      if (!silent) {
        state.line = startLine + 1;
        const lines = [];
        while (state.line < state.lineMax) {
          const lineContent = state.src.slice(state.bMarks[state.line], state.eMarks[state.line]);
          if (lineContent.trim().startsWith("```")) break;
          lines.push(lineContent);
          state.line++;
        }
        let token = state.push("mermaid_open", "div", 1);
        token.attrSet("class", "mermaid-block");
        token = state.push("mermaid_code", "", 0);
        token.content = lines.join("\n");
        token = state.push("mermaid_close", "div", -1);
      }
      return true;
    }
  );
  md.renderer.rules.mermaid_open = () => '<div class="mermaid-block">';
  md.renderer.rules.mermaid_code = (tokens, idx) => {
    const code = tokens[idx].content;
    return `<pre class="mermaid">${md.utils.escapeHtml(code)}</pre>`;
  };
  md.renderer.rules.mermaid_close = () => "</div>";
  md.inline.ruler.after("text", "citation", (state, silent) => {
    const start = state.pos;
    if (state.src.charCodeAt(start) !== 91) return false;
    const match = state.src.slice(start).match(/^\[(\d+)\]/);
    if (!match) return false;
    if (!silent) {
      const id = match[1];
      let token = state.push("citation_open", "sup", 1);
      token.attrs = [
        ["class", "yh-ai-citation"],
        ["data-id", id]
      ];
      token = state.push("text", "", 0);
      token.content = id;
      state.push("citation_close", "sup", -1);
    }
    state.pos += match[0].length;
    return true;
  });
  return md;
};
const mdi = shallowRef(null);
const isMdLoading = ref(false);
const loadMarkdownIt = async () => {
  if (mdi.value || isMdLoading.value) return;
  isMdLoading.value = true;
  try {
    const MarkdownItClass = await loadMarkdown();
    cachedMarkdownItClass = MarkdownItClass;
    mdi.value = getMarkdownInstance(MarkdownItClass);
  } catch (err) {
    console.error("[YhAiBubble] \u65E0\u6CD5\u52A0\u8F7D markdown-it\u3002", err);
  } finally {
    isMdLoading.value = false;
  }
};
watch(
  [() => props.markdown, () => mdi.value],
  ([markdown, mdiVal]) => {
    if (markdown && !mdiVal) {
      loadMarkdownIt();
    }
  },
  { immediate: true }
);
onBeforeUnmount(() => {
  if (audioInstance.value) {
    audioInstance.value.pause();
    audioInstance.value.src = "";
    audioInstance.value = null;
  }
  if (streamTimer) {
    clearInterval(streamTimer);
    streamTimer = null;
  }
  if (monacoEditor.value) {
    monacoEditor.value.dispose();
    monacoEditor.value = null;
  }
  mdi.value = null;
});
const streamRender = (fullContent, mode, speed, interval) => {
  if (!fullContent) {
    parsedContent.value = "";
    return;
  }
  if (streamTimer) {
    clearInterval(streamTimer);
    streamTimer = null;
  }
  streamPosition = 0;
  streamBuffer = "";
  const getChunks = () => {
    if (mode === "paragraph") {
      return fullContent.split(/(?:\r?\n){2,}/);
    } else if (mode === "sentence") {
      return fullContent.split(/(?<=[.!?。！？])\s+/);
    } else {
      const chunks2 = [];
      for (let i = 0; i < fullContent.length; i += speed) {
        chunks2.push(fullContent.slice(i, i + speed));
      }
      return chunks2;
    }
  };
  const chunks = getChunks();
  streamTimer = setInterval(() => {
    if (streamPosition < chunks.length) {
      streamBuffer += chunks[streamPosition];
      streamPosition++;
      parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(streamBuffer)) : streamBuffer;
    } else {
      if (streamTimer) {
        clearInterval(streamTimer);
        streamTimer = null;
      }
      parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(fullContent)) : fullContent;
      if (props.onStreamComplete) {
        props.onStreamComplete();
      }
    }
  }, interval);
};
watch(
  [() => props.content, mdi],
  ([newContent]) => {
    if (streamTimer) {
      clearInterval(streamTimer);
      streamTimer = null;
    }
    if (!props.markdown || !props.typing) {
      if (renderRafId && typeof cancelAnimationFrame !== "undefined") {
        cancelAnimationFrame(renderRafId);
        renderRafId = null;
      }
      parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
      return;
    }
    if (props.streaming && props.typing) {
      streamRender(newContent, props.streamMode, props.streamSpeed, props.streamInterval);
      return;
    }
    if (typeof requestAnimationFrame === "undefined") {
      parsedContent.value = mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
      return;
    }
    if (renderRafId) return;
    renderRafId = requestAnimationFrame(() => {
      parsedContent.value = mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
      renderRafId = null;
    });
  },
  { immediate: true }
);
const computedPlacement = computed(() => {
  if (props.placement) return props.placement;
  return props.role === "user" ? "end" : "start";
});
const classes = computed(() => [
  ns.b(),
  ns.m(props.role),
  ns.m(`placement-${computedPlacement.value}`),
  ns.m(`shape-${props.shape}`),
  ns.m(`variant-${props.variant}`),
  ns.is("loading", props.loading),
  ns.is("typing", props.typing)
]);
const handleCodeBlockAction = async (e) => {
  const target = e.target;
  if (!target.classList.contains("code-action-btn")) return;
  const code = decodeURIComponent(target.getAttribute("data-code") || "");
  const lang = target.getAttribute("data-lang") || "";
  const id = target.getAttribute("data-id") || "";
  if (target.classList.contains("copy-btn")) {
    await copyCode(code, id);
  } else if (target.classList.contains("edit-btn")) {
    await openCodeEditor(code, id, lang);
  } else if (target.classList.contains("run-btn")) {
    await runCode(code, lang, id);
  } else if (target.classList.contains("explain-btn")) {
    const explanation = await explainCode(code, lang);
    if (explanation) {
      codeOutput.value[id] = explanation.split("\n");
    }
  } else if (target.classList.contains("collapse-btn")) {
    toggleCodeBlock(id);
  }
};
onMounted(() => {
  hasMounted.value = true;
  loadHighlightStyle();
  renderMermaidBlocks();
});
</script>

<template>
  <div ref="bubbleRef" :class="classes" :style="themeStyle" @click="handleCodeBlockAction">
    <!-- Avatar -->
    <div :class="ns.e('avatar')" v-if="role !== 'system'">
      <slot name="avatar">
        <YhAvatar v-if="avatar" :src="avatar" crossorigin="anonymous" />
        <YhAvatar v-else>
          <YhIcon :name="role === 'user' ? 'user' : 'robot'" />
        </YhAvatar>
      </slot>
    </div>

    <!-- Content Wrapper -->
    <div :class="ns.e('content-wrapper')">
      <!-- Header -->
      <div :class="ns.e('header')" v-if="$slots.header || time">
        <slot name="header">
          <span v-if="time" :class="ns.e('time')">{{ time }}</span>
        </slot>
      </div>

      <!-- Body -->
      <div :class="[ns.e('body'), ns.is('typing', loading)]">
        <template v-if="loading && !content">
          <span :class="ns.e('typing-indicator')"> <span></span><span></span><span></span> </span>
        </template>
        <template v-else>
          <!-- Multimodal: Images (Above text) -->
          <div
            v-if="multimodal && multimodal.some(m => m.type === 'image')"
            :class="ns.e('image-grid')"
          >
            <div
              v-for="(img, idx) in multimodal.filter(m => m.type === 'image')"
              :key="idx"
              :class="ns.e('image-item')"
            >
              <img :src="img.url" :alt="img.title" loading="lazy" crossorigin="anonymous" />
            </div>
          </div>

          <slot>
            <!-- eslint-disable vue/no-v-html -->
            <div
              v-if="markdown"
              :class="[ns.e('text'), ns.e('markdown')]"
              v-html="parsedContent"
              @mouseover="handleCitationHover"
              @mouseout="handleCitationLeave"
            ></div>
            <!-- eslint-enable vue/no-v-html -->
            <div v-else :class="ns.e('text')">{{ content }}</div>
          </slot>

          <!-- Structured Data Display (Below main text) -->
          <div v-if="structuredData" :class="ns.e('structured-data')">
            <!-- JSON pretty view -->
            <!-- eslint-disable vue/no-v-html -->
            <div v-if="structuredData.type === 'json'" :class="[ns.e('json-viewer')]">
              <pre class="hljs json"><code v-html="jsonHtml" /></pre>
            </div>
            <!-- eslint-enable vue/no-v-html -->

            <!-- Table view -->
            <div v-else-if="structuredData.type === 'table'" :class="[ns.e('table-viewer')]">
              <table>
                <thead>
                  <tr
                    v-if="structuredData.data && structuredData.data.headers"
                  >
                    <th
                      v-for="h in structuredData.data.headers"
                      :key="h"
                    >
                      {{ h }}
                    </th>
                  </tr>
                </thead>
                <tbody v-if="structuredData.data && typeof structuredData.data === 'object'">
                  <tr
                    v-for="(row, idx) in structuredData.data.rows"
                    :key="idx"
                  >
                    <td v-for="(cell, cIdx) in row" :key="cIdx">{{ cell }}</td>
                  </tr>
                </tbody>
              </table>
            </div>

            <!-- Thought chain: delegate to AiThoughtChain for consistent style -->
            <YhAiThoughtChain
              v-else-if="structuredData.type === 'thought-chain' && structuredData.data"
              :items="structuredData.data"
              :title="content || void 0"
              status="none"
              dot-size="small"
              :auto-collapse="false"
              line-gradient
            />

            <!-- Fallback: raw JSON -->
            <div v-else :class="[ns.e('chart-viewer')]">
              <pre>{{ JSON.stringify(structuredData.data, null, 2) }}</pre>
            </div>
          </div>

          <!-- Multimodal: Audio/Files (Below text) -->
          <div
            v-if="multimodal && multimodal.some(m => m.type !== 'image')"
            :class="ns.e('multimodal-assets')"
          >
            <template v-for="(asset, idx) in multimodal" :key="idx">
              <!-- Audio Player -->
              <div
                v-if="asset.type === 'audio'"
                :class="[ns.e('audio-player'), {
  'is-active': playingAsset === asset.url
}]"
              >
                <YhButton
                  circle
                  size="small"
                  :type="role === 'user' ? 'default' : 'primary'"
                  @click="handleAudioToggle(asset.url)"
                >
                  <template #icon>
                    <YhIcon :name="playingAsset === asset.url ? 'video-pause' : 'video-play'" />
                  </template>
                </YhButton>
                <div :class="[ns.e('audio-waveform'), {
  'is-active': playingAsset === asset.url
}]">
                  <span v-for="i in 12" :key="i" :style="{
  '--delay': i * 0.1 + 's'
}"></span>
                </div>
                <span :class="ns.e('audio-duration')">{{ asset.extra?.duration || "0:00" }}</span>
              </div>

              <!-- File/Table Card -->
              <div v-else-if="asset.type === 'file'" :class="ns.e('file-card')">
                <div :class="ns.e('file-icon')">
                  <YhIcon :name="getFileIcon(asset.url)" />
                </div>
                <div :class="ns.e('file-info')">
                  <div :class="ns.e('file-name')">{{ asset.title }}</div>
                  <div :class="ns.e('file-meta')">{{ asset.size }}</div>
                </div>
                <YhButton text circle @click="handleDownload(asset.url)">
                  <template #icon>
                    <YhIcon name="download" />
                  </template>
                </YhButton>
              </div>
            </template>
          </div>

          <!-- Citations -->
          <div v-if="citations && citations.length > 0" :class="ns.e('citations')">
            <div :class="ns.e('citations-title')">
              <YhIcon name="document" />
              <span>{{ t("ai.bubble.citations") }}</span>
              <span :class="ns.e('citations-count')">{{ citations.length }}</span>
            </div>
            <div :class="ns.e('citations-list')">
              <a
                v-for="(cite, index) in citations"
                :key="index"
                :href="cite.url"
                target="_blank"
                :class="ns.e('citation-item')"
                :title="cite.title"
                @click.prevent="handleCitationClick(cite)"
              >
                <span :class="ns.e('citation-index')">{{ cite.id || index + 1 }}</span>
                <span :class="ns.e('citation-text')">{{ cite.title }}</span>
                <YhIcon v-if="cite.url" name="arrow-right" :class="ns.e('citation-link-icon')" />
              </a>
            </div>
          </div>
        </template>
      </div>

      <!-- Footer -->
      <div :class="ns.e('footer')" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>
    </div>

    <!-- Floating Citation Tooltip -->
    <Teleport v-if="hasMounted" to="body">
      <Transition name="yh-fade-in-scale-up">
        <div
          v-if="hoveredCitation"
          :class="ns.e('citation-tooltip-wrapper')"
          :style="citationTooltipStyle"
          @mouseenter="handleTooltipEnter"
          @mouseleave="handleTooltipLeave"
        >
          <div :class="ns.e('citation-tooltip')">
            <div :class="ns.e('citation-tooltip-header')">
              <YhIcon :name="hoveredCitation.icon || 'document'" />
              <span>{{ hoveredCitation.source || hoveredCitation.title }}</span>
            </div>
            <div :class="ns.e('citation-tooltip-body')">
              <h4>{{ hoveredCitation.title }}</h4>
              <p v-if="hoveredCitation.abstract">{{ hoveredCitation.abstract }}</p>
              <span v-if="hoveredCitation.publishTime" class="publish-time">{{
                hoveredCitation.publishTime
              }}</span>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>

    <!-- Code Edit Modal -->
    <Teleport v-if="hasMounted" to="body">
      <Transition name="yh-fade-in">
        <div v-if="editingCodeBlock" class="code-edit-modal-overlay" @click.self="cancelEditCode">
          <div class="code-edit-modal">
            <div class="code-edit-header">
              <h3>Edit Code</h3>
              <YhButton text @click="cancelEditCode">
                <YhIcon name="close" />
              </YhButton>
            </div>
            <div class="code-edit-body">
              <div ref="monacoContainer" class="code-edit-monaco"></div>
              <textarea v-if="!monaco" v-model="editCodeContent" rows="20"></textarea>
            </div>
            <div class="code-edit-footer">
              <YhButton @click="cancelEditCode">Cancel</YhButton>
              <YhButton type="primary" @click="saveEditCode(editingCodeBlock)">Save</YhButton>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-ai-bubble {
  --yh-ai-bubble-user-bg: var(--yh-color-primary);
  --yh-ai-bubble-user-color: var(--yh-color-white);
  --yh-ai-bubble-assistant-bg: var(--yh-fill-color-light);
  --yh-ai-bubble-assistant-color: var(--yh-text-color-primary);
  --yh-ai-bubble-border-radius: 12px;
  display: flex;
  margin-bottom: var(--yh-spacing-lg);
  gap: var(--yh-spacing-md);
  animation: yh-fade-in-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  /* Placement */
}
.yh-ai-bubble--placement-start {
  flex-direction: row;
}
.yh-ai-bubble--placement-start .yh-ai-bubble__content-wrapper {
  align-items: flex-start;
}

.yh-ai-bubble--placement-end {
  flex-direction: row-reverse;
}
.yh-ai-bubble--placement-end .yh-ai-bubble__content-wrapper {
  align-items: flex-end;
}

.yh-ai-bubble__avatar {
  flex-shrink: 0;
}

.yh-ai-bubble__content-wrapper {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  max-width: 85%;
}

.yh-ai-bubble__header {
  font-size: 12px;
  color: var(--yh-text-color-placeholder);
  display: flex;
  align-items: center;
  gap: 8px;
}

.yh-ai-bubble__time {
  opacity: 0.8;
}

.yh-ai-bubble__body {
  line-height: 1.6;
  word-break: break-word;
  border-radius: var(--yh-ai-bubble-border-radius);
  transition: all 0.3s;
  overflow: hidden;
  padding: 12px 16px;
  position: relative;
  border: 1px solid transparent;
}
.yh-ai-bubble__body.is-typing .yh-ai-bubble__text:not(.yh-ai-bubble__markdown) {
  position: relative;
  display: inline-block;
}
.yh-ai-bubble__body.is-typing .yh-ai-bubble__text:not(.yh-ai-bubble__markdown)::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1.1em;
  background-color: var(--yh-color-primary);
  margin-left: 4px;
  vertical-align: -2px;
  animation: yh-cursor-blink 1s infinite;
}

.yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > p:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > li:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h1:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h2:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h3:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h4:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h5:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h6:last-child {
  position: relative;
  display: inline-block;
}
.yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > p:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > li:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h1:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h2:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h3:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h4:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h5:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown > h6:last-child::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1.1em;
  background-color: var(--yh-color-primary);
  margin-left: 4px;
  vertical-align: -2px;
  animation: yh-cursor-blink 1s infinite;
}
.yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown ul:last-child > li:last-child, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown ol:last-child > li:last-child {
  position: relative;
  display: inline-block;
}
.yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown ul:last-child > li:last-child::after, .yh-ai-bubble__body.is-typing .yh-ai-bubble__markdown ol:last-child > li:last-child::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1.1em;
  background-color: var(--yh-color-primary);
  margin-left: 4px;
  vertical-align: -2px;
  animation: yh-cursor-blink 1s infinite;
}
.yh-ai-bubble__body.is-typing::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(var(--yh-color-primary-rgb), 0.05), transparent);
  background-size: 200% 100%;
  animation: yh-ripple-light 2s infinite linear;
  pointer-events: none;
  z-index: 5;
  border-radius: inherit;
}

.yh-ai-bubble__body {
  /* Theme-specific adjustments for quality */
}
html.dark .yh-ai-bubble__body {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);
}

.yh-ai-bubble {
  /* Shape Modifiers */
}
.yh-ai-bubble--shape-corner.yh-ai-bubble--placement-start .yh-ai-bubble__body {
  border-top-left-radius: 2px;
}

.yh-ai-bubble--shape-corner.yh-ai-bubble--placement-end .yh-ai-bubble__body {
  border-top-right-radius: 2px;
}

.yh-ai-bubble--shape-round .yh-ai-bubble__body {
  border-radius: 24px;
  padding: 12px 20px;
}

.yh-ai-bubble {
  /* Variant Modifiers */
}
.yh-ai-bubble--variant-filled.yh-ai-bubble--user .yh-ai-bubble__body {
  background-color: var(--yh-ai-bubble-user-bg);
  color: var(--yh-ai-bubble-user-color);
}

.yh-ai-bubble--variant-filled.yh-ai-bubble--assistant .yh-ai-bubble__body {
  background-color: var(--yh-ai-bubble-assistant-bg);
  color: var(--yh-ai-bubble-assistant-color);
  border-color: var(--yh-border-color-extra-light);
}

.yh-ai-bubble--variant-outlined .yh-ai-bubble__body {
  background-color: transparent;
  border: 1px solid var(--yh-border-color);
  color: var(--yh-text-color-primary);
}

.yh-ai-bubble--variant-outlined.yh-ai-bubble--user .yh-ai-bubble__body {
  border-color: var(--yh-color-primary);
}

.yh-ai-bubble--variant-shadow .yh-ai-bubble__body {
  background-color: var(--yh-bg-color-overlay);
  color: var(--yh-text-color-primary);
  box-shadow: var(--yh-shadow-md);
}

.yh-ai-bubble__text {
  white-space: pre-wrap;
}

.yh-ai-bubble__markdown {
  white-space: normal;
}
.yh-ai-bubble__markdown p {
  margin: 0 0 8px 0;
}
.yh-ai-bubble__markdown p:last-child {
  margin-bottom: 0;
}
.yh-ai-bubble__markdown p:last-child {
  margin-bottom: 0;
}
.yh-ai-bubble__markdown {
  /* 学术脚注气泡角标样式 */
}
.yh-ai-bubble__markdown .yh-ai-citation {
  color: var(--yh-color-primary);
  background-color: var(--yh-color-primary-light-9);
  border: 1px solid var(--yh-color-primary-light-5);
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
  padding: 0 4px;
  margin: 0 2px;
  border-radius: 4px;
  cursor: pointer;
  text-decoration: none;
  transition: all var(--yh-transition-duration-fast) ease;
  vertical-align: super;
}
.yh-ai-bubble__markdown .yh-ai-citation:hover {
  background-color: var(--yh-color-primary);
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(var(--yh-color-primary-rgb), 0.3);
}
.yh-ai-bubble__markdown pre {
  background: #282c34;
  color: #abb2bf;
  padding: 12px;
  border-radius: 8px;
  overflow-x: auto;
  margin: 12px 0;
}
.yh-ai-bubble__markdown code {
  background: rgba(127, 127, 127, 0.15);
  padding: 2px 4px;
  border-radius: 4px;
  font-size: 0.9em;
}

.yh-ai-bubble {
  /* Code Output - 流式输出样式 */
}
.yh-ai-bubble .code-output {
  background: #1e1e1e;
  border-top: 1px solid #3e3e3e;
  padding: 12px;
  border-radius: 0 0 8px 8px;
  margin-top: -8px;
  font-family: "SF Mono", "Fira Code", Consolas, monospace;
  font-size: 13px;
  line-height: 1.6;
  color: #d4d4d4;
  max-height: 300px;
  overflow-y: auto;
}
.yh-ai-bubble .code-output pre {
  margin: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  white-space: pre-wrap;
  word-break: break-all;
}
.yh-ai-bubble .code-output .output-prefix {
  color: #569cd6;
  display: block;
  margin-bottom: 4px;
}
.yh-ai-bubble .code-output .output-return {
  color: #c586c0;
  display: block;
  margin-top: 8px;
  font-style: italic;
}
.yh-ai-bubble .code-output .output-success {
  color: #4ec9b0;
  display: block;
  margin-top: 8px;
}
.yh-ai-bubble .code-output .output-error {
  color: #f14c4c;
  display: block;
  margin-top: 8px;
}
.yh-ai-bubble .code-output .output-warning {
  color: #cca700;
  display: block;
}
.yh-ai-bubble {
  /* Multimodal: Image Grid */
}
.yh-ai-bubble__image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
  width: 100%;
}

.yh-ai-bubble__image-item {
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--yh-border-color-lighter);
}
.yh-ai-bubble__image-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s;
  cursor: zoom-in;
}
.yh-ai-bubble__image-item img:hover {
  transform: scale(1.05);
}

.yh-ai-bubble {
  /* Multimodal: Assets (Audio, Files) */
}
.yh-ai-bubble__multimodal-assets {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.yh-ai-bubble__audio-player {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.03);
  border-radius: 20px;
  width: fit-content;
  min-width: 200px;
  border: 1px solid var(--yh-border-color-extra-light);
}
html.dark .yh-ai-bubble__audio-player {
  background: rgba(255, 255, 255, 0.05);
}

.yh-ai-bubble__audio-waveform {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 16px;
  flex: 1;
}
.yh-ai-bubble__audio-waveform span {
  width: 2px;
  height: 60%;
  background: currentColor;
  border-radius: 1px;
  opacity: 0.4;
  animation: yh-waveform 1.2s ease-in-out infinite;
  animation-play-state: paused;
  animation-delay: var(--delay);
}
.is-active .yh-ai-bubble__audio-waveform span {
  animation-play-state: running;
  opacity: 0.8;
}

.yh-ai-bubble__audio-duration {
  font-size: 12px;
  opacity: 0.7;
  font-family: var(--yh-font-family-mono);
}

.yh-ai-bubble__file-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: var(--yh-bg-color-overlay);
  border: 1px solid var(--yh-border-color-lighter);
  border-radius: 10px;
  transition: all 0.2s;
}
.yh-ai-bubble__file-card:hover {
  border-color: var(--yh-color-primary);
  box-shadow: var(--yh-shadow-sm);
}

.yh-ai-bubble__file-icon {
  width: 36px;
  height: 36px;
  background: var(--yh-fill-color-light);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--yh-color-primary);
  font-size: 20px;
}

.yh-ai-bubble__file-info {
  flex: 1;
  min-width: 0;
}

.yh-ai-bubble__file-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--yh-text-color-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.yh-ai-bubble__file-meta {
  font-size: 11px;
  color: var(--yh-text-color-placeholder);
  margin-top: 2px;
}

.yh-ai-bubble {
  /* Citations Section - Correctly placed */
}
.yh-ai-bubble__citations {
  margin-top: var(--yh-spacing-md);
  padding-top: var(--yh-spacing-sm);
  border-top: 1px solid var(--yh-border-color-lighter);
  display: flex;
  flex-direction: column;
  gap: var(--yh-spacing-xs);
}

.yh-ai-bubble__citations-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--yh-text-color-secondary);
  font-weight: 500;
}

.yh-ai-bubble__citations-count {
  background: var(--yh-fill-color-light);
  padding: 0 6px;
  border-radius: 10px;
  font-size: 11px;
  margin-left: 4px;
}

.yh-ai-bubble__citations-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.yh-ai-bubble__citation-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 6px;
  background: var(--yh-bg-color-overlay);
  border: 1px solid var(--yh-border-color-lighter);
  border-radius: 6px;
  font-size: 12px;
  text-decoration: none;
  color: var(--yh-text-color-primary);
  max-width: 220px;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.yh-ai-bubble__citation-item:hover {
  background: var(--yh-fill-color-light);
  border-color: var(--yh-color-primary-light-5);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.yh-ai-bubble__citation-item:hover .yh-ai-bubble__citation-index {
  background: var(--yh-color-primary);
  color: white;
}

.yh-ai-bubble__citation-index {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  background: var(--yh-fill-color-dark);
  border-radius: 4px;
  font-size: 10px;
  font-weight: bold;
  color: var(--yh-text-color-secondary);
}

.yh-ai-bubble__citation-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.9;
}

.yh-ai-bubble__typing-indicator {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.yh-ai-bubble__typing-indicator span {
  width: 4px;
  height: 4px;
  background-color: currentColor;
  border-radius: 50%;
  animation: yh-typing-bounce 1.4s infinite ease-in-out both;
}
.yh-ai-bubble__typing-indicator span:nth-child(1) {
  animation-delay: -0.32s;
}
.yh-ai-bubble__typing-indicator span:nth-child(2) {
  animation-delay: -0.16s;
}

.yh-ai-bubble__footer {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
  font-size: 12px;
}

@keyframes yh-typing-bounce {
  0%, 80%, 100% {
    transform: scale(0);
    opacity: 0.5;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}
@keyframes yh-waveform {
  0%, 100% {
    height: 40%;
  }
  50% {
    height: 100%;
  }
}
@keyframes yh-fade-in-up {
  0% {
    opacity: 0;
    transform: translateY(10px) scale(0.95);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}
@keyframes yh-cursor-blink {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
@keyframes yh-ripple-light {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
/* 引用专属独立悬浮卡片 (Teleported 到 Body) */
.yh-ai-bubble__citation-tooltip-wrapper {
  position: fixed;
  padding-bottom: 10px;
  z-index: 9999;
  pointer-events: auto;
}
.yh-ai-bubble__citation-tooltip-wrapper .yh-ai-bubble__citation-tooltip {
  min-width: 260px;
  max-width: 380px;
  background: var(--yh-bg-color-overlay, #ffffff);
  border: 1px solid var(--yh-border-color-light, #ebeef5);
  border-radius: var(--yh-border-radius-large, 8px);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  padding: 12px 16px;
  position: relative;
}
.yh-ai-bubble__citation-tooltip-wrapper .yh-ai-bubble__citation-tooltip::after {
  content: "";
  position: absolute;
  bottom: -6px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
  width: 12px;
  height: 12px;
  background: inherit;
  border-right: 1px solid var(--yh-border-color-light, #ebeef5);
  border-bottom: 1px solid var(--yh-border-color-light, #ebeef5);
}
.yh-ai-bubble__citation-tooltip-wrapper .yh-ai-bubble__citation-tooltip-header {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  color: var(--yh-text-color-secondary, #909399);
  font-size: 12px;
  font-weight: 500;
}
.yh-ai-bubble__citation-tooltip-wrapper .yh-ai-bubble__citation-tooltip-body h4 {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--yh-text-color-primary, #303133);
}

.yh-fade-in-scale-up-enter-active,
.yh-fade-in-scale-up-leave-active {
  transition: opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1), transform 0.2s cubic-bezier(0.23, 1, 0.32, 1);
  transform-origin: bottom center;
}

.yh-fade-in-scale-up-enter-from,
.yh-fade-in-scale-up-leave-to {
  opacity: 0;
  transform: translate(-50%, -85%) scale(0.95) !important;
}

.code-block-wrapper {
  margin: 12px 0;
  border-radius: 8px;
  overflow: hidden;
  background: #282c34;
}
.code-block-wrapper .code-block-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  background: #21252b;
}
.code-block-wrapper .code-block-header .code-lang {
  font-size: 12px;
  color: #abb2bf;
  text-transform: uppercase;
}
.code-block-wrapper .code-block-header .code-actions {
  display: flex;
  gap: 8px;
}
.code-block-wrapper .code-block-header .code-actions .code-action-btn {
  padding: 4px 8px;
  font-size: 11px;
  color: #abb2bf;
  background: transparent;
  border: 1px solid #3e4451;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}
.code-block-wrapper .code-block-header .code-actions .code-action-btn:hover {
  background: #3e4451;
  color: #fff;
}
.code-block-wrapper .code-block-header .code-actions .code-action-btn.copy-btn.copied {
  color: #98c379;
  border-color: #98c379;
}
.code-block-wrapper .code-block-header .code-actions .code-action-btn.run-btn.running {
  color: #61afef;
  border-color: #61afef;
}
.code-block-wrapper .code-block-body {
  padding: 12px;
  overflow-x: auto;
}
.code-block-wrapper .code-block-body.collapsed {
  max-height: 200px;
  overflow: hidden;
  position: relative;
}
.code-block-wrapper .code-block-body.collapsed::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 40px;
  background: linear-gradient(transparent, #282c34);
}
.code-block-wrapper .code-block-body pre {
  margin: 0;
  font-family: "Fira Code", "Consolas", monospace;
  font-size: 13px;
  line-height: 1.5;
}
.code-block-wrapper .code-block-body code {
  font-family: inherit;
}
.code-block-wrapper .code-output {
  padding: 12px;
  background: #1e1e1e;
  border-top: 1px solid #3e4451;
}
.code-block-wrapper .code-output pre {
  margin: 0;
  color: #d4d4d4;
  font-size: 12px;
}

.mermaid-block {
  margin: 16px 0;
  padding: 16px;
  background: #fafafa;
  border-radius: 8px;
  text-align: center;
}
.mermaid-block .mermaid-rendered svg {
  max-width: 100%;
  height: auto;
}
.mermaid-block .mermaid-error {
  color: #e74c3c;
  text-align: left;
  font-size: 12px;
}

.structured-data {
  margin: 12px 0;
}
.structured-data .json-viewer {
  background: #282c34;
  border-radius: 8px;
  padding: 12px;
  overflow-x: auto;
}
.structured-data .json-viewer pre {
  margin: 0;
  color: #abb2bf;
  font-size: 13px;
  font-family: "Fira Code", monospace;
}
.structured-data .table-viewer {
  overflow-x: auto;
}
.structured-data .table-viewer table {
  width: 100%;
  border-collapse: collapse;
}
.structured-data .table-viewer table th,
.structured-data .table-viewer table td {
  padding: 8px 12px;
  border: 1px solid var(--yh-border-color-light);
  text-align: left;
}
.structured-data .table-viewer table th {
  background: var(--yh-bg-color-page);
  font-weight: 600;
}
.structured-data .thought-chain {
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: relative;
  padding-left: 8px;
}
.structured-data .thought-chain .thought-step {
  display: flex;
  gap: 12px;
  position: relative;
  padding-bottom: 12px;
}
.structured-data .thought-chain .thought-step:last-child {
  padding-bottom: 0;
}
.structured-data .thought-chain .thought-step .thought-index {
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--yh-color-primary);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  flex-shrink: 0;
  margin-top: 4px;
}
.structured-data .thought-chain .thought-step::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 20px;
  bottom: 0;
  width: 2px;
  background: var(--yh-border-color-lighter);
}
.structured-data .thought-chain .thought-step:last-child::before {
  bottom: 18px;
}
.structured-data .thought-chain .thought-step .thought-content {
  flex: 1;
  background: var(--yh-fill-color-light);
  border-radius: 6px;
  padding: 8px 10px;
}
.structured-data .thought-chain .thought-step .thought-content .thought-title {
  font-weight: 600;
  margin-bottom: 4px;
  font-size: 13px;
}
.structured-data .thought-chain .thought-step .thought-content .thought-text {
  font-size: 13px;
  color: var(--yh-text-color-secondary);
  white-space: pre-wrap;
}

.citation-tooltip-wrapper .citation-tooltip {
  max-width: 320px;
  padding: 12px;
  background: var(--yh-bg-color);
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.citation-tooltip-wrapper .citation-tooltip .citation-tooltip-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 12px;
  color: var(--yh-text-color-secondary);
}
.citation-tooltip-wrapper .citation-tooltip .citation-tooltip-body h4 {
  margin: 0 0 8px;
  font-size: 14px;
  font-weight: 600;
}
.citation-tooltip-wrapper .citation-tooltip .citation-tooltip-body p {
  margin: 0;
  font-size: 13px;
  color: var(--yh-text-color-secondary);
  line-height: 1.5;
}
.citation-tooltip-wrapper .citation-tooltip .citation-tooltip-body .publish-time {
  display: block;
  margin-top: 8px;
  font-size: 11px;
  color: var(--yh-text-color-disabled);
}

.code-edit-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
}
.code-edit-modal-overlay .code-edit-modal {
  width: 80%;
  max-width: 800px;
  background: var(--yh-bg-color);
  border-radius: 12px;
  overflow: hidden;
}
.code-edit-modal-overlay .code-edit-modal .code-edit-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid var(--yh-border-color-light);
}
.code-edit-modal-overlay .code-edit-modal .code-edit-header h3 {
  margin: 0;
  font-size: 16px;
}
.code-edit-modal-overlay .code-edit-modal .code-edit-body {
  padding: 16px;
}
.code-edit-modal-overlay .code-edit-modal .code-edit-body textarea {
  width: 100%;
  padding: 12px;
  font-family: "Fira Code", monospace;
  font-size: 13px;
  border: 1px solid var(--yh-border-color);
  border-radius: 8px;
  resize: vertical;
  background: #282c34;
  color: #abb2bf;
}
.code-edit-modal-overlay .code-edit-modal .code-edit-body textarea:focus {
  outline: none;
  border-color: var(--yh-color-primary);
}
.code-edit-modal-overlay .code-edit-modal .code-edit-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px;
  border-top: 1px solid var(--yh-border-color-light);
}
</style>
