/* =========================================================
 *  vLLM WebUI —— 原生 HTML+JS+CSS 实现
 *  兼容 vLLM 的 OpenAI 风格接口 ( /v1/models, /v1/chat/completions )
 * ========================================================= */

(() => {
  'use strict';

  // -------------------- 默认配置 --------------------
  const DEFAULT_CONFIG = {
    baseUrl: 'http://localhost:4321/v1',
    apiKey: 'EMPTY',
    model: '',
    system: '你是一个有用的助手。',
    temperature: 0.7,
    top_p: 0.9,
    top_k: -1,
    max_tokens: 2048,
    frequency_penalty: 0,
    presence_penalty: 0,
    repetition_penalty: 1,
    seed: '',
    stop: '',
    stream: true,
    renderMd: true,
    showUsage: true,
    theme: 'light',
  };

  const STORAGE_KEY = 'vllm-webui:v1';
  const CONV_KEY = 'vllm-webui:conversations:v1';

  // -------------------- 状态 --------------------
  const state = {
    config: loadConfig(),
    conversations: loadConversations(),
    currentId: null,
    controller: null,    // AbortController for streaming
    generating: false,
  };

  // -------------------- DOM --------------------
  const $ = (id) => document.getElementById(id);
  const el = {
    sidebar: $('sidebar'),
    toggleSidebar: $('toggle-sidebar'),
    showSidebar: $('show-sidebar'),
    newChat: $('new-chat-btn'),
    convList: $('conversation-list'),
    settingsBtn: $('settings-btn'),
    themeBtn: $('theme-btn'),
    themeIcon: $('theme-icon'),
    hljsTheme: $('hljs-theme'),
    currentTitle: $('current-title'),
    connStatus: $('conn-status'),
    modelSelect: $('model-select'),
    messages: $('messages'),
    input: $('input'),
    sendBtn: $('send-btn'),
    stopBtn: $('stop-btn'),
    statsHint: $('stats-hint'),
    modal: $('settings-modal'),
    toast: $('toast'),
    // config inputs
    cfg: {
      baseUrl: $('cfg-base-url'),
      apiKey: $('cfg-api-key'),
      model: $('cfg-model'),
      fetchModels: $('cfg-fetch-models'),
      system: $('cfg-system'),
      temperature: $('cfg-temperature'),
      top_p: $('cfg-top_p'),
      top_k: $('cfg-top_k'),
      max_tokens: $('cfg-max_tokens'),
      frequency_penalty: $('cfg-frequency_penalty'),
      presence_penalty: $('cfg-presence_penalty'),
      repetition_penalty: $('cfg-repetition_penalty'),
      seed: $('cfg-seed'),
      stop: $('cfg-stop'),
      stream: $('cfg-stream'),
      renderMd: $('cfg-render-md'),
      showUsage: $('cfg-show-usage'),
      save: $('cfg-save'),
    },
  };

  // -------------------- 持久化 --------------------
  function loadConfig() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return { ...DEFAULT_CONFIG };
      return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
    } catch {
      return { ...DEFAULT_CONFIG };
    }
  }
  function saveConfig() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state.config));
  }
  function loadConversations() {
    try {
      return JSON.parse(localStorage.getItem(CONV_KEY) || '[]');
    } catch {
      return [];
    }
  }
  function saveConversations() {
    localStorage.setItem(CONV_KEY, JSON.stringify(state.conversations));
  }

  // -------------------- 工具函数 --------------------
  function uid() {
    return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
  }
  function toast(msg, isError = false, ms = 2500) {
    el.toast.textContent = msg;
    el.toast.classList.remove('hidden');
    el.toast.classList.toggle('error', isError);
    clearTimeout(toast._t);
    toast._t = setTimeout(() => el.toast.classList.add('hidden'), ms);
  }
  function applyTheme() {
    document.documentElement.dataset.theme = state.config.theme;
    // 根据主题切换 highlight.js 样式
    const dark = state.config.theme === 'dark';
    el.hljsTheme.href = dark
      ? 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css'
      : 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css';
    el.themeIcon.innerHTML = dark
      ? '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>'
      : '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
  }

  // -------------------- Markdown 渲染 --------------------
  if (window.marked) {
    marked.setOptions({ breaks: true, gfm: true });
  }

  function renderContent(text, isMarkdown) {
    if (!isMarkdown) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML.replace(/\n/g, '<br>');
    }
    const html = marked.parse(text || '');
    return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
  }

  function enhanceCodeBlocks(container) {
    container.querySelectorAll('pre code').forEach((code) => {
      if (code.dataset.hl === '1') return;
      try {
        hljs.highlightElement(code);
      } catch { /* ignore */ }
      code.dataset.hl = '1';
    });
    container.querySelectorAll('pre').forEach((pre) => {
      if (pre.querySelector('.copy-code')) return;
      const btn = document.createElement('button');
      btn.className = 'copy-code';
      btn.textContent = '复制';
      btn.addEventListener('click', () => {
        const code = pre.querySelector('code');
        if (!code) return;
        navigator.clipboard.writeText(code.innerText).then(() => {
          btn.textContent = '已复制';
          setTimeout(() => (btn.textContent = '复制'), 1500);
        });
      });
      pre.appendChild(btn);
    });
  }

  // -------------------- 对话管理 --------------------
  function createConversation() {
    const conv = {
      id: uid(),
      title: '新对话',
      created: Date.now(),
      messages: [],
    };
    state.conversations.unshift(conv);
    state.currentId = conv.id;
    saveConversations();
    renderConversationList();
    renderMessages();
    return conv;
  }
  function getConv() {
    return state.conversations.find((c) => c.id === state.currentId);
  }
  function selectConversation(id) {
    state.currentId = id;
    renderConversationList();
    renderMessages();
  }
  function deleteConversation(id) {
    const idx = state.conversations.findIndex((c) => c.id === id);
    if (idx < 0) return;
    state.conversations.splice(idx, 1);
    saveConversations();
    if (state.currentId === id) {
      if (state.conversations.length) {
        state.currentId = state.conversations[0].id;
      } else {
        createConversation();
        return;
      }
    }
    renderConversationList();
    renderMessages();
  }
  function updateConversationTitle(conv) {
    const first = conv.messages.find((m) => m.role === 'user');
    if (first && (conv.title === '新对话' || !conv.title)) {
      conv.title = first.content.slice(0, 30).replace(/\n/g, ' ') || '新对话';
      saveConversations();
      renderConversationList();
    }
  }

  // -------------------- UI 渲染 --------------------
  function bucketOf(ts) {
    const now = new Date();
    const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
    const dayMs = 86400000;
    if (ts >= startOfToday) return '今天';
    if (ts >= startOfToday - dayMs) return '昨天';
    if (ts >= startOfToday - 7 * dayMs) return '过去 7 天';
    if (ts >= startOfToday - 30 * dayMs) return '过去 30 天';
    return '更早';
  }

  function renderConversationList() {
    el.convList.innerHTML = '';
    const buckets = new Map();
    state.conversations.forEach((c) => {
      const b = bucketOf(c.created || 0);
      if (!buckets.has(b)) buckets.set(b, []);
      buckets.get(b).push(c);
    });
    const order = ['今天', '昨天', '过去 7 天', '过去 30 天', '更早'];
    order.forEach((name) => {
      const group = buckets.get(name);
      if (!group || !group.length) return;
      const label = document.createElement('div');
      label.className = 'conv-group';
      label.textContent = name;
      el.convList.appendChild(label);
      group.forEach((c) => {
        const item = document.createElement('div');
        item.className = 'conv-item' + (c.id === state.currentId ? ' active' : '');
        item.innerHTML = `
          <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
          <span class="title"></span>
          <button class="delete" title="删除">
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
          </button>`;
        item.querySelector('.title').textContent = c.title || '新对话';
        item.addEventListener('click', () => selectConversation(c.id));
        item.querySelector('.delete').addEventListener('click', (e) => {
          e.stopPropagation();
          if (confirm(`删除对话 "${c.title}" ？`)) deleteConversation(c.id);
        });
        el.convList.appendChild(item);
      });
    });
  }

  function renderMessages() {
    const conv = getConv();
    el.messages.innerHTML = '';
    if (!conv || conv.messages.length === 0) {
      const hint = document.createElement('div');
      hint.className = 'welcome';
      const modelName = state.config.model || 'vLLM';
      const suggestions = [
        { icon: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>', title: '解释概念', desc: '解释 Transformer 的注意力机制', prompt: '用简洁的语言解释一下 Transformer 里的注意力机制，并举个例子。' },
        { icon: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>', title: '写代码', desc: '用 Python 实现带注释的快速排序', prompt: '帮我用 Python 写一个快速排序，并加上详细的注释说明每一步。' },
        { icon: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>', title: '学习规划', desc: '深度学习入门路径', prompt: '给我一份面向初学者的深度学习学习路径，包含推荐的书籍、课程和项目。' },
        { icon: '<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>', title: '翻译', desc: '中文翻译成地道英文', prompt: '把下面这段话翻译成地道的英文：\n人工智能正在深刻改变各行各业的工作方式。' },
      ];
      hint.innerHTML = `
        <div class="welcome-aurora"></div>
        <div class="welcome-logo">
          <svg viewBox="0 0 24 24" width="38" height="38" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
            <path d="M12 2 L2 7 L12 12 L22 7 Z"/>
            <path d="M2 17 L12 22 L22 17"/>
            <path d="M2 12 L12 17 L22 12"/>
          </svg>
        </div>
        <h1 class="welcome-title">你好，我是 <span>${escapeHtml(modelName)}</span></h1>
        <p class="welcome-sub">基于 vLLM · <code>${escapeHtml(state.config.baseUrl)}</code></p>
        <div class="welcome-grid">
          ${suggestions.map(s => `
            <button class="suggest" data-prompt="${escapeHtml(s.prompt)}">
              <span class="suggest-icon">
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${s.icon}</svg>
              </span>
              <span class="suggest-text">
                <span class="suggest-title">${s.title}</span>
                <span class="suggest-desc">${s.desc}</span>
              </span>
            </button>
          `).join('')}
        </div>`;
      el.messages.appendChild(hint);
      hint.querySelectorAll('.suggest').forEach((btn) => {
        btn.addEventListener('click', () => {
          el.input.value = btn.dataset.prompt;
          autoResize();
          el.input.focus();
        });
      });
      el.currentTitle.textContent = conv ? conv.title : '新对话';
      return;
    }
    el.currentTitle.textContent = conv.title;
    conv.messages.forEach((m, idx) => {
      el.messages.appendChild(renderMessage(m, idx));
    });
    scrollToBottom();
  }

  function escapeHtml(s) {
    return (s || '').replace(/[&<>"']/g, (c) =>
      ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])
    );
  }

  function renderMessage(msg, idx) {
    const wrap = document.createElement('div');
    wrap.className = 'msg ' + msg.role;
    wrap.dataset.idx = idx;

    const avatar = document.createElement('div');
    avatar.className = 'avatar';
    avatar.innerHTML = msg.role === 'user'
      ? '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'
      : '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L3 7 L3 17 L12 22 L21 17 L21 7 Z"/><path d="M12 2 L12 22"/><path d="M3 7 L21 17"/><path d="M21 7 L3 17"/></svg>';

    const body = document.createElement('div');
    body.className = 'body';

    const role = document.createElement('div');
    role.className = 'role';
    role.textContent = msg.role === 'user' ? '用户' : '助手';

    const content = document.createElement('div');
    content.className = 'content';
    const useMd = state.config.renderMd && msg.role !== 'user';
    content.innerHTML = renderContent(msg.content, useMd);
    if (useMd) enhanceCodeBlocks(content);

    const actions = document.createElement('div');
    actions.className = 'actions';
    const ICON_COPY = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
    const ICON_EDIT = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
    const ICON_REGEN = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>';
    const ICON_DEL = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
    actions.appendChild(makeAction(ICON_COPY + '复制', () => {
      navigator.clipboard.writeText(msg.content);
      toast('已复制');
    }));
    if (msg.role === 'user') {
      actions.appendChild(makeAction(ICON_EDIT + '编辑', () => editMessage(idx)));
    } else {
      actions.appendChild(makeAction(ICON_REGEN + '重新生成', () => regenerate(idx)));
    }
    actions.appendChild(makeAction(ICON_DEL + '删除', () => {
      const conv = getConv();
      conv.messages.splice(idx, 1);
      saveConversations();
      renderMessages();
    }));

    body.appendChild(role);
    body.appendChild(content);
    if (msg.stats) {
      const stats = document.createElement('div');
      stats.className = 'stats';
      stats.textContent = formatStats(msg.stats);
      body.appendChild(stats);
    }
    body.appendChild(actions);

    wrap.appendChild(avatar);
    wrap.appendChild(body);
    return wrap;
  }

  function makeAction(html, handler) {
    const btn = document.createElement('button');
    btn.innerHTML = html;
    btn.addEventListener('click', handler);
    return btn;
  }

  function formatStats(s) {
    const parts = [];
    if (s.prompt_tokens != null) parts.push(`prompt: ${s.prompt_tokens}`);
    if (s.completion_tokens != null) parts.push(`completion: ${s.completion_tokens}`);
    if (s.total_tokens != null) parts.push(`total: ${s.total_tokens}`);
    if (s.elapsed != null) parts.push(`${s.elapsed.toFixed(2)}s`);
    if (s.tps != null) parts.push(`${s.tps.toFixed(1)} tok/s`);
    return parts.join(' · ');
  }

  function scrollToBottom(smooth = false) {
    const container = document.getElementById('chat');
    container.scrollTo({ top: container.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
  }

  // -------------------- 发送消息 --------------------
  function editMessage(idx) {
    const conv = getConv();
    const msg = conv.messages[idx];
    if (!msg || msg.role !== 'user') return;
    el.input.value = msg.content;
    conv.messages.splice(idx);
    saveConversations();
    renderMessages();
    autoResize();
    el.input.focus();
  }

  function regenerate(assistantIdx) {
    const conv = getConv();
    // 从 assistant 消息位置截断
    conv.messages.splice(assistantIdx);
    saveConversations();
    renderMessages();
    runGeneration();
  }

  async function sendMessage() {
    const text = el.input.value.trim();
    if (!text || state.generating) return;
    if (!state.currentId) createConversation();

    const conv = getConv();
    conv.messages.push({ role: 'user', content: text });
    updateConversationTitle(conv);
    saveConversations();

    el.input.value = '';
    autoResize();
    renderMessages();
    await runGeneration();
  }

  function buildRequestMessages() {
    const conv = getConv();
    const msgs = [];
    if (state.config.system && state.config.system.trim()) {
      msgs.push({ role: 'system', content: state.config.system });
    }
    conv.messages.forEach((m) => {
      msgs.push({ role: m.role, content: m.content });
    });
    return msgs;
  }

  function buildRequestBody() {
    const c = state.config;
    const body = {
      model: c.model || 'default',
      messages: buildRequestMessages(),
      stream: !!c.stream,
      temperature: Number(c.temperature),
      top_p: Number(c.top_p),
      max_tokens: Number(c.max_tokens),
      frequency_penalty: Number(c.frequency_penalty),
      presence_penalty: Number(c.presence_penalty),
    };
    if (c.stream) body.stream_options = { include_usage: true };
    if (Number(c.top_k) >= 0) body.top_k = Number(c.top_k);
    if (Number(c.repetition_penalty) && Number(c.repetition_penalty) !== 1) {
      body.repetition_penalty = Number(c.repetition_penalty);
    }
    if (c.seed !== '' && c.seed != null) body.seed = Number(c.seed);
    if (c.stop && c.stop.trim()) {
      body.stop = c.stop.split(',').map((s) => s.trim()).filter(Boolean);
    }
    return body;
  }

  async function runGeneration() {
    const conv = getConv();
    if (!conv) return;

    // 先构造请求体（此时 messages 中还没有空的 assistant 占位，避免把空消息发给模型）
    const body = buildRequestBody();

    const assistantMsg = { role: 'assistant', content: '', stats: null };
    conv.messages.push(assistantMsg);
    saveConversations();
    renderMessages();

    // 定位刚插入的 assistant DOM
    const assistantIdx = conv.messages.length - 1;
    const msgEl = el.messages.querySelector(`.msg[data-idx="${assistantIdx}"]`);
    const contentEl = msgEl ? msgEl.querySelector('.content') : null;
    if (contentEl) {
      contentEl.classList.add('typing');
      contentEl.innerHTML = '<span class="typing-dots"><span></span><span></span><span></span></span>';
    }

    state.generating = true;
    el.sendBtn.style.display = 'none';
    el.stopBtn.style.display = 'flex';

    state.controller = new AbortController();
    const url = joinUrl(state.config.baseUrl, '/chat/completions');
    const headers = { 'Content-Type': 'application/json' };
    if (state.config.apiKey && state.config.apiKey !== 'EMPTY') {
      headers['Authorization'] = `Bearer ${state.config.apiKey}`;
    }
    const started = performance.now();

    try {
      const resp = await fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify(body),
        signal: state.controller.signal,
      });

      if (!resp.ok) {
        const txt = await resp.text();
        throw new Error(`HTTP ${resp.status}: ${txt.slice(0, 400)}`);
      }

      if (body.stream) {
        await readStream(resp, assistantMsg, contentEl, started);
      } else {
        const data = await resp.json();
        const choice = data.choices?.[0];
        assistantMsg.content = choice?.message?.content || '';
        if (data.usage) {
          const elapsed = (performance.now() - started) / 1000;
          assistantMsg.stats = {
            ...data.usage,
            elapsed,
            tps: data.usage.completion_tokens / elapsed,
          };
        }
      }
      setConnStatus('ok', '已连接');
    } catch (err) {
      if (err.name === 'AbortError') {
        assistantMsg.content += '\n\n_（已中止）_';
      } else {
        assistantMsg.content += `\n\n**错误**：${escapeHtml(err.message)}`;
        setConnStatus('err', '错误');
        toast('请求失败：' + err.message, true, 4000);
      }
    } finally {
      state.generating = false;
      state.controller = null;
      el.sendBtn.style.display = 'flex';
      el.stopBtn.style.display = 'none';
      saveConversations();
      renderMessages();
    }
  }

  async function readStream(resp, assistantMsg, contentEl, started) {
    const reader = resp.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let buffer = '';
    let firstTokenAt = null;
    let usage = null;
    let rafPending = false;

    const updateUI = () => {
      rafPending = false;
      if (!contentEl) return;
      contentEl.classList.remove('typing');
      const useMd = state.config.renderMd;
      contentEl.innerHTML = renderContent(assistantMsg.content, useMd);
      if (useMd) enhanceCodeBlocks(contentEl);
      contentEl.classList.add('cursor');
      scrollToBottom();
      if (state.config.showUsage && firstTokenAt) {
        const elapsed = (performance.now() - started) / 1000;
        const charsPerSec = assistantMsg.content.length / elapsed;
        el.statsHint.textContent = `生成中… ${elapsed.toFixed(1)}s · ≈${charsPerSec.toFixed(0)} ch/s`;
      }
    };

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });

      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || !trimmed.startsWith('data:')) continue;
        const data = trimmed.slice(5).trim();
        if (data === '[DONE]') continue;
        try {
          const json = JSON.parse(data);
          const delta = json.choices?.[0]?.delta?.content;
          if (delta) {
            if (firstTokenAt == null) firstTokenAt = performance.now();
            assistantMsg.content += delta;
            if (!rafPending) {
              rafPending = true;
              requestAnimationFrame(updateUI);
            }
          }
          if (json.usage) usage = json.usage;
        } catch (e) {
          // 忽略解析错误行（可能是心跳）
        }
      }
    }

    // 最终渲染
    if (contentEl) {
      contentEl.classList.remove('cursor');
      const useMd = state.config.renderMd;
      contentEl.innerHTML = renderContent(assistantMsg.content, useMd);
      if (useMd) enhanceCodeBlocks(contentEl);
    }

    const elapsed = (performance.now() - started) / 1000;
    if (usage) {
      assistantMsg.stats = {
        ...usage,
        elapsed,
        tps: usage.completion_tokens ? usage.completion_tokens / elapsed : undefined,
      };
    } else {
      assistantMsg.stats = { elapsed };
    }
    el.statsHint.innerHTML = '按 <kbd>Enter</kbd> 发送 · <kbd>Shift</kbd> + <kbd>Enter</kbd> 换行';
  }

  function stopGeneration() {
    if (state.controller) state.controller.abort();
  }

  // -------------------- 连接 / 模型 --------------------
  function joinUrl(base, path) {
    return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
  }

  function setConnStatus(kind, text) {
    el.connStatus.classList.remove('ok', 'err');
    if (kind) el.connStatus.classList.add(kind);
    el.connStatus.querySelector('.text').textContent = text;
  }

  async function fetchModels(silent = false) {
    try {
      const url = joinUrl(state.config.baseUrl, '/models');
      const headers = {};
      if (state.config.apiKey && state.config.apiKey !== 'EMPTY') {
        headers['Authorization'] = `Bearer ${state.config.apiKey}`;
      }
      const resp = await fetch(url, { headers });
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      const data = await resp.json();
      const models = (data.data || []).map((m) => m.id);
      populateModelSelect(models);
      setConnStatus('ok', `已连接 (${models.length} 个模型)`);
      if (!silent) toast(`发现 ${models.length} 个模型`);
      return models;
    } catch (err) {
      setConnStatus('err', '未连接');
      if (!silent) toast('获取模型失败：' + err.message, true, 3500);
      return [];
    }
  }

  function populateModelSelect(models) {
    el.modelSelect.innerHTML = '';
    if (models.length === 0) {
      const opt = document.createElement('option');
      opt.value = '';
      opt.textContent = '(未获取到模型)';
      el.modelSelect.appendChild(opt);
      return;
    }
    models.forEach((m) => {
      const opt = document.createElement('option');
      opt.value = m;
      opt.textContent = m;
      el.modelSelect.appendChild(opt);
    });
    // 选中当前配置的模型，若无则使用第一个
    if (state.config.model && models.includes(state.config.model)) {
      el.modelSelect.value = state.config.model;
    } else {
      state.config.model = models[0];
      el.modelSelect.value = models[0];
      saveConfig();
    }
  }

  // -------------------- 设置弹窗 --------------------
  function openSettings() {
    const c = state.config;
    el.cfg.baseUrl.value = c.baseUrl;
    el.cfg.apiKey.value = c.apiKey;
    el.cfg.model.value = c.model;
    el.cfg.system.value = c.system;
    el.cfg.temperature.value = c.temperature;
    el.cfg.top_p.value = c.top_p;
    el.cfg.top_k.value = c.top_k;
    el.cfg.max_tokens.value = c.max_tokens;
    el.cfg.frequency_penalty.value = c.frequency_penalty;
    el.cfg.presence_penalty.value = c.presence_penalty;
    el.cfg.repetition_penalty.value = c.repetition_penalty;
    el.cfg.seed.value = c.seed;
    el.cfg.stop.value = c.stop;
    el.cfg.stream.checked = c.stream;
    el.cfg.renderMd.checked = c.renderMd;
    el.cfg.showUsage.checked = c.showUsage;
    updateRangeLabels();
    el.modal.classList.remove('hidden');
  }
  function closeSettings() {
    el.modal.classList.add('hidden');
  }
  function saveSettings() {
    const c = state.config;
    c.baseUrl = el.cfg.baseUrl.value.trim() || DEFAULT_CONFIG.baseUrl;
    c.apiKey = el.cfg.apiKey.value.trim() || 'EMPTY';
    c.model = el.cfg.model.value.trim();
    c.system = el.cfg.system.value;
    c.temperature = parseFloat(el.cfg.temperature.value);
    c.top_p = parseFloat(el.cfg.top_p.value);
    c.top_k = parseInt(el.cfg.top_k.value, 10);
    c.max_tokens = parseInt(el.cfg.max_tokens.value, 10);
    c.frequency_penalty = parseFloat(el.cfg.frequency_penalty.value);
    c.presence_penalty = parseFloat(el.cfg.presence_penalty.value);
    c.repetition_penalty = parseFloat(el.cfg.repetition_penalty.value);
    c.seed = el.cfg.seed.value.trim();
    c.stop = el.cfg.stop.value;
    c.stream = el.cfg.stream.checked;
    c.renderMd = el.cfg.renderMd.checked;
    c.showUsage = el.cfg.showUsage.checked;
    saveConfig();
    closeSettings();
    toast('设置已保存');
    fetchModels(true);
    renderMessages();
  }
  function updateRangeLabels() {
    ['temperature', 'top_p', 'top_k', 'max_tokens', 'frequency_penalty', 'presence_penalty', 'repetition_penalty']
      .forEach((k) => {
        const input = el.cfg[k];
        const label = document.getElementById('v-' + k);
        if (input && label) label.textContent = input.value;
      });
  }

  // -------------------- 输入框 --------------------
  function autoResize() {
    el.input.style.height = 'auto';
    el.input.style.height = Math.min(200, el.input.scrollHeight) + 'px';
  }

  // -------------------- 事件绑定 --------------------
  function bindEvents() {
    // 侧边栏
    el.toggleSidebar.addEventListener('click', () => {
      el.sidebar.classList.add('collapsed');
      el.showSidebar.style.display = 'inline-flex';
    });
    el.showSidebar.addEventListener('click', () => {
      el.sidebar.classList.remove('collapsed');
      el.showSidebar.style.display = 'none';
    });
    el.newChat.addEventListener('click', () => createConversation());

    // 设置
    el.settingsBtn.addEventListener('click', openSettings);
    el.cfg.save.addEventListener('click', saveSettings);
    document.querySelectorAll('[data-close-modal]').forEach((b) =>
      b.addEventListener('click', closeSettings)
    );
    el.modal.querySelector('.modal-backdrop').addEventListener('click', closeSettings);
    el.cfg.fetchModels.addEventListener('click', async () => {
      // 临时使用当前 dialog 里的 baseUrl/apiKey 来探测
      const tmp = { ...state.config, baseUrl: el.cfg.baseUrl.value.trim(), apiKey: el.cfg.apiKey.value.trim() || 'EMPTY' };
      const prev = state.config;
      state.config = tmp;
      const models = await fetchModels();
      state.config = prev;
      if (models.length) el.cfg.model.value = models[0];
    });
    ['temperature', 'top_p', 'top_k', 'max_tokens', 'frequency_penalty', 'presence_penalty', 'repetition_penalty']
      .forEach((k) => el.cfg[k].addEventListener('input', updateRangeLabels));

    // 主题
    el.themeBtn.addEventListener('click', () => {
      state.config.theme = state.config.theme === 'dark' ? 'light' : 'dark';
      saveConfig();
      applyTheme();
    });

    // 发送
    el.sendBtn.addEventListener('click', sendMessage);
    el.stopBtn.addEventListener('click', stopGeneration);

    // 输入法兼容：维护自己的 composing 标志，并在 compositionend 后保留短暂窗口
    let imeComposing = false;
    let imeEndAt = 0;
    el.input.addEventListener('compositionstart', () => { imeComposing = true; });
    el.input.addEventListener('compositionend',   () => {
      imeComposing = false;
      imeEndAt = Date.now();
    });
    el.input.addEventListener('keydown', (e) => {
      if (e.key !== 'Enter' || e.shiftKey) return;
      // 1) 原生 isComposing / 2) 自维护标志 / 3) keyCode 229 / 4) 刚结束 composition 30ms 内
      if (e.isComposing || imeComposing || e.keyCode === 229 || (Date.now() - imeEndAt) < 30) return;
      e.preventDefault();
      sendMessage();
    });
    el.input.addEventListener('input', autoResize);

    // 模型选择
    el.modelSelect.addEventListener('change', () => {
      state.config.model = el.modelSelect.value;
      saveConfig();
    });
  }

  // -------------------- 启动 --------------------
  function init() {
    applyTheme();
    bindEvents();

    if (state.conversations.length === 0) {
      createConversation();
    } else {
      state.currentId = state.conversations[0].id;
      renderConversationList();
      renderMessages();
    }

    // 初始探测模型
    fetchModels(true);
    autoResize();
  }

  document.addEventListener('DOMContentLoaded', init);
})();
