📂 solution-1.0

← 返回上级
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Hermes Chat v4 — 流式对话</title>
<!-- Markdown 渲染引擎 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.6/marked.min.js"></script>
<!-- 代码高亮 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', 'PingFang SC', sans-serif;
    background: #0a0a0a; color: #e0e0e0; height: 100vh; overflow: hidden;
  }

  /* ── 整体布局:左 + 右 ── */
  #app { display: flex; height: 100vh; }

  /* ── 左侧面板(IDEA 风格可折叠多面板) ── */
  #sidebar {
    width: 320px; min-width: 320px;
    display: flex; flex-direction: column;
    background: #111; border-right: 1px solid #222;
    flex-shrink: 0;
  }

  #sidebar-header {
    padding: 8px 14px; background: #0d0d0d;
    border-bottom: 1px solid #222;
    font-size: 13px; font-weight: 600; color: #888;
    display: flex; align-items: center; gap: 6px;
    flex-shrink: 0;
  }
  #sidebar-header .badge {
    font-size: 10px; background: #1a3a2a; color: #4ade80;
    padding: 1px 7px; border-radius: 8px;
  }
  #add-task-btn {
    margin-left: auto; background: none; border: 1px solid #333;
    color: #888; width: 22px; height: 22px; border-radius: 4px;
    cursor: pointer; font-size: 13px; line-height: 1; display: flex;
    align-items: center; justify-content: center; flex-shrink: 0;
    transition: all 0.15s;
  }
  #add-task-btn:hover { background: #1a3a2a; border-color: #4ade80; color: #4ade80; }

  /* IDEA 风格可折叠面板 */
  .side-panel { display: flex; flex-direction: column; border-bottom: 1px solid #1a1a1a; }
  .side-panel .panel-header {
    padding: 6px 14px; cursor: pointer; display: flex; align-items: center;
    font-size: 12px; color: #aaa; user-select: none; flex-shrink: 0;
    background: #0d0d0d; transition: background 0.15s;
    gap: 6px;
  }
  .side-panel .panel-header:hover { background: #1a1a1a; color: #4ade80; }
  .side-panel .panel-header .panel-icon { font-size: 13px; }
  .side-panel .panel-header .panel-title { flex: 1; font-weight: 500; }
  .side-panel .panel-header .panel-count {
    font-size: 10px; color: #555; background: #1a1a1a;
    padding: 0 6px; border-radius: 6px;
  }
  .side-panel .panel-header .panel-arrow {
    font-size: 10px; color: #555; transition: transform 0.2s;
  }
  .side-panel.collapsed .panel-header .panel-arrow { transform: rotate(-90deg); }
  .side-panel .panel-body { flex: 1; overflow-y: auto; min-height: 0; }
  .side-panel.collapsed .panel-body { display: none; }
  .side-panel.active { flex: 1; min-height: 100px; }
  .side-panel.active .panel-body { flex: 1; overflow-y: auto; }
  .side-panel:not(.active):not(.collapsed) .panel-body { max-height: 200px; overflow-y: auto; }

  /* 归属skill面板 */
  .skill-assign-item {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 14px; font-size: 12px; color: #ccc;
    border-bottom: 1px solid #1a1a1a; cursor: default;
  }
  .skill-assign-item:hover { background: #1e1e1e; }
  .skill-assign-item .sname { flex: 1; }
  .skill-assign-item .stask-count { font-size: 10px; color: #666; }
  .skill-assign-empty {
    padding: 20px; text-align: center; color: #555; font-size: 12px;
  }

  /* 表单列表 */
  .form-item {
    display: flex; align-items: center; gap: 6px;
    padding: 8px 14px; font-size: 12px; color: #ccc;
    border-bottom: 1px solid #1a1a1a; cursor: pointer;
  }
  .form-item:hover { background: #1e1e1e; }
  .form-item .fname { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .form-empty {
    padding: 20px; text-align: center; color: #555; font-size: 12px;
  }

  /* ── 表单抽屉覆写 ── */
  .form-overlay {
    display: none; position: fixed; inset: 0; z-index: 9999;
    background: rgba(0,0,0,0.7);
    justify-content: center; align-items: flex-start;
    padding: 20px; overflow-y: auto;
  }
  .form-overlay.show { display: flex; }
  .form-overlay-card {
    width: 100%; max-width: 520px; margin: 20px auto;
    background: #1a1a2e; border-radius: 12px;
    padding: 24px; border: 1px solid #2a2a4a;
  }
  .form-overlay-title {
    font-size: 17px; font-weight: 600; color: #FFD700;
    margin-bottom: 20px; line-height: 1.4; padding-right: 30px;
  }
  .form-overlay-close {
    position: absolute; top: 12px; right: 14px;
    background: none; border: none; color: #888;
    font-size: 20px; cursor: pointer; padding: 4px 8px;
  }
  .form-overlay-header {
    position: relative; border-bottom: 1px solid #2a2a4a;
    margin-bottom: 20px; padding-bottom: 12px;
  }

  /* 表单字段 */
  .form-field { margin-bottom: 18px; }
  .form-field-label {
    font-size: 13px; color: #aaa; margin-bottom: 6px; font-weight: 500;
  }
  .form-field-label .required { color: #ef5350; }
  .form-field-text input {
    width: 100%; padding: 10px 12px;
    background: #16162a; border: 1px solid #2a2a4a;
    border-radius: 6px; color: #e0e0e0; font-size: 15px;
    outline: none;
  }
  .form-field-text input:focus { border-color: #FFD700; }
  .form-field-radio {
    display: flex; flex-direction: column; gap: 6px;
  }
  .form-field-radio label {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 12px; background: #16162a;
    border: 1px solid #2a2a4a; border-radius: 6px;
    cursor: pointer; font-size: 14px;
  }
  .form-field-radio label:active { background: #1e1e3a; }
  .form-field-radio input[type="radio"] {
    width: 16px; height: 16px; accent-color: #FFD700; flex-shrink: 0;
  }
  .form-field-radio label:has(input:checked) { border-color: #FFD700; background: #1a1a30; }
  .form-field-radio input[type="radio"]:checked + span { color: #FFD700; }

  .form-overlay-btn {
    width: 100%; padding: 12px;
    background: #FFD700; color: #0a0a0a;
    border: none; border-radius: 6px;
    font-size: 15px; font-weight: 600; cursor: pointer;
    margin-top: 4px;
  }
  .form-overlay-btn:active { opacity: 0.85; }
  .form-overlay-btn:disabled { opacity: 0.4; cursor: not-allowed; }
  .form-overlay-saved {
    text-align: center; padding: 8px;
    color: #4ade80; font-size: 13px; display: none;
  }

  /* ── Skill 抽屉 ── */
  .skill-drawer-overlay {
    display: none; position: fixed; inset: 0; z-index: 9999;
    background: rgba(0,0,0,0.7);
    justify-content: center; align-items: flex-start;
    padding: 20px; overflow-y: auto;
  }
  .skill-drawer-overlay.show { display: flex; }
  .skill-drawer-card {
    width: 100%; max-width: 480px; margin: 20px auto;
    background: #1a1a2e; border-radius: 12px;
    padding: 24px; border: 1px solid #2a2a4a;
  }
  .skill-drawer-header {
    position: relative; border-bottom: 1px solid #2a2a4a;
    margin-bottom: 16px; padding-bottom: 12px;
  }
  .skill-drawer-name { font-size: 16px; font-weight: 600; color: #4ade80; }
  .skill-drawer-cat { font-size: 11px; color: #666; margin-top: 2px; }
  .skill-drawer-desc { font-size: 13px; color: #ccc; line-height: 1.6; margin-bottom: 12px; }
  .skill-drawer-tags {
    display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 12px;
  }
  .skill-drawer-tag {
    font-size: 10px; background: #1a1a2e; color: #888;
    border: 1px solid #2a2a4a; padding: 2px 8px; border-radius: 8px;
  }
  .skill-drawer-tasks { font-size: 12px; color: #555; }

  /* ── 新建任务表单 ── */
  #new-task-form {
    display: none; padding: 6px 14px; border-bottom: 1px solid #222;
    background: #0d0d0d; flex-shrink: 0;
  }
  #new-task-form.open { display: block; }
  #new-task-form .form-row {
    margin-bottom: 4px; display: flex; flex-direction: column; gap: 3px;
  }
  #new-task-form label {
    font-size: 10px; color: #888;
  }
  #new-task-form select,
  #new-task-form input[type="text"] {
    background: #1a1a1a; border: 1px solid #333; border-radius: 4px;
    padding: 5px 8px; color: #e0e0e0; font-size: 12px; outline: none;
    width: 100%;
  }
  #new-task-form select:focus,
  #new-task-form input[type="text"]:focus {
    border-color: #4ade80;
  }
  #new-task-form .form-actions {
    display: flex; gap: 6px; justify-content: flex-end;
  }
  #new-task-form .form-actions button {
    padding: 4px 12px; border-radius: 4px; border: none;
    cursor: pointer; font-size: 11px; font-weight: 500;
  }
  #new-task-form .btn-create {
    background: #4ade80; color: #0a0a0a;
  }
  #new-task-form .btn-create:hover { opacity: 0.85; }
  #new-task-form .btn-cancel {
    background: #333; color: #aaa;
  }
  #new-task-form .btn-cancel:hover { background: #444; }
  #new-task-form .form-error {
    font-size: 11px; color: #ef4444; display: none;
  }
  #new-task-form .form-error.show { display: block; }

  /* ── 任务列表 ── */
  #task-list {
    overflow-y: auto;
  }
  .task-item {
    padding: 5px 12px; cursor: pointer;
    transition: background 0.15s;
    display: flex; align-items: center; gap: 6px;
  }
  .task-item:hover { background: #1a1a2e; }
  .task-item.active { background: #1a2a3a; border-left: 3px solid #4ade80; }
  .task-status {
    width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
  }
  .task-status.created { background: #666; }
  .task-status.running { background: #4ade80; }
  .task-status.paused { background: #f59e0b; }
  .task-status.completed { background: #3b82f6; }
  .task-status.cancelled { background: #ef4444; }
  .task-info { flex: 1; min-width: 0; }
  .task-name { font-size: 12px; color: #e0e0e0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .task-meta { font-size: 10px; color: #555; margin-top: 1px; }
  .task-status-text { font-size: 10px; color: #777; }
  .task-empty {
    padding: 14px 12px; text-align: center; color: #444; font-size: 12px;
  }

  /* ── 任务搜索框 ── */
  .task-search-box {
    padding: 6px 12px; background: #0d0d0d;
    border-bottom: 1px solid #1a1a1a;
  }
  .task-search-box input {
    width: 100%; padding: 5px 10px;
    border: 1px solid #2a2a2a; border-radius: 4px;
    background: #161616; color: #e0e0e0;
    font-size: 11px; outline: none;
  }
  .task-search-box input:focus { border-color: #4ade80; }
  .task-search-box input::placeholder { color: #555; }

  /* 任务文件夹树 */
  .task-folder {}
  .task-folder-header {
    padding: 5px 10px; cursor: pointer; display: flex; align-items: center; gap: 4px;
    font-size: 12px; color: #888; user-select: none;
  }
  .task-folder-header:hover { background: #1a1a1a; color: #ccc; }
  .task-folder-header .arrow { font-size: 9px; width: 12px; text-align: center; color: #555; transition: transform .15s; }
  .task-folder-header .arrow.open { transform: rotate(90deg); }
  .task-folder-header .fname { font-weight: 500; }
  .task-folder-children { display: none; }
  .task-folder.open > .task-folder-children { display: block; }
  .task-folder-item {
    padding: 5px 8px 5px 22px; cursor: pointer; display: flex; align-items: center; gap: 4px;
    font-size: 12px; color: #999; transition: background 0.1s; border-left: 2px solid transparent;
  }
  .task-folder-item:hover { background: #1a1a1a; color: #4ade80; }
  .task-folder-item.active { background: #1a3a2a; color: #4ade80; border-left-color: #4ade80; }
  .task-folder-item .dot {
    width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
  }
  .task-folder-item .dot.active { background: #4caf50; }
  .task-folder-item .dot.running { background: #ffa726; }
  .task-folder-item .dot.created { background: #666; }
  .task-folder-item .dot.completed { background: #888; }
  .task-folder-item .tname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .task-section-title {
    font-size: 10px; color: #555; padding: 6px 10px 3px; text-transform: uppercase; letter-spacing: .5px;
  }

  /* ── 上下分隔条 ── */
  #sidebar-divider {
    height: 1px; background: #222; cursor: ns-resize; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    position: relative;
  }
  #sidebar-divider::after {
    content: '⋯'; color: #444; font-size: 9px; letter-spacing: 2px;
  }

  /* ── wiki 知识树 ── */
  #wiki-panel {
    flex: 1; overflow-y: auto;
    background: #0d0d0d;
  }
  #wiki-header {
    padding: 6px 14px; font-size: 11px; color: #666;
    border-bottom: 1px solid #1a1a1a;
    display: flex; align-items: center; gap: 6px;
    flex-shrink: 0; position: sticky; top: 0; background: #0d0d0d;
  }
  .wiki-doc {
    padding: 5px 14px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 6px;
    transition: background 0.1s; color: #aaa; border-left: 2px solid transparent;
  }
  .wiki-doc:hover { background: #1a1a1a; color: #ccc; }
  .wiki-doc::before { content: '📄'; font-size: 11px; }
  .wiki-doc-empty { padding: 20px 14px; text-align: center; color: #444; font-size: 12px; }

  /* ── 附件列表 ── */
  .att-item {
    padding: 6px 14px; cursor: pointer; display: flex; align-items: center; gap: 8px;
    font-size: 12px; color: #aaa; transition: background 0.1s;
    border-left: 2px solid transparent;
  }
  .att-item:hover { background: #1a1a2e; color: #4ade80; border-left-color: #4ade80; }
  .att-item .att-icon { font-size: 14px; flex-shrink: 0; }
  .att-item .att-info { flex: 1; min-width: 0; }
  .att-item .att-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .att-item .att-meta { font-size: 10px; color: #555; margin-top: 1px; }
  .att-item .att-size { font-size: 10px; color: #555; flex-shrink: 0; }
  .att-empty { padding: 20px 14px; text-align: center; color: #444; font-size: 12px; }

  /* ── 附件预览遮罩 ── */
  .att-preview-overlay {
    display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.7); z-index: 200;
    justify-content: center; align-items: center;
  }
  .att-preview-overlay.show { display: flex; }
  .att-preview-modal {
    background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
    width: 80vw; max-width: 900px; max-height: 85vh;
    display: flex; flex-direction: column; box-shadow: 0 4px 24px rgba(0,0,0,0.5);
  }
  .att-preview-header {
    padding: 10px 16px; border-bottom: 1px solid #222;
    display: flex; align-items: center; gap: 8px;
  }
  .att-preview-header .att-preview-name { flex: 1; font-size: 14px; font-weight: 500; }
  .att-preview-header .att-preview-close {
    background: none; border: none; color: #666; font-size: 18px;
    cursor: pointer; padding: 0 4px;
  }
  .att-preview-header .att-preview-close:hover { color: #ef4444; }
  .att-preview-body {
    flex: 1; overflow: auto; padding: 16px;
    display: flex; justify-content: center; align-items: flex-start;
  }
  .att-preview-body img, .att-preview-body video {
    max-width: 100%; max-height: 70vh; border-radius: 4px;
  }
  .att-preview-body iframe {
    width: 100%; height: 70vh; border: none;
  }
  .att-preview-body .att-text-preview {
    width: 100%; white-space: pre-wrap; font-size: 13px;
    color: #ccc; line-height: 1.6; font-family: monospace;
  }
  .att-preview-body .att-unsupported {
    text-align: center; color: #666; padding: 40px;
  }

  /* ── 右侧:聊天窗口 ── */
  #chat-area {
    flex: 1; display: flex; flex-direction: column;
    background: #0a0a0a;
  }
  #chat-header {
    padding: 8px 16px; background: #111;
    border-bottom: 1px solid #222;
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  }
  #chat-header h1 { font-size: 14px; font-weight: 600; color: #4ade80; }
  #chat-header .subtitle { font-size: 11px; color: #666; margin-left: auto; }
  #status { font-size: 11px; }
  #status.online { color: #4ade80; }
  #status.offline { color: #ef4444; }
  #no-task-msg {
    flex: 1; display: flex; align-items: center; justify-content: center;
    color: #555; font-size: 13px;
  }

  #messages {
    flex: 1; overflow-y: auto; padding: 12px;
    display: flex; flex-direction: column; gap: 8px;
  }
  .msg { max-width: 85%; padding: 8px 12px; border-radius: 10px; line-height: 1.5; font-size: 13px; word-wrap: break-word; }
  .msg.user { background: #1a3a2a; color: #e0e0e0; align-self: flex-end; border-bottom-right-radius: 3px; }
  .msg.ai { background: #1a1a2e; color: #e0e0e0; align-self: flex-start; border-bottom-left-radius: 3px; }
  .msg.system { background: #222; color: #888; align-self: center; font-size: 11px; padding: 4px 10px; border-radius: 4px; }
  .msg.ai p { margin: 3px 0; }
  .msg.ai code { background: #2a2a3e; padding: 1px 5px; border-radius: 3px; font-size: 12px; }
  .msg.ai pre { background: #2a2a3e; padding: 8px; border-radius: 6px; overflow-x: auto; margin: 6px 0; font-size: 12px; }
  .msg.ai pre code { background: none; padding: 0; }

  /* ── 增强 Markdown 样式 ── */
  .msg.ai .code-block-wrapper {
    margin: 8px 0; border-radius: 6px; overflow: hidden;
    border: 1px solid #333; background: #1a1a2e;
  }
  .msg.ai .code-header {
    display: flex; justify-content: space-between; align-items: center;
    padding: 4px 10px; background: #222; border-bottom: 1px solid #333;
    font-size: 11px;
  }
  .msg.ai .code-lang { color: #888; font-weight: 500; }
  .msg.ai .copy-btn {
    background: none; border: 1px solid #444; color: #aaa;
    padding: 1px 8px; border-radius: 3px; cursor: pointer;
    font-size: 10px; transition: all 0.15s;
  }
  .msg.ai .copy-btn:hover { background: #333; color: #fff; border-color: #666; }
  .msg.ai .code-block-wrapper pre { margin: 0; padding: 8px; overflow-x: auto; border-radius: 0; }
  .msg.ai .code-block-wrapper code { font-size: 12px; line-height: 1.4; }

  .msg.ai table {
    border-collapse: collapse; margin: 8px 0; width: 100%; font-size: 12px;
  }
  .msg.ai th, .msg.ai td {
    border: 1px solid #333; padding: 5px 8px; text-align: left;
  }
  .msg.ai th { background: #1a1a2e; color: #4ade80; font-weight: 600; }
  .msg.ai tr:nth-child(even) { background: #111; }
  .msg.ai blockquote {
    border-left: 3px solid #4ade80; padding: 3px 10px; margin: 6px 0;
    color: #999; background: #111; border-radius: 0 3px 3px 0;
  }
  .msg.ai ul, .msg.ai ol { padding-left: 18px; margin: 6px 0; }
  .msg.ai li { margin: 2px 0; }
  .msg.ai h1, .msg.ai h2, .msg.ai h3, .msg.ai h4 {
    color: #4ade80; margin: 12px 0 6px; font-weight: 600;
  }
  .msg.ai h1 { font-size: 18px; }
  .msg.ai h2 { font-size: 16px; }
  .msg.ai h3 { font-size: 14px; }
  .msg.ai h4 { font-size: 13px; }
  .msg.ai p { margin: 6px 0; line-height: 1.6; }
  .msg.ai a { color: #4ade80; text-decoration: underline; }
  .msg.ai hr { border: none; border-top: 1px solid #333; margin: 10px 0; }
  .msg.ai img { max-width: 100%; border-radius: 6px; margin: 6px 0; }
  .msg.ai strong { color: #4ade80; }
  .msg .time { font-size: 10px; color: #555; margin-top: 2px; text-align: right; }

  #input-area {
    padding: 8px 16px; background: #111; border-top: 1px solid #222;
    display: flex; gap: 8px; align-items: center; flex-shrink: 0;
  }
  #input {
    flex: 1; background: #1a1a1a; border: 1px solid #333; border-radius: 20px;
    padding: 8px 14px; color: #e0e0e0; font-size: 13px; outline: none;
    transition: border-color 0.2s;
  }
  #input:focus { border-color: #4ade80; }
  #input:disabled { opacity: 0.5; }
  #send {
    width: 34px; height: 34px; border-radius: 50%; border: none;
    background: #4ade80; color: #0a0a0a; font-size: 16px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: opacity 0.2s; flex-shrink: 0;
  }
  #send:disabled { opacity: 0.4; cursor: not-allowed; }
  #send:hover:not(:disabled) { opacity: 0.85; }

  /* ── 附件上传 ── */
  #upload-btn {
    background: none; border: 1px solid #444; color: #888;
    width: 32px; height: 32px; border-radius: 8px; cursor: pointer;
    font-size: 16px; display: flex; align-items: center; justify-content: center;
    flex-shrink: 0; transition: all 0.15s; position: relative;
  }
  #upload-btn:hover { border-color: #4ade80; color: #4ade80; background: #1a3a2a22; }
  #upload-btn:disabled { opacity: 0.4; cursor: not-allowed; }
  #upload-btn.uploading { pointer-events: none; opacity: 0.7; }
  #upload-progress {
    display: none; position: absolute; bottom: -3px; left: 2px; right: 2px;
    height: 2px; background: #333; border-radius: 1px; overflow: hidden;
  }
  #upload-progress .bar {
    height: 100%; background: #4ade80; width: 0%; border-radius: 1px;
    transition: width 0.3s;
  }
  #upload-btn.uploading #upload-progress { display: block; }
  #upload-name {
    font-size: 11px; color: #888; max-width: 120px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    flex-shrink: 1; min-width: 0;
  }

  #typing { font-size: 11px; color: #666; padding: 2px 16px; flex-shrink: 0; min-height: 18px; }
  .typing-dots::after { content: '...'; animation: dots 1.5s infinite; }
  @keyframes dots { 0%,20% { content: '.'; } 40% { content: '..'; } 60%,100% { content: '...'; } }

  /* Markdown */
  .msg.ai table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; }
  .msg.ai th, .msg.ai td { border: 1px solid #333; padding: 4px 8px; text-align: left; }
  .msg.ai th { background: #2a2a3e; color: #4ade80; }
  .msg.ai blockquote { border-left: 3px solid #4ade80; padding-left: 10px; margin: 6px 0; color: #999; }

  /* 滚动条 */
  #task-list::-webkit-scrollbar, #wiki-panel::-webkit-scrollbar, #messages::-webkit-scrollbar { width: 3px; }
  #task-list::-webkit-scrollbar-thumb, #wiki-panel::-webkit-scrollbar-thumb, #messages::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
  #task-list::-webkit-scrollbar-track, #wiki-panel::-webkit-scrollbar-track, #messages::-webkit-scrollbar-track { background: transparent; }

  @media (max-width: 768px) {
    #sidebar { width: 100%; min-width: 0; }
    #app { flex-direction: column; }
    #chat-area { height: 50vh; }
    #sidebar { height: 50vh; }
  }

  /* ── 文档抽屉 ── */
  #doc-drawer-overlay {
    display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.4); z-index: 100; animation: fadeIn .15s ease;
  }
  #doc-drawer-overlay.open { display: block; }
  #doc-drawer {
    position: fixed; top: 0; right: -50vw; width: 50vw; height: 100vh;
    background: #f0f0f0; z-index: 101;
    display: flex; flex-direction: column;
    box-shadow: -4px 0 20px rgba(0,0,0,0.3);
    transition: right .25s ease;
  }
  #doc-drawer.open { right: 0; }
  #doc-drawer-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 16px; background: #e6e6e6; border-bottom: 1px solid #ccc;
    flex-shrink: 0;
  }
  #doc-drawer-title {
    font-size: 14px; font-weight: 600; color: #1a1a2e; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; flex: 1; margin-right: 12px;
  }
  #doc-drawer-close {
    background: none; border: 1px solid #bbb; color: #555; width: 28px; height: 28px;
    border-radius: 4px; cursor: pointer; font-size: 14px; display: flex;
    align-items: center; justify-content: center; flex-shrink: 0;
  }
  #doc-drawer-close:hover { background: #ddd; }
  #doc-drawer-body {
    flex: 1; overflow-y: auto; background: #fff;
  }
  #doc-drawer-body iframe {
    width: 100%; height: 100%; border: none; display: block;
  }
  #doc-drawer-loading {
    display: flex; align-items: center; justify-content: center;
    height: 100%; color: #888; font-size: 14px;
  }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

  @media (max-width: 768px) {
    #doc-drawer { width: 100vw; right: -100vw; }
  }

  /* ── 推理折叠面板 ── */
  .reasoning-panel {
    margin: 4px 0; border-radius: 6px; overflow: hidden;
    border: 1px solid #333; background: #141422;
  }
  .reasoning-header {
    display: flex; align-items: center; gap: 6px;
    padding: 5px 10px; cursor: pointer; user-select: none;
    font-size: 12px; color: #888; transition: background 0.15s;
  }
  .reasoning-header:hover { background: #1a1a2e; }
  .reasoning-header .arrow {
    transition: transform 0.2s; font-size: 9px;
  }
  .reasoning-header .arrow.open { transform: rotate(90deg); }
  .reasoning-header .icon { font-size: 12px; }
  .reasoning-body {
    display: none; padding: 0 10px 8px;
    font-size: 12px; color: #888; line-height: 1.5;
    border-top: 1px solid #222;
  }
  .reasoning-body.open { display: block; }
  .reasoning-body .thinking-text {
    padding: 6px 0; white-space: pre-wrap; word-wrap: break-word;
    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
    font-size: 11px; color: #777;
  }

  /* ── 打字动画 ── */
  .typing-cursor::after {
    content: ' ▉'; animation: blink 0.8s step-end infinite;
  }
  @keyframes blink {
    50% { opacity: 0; }
  }

  /* ── 工具调用卡片 ── */
  .tool-call-card {
    margin: 4px 0; border-radius: 6px; overflow: hidden;
    border: 1px solid #333; background: #151525;
  }
  .tool-call-header {
    display: flex; align-items: center; gap: 6px;
    padding: 5px 10px; font-size: 12px; color: #aaa;
    background: #1a1a2e; border-bottom: 1px solid #222;
  }
  .tool-call-header .tool-icon { font-size: 12px; }
  .tool-call-header .tool-name {
    font-weight: 500; color: #7c9bff; font-family: 'SF Mono', 'Consolas', monospace;
  }
  .tool-call-header .tool-status {
    margin-left: auto; font-size: 10px;
    display: flex; align-items: center; gap: 3px;
  }
  .tool-call-header .tool-status.running { color: #f59e0b; }
  .tool-call-header .tool-status.done { color: #4ade80; }
  .tool-call-header .tool-status.error { color: #ef4444; }
  .tool-status-spinner {
    display: inline-block; width: 10px; height: 10px;
    border: 2px solid #555; border-top-color: #f59e0b;
    border-radius: 50%; animation: spin 0.8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .tool-call-body {
    padding: 5px 10px; font-size: 11px; color: #777;
    font-family: 'SF Mono', 'Consolas', monospace;
    white-space: pre-wrap; word-wrap: break-word;
    max-height: 150px; overflow-y: auto;
  }
  .tool-call-body .tool-args {
    color: #888; margin-bottom: 2px;
  }
  .tool-call-body .tool-args-key { color: #7c9bff; }
  .tool-call-body .tool-args-val { color: #ce9178; }
  .tool-call-body .tool-result {
    color: #aaa; border-top: 1px solid #222; padding-top: 4px; margin-top: 2px;
  }
  .tool-call-body .tool-result-preview {
    color: #888; font-size: 10px;
  }
  .tool-call-card.collapsed .tool-call-body { display: none; }

  /* ── 消息操作按钮(编辑/删除) ── */
  .msg-actions {
    position: absolute; top: 4px; right: 4px;
    display: none; gap: 2px; z-index: 10;
  }
  .msg:hover .msg-actions { display: flex; }
  .msg .msg-actions button {
    background: #222; border: 1px solid #444; color: #888;
    padding: 2px 8px; border-radius: 4px; cursor: pointer;
    font-size: 11px; transition: all 0.15s;
  }
  .msg .msg-actions button:hover { background: #333; color: #fff; }
  .msg .msg-actions .btn-edit:hover { border-color: #4ade80; color: #4ade80; }
  .msg .msg-actions .btn-delete:hover { border-color: #ef4444; color: #ef4444; }

  /* ── 编辑模式 ── */
  .msg.editing .msg-content { display: none; }
  .msg .edit-area { display: none; }
  .msg.editing .edit-area { display: block; }
  .msg .edit-area textarea {
    width: 100%; background: #1a1a2e; border: 1px solid #4ade80;
    color: #e0e0e0; border-radius: 8px; padding: 10px;
    font-size: 14px; font-family: inherit; line-height: 1.6;
    resize: vertical; min-height: 80px; outline: none;
  }
  .msg .edit-area .edit-buttons {
    display: flex; gap: 8px; margin-top: 8px;
  }
  .msg .edit-area .edit-buttons button {
    padding: 6px 16px; border-radius: 6px; border: none;
    cursor: pointer; font-size: 13px; font-weight: 500;
  }
  .msg .edit-area .btn-save {
    background: #4ade80; color: #0a0a0a;
  }
  .msg .edit-area .btn-cancel {
    background: #333; color: #aaa;
  }
  .msg .edit-area .btn-save:hover { opacity: 0.85; }
  .msg .edit-area .btn-cancel:hover { background: #444; }

  /* ── 删除确认弹窗 ── */
  .delete-confirm {
    display: none; margin-top: 8px; padding: 8px 12px;
    background: #2a1a1a; border: 1px solid #ef4444; border-radius: 8px;
    font-size: 13px; color: #ef4444;
  }
  .delete-confirm .del-buttons { display: flex; gap: 8px; margin-top: 6px; }
  .delete-confirm .del-buttons button {
    padding: 4px 14px; border-radius: 6px; border: none;
    cursor: pointer; font-size: 12px;
  }
  .delete-confirm .btn-confirm-del { background: #ef4444; color: #fff; }
  .delete-confirm .btn-cancel-del { background: #333; color: #aaa; }
  .delete-confirm .btn-confirm-del:hover { opacity: 0.85; }
  .delete-confirm .btn-cancel-del:hover { background: #444; }

  .msg { position: relative; }

  /* ── 子代理卡片 ── */
  .subagent-card {
    margin: 4px 0; border-radius: 6px; overflow: hidden;
    border: 1px solid #334; background: #0d0d1a;
  }
  .subagent-header {
    display: flex; align-items: center; gap: 6px;
    padding: 4px 10px; font-size: 11px; color: #889;
    background: #15152a; border-bottom: 1px solid #222;
  }
  .subagent-header .sa-icon { font-size: 11px; }
  .subagent-header .sa-name { color: #7c9bff; font-weight: 500; }
  .subagent-header .sa-badge {
    font-size: 9px; background: #1a1a3a; color: #7c9bff;
    padding: 1px 5px; border-radius: 6px;
  }
  .subagent-body {
    padding: 4px 10px; font-size: 11px; color: #777;
    font-family: 'SF Mono', 'Consolas', monospace;
    white-space: pre-wrap; word-wrap: break-word;
    max-height: 100px; overflow-y: auto;
  }
  .subagent-body .sa-thinking { color: #f59e0b; }
  .subagent-body .sa-text { color: #aaa; }
  .subagent-body .sa-tool { color: #7c9bff; }
  .subagent-footer {
    padding: 2px 10px 4px; font-size: 10px; color: #555;
    border-top: 1px solid #1a1a2e; display: flex; gap: 10px;
  }
  .subagent-footer .sa-stat { display: flex; align-items: center; gap: 2px; }

  /* ── 搜索面板 ── */
  #search-panel {
    display: none; padding: 6px 16px;
    background: #111; border-bottom: 1px solid #222;
    flex-shrink: 0;
  }
  #search-panel.open { display: flex; gap: 6px; align-items: center; }
  #search-input {
    flex: 1; background: #1a1a1a; border: 1px solid #333;
    border-radius: 16px; padding: 5px 12px; color: #e0e0e0;
    font-size: 12px; outline: none;
  }
  #search-input:focus { border-color: #4ade80; }
  #search-btn, #search-close {
    padding: 4px 12px; border-radius: 4px; border: none;
    cursor: pointer; font-size: 11px;
  }
  #search-btn { background: #4ade80; color: #0a0a0a; }
  #search-close { background: #333; color: #aaa; }
  #search-btn:hover { opacity: 0.85; }
  #search-close:hover { background: #444; }
  #search-count { font-size: 11px; color: #666; }
  .search-highlight { background: #4ade8033; padding: 0 2px; border-radius: 2px; }

  /* ── 导出按钮 ── */
  #export-btn {
    background: none; border: 1px solid #444; color: #888;
    padding: 2px 8px; border-radius: 4px; cursor: pointer;
    font-size: 11px; margin-left: auto;
  }
  #export-btn:hover { border-color: #4ade80; color: #4ade80; }

.move-overlay {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.5); z-index: 10000;
  display: flex; align-items: center; justify-content: center;
}
.move-modal {
  background: #1e1e1e; border: 1px solid #444; border-radius: 12px;
  padding: 24px; min-width: 320px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.move-modal-title { font-size: 16px; color: #e0e0e0; margin-bottom: 16px; }
.move-task-select {
  width: 100%; padding: 10px 12px; border: 1px solid #444;
  border-radius: 8px; background: #2a2a2a; color: #e0e0e0;
  font-size: 14px; margin-bottom: 20px;
}
.move-modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
.move-modal-actions .btn-cancel {
  padding: 8px 20px; border: 1px solid #555; border-radius: 8px;
  background: transparent; color: #aaa; cursor: pointer;
}
.move-modal-actions .btn-confirm {
  padding: 8px 20px; border: none; border-radius: 8px;
  background: #4a9eff; color: #fff; cursor: pointer;
}
.move-modal-actions .btn-confirm:hover { background: #3a8eef; }

</style>
</head>
<body>
<div id="app">
  <!-- ── 左侧栏 ── -->
  <div id="sidebar">
    <div id="sidebar-header">
      <span>⚡</span>
      <span>Hermes</span>
      <span class="badge" id="taskCount">0</span>
      <button id="add-task-btn" onclick="toggleNewTaskForm()" title="新建任务">+</button>
    </div>

    <!-- IDEA 风格折叠面板:任务列表 -->
    <div class="side-panel active" id="panel-tasks">
      <div class="panel-header" onclick="togglePanel('tasks')">
        <span class="panel-icon">📋</span>
        <span class="panel-title">任务列表</span>
        <span class="panel-count" id="taskCountPanel">0</span>
        <span class="panel-arrow">▼</span>
      </div>
      <div class="panel-body" id="panel-body-tasks">
        <div id="new-task-form">
          <div class="form-row">
            <label>任务名称</label>
            <input type="text" id="new-task-name" placeholder="输入任务名称" onkeydown="if(event.key==='Enter')createTask()">
          </div>
          <div class="form-row">
            <label>所属分类</label>
            <select id="new-task-folder"></select>
          </div>
          <div class="form-error" id="task-form-error">⚠️ 请填写任务名称</div>
          <div class="form-actions">
            <button class="btn-cancel" onclick="toggleNewTaskForm()">取消</button>
            <button class="btn-create" onclick="createTask()">创建</button>
          </div>
        </div>
        <div class="task-search-box">
          <input type="text" id="taskSearch" placeholder="搜索task..." oninput="onTaskSearchInput()">
        </div>
        <div id="task-list"></div>
      </div>
    </div>

    <!-- IDEA 风格折叠面板:知识沉淀 -->
    <div class="side-panel" id="panel-wiki">
      <div class="panel-header" onclick="togglePanel('wiki')">
        <span class="panel-icon">📚</span>
        <span class="panel-title">知识沉淀</span>
        <span class="panel-count" id="wikiCountPanel">0</span>
        <span class="panel-arrow">▼</span>
      </div>
      <div class="panel-body" id="panel-body-wiki">
        <div id="wiki-panel"></div>
      </div>
    </div>

    <!-- IDEA 风格折叠面板:相关附件 -->
    <div class="side-panel" id="panel-attachments">
      <div class="panel-header" onclick="togglePanel('attachments')">
        <span class="panel-icon">📎</span>
        <span class="panel-title">相关附件</span>
        <span class="panel-count" id="attCountPanel">0</span>
        <span class="panel-arrow">▼</span>
      </div>
      <div class="panel-body" id="panel-body-attachments">
        <div id="attachment-list"><div class="att-empty">暂无附件</div></div>
      </div>
    </div>

    <!-- IDEA 风格折叠面板:表单 -->
    <div class="side-panel" id="panel-forms">
      <div class="panel-header" onclick="togglePanel('forms')">
        <span class="panel-icon">📝</span>
        <span class="panel-title">表单</span>
        <span class="panel-count" id="formCountPanel">0</span>
        <span class="panel-arrow">▼</span>
      </div>
      <div class="panel-body" id="panel-body-forms">
        <div id="form-list"><div class="form-empty">暂无表单</div></div>
      </div>
    </div>

    <!-- IDEA 风格折叠面板:归属Skill -->
    <div class="side-panel" id="panel-skills">
      <div class="panel-header" onclick="togglePanel('skills')">
        <span class="panel-icon">🎯</span>
        <span class="panel-title">归属 Skill</span>
        <span class="panel-count" id="skillCountPanel">0</span>
        <span class="panel-arrow">▼</span>
      </div>
      <div class="panel-body" id="panel-body-skills">
        <div id="skill-assign-list"><div class="skill-assign-empty">🏗️ 暂无归属 — 通过 task-org 配置</div></div>
      </div>
    </div>
  </div>

  <!-- 右侧聊天窗口 -->
  <div id="chat-area">
    <div id="chat-header">
      <h1 id="chat-title">💬 Hermes Chat</h1>
      <div class="subtitle">按任务隔离聊天</div>
      <button id="search-toggle" onclick="toggleSearch()" style="background:none;border:1px solid #444;color:#888;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px;">🔍</button>
      <div id="status" class="offline">● 离线</div>
    </div>
    <!-- 搜索面板 -->
    <div id="search-panel">
      <input id="search-input" type="text" placeholder="搜索消息关键字..." onkeydown="if(event.key==='Enter') doSearch()">
      <button id="search-btn" onclick="doSearch()">搜索</button>
      <span id="search-count"></span>
      <button id="search-close" onclick="closeSearch()">✕</button>
    </div>
    <div id="no-task-msg">← 从左侧选择一个任务开始对话</div>
    <div id="messages" style="display:none;"></div>
    <div id="typing"></div>
    <div id="input-area" style="display:none;">
      <button id="upload-btn" onclick="document.getElementById('file-input').click()" title="上传附件">
        📎
        <span id="upload-progress"><span class="bar" id="upload-progress-bar"></span></span>
      </button>
      <input type="file" id="file-input" style="display:none" onchange="handleFileSelect(this)">
      <span id="upload-name"></span>
      <textarea id="input" rows="1" placeholder="Enter 换行 · Shift+Enter 发送" autofocus disabled style="resize:none;line-height:1.5;max-height:120px;overflow-y:auto;"></textarea>
      <button id="send" disabled>↵</button>
    </div>
  </div>
</div>

<!-- 文档阅读抽屉 -->
<div id="doc-drawer-overlay" onclick="closeDocDrawer()"></div>
<div id="doc-drawer">
  <div id="doc-drawer-header">
    <span id="doc-drawer-title">📄 文档</span>
    <button id="doc-drawer-close" onclick="closeDocDrawer()">✕</button>
  </div>
  <div id="doc-drawer-body">
    <div id="doc-drawer-loading">加载中...</div>
    <iframe id="doc-drawer-iframe" style="display:none;"></iframe>
  </div>
</div>

<script>
// ═══════════════════════════════════════════
//  Hermes Chat v3 — 任务驱动前端
// ═══════════════════════════════════════════

const WS_PROTO = location.protocol === 'https:' ? 'wss:' : 'ws:';
const WS_URL = `${WS_PROTO}//${location.host}/ws/`;
const API_BASE = `/api`;

let ws = null;
let msgId = 0;
let currentTaskId = null;
let currentTaskName = '';
let tasks = [];
let allDirs = [];  // task_dirs 目录树
let streamingTaskId = null; // 当前正在流式回复的 task_id,用于任务隔离
let openDirIds = new Set(); // 目录展开状态持久化
let pendingUpdates = new Set(); // 有新消息但被隔离的任务ID,切回时自动刷新

const el = id => document.getElementById(id);
const taskList = el('task-list');
const wikiPanel = el('wiki-panel');
const messages = el('messages');
const noTaskMsg = el('no-task-msg');
const input = el('input');
const sendBtn = el('send');
const status = el('status');
const typing = el('typing');
const chatTitle = el('chat-title');
const taskCount = el('task-count');

// ═══════════════════════════════════════════
//  API 调用
// ═══════════════════════════════════════════

async function apiGet(path) {
  try {
    const resp = await fetch(`${API_BASE}${path}`, { cache: 'no-store' });
    return await resp.json();
  } catch(e) {
    console.error('API error:', e);
    return null;
  }
}

async function loadTasks() {
  // 加载 task_dirs 目录树
  const [dirData, taskData] = await Promise.all([
    apiGet('/task-dirs'),
    apiGet('/tasks-with-folder')
  ]);
  if (dirData && dirData.dirs) {
    allDirs = dirData.dirs;
  }
  if (taskData && taskData.tasks) {
    tasks = taskData.tasks.sort((a,b) => a.name.localeCompare(b.name));
    el('taskCount').textContent = tasks.length + ' 任务';
    el('taskCountPanel').textContent = tasks.length;
  }
  renderTaskList();
  // 自动恢复上次的task
  const lastTaskId = localStorage.getItem('hermes_last_task_id');
  if (lastTaskId && !currentTaskId && tasks.some(t => t.id === lastTaskId)) {
    selectTask(lastTaskId);
  }
}

// ═══════════════════════════════════════════
//  浏览器端聊天缓存(localStorage)
// ═══════════════════════════════════════════

const CACHE_KEY = 'webchat_cache_v2';

function cacheGet(taskId) {
  try {
    const all = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    return all[taskId] || null;
  } catch(e) { return null; }
}

function cacheSetAll(taskId, messages) {
  try {
    const all = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    all[taskId] = messages;
    localStorage.setItem(CACHE_KEY, JSON.stringify(all));
  } catch(e) {}
}

function cacheAppend(taskId, msg) {
  try {
    const all = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    if (!all[taskId]) all[taskId] = [];
    const exists = all[taskId].some(m => m.id === msg.id || (m._temp && m._temp === msg._temp));
    if (!exists) all[taskId].push(msg);
    localStorage.setItem(CACHE_KEY, JSON.stringify(all));
  } catch(e) {}
}

function cacheUpdateLast(taskId, content, id) {
  try {
    const all = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    if (all[taskId] && all[taskId].length > 0) {
      const last = all[taskId][all[taskId].length - 1];
      if (last.role === 'assistant') {
        last.content = content;
        if (id) last.id = id;
      }
    }
    localStorage.setItem(CACHE_KEY, JSON.stringify(all));
  } catch(e) {}
}

function cacheClear(taskId) {
  try {
    const all = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    delete all[taskId];
    localStorage.setItem(CACHE_KEY, JSON.stringify(all));
  } catch(e) {}
}

// ═══════════════════════════════════════════
//  新建任务
// ═══════════════════════════════════════════

function toggleNewTaskForm() {
  const form = el('new-task-form');
  const isOpen = form.classList.contains('open');
  form.classList.toggle('open');
  if (!isOpen) {
    // 填充目录下拉
    const sel = document.getElementById('new-task-folder');
    sel.innerHTML = '<option value="">— 未分类 —</option>';
    if (allDirs && allDirs.length) {
      // 展平显示目录层级(跳过根节点自身)
      function flatDirs(parentId, depth) {
        const children = allDirs.filter(d => d.parent_id === parentId)
          .sort((a,b) => (a.sort_order||0) - (b.sort_order||0));
        children.forEach(d => {
          const indent = ' '.repeat(depth);
          sel.innerHTML += `<option value="${d.id}">${indent}${d.name}</option>`;
          flatDirs(d.id, depth + 1);
        });
      }
      const rootDir = allDirs.find(d => d.parent_id === null);
      if (rootDir) {
        flatDirs(rootDir.id, 0); // 从根节点的子目录开始
      } else {
        flatDirs(null, 0);
      }
    }
    el('task-form-error').classList.remove('show');
    el('task-form-error').textContent = '';
    el('new-task-name').value = '';
    el('new-task-name').focus();
  }
}

async function createTask() {
  const name = el('new-task-name').value.trim();
  const dirId = document.getElementById('new-task-folder').value;
  const errorEl = el('task-form-error');

  if (!name) {
    errorEl.textContent = '请输入标题';
    errorEl.classList.add('show');
    el('new-task-title').focus();
    return;
  }

  errorEl.classList.remove('show');
  try {
    const resp = await fetch(`${API_BASE}/tasks/create`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({name, dir_id: dirId || null}),
    });
    const data = await resp.json();
    if (data.ok) {
      // 关闭表单 + 刷新树
      el('new-task-form').classList.remove('open');
      await loadTasks();
    } else {
      errorEl.textContent = data.error || '创建失败';
      errorEl.classList.add('show');
    }
  } catch(e) {
    errorEl.textContent = '网络错误: ' + e.message;
    errorEl.classList.add('show');
  }
}

// ── IDEA 面板折叠切换 ──
let activePanel = 'tasks';  // 当前展开的面板

function togglePanel(name) {
  const panels = ['tasks', 'wiki', 'attachments', 'forms', 'skills'];
  
  panels.forEach(p => {
    const el = document.getElementById('panel-' + p);
    if (p === name) {
      el.classList.add('active');
      el.classList.remove('collapsed');
      activePanel = p;
    } else {
      el.classList.remove('active');
      el.classList.add('collapsed');
    }
  });

  // 切换到附件面板时自动加载
  if (name === 'attachments' && currentTaskId) {
    renderAttachments(currentTaskId);
  }
}

// ── 附件列表 ──
async function renderAttachments(taskId) {
  const container = document.getElementById('attachment-list');
  try {
    const data = await apiGet(`/chat/attachments?task_id=${taskId}&limit=50`);
    if (!data || !data.attachments || data.attachments.length === 0) {
      container.innerHTML = '<div class="att-empty">暂无附件</div>';
      el('attCountPanel').textContent = '0';
      return;
    }
    el('attCountPanel').textContent = data.attachments.length;
    
    const imgTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
    const codeTypes = ['text/plain', 'text/markdown', 'text/html', 'application/json', 'text/css', 'text/javascript'];
    
    let html = '';
    data.attachments.forEach(a => {
      const isImg = imgTypes.includes(a.mime_type);
      const isCode = codeTypes.includes(a.mime_type);
      const icon = isImg ? '🖼️' : isCode ? '📄' : '📎';
      const sizeStr = a.filesize > 1024*1024
        ? (a.filesize/1024/1024).toFixed(1) + 'MB'
        : a.filesize > 1024
          ? (a.filesize/1024).toFixed(0) + 'KB'
          : a.filesize + 'B';
      html += `<div class="att-item" onclick="previewAttachment('${a.id}')" title="${a.display_name || a.filename}">
        <span class="att-icon">${icon}</span>
        <span class="att-info">
          <span class="att-name">${a.display_name || a.filename}</span>
          <span class="att-meta">${a.created_at}</span>
        </span>
        <span class="att-size">${sizeStr}</span>
      </div>`;
    });
    container.innerHTML = html;
  } catch(e) {
    container.innerHTML = '<div class="att-empty">加载失败</div>';
  }
}

// ── 表单列表 ──
async function renderFormList() {
  const container = document.getElementById('form-list');
  const countEl = document.getElementById('formCountPanel');
  if (!currentTaskId) {
    container.innerHTML = '<div class="form-empty">← 选择一个Task查看表单</div>';
    if (countEl) countEl.textContent = '0';
    return;
  }
  try {
    const r = await fetch(`${API_BASE}/forms?task_id=${currentTaskId}`);
    const data = await r.json();
    const forms = data.forms || [];
    if (countEl) countEl.textContent = forms.length;
    if (!forms.length) {
      container.innerHTML = '<div class="form-empty">暂无表单</div>';
      return;
    }
    let html = '';
    forms.forEach(f => {
      html += `<div class="form-item" onclick="openFormOverlay('${f.id}')">
        <span class="fname">📝 ${escapeHtml(f.title)}</span>
        <span style="font-size:10px;color:#555;">${(f.schema_json.fields||[]).length}项</span>
      </div>`;
    });
    container.innerHTML = html;
  } catch(e) {
    container.innerHTML = '<div class="form-empty">加载失败</div>';
  }
}

// ── 表单内嵌填写(覆写层)──
function openFormOverlay(formId) {
  const overlay = document.getElementById('form-overlay') || createFormOverlay();
  overlay.classList.add('show');
  overlay.querySelector('.form-overlay-saved').style.display = 'none';
  const body = overlay.querySelector('.form-overlay-body');
  body.innerHTML = '<div style="text-align:center;padding:20px;color:#555;">加载中...</div>';

  // 并行加载表单定义 + 已有值
  Promise.all([
    fetch(`${API_BASE}/forms/render/${formId}`).then(r => r.json()),
    fetch(`${API_BASE}/forms/${formId}/values`).then(r => r.status === 200 ? r.json() : {values_json:{}})
  ]).then(([formData, valData]) => {
    if (!formData.form_id) {
      body.innerHTML = '<div style="text-align:center;padding:20px;color:#555;">表单不存在</div>';
      return;
    }
    const schema = formData.schema || {};
    const fields = schema.fields || [];
    const values = valData.values_json || {};

    overlay.querySelector('.form-overlay-title').textContent = formData.title;

    let html = '';
    fields.forEach(f => {
      const required = f.required ? '<span class="required">*</span>' : '';
      html += `<div class="form-field"><div class="form-field-label">${escapeHtml(f.label)}${required}</div>`;
      if (f.type === 'text') {
        const v = escapeHtml(values[f.name] || '');
        html += `<div class="form-field-text"><input type="text" id="form-field-${f.name}" value="${v}"></div>`;
      } else if (f.type === 'radio') {
        html += '<div class="form-field-radio">';
        (f.options || []).forEach(opt => {
          const checked = values[f.name] === opt.value ? 'checked' : '';
          html += `<label><input type="radio" name="form-${f.name}" value="${escapeHtml(opt.value)}" ${checked}><span>${escapeHtml(opt.label)}</span></label>`;
        });
        html += '</div>';
      }
      html += '</div>';
    });
    body.innerHTML = html;
    // 保存事件
    overlay.querySelector('.form-overlay-btn').onclick = () => saveFormOverlay(formId, fields, overlay);
  }).catch(() => {
    body.innerHTML = '<div style="text-align:center;padding:20px;color:#555;">加载失败</div>';
  });
}

function createFormOverlay() {
  const div = document.createElement('div');
  div.className = 'form-overlay';
  div.id = 'form-overlay';
  div.onclick = function(e) { if (e.target === this) this.classList.remove('show'); };
  div.innerHTML = `
    <div class="form-overlay-card" onclick="event.stopPropagation()">
      <div class="form-overlay-header">
        <div class="form-overlay-title"></div>
        <button class="form-overlay-close" onclick="document.getElementById('form-overlay').classList.remove('show')">✕</button>
      </div>
      <div class="form-overlay-body"></div>
      <div class="form-overlay-saved">✅ 已保存</div>
      <button class="form-overlay-btn">保存</button>
    </div>`;
  document.body.appendChild(div);
  return div;
}

async function saveFormOverlay(formId, fields, overlay) {
  const values = {};
  fields.forEach(f => {
    if (f.type === 'text') {
      const el = document.getElementById(`form-field-${f.name}`);
      if (el) values[f.name] = el.value;
    } else if (f.type === 'radio') {
      const el = document.querySelector(`input[name="form-${f.name}"]:checked`);
      if (el) values[f.name] = el.value;
    }
  });
  try {
    const r = await fetch(`${API_BASE}/forms/${formId}/values`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({values})
    });
    const data = await r.json();
    if (data.ok) {
      const hint = overlay.querySelector('.form-overlay-saved');
      hint.style.display = 'block';
      setTimeout(() => { hint.style.display = 'none'; }, 2000);
    }
  } catch(e) { alert('保存失败'); }
}

// ── 归属 Skill ──
async function renderSkillAssign() {
  const container = document.getElementById('skill-assign-list');
  try {
    const r = await fetch(`${API_BASE}/skills`);
    const skills = await r.json();
    if (!skills || !skills.length) {
      container.innerHTML = '<div class="skill-assign-empty">🏗️ 暂无归属 — 通过 task-org 配置</div>';
      return;
    }
    let html = '';
    skills.forEach(s => {
      const category = s.category || '未分类';
      html += `<div class="skill-assign-item" onclick="openSkillDrawer('${escapeHtml(s.name)}', '${escapeHtml(s.description||'')}', '${escapeHtml(category)}')">
        <span class="sname">🧠 ${escapeHtml(s.name)}</span>
        <span class="stask-count">${escapeHtml(category)}</span>
      </div>`;
    });
    container.innerHTML = html;
  } catch(e) {
    container.innerHTML = '<div class="skill-assign-empty">加载失败</div>';
  }
}

function openSkillDrawer(name, desc, category) {
  const overlay = document.getElementById('skill-drawer') || createSkillDrawer();
  overlay.classList.add('show');
  overlay.querySelector('.skill-drawer-name').textContent = name;
  overlay.querySelector('.skill-drawer-cat').textContent = category;
  overlay.querySelector('.skill-drawer-desc').textContent = desc || '暂无描述';
}

function createSkillDrawer() {
  const div = document.createElement('div');
  div.className = 'skill-drawer-overlay';
  div.id = 'skill-drawer';
  div.onclick = function(e) { if (e.target === this) this.classList.remove('show'); };
  div.innerHTML = `
    <div class="skill-drawer-card" onclick="event.stopPropagation()">
      <div class="skill-drawer-header">
        <button class="form-overlay-close" onclick="document.getElementById('skill-drawer').classList.remove('show')">✕</button>
        <div class="skill-drawer-name"></div>
        <div class="skill-drawer-cat"></div>
      </div>
      <div class="skill-drawer-desc"></div>
      <div style="font-size:11px;color:#555;text-align:center;padding-top:8px;">通过 task-org 配置归属</div>
    </div>`;
  document.body.appendChild(div);
  return div;
}

// ── 附件预览 ──
function previewAttachment(id) {
  // 获取详情
  fetch(`${API_BASE}/chat/attachment/${id}/download`)
    .then(r => r.json())
    .then(data => {
      if (!data.url) { alert('附件不存在'); return; }
      
      const overlay = document.getElementById('att-preview-overlay') || createPreviewOverlay();
      document.getElementById('att-preview-name').textContent = data.filename || '附件';
      document.getElementById('att-preview-size').textContent = data.filesize ? 
        (data.filesize > 1024*1024 ? (data.filesize/1024/1024).toFixed(1)+'MB' : 
         data.filesize > 1024 ? (data.filesize/1024).toFixed(0)+'KB' : data.filesize+'B') : '';
      
      const body = document.getElementById('att-preview-body');
      const ext = (data.filename || '').split('.').pop().toLowerCase();
      const imgExts = ['png','jpg','jpeg','gif','webp','svg','bmp','ico'];
      const videoExts = ['mp4','webm','ogg','mov'];
      const textExts = ['txt','md','html','json','css','js','py','sh','go','rs','java','c','cpp','h','yaml','yml','toml','xml','cfg','conf','ini'];
      
      if (imgExts.includes(ext)) {
        body.innerHTML = `<img src="${data.url}" alt="${data.filename}" onerror="this.outerHTML='<div class=\\'att-unsupported\\'>图片加载失败</div>'">`;
      } else if (videoExts.includes(ext)) {
        body.innerHTML = `<video controls src="${data.url}"></video>`;
      } else if (textExts.includes(ext)) {
        fetch(data.url).then(r => r.text()).then(text => {
          body.innerHTML = `<div class="att-text-preview">${escapeHtml(text)}</div>`;
        }).catch(() => {
          body.innerHTML = `<div class="att-text-preview">(无法加载文本预览)</div>`;
        });
      } else if (ext === 'pdf') {
        body.innerHTML = `<iframe src="${data.url}"></iframe>`;
      } else {
        body.innerHTML = `<div class="att-unsupported">⚠️ 该类型无法预览<br><a href="${data.url}" target="_blank" style="color:#4ade80;">下载文件</a></div>`;
      }
      
      overlay.classList.add('show');
    })
    .catch(e => alert('加载失败: ' + e.message));
}

function createPreviewOverlay() {
  const div = document.createElement('div');
  div.className = 'att-preview-overlay';
  div.id = 'att-preview-overlay';
  div.onclick = function(e) { if (e.target === this) this.classList.remove('show'); };
  div.innerHTML = `
    <div class="att-preview-modal" onclick="event.stopPropagation()">
      <div class="att-preview-header">
        <span class="att-preview-name" id="att-preview-name">附件</span>
        <span style="font-size:11px;color:#666;" id="att-preview-size"></span>
        <button class="att-preview-close" onclick="document.getElementById('att-preview-overlay').classList.remove('show')">✕</button>
      </div>
      <div class="att-preview-body" id="att-preview-body">
        <div class="att-unsupported">加载中...</div>
      </div>
    </div>
  `;
  document.body.appendChild(div);
  return div;
}

// ── Wiki 知识树 ──
async function loadWiki(taskId, folderId) {
  if (folderId) {
    const data = await apiGet(`/chat/tree?folder_id=${folderId}`);
    if (data && data.tree) {
      renderWikiTree(data.tree);
      return;
    }
  }
  if (taskId) {
    const data = await apiGet(`/chat/tree?task_id=${taskId}`);
    if (data && data.tree) {
      renderWikiTree(data.tree);
      return;
    }
  }
  wikiPanel.innerHTML = '<div class="wiki-doc-empty">暂无知识沉淀</div>';
}

// 全局锁:目前正在加载历史的 task id
let loadingTaskId = null;

async function loadHistory(taskId, offset, limit, append) {
  // 如果正在加载其他task的历史,等500ms重试
  if (loadingTaskId && loadingTaskId !== taskId) {
    await new Promise(r => setTimeout(r, 500));
    if (loadingTaskId && loadingTaskId !== taskId) return;
  }
  loadingTaskId = taskId;
  try {
    const data = await apiGet(`/chat/history?task_id=${taskId}&offset=${offset}&limit=${limit}`);
    if (data && data.messages) {
      hasMoreHistory = data.has_more;
      // 保存到浏览器缓存(仅首次加载时覆盖,丢弃 reasoning 等大字段)
      if (!append && offset === 0) {
        const clean = data.messages.map(m => ({
          id: m.id, role: m.role, content: m.content,
          timestamp: m.timestamp || m.created_at,
        }));
        cacheSetAll(taskId, clean);
      } else if (append && data.messages.length > 0) {
        // 追加历史时也补充到缓存
        const cached = cacheGet(taskId);
        if (cached) {
          // 把新加载的旧消息插入到缓存头部
          const merged = [...data.messages.filter(m => !cached.some(c => c.id === m.id)), ...cached];
          cacheSetAll(taskId, merged);
        }
      }
      renderMessages(data.messages, append);
    }
  } finally {
    loadingTaskId = null;
  }
}

// 延迟加载历史 — 等 loadingTaskId 释放后重试
function delayedLoadHistory(taskId, offset, limit, delay) {
  setTimeout(async () => {
    for (let retry = 0; retry < 5; retry++) {
      if (!loadingTaskId || loadingTaskId === taskId) {
        await loadHistory(taskId, offset, limit);
        return;
      }
      await new Promise(r => setTimeout(r, 300));
    }
    await loadHistory(taskId, offset, limit);
  }, delay);
}

async function loadMoreHistory() {
  if (isLoadingMore || !hasMoreHistory || loadingTaskId || !currentTaskId) return;
  isLoadingMore = true;
  historyOffset += historyLimit;
  await loadHistory(currentTaskId, historyOffset, historyLimit, true);
  isLoadingMore = false;
}

// ═══════════════════════════════════════════
//  渲染
// ═══════════════════════════════════════════

function renderTaskList() {
  if (!tasks.length && !allDirs.length) {
    taskList.innerHTML = '<div class="task-empty">暂无任务</div>';
    return;
  }

  const q = getTaskSearchQuery();

  // 使用 task_dirs 目录树
  if (allDirs && allDirs.length > 0) {
    const root = allDirs.find(d => d.parent_id === null);
    const orphans = tasks.filter(t => !t.dir_id).filter(t => taskMatchesSearch(t));

    let html = '';
    if (root) {
      html += renderDirNodes(allDirs, root.id, 0);
    }

    // 未分类任务(无 dir_id)
    if (orphans.length > 0) {
      html += '<div class="task-folder">';
      html += `<div class="task-folder-header" onclick="toggleTaskFolder(this)">
        <span class="arrow">▶</span>
        <span class="fname">📋 未分类</span>
        <span style="margin-left:auto;font-size:10px;color:#555;">${orphans.length}</span>
      </div>`;
      html += '<div class="task-folder-children">';
      orphans.forEach(t => {
        const dotCls = t.status === 'running' ? 'running' : t.status === 'active' ? 'active' : t.status === 'created' ? 'created' : 'completed';
        const active = t.id === currentTaskId ? ' active' : '';
        html += `<div class="task-folder-item${active}" onclick="selectTask('${t.id}')">
          <span class="dot ${dotCls}"></span>
          <span class="tname">${escapeHtml(t.name)}</span>
        </div>`;
      });
      html += '</div></div>';
    }

    taskList.innerHTML = html;
  } else {
    // 降级:平铺列表
    const filtered = q ? tasks.filter(t => taskMatchesSearch(t)) : tasks;
    taskList.innerHTML = filtered.map(t => {
      const active = t.id === currentTaskId ? 'active' : '';
      const statusLabel = {created:'未开始', running:'进行中', paused:'已暂停', completed:'已完成', cancelled:'已取消'}[t.status] || t.status;
      const updatedStr = t.updated_at ? t.updated_at.slice(0, 16).replace('T', ' ') : '';
      return `
        <div class="task-item ${active}" data-task-id="${t.id}" onclick="selectTask('${t.id}')">
          <div class="task-status ${t.status}"></div>
          <div class="task-info">
            <div class="task-name">${escapeHtml(t.name)}</div>
            <div class="task-meta">${t.msg_count || 0} 条消息 · ${updatedStr}</div>
          </div>
          <div class="task-status-text">${statusLabel}</div>
        </div>
      `;
    }).join('');
    // 无结果提示
    if (!filtered.length) {
      taskList.innerHTML = '<div class="task-empty">无匹配任务</div>';
    }
  }
}

function renderDirNodes(dirs, parentId, depth) {
  const children = dirs.filter(d => d.parent_id === parentId)
    .sort((a,b) => (a.sort_order||0) - (b.sort_order||0));
  if (!children.length) return '';

  const q = getTaskSearchQuery();
  let html = '';
  children.forEach(d => {
    const hasChildren = dirs.some(x => x.parent_id === d.id);
    const tasksHere = tasks.filter(t => t.dir_id === d.id);
    const subHtml = hasChildren ? renderDirNodes(dirs, d.id, depth + 1) : '';
    const matchingTasks = tasksHere.filter(t => taskMatchesSearch(t));

    // 搜索过滤:目录名匹配 or 有匹配的task or 有匹配的子目录
    const dirMatch = !q || (d.name || '').toLowerCase().includes(q);
    const taskMatch = !q || matchingTasks.length > 0;
    const subMatch = !q || subHtml.trim();
    if (q && !dirMatch && !taskMatch && !subMatch) return;

    const isOpen = openDirIds.has(d.id) || q ? 'open' : '';
    html += `<div class="task-folder ${isOpen}" data-dir-id="${d.id}">`;
    html += `<div class="task-folder-header" onclick="toggleTaskFolder(this)">
      <span class="arrow">${isOpen ? '▼' : '▶'}</span>
      <span class="fname">📁 ${escapeHtml(d.name)}</span>
      <span style="margin-left:auto;font-size:10px;color:#555;">${matchingTasks.length}</span>
    </div>`;
    html += '<div class="task-folder-children">';
    // 子目录
    html += subHtml;
    // 本目录下的 task(已过滤)
    matchingTasks.forEach(t => {
      const dotCls = t.status === 'running' ? 'running' : t.status === 'active' ? 'active' : t.status === 'created' ? 'created' : 'completed';
      const active = t.id === currentTaskId ? ' active' : '';
      html += `<div class="task-folder-item${active}" onclick="selectTask('${t.id}')">
        <span class="dot ${dotCls}"></span>
        <span class="tname">${escapeHtml(t.name)}</span>
      </div>`;
    });
    html += '</div></div>';
  });
  return html;
}

function toggleTaskFolder(headerEl) {
  const folder = headerEl.closest('.task-folder');
  const dirId = folder ? folder.dataset.dirId : null;
  folder.classList.toggle('open');
  const nowOpen = folder.classList.contains('open');
  headerEl.querySelector('.arrow').textContent = nowOpen ? '▼' : '▶';
  if (dirId) {
    if (nowOpen) openDirIds.add(dirId);
    else openDirIds.delete(dirId);
  }
}

// ── 任务搜索 ──
function onTaskSearchInput() {
  renderTaskList();
}
function getTaskSearchQuery() {
  const el = document.getElementById('taskSearch');
  return el ? (el.value || '').toLowerCase() : '';
}
function taskMatchesSearch(task) {
  const q = getTaskSearchQuery();
  if (!q) return true;
  return (task.name || '').toLowerCase().includes(q);
}

function renderWikiTree(tree) {
  // 更新计数
  const docs = [];
  function flatten(nodes) {
    if (!nodes || !nodes.length) return;
    nodes.forEach(n => {
      if (n.type === 'doc') docs.push(n);
      if (n.children) flatten(n.children);
    });
  }
  flatten(tree);
  el('wikiCountPanel').textContent = docs.length;

  if (!tree || tree.length === 0) {
    wikiPanel.innerHTML = '<div class="wiki-doc-empty">暂无知识沉淀</div>';
    return;
  }
  wikiPanel.innerHTML = docs.map(n =>
    `<div class="wiki-doc" onclick="openDoc('${n.id}')">${escapeHtml(n.name)}</div>`
  ).join('');
}

function renderMessages(msgs, append = false) {
  if (!append) {
    messages.innerHTML = '';
    historyOffset = 0;
    hasMoreHistory = true;
  }
  
  msgs.forEach(m => {
    const div = document.createElement('div');
    div.className = `msg ${m.role === 'assistant' ? 'ai' : m.role}`;
    div.dataset.msgId = m.id;
    div.dataset.role = m.role;
    
    // 内容区
    const contentDiv = document.createElement('div');
    contentDiv.className = 'msg-content';
    contentDiv.innerHTML = m.role === 'assistant' ? renderMarkdown(m.content) : escapeHtml(m.content);
    div.appendChild(contentDiv);
    
    // 编辑区(仅助理消息可编辑)
    if (m.role === 'assistant' || m.role === 'user') {
      const editArea = document.createElement('div');
      editArea.className = 'edit-area';
      editArea.innerHTML = `
        <textarea>${escapeHtml(m.content)}</textarea>
        <div class="edit-buttons">
          <button class="btn-save" onclick="saveEdit('${m.id}', this)">💾 保存</button>
          <button class="btn-cancel" onclick="cancelEdit('${m.id}')">取消</button>
        </div>`;
      div.appendChild(editArea);
    }
    
    // 删除确认区
    const delConfirm = document.createElement('div');
    delConfirm.className = 'delete-confirm';
    delConfirm.innerHTML = `
      <span>确定删除此消息?</span>
      <div class="del-buttons">
        <button class="btn-confirm-del" onclick="confirmDelete('${m.id}')">删除</button>
        <button class="btn-cancel-del" onclick="cancelDelete('${m.id}')">取消</button>
      </div>`;
    div.appendChild(delConfirm);
    
    // 操作按钮
    const actions = document.createElement('div');
    actions.className = 'msg-actions';
    actions.innerHTML = `
      <button class="btn-edit" onclick="startEdit('${m.id}')">✏️</button>
      <button class="btn-move" onclick="showMoveMessage('${m.id}')">⇄</button>
      <button class="btn-delete" onclick="showDelete('${m.id}')">🗑️</button>`;
    div.appendChild(actions);
    
    // 时间
    const time = document.createElement('div');
    time.className = 'time';
    time.textContent = m.created_at || new Date().toLocaleTimeString();
    div.appendChild(time);
    
    if (append) {
      // 追加到顶部(历史记录加载)
      messages.insertBefore(div, messages.firstChild);
    } else {
      messages.appendChild(div);
    }
  });
  
  // 保持滚动位置
  if (!append) {
    messages.scrollTop = messages.scrollHeight;
  }
}

// ── 消息编辑 ──
function startEdit(msgId) {
  const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
  if (!msg) return;
  msg.classList.add('editing');
  const textarea = msg.querySelector('.edit-area textarea');
  if (textarea) {
    textarea.focus();
    textarea.setSelectionRange(textarea.value.length, textarea.value.length);
  }
}

function cancelEdit(msgId) {
  const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
  if (!msg) return;
  msg.classList.remove('editing');
}

async function saveEdit(msgId, btn) {
  const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
  if (!msg) return;
  const textarea = msg.querySelector('.edit-area textarea');
  const newContent = textarea ? textarea.value.trim() : '';
  if (!newContent) return;
  
  btn.textContent = '保存中…';
  btn.disabled = true;
  
  try {
    const resp = await fetch(`http://${location.host}/api/chat/message/edit`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({id: parseInt(msgId), content: newContent}),
    });
    const data = await resp.json();
    if (data.success) {
      // 更新显示
      const contentDiv = msg.querySelector('.msg-content');
      if (contentDiv) {
        const role = msg.dataset.role;
        contentDiv.innerHTML = role === 'assistant' ? renderMarkdown(newContent) : escapeHtml(newContent);
      }
      msg.classList.remove('editing');
    } else {
      alert('编辑失败:' + (data.error || '未知错误'));
    }
  } catch(e) {
    alert('网络错误:' + e.message);
  }
  
  btn.textContent = '💾 保存';
  btn.disabled = false;
}

// ── 消息删除 ──
function showDelete(msgId) {
  const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
  if (!msg) return;
  const confirm = msg.querySelector('.delete-confirm');
  if (confirm) confirm.style.display = 'block';
}

function cancelDelete(msgId) {
  const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
  if (!msg) return;
  const confirm = msg.querySelector('.delete-confirm');
  if (confirm) confirm.style.display = 'none';
}

async function confirmDelete(msgId) {
  try {
    const resp = await fetch(`http://${location.host}/api/chat/message/delete`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({id: parseInt(msgId)}),
    });
    const data = await resp.json();
    if (data.success) {
      const msg = messages.querySelector(`[data-msg-id="${msgId}"]`);
      if (msg) {
        const contentDiv = msg.querySelector('.msg-content');
        if (contentDiv) contentDiv.innerHTML = '<em style="color:#555">[消息已删除]</em>';
        const confirm = msg.querySelector('.delete-confirm');
        if (confirm) confirm.style.display = 'none';
      }
    }
  } catch(e) {
    alert('删除失败:' + e.message);
  }
}

// ── 搜索 ──
let searchActive = false;

function toggleSearch() {
  const panel = el('search-panel');
  panel.classList.toggle('open');
  if (panel.classList.contains('open')) {
    el('search-input').focus();
  }
}

function closeSearch() {
  el('search-panel').classList.remove('open');
  el('search-count').textContent = '';
  searchActive = false;
  messages.querySelectorAll('.search-highlight').forEach(el => {
    const parent = el.parentNode;
    parent.replaceChild(document.createTextNode(el.textContent), el);
    parent.normalize();
  });
}

async function doSearch() {
  if (!currentTaskId) return;
  const keyword = el('search-input').value.trim();
  if (!keyword) return;
  
  searchActive = true;
  el('search-count').textContent = '搜索中...';
  
  try {
    const resp = await fetch(`http://${location.host}/api/chat/search?task_id=${currentTaskId}&q=${encodeURIComponent(keyword)}`);
    const data = await resp.json();
    const msgs = data.messages || [];
    el('search-count').textContent = `找到 ${msgs.length} 条`;
    
    if (msgs.length > 0) {
      selectTask(currentTaskId);
      setTimeout(() => {
        renderMessages(msgs);
        highlightKeyword(keyword);
      }, 100);
    }
  } catch(e) {
    el('search-count').textContent = '搜索失败';
  }
}

function highlightKeyword(keyword) {
  if (!keyword) return;
  const regex = new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
  messages.querySelectorAll('.msg-content').forEach(el => {
    el.innerHTML = el.innerHTML.replace(regex, '<span class="search-highlight">$1</span>');
  });
}

// ═══════════════════════════════════════════
//  交互
function isForCurrentTask(data) {
  // 检查消息是否属于当前选中的任务,用于任务隔离
  if (data.task_id && data.task_id !== currentTaskId) return false;
  return true;
}

async function selectTask(taskId) {
  if (currentTaskId === taskId) return;

  currentTaskId = taskId;
  localStorage.setItem('hermes_last_task_id', taskId);
  currentTaskName = '';
  const task = tasks.find(t => t.id === taskId);
  if (task) currentTaskName = task.name;

  renderTaskList();
  chatTitle.textContent = `💬 ${escapeHtml(currentTaskName) || 'Hermes Chat'}`;

  noTaskMsg.style.display = 'none';
  messages.style.display = 'flex';
  messages.innerHTML = '';
  
  // 切换任务时清空流式残留
  if (streamTimer) {
    clearTimeout(streamTimer);
    streamTimer = null;
  }
  streamBuffer = '';
  streamingTaskId = null;
  
  // 重置分页
  historyOffset = 0;
  hasMoreHistory = true;
  isLoadingMore = false;
  
  input.disabled = false;
  sendBtn.disabled = false;
  el('input-area').style.display = 'flex';
  input.focus();

  loadWiki(taskId);

  // 加载附件
  renderAttachments(taskId);

  // 加载表单
  renderFormList();

  // 从浏览器缓存优先渲染(无需等待接口)
  const cached = cacheGet(taskId);
  if (cached && cached.length > 0) {
    hasMoreHistory = true;
    renderMessages(cached, false);
  }

  // 后台同步服务端最新消息(不阻塞切换)
  setTimeout(async () => {
    try {
      const data = await apiGet(`/chat/history?task_id=${taskId}&offset=0&limit=${historyLimit}`);
      if (data && data.messages && data.messages.length > 0) {
        // 比较缓存长度和服务端长度,有新消息才渲染
        const cachedNow = cacheGet(taskId);
        if (!cachedNow || data.messages.length > cachedNow.length) {
          const clean = data.messages.map(m => ({
            id: m.id, role: m.role, content: m.content,
            timestamp: m.timestamp || m.created_at,
          }));
          cacheSetAll(taskId, clean);
          // 如果消息数有变化,重绘(避免覆盖用户正在查看的缓存渲染)
          if (!cachedNow || data.messages.length !== cachedNow.length) {
            renderMessages(clean, false);
          }
        }
        hasMoreHistory = data.has_more;
      }
    } catch(e) {
      console.warn('[SYNC] background sync failed:', e);
    }
  }, 300);
  const currentTask = taskId;
  setTimeout(async () => {
    if (currentTaskId === currentTask && messages.children.length === 0) {
      console.warn('[DEFENSIVE] history empty after selectTask, retrying...');
      await loadHistory(currentTask, 0, historyLimit);
    }
  }, 2000);

  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'select_task', task_id: taskId }));
  }
}

function openDoc(docId) {
  const iframe = document.getElementById('doc-drawer-iframe');
  const loading = document.getElementById('doc-drawer-loading');
  const title = document.getElementById('doc-drawer-title');
  const docUrl = `/doc/${docId}`;

  // 重置状态
  iframe.style.display = 'none';
  loading.style.display = 'flex';
  title.textContent = '📄 加载中...';
  document.getElementById('doc-drawer').classList.add('open');
  document.getElementById('doc-drawer-overlay').classList.add('open');

  // 预加载文档标题
  fetch(`/api/doc-name?doc_id=${docId}`)
    .then(r => r.json())
    .then(d => { if (d.name) title.textContent = `📄 ${d.name}`; })
    .catch(() => {});

  // 设置 iframe src(加载完成后自动显示)
  iframe.onload = () => {
    loading.style.display = 'none';
    iframe.style.display = 'block';
    // 尝试从iframe提取标题
    try {
      const docTitle = iframe.contentDocument?.title;
      if (docTitle && !docTitle.includes('文档系统')) {
        title.textContent = `📄 ${docTitle}`;
      }
    } catch(e) {}
  };
  iframe.src = docUrl;
}

function closeDocDrawer() {
  document.getElementById('doc-drawer').classList.remove('open');
  document.getElementById('doc-drawer-overlay').classList.remove('open');
  // 延迟清空 iframe src,释放内存
  setTimeout(() => {
    const iframe = document.getElementById('doc-drawer-iframe');
    if (!document.getElementById('doc-drawer').classList.contains('open')) {
      iframe.src = '';
      iframe.style.display = 'none';
      document.getElementById('doc-drawer-loading').style.display = 'flex';
    }
  }, 300);
}

// Esc 键关闭抽屉
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape') closeDocDrawer();
});

function escapeHtml(text) {
  const d = document.createElement('div');
  d.textContent = text;
  return d.innerHTML;
}

function renderMarkdown(text) {
  if (!text) return '';
  try {
    return marked.parse(text, {
      breaks: true,
      gfm: true,
    });
  } catch(e) {
    return escapeHtml(text);
  }
}

function copyCode(btn) {
  const pre = btn.closest('.code-block-wrapper').querySelector('pre code');
  const text = pre.textContent;
  navigator.clipboard.writeText(text).then(() => {
    btn.textContent = '已复制';
    setTimeout(() => { btn.textContent = '复制'; }, 2000);
  }).catch(() => {
    btn.textContent = '复制失败';
  });
}

// 配置 marked 使用 highlight.js(marked v15 新写法)
const renderer = new marked.Renderer();
renderer.code = function({ text, lang }) {
  let highlighted;
  try {
    if (lang && hljs.getLanguage(lang)) {
      highlighted = hljs.highlight(text, { language: lang }).value;
    } else {
      highlighted = hljs.highlightAuto(text).value;
    }
  } catch(e) {
    highlighted = text;
  }
  const langLabel = lang || 'code';
  return `<div class="code-block-wrapper"><div class="code-header"><span class="code-lang">${langLabel}</span><button class="copy-btn" onclick="copyCode(this)">复制</button></div><pre><code class="language-${langLabel}">${highlighted}</code></pre></div>`;
};

marked.use({ renderer });

let streamBuffer = '';
let streamTimer = null;
let thinkingBuffer = '';
let reasoningText = '';
let pendingReasoningDone = false;
const ENABLE_REASONING = true;

// ── 无限滚动 ──
let historyOffset = 0;
let historyLimit = 20;
let isLoadingMore = false;
let hasMoreHistory = true;

// ── 推理折叠面板 ──
function renderReasoning(text, container) {
  const panel = document.createElement('div');
  panel.className = 'reasoning-panel';
  panel.innerHTML = `
    <div class="reasoning-header" onclick="toggleReasoning(this)">
      <span class="arrow">▶</span>
      <span class="icon">💭</span>
      <span class="label">思考过程</span>
    </div>
    <div class="reasoning-body">
      <div class="thinking-text">${escapeHtml(text)}</div>
    </div>`;
  container.appendChild(panel);
}

function toggleReasoning(header) {
  const arrow = header.querySelector('.arrow');
  const body = header.parentElement.querySelector('.reasoning-body');
  arrow.classList.toggle('open');
  body.classList.toggle('open');
}

// ── 实时更新推理内容 ──
function updateReasoningDisplay(chunk) {
  // 找到当前消息中的推理面板,或创建新的
  const lastMsg = messages.lastElementChild;
  let panel = lastMsg ? lastMsg.querySelector('.reasoning-panel') : null;
  
  if (panel) {
    const textEl = panel.querySelector('.thinking-text');
    if (textEl) textEl.textContent += chunk;
  } else if (lastMsg && lastMsg.classList.contains('ai')) {
    // 在消息最前面插入推理面板
    panel = document.createElement('div');
    panel.className = 'reasoning-panel';
    panel.innerHTML = `
      <div class="reasoning-header" onclick="toggleReasoning(this)">
        <span class="arrow">▶</span>
        <span class="icon">💭</span>
        <span class="label">思考过程</span>
      </div>
      <div class="reasoning-body open">
        <div class="thinking-text">${escapeHtml(chunk)}</div>
      </div>`;
    // 推理面板后面插入一个包裹剩余内容的容器
    const contentWrap = document.createElement('div');
    contentWrap.className = 'reasoning-content';
    while (lastMsg.firstChild) {
      contentWrap.appendChild(lastMsg.firstChild);
    }
    lastMsg.appendChild(panel);
    // 把旧内容移到推理面板下方
    panel.after(contentWrap);
  }
}

// ── 最终完成推理(收起面板)──
function finalizeReasoning() {
  const panels = messages.querySelectorAll('.reasoning-panel');
  const lastPanel = panels[panels.length - 1];
  if (lastPanel) {
    const body = lastPanel.querySelector('.reasoning-body');
    if (body) body.classList.remove('open');
    const arrow = lastPanel.querySelector('.arrow');
    if (arrow) arrow.classList.remove('open');
  }
}

// ── 在消息底部加上打字光标 ──
function addTypingCursor(msgEl) {
  let cursor = msgEl.querySelector('.typing-cursor');
  if (!cursor) {
    cursor = document.createElement('span');
    cursor.className = 'typing-cursor';
    const contentArea = msgEl.querySelector('.msg-content') || msgEl.querySelector('.tool-calls-area');
    if (contentArea) contentArea.after(cursor);
    else msgEl.appendChild(cursor);
  }
}

function removeTypingCursor(msgEl) {
  const cursor = msgEl.querySelector('.typing-cursor');
  if (cursor) cursor.remove();
}

// ── 工具调用卡片 ──
let toolCallState = {
  activeTool: null,    // 当前正在运行的 tool 对象
  toolIdCounter: 0,
};

function getToolIcon(name) {
  const iconMap = {
    'web_search': '🔍', 'web_search_news': '📰',
    'terminal': '💻', 'read_file': '📖', 'write_file': '✏️',
    'search_files': '🔎', 'patch': '🔧',
    'execute_code': '⚡', 'python': '🐍',
    'vision_analyze': '👁️', 'text_to_speech': '🔊',
    'memory': '🧠', 'skill_manage': '📦', 'delegate_task': '👥',
    'cronjob': '⏰', 'todo': '✅',
    'browser': '🌐', 'clarify': '❓',
  };
  return iconMap[name] || '🔧';
}

function createToolCard(name, args) {
  toolCallState.toolIdCounter++;
  const toolId = toolCallState.toolIdCounter;
  
  const card = document.createElement('div');
  card.className = 'tool-call-card';
  card.dataset.toolId = toolId;
  
  const icon = getToolIcon(name);
  let argsHtml = '';
  if (args && typeof args === 'object') {
    const lines = [];
    for (const [k, v] of Object.entries(args)) {
      const val = typeof v === 'string' ? v : JSON.stringify(v);
      lines.push(`<span class="tool-args-key">${escapeHtml(k)}</span>=<span class="tool-args-val">${escapeHtml(String(val).substring(0, 100))}</span>`);
    }
    if (lines.length) argsHtml = '<div class="tool-args">' + lines.join(', ') + '</div>';
  }
  
  card.innerHTML = `
    <div class="tool-call-header" onclick="toggleToolCard(this)">
      <span class="tool-icon">${icon}</span>
      <span class="tool-name">${escapeHtml(name)}</span>
      <span class="tool-status running">
        <span class="tool-status-spinner"></span> 运行中…
      </span>
    </div>
    <div class="tool-call-body">
      ${argsHtml}
      <div class="tool-result"></div>
    </div>`;
  
  toolCallState.activeTool = { card, name, toolId, status: 'running' };
  return card;
}

function updateToolResult(text) {
  if (!toolCallState.activeTool) return;
  const body = toolCallState.activeTool.card.querySelector('.tool-call-body');
  const resultDiv = body.querySelector('.tool-result');
  if (resultDiv) {
    const preview = text.length > 200 ? text.substring(0, 200) + '…' : text;
    resultDiv.innerHTML = `<div class="tool-result-preview">${escapeHtml(preview)}</div>`;
  }
}

function completeActiveTool() {
  if (!toolCallState.activeTool) return;
  const card = toolCallState.activeTool.card;
  const statusEl = card.querySelector('.tool-status');
  statusEl.className = 'tool-status done';
  statusEl.innerHTML = '✅ 完成';
  const spinner = statusEl.querySelector('.tool-status-spinner');
  if (spinner) spinner.remove();
  toolCallState.activeTool = null;
}

// ── 子代理事件处理 ──
let subagentCards = {}; // subagent_id -> card element

function handleSubagentEvent(data) {
  const saType = data.subagent_type;
  const saId = data.subagent_id || 'default';
  const msgEl = getOrCreateAiMessage();
  const callsArea = getOrCreateCallsArea(msgEl);
  
  let card = subagentCards[saId];
  
  if (saType === 'start') {
    // 创建新卡片
    card = document.createElement('div');
    card.className = 'subagent-card';
    card.dataset.saId = saId;
    card.innerHTML = `
      <div class="subagent-header">
        <span class="sa-icon">👤</span>
        <span class="sa-name">${escapeHtml(data.name || '子任务')}</span>
        <span class="sa-badge">${data.task_index + 1}/${data.task_count}</span>
        <span style="margin-left:auto;font-size:11px;color:#f59e0b;">🔄 运行中</span>
      </div>
      <div class="subagent-body"></div>
      <div class="subagent-footer" style="display:none;"></div>`;
    callsArea.appendChild(card);
    subagentCards[saId] = card;
    messages.scrollTop = messages.scrollHeight;
    
  } else if (!card) {
    // 如果没有 start 事件但收到后续事件,创建骨架
    card = document.createElement('div');
    card.className = 'subagent-card';
    card.dataset.saId = saId;
    card.innerHTML = `
      <div class="subagent-header">
        <span class="sa-icon">👤</span>
        <span class="sa-name">子任务</span>
        <span style="margin-left:auto;font-size:11px;color:#f59e0b;">🔄 运行中</span>
      </div>
      <div class="subagent-body"></div>
      <div class="subagent-footer" style="display:none;"></div>`;
    callsArea.appendChild(card);
    subagentCards[saId] = card;
  }
  
  const body = card.querySelector('.subagent-body');
  const footer = card.querySelector('.subagent-footer');
  
  switch (saType) {
    case 'thinking':
      if (body) {
        const line = document.createElement('div');
        line.className = 'sa-thinking';
        line.textContent = '💭 ' + (data.text || '');
        body.appendChild(line);
        body.scrollTop = body.scrollHeight;
      }
      break;
    case 'text':
      if (body) {
        body.appendChild((() => {
          const el = document.createElement('div');
          el.className = 'sa-text';
          el.textContent = data.text || '';
          return el;
        })());
        body.scrollTop = body.scrollHeight;
      }
      break;
    case 'tool':
      if (body) {
        body.appendChild((() => {
          const el = document.createElement('div');
          el.className = 'sa-tool';
          el.textContent = '🔧 ' + (data.tool_name || '') + (data.text ? ': ' + data.text : '');
          return el;
        })());
        body.scrollTop = body.scrollHeight;
      }
      break;
    case 'complete':
      // 更新头部状态
      const header = card.querySelector('.subagent-header');
      if (header) {
        const statusSpan = header.querySelector('[style*="margin-left:auto"]') || header.lastElementChild;
        if (statusSpan) {
          statusSpan.textContent = '✅ 完成';
          statusSpan.style.color = '#4ade80';
        }
      }
      // 显示 footer 统计
      if (footer) {
        footer.style.display = 'flex';
        const stats = [];
        if (data.input_tokens) stats.push(`📥 ${data.input_tokens} tokens`);
        if (data.output_tokens) stats.push(`📤 ${data.output_tokens} tokens`);
        if (data.summary) stats.push(`📋 ${data.summary.substring(0, 60)}`);
        footer.innerHTML = stats.map(s => `<span class="sa-stat">${s}</span>`).join('');
      }
      // 延迟清理
      setTimeout(() => delete subagentCards[saId], 5000);
      break;
  }
}

function getOrCreateAiMessage() {
  let last = messages.lastElementChild;
  if (last && last.classList.contains('ai') && last.dataset.streaming === 'true') {
    return last;
  }
  // 创建一个新的 AI 消息容器
  const div = document.createElement('div');
  div.className = 'msg ai';
  div.dataset.streaming = 'true';
  const callsArea = document.createElement('div');
  callsArea.className = 'tool-calls-area';
  div.appendChild(callsArea);
  const contentDiv = document.createElement('div');
  contentDiv.className = 'msg-content';
  div.appendChild(contentDiv);
  const time = document.createElement('div');
  time.className = 'time';
  time.textContent = new Date().toLocaleTimeString();
  div.appendChild(time);
  messages.appendChild(div);
  return div;
}

function getOrCreateCallsArea(msgEl) {
  let area = msgEl.querySelector('.tool-calls-area');
  if (!area) {
    area = document.createElement('div');
    area.className = 'tool-calls-area';
    msgEl.insertBefore(area, msgEl.querySelector('.msg-content') || msgEl.firstChild);
  }
  return area;
}

function toggleToolCard(header) {
  const card = header.parentElement;
  card.classList.toggle('collapsed');
}

function appendStream(chunk) {
  streamBuffer += chunk;
  if (streamTimer) return;

  streamTimer = setTimeout(() => {
    streamTimer = null;
    const full = streamBuffer.trim();
    streamBuffer = '';
    
    // 如果已切换任务或内容为空,丢弃
    if (!messages.children.length || !full) return;

    const last = messages.lastElementChild;
    if (last && last.classList.contains('ai') && last.dataset.streaming === 'true') {
      // 只更新 msg-content,不破坏 tool-calls-area
      let contentDiv = last.querySelector('.msg-content');
      if (!contentDiv) {
        contentDiv = document.createElement('div');
        contentDiv.className = 'msg-content';
        // 插入到 tool-calls-area 之后或消息开头
        const callsArea = last.querySelector('.tool-calls-area');
        if (callsArea) {
          callsArea.after(contentDiv);
        } else {
          last.prepend(contentDiv);
        }
        const time = document.createElement('div');
        time.className = 'time';
        time.textContent = new Date().toLocaleTimeString();
        last.appendChild(time);
      }
      contentDiv.innerHTML = renderMarkdown(full);
      addTypingCursor(last);
    } else {
      const div = document.createElement('div');
      div.className = 'msg ai';
      div.dataset.streaming = 'true';
      const contentDiv = document.createElement('div');
      contentDiv.className = 'msg-content';
      contentDiv.innerHTML = renderMarkdown(full);
      div.appendChild(contentDiv);
      addTypingCursor(div);
      const time = document.createElement('div');
      time.className = 'time';
      time.textContent = new Date().toLocaleTimeString();
      div.appendChild(time);
      messages.appendChild(div);
    }
    // 对新的代码块做高亮
    messages.querySelectorAll('pre code[class*="language-"]').forEach(el => {
      if (!el.dataset.highlighted) {
        hljs.highlightElement(el);
        el.dataset.highlighted = 'true';
      }
    });
    messages.scrollTop = messages.scrollHeight;
  }, 50);
}

// ═══════════════════════════════════════════
//  WebSocket
// ═══════════════════════════════════════════

function connect() {
  ws = new WebSocket(WS_URL);
  status.textContent = '● 连接中...';
  status.className = 'offline';

  ws.onopen = () => {
    status.textContent = '● 在线';
    status.className = 'online';
    input.disabled = false;
    sendBtn.disabled = false;
    input.focus();
    // 重试等待 currentTaskId 就绪(loadTasks 异步恢复可能还没完成)
    const waitForTask = () => {
      if (currentTaskId) {
        ws.send(JSON.stringify({ type: 'select_task', task_id: currentTaskId }));
        // 页面刷新:优先从缓存恢复,后台同步
        const cached = cacheGet(currentTaskId);
        if (cached && cached.length > 0) {
          hasMoreHistory = true;
          renderMessages(cached, false);
          setTimeout(() => loadHistory(currentTaskId, 0, historyLimit), 500);
        } else {
          setTimeout(() => loadHistory(currentTaskId, 0, historyLimit), 300);
        }
      } else {
        // loadTasks 还没跑完,等 500ms 再试
        setTimeout(waitForTask, 500);
      }
    };
    waitForTask();
  };

  ws.onmessage = (e) => {
    try {
      const data = JSON.parse(e.data);
      
      // ── 任务隔离检查 ──
      // 所有携带 task_id 的消息,如果 task_id 与当前选中任务不一致则忽略
      if (data.task_id && data.task_id !== currentTaskId) {
        if (data.type === 'reply_done' || data.type === 'reply_chunk') {
          pendingUpdates.add(data.task_id);
        }
        return;
      }
      
      switch (data.type) {
        case 'task_selected':
          break;
        case 'ack':
          break;
        case 'reply_chunk':
          // 获取 stage(streaming / thinking / tool_start / tool_complete / thinking_start)
          const stage = data.stage || 'streaming';
          
          if (stage === 'thinking_start') {
            // 开始推理:记录当前流式任务
            streamingTaskId = data.task_id || currentTaskId;
            reasoningText = '';
            pendingReasoningDone = false;
            break;
          }
          
          if (stage === 'thinking') {
            appendStream(data.chunk);
            break;
          }
          
          if (stage === 'tool_start') {
            // 创建工具调用卡片
            const toolData = data.tool || {};
            const card = createToolCard(
              toolData.name || data.chunk.replace(/.*?\[(.*?)\].*/, '$1') || 'tool',
              toolData.args || {}
            );
            // 插入到当前消息
            const lastMsg = messages.lastElementChild;
            if (lastMsg && lastMsg.classList.contains('ai') && lastMsg.dataset.streaming === 'true') {
              let callsArea = lastMsg.querySelector('.tool-calls-area');
              if (!callsArea) {
                callsArea = document.createElement('div');
                callsArea.className = 'tool-calls-area';
                lastMsg.insertBefore(callsArea, lastMsg.querySelector('.msg-content') || lastMsg.firstChild);
              }
              callsArea.appendChild(card);
            } else {
              // 还没消息区域,创建一个 AI 消息并插入卡片
              const div = document.createElement('div');
              div.className = 'msg ai';
              div.dataset.streaming = 'true';
              const callsArea = document.createElement('div');
              callsArea.className = 'tool-calls-area';
              callsArea.appendChild(card);
              div.appendChild(callsArea);
              const contentDiv = document.createElement('div');
              contentDiv.className = 'msg-content';
              div.appendChild(contentDiv);
              const time = document.createElement('div');
              time.className = 'time';
              time.textContent = new Date().toLocaleTimeString();
              div.appendChild(time);
              messages.appendChild(div);
            }
            messages.scrollTop = messages.scrollHeight;
            break;
          }
          
          if (stage === 'tool_complete') {
            completeActiveTool();
            break;
          }
          
          // 默认:普通流式文本
          appendStream(data.chunk);
          break;
          
        case 'reasoning_delta':
          if (ENABLE_REASONING) {
            reasoningText += data.text || '';
            // 实时更新推理面板
            updateReasoningDisplay(data.text || '');
          }
          break;
          
        case 'reasoning_done':
          if (ENABLE_REASONING && reasoningText) {
            pendingReasoningDone = true;
            finalizeReasoning();
          }
          break;
          
        case 'subagent_event':
          handleSubagentEvent(data);
          break;
        case 'reply_done':
          streamingTaskId = null;
          if (streamTimer) {
            clearTimeout(streamTimer);
            streamTimer = null;
          }
          // flush 残留 buffer:仅更新 msg-content,不破坏结构
          if (streamBuffer) {
            const pendingText = streamBuffer;
            streamBuffer = '';
            const lastPending = messages.lastElementChild;
            if (lastPending && lastPending.classList.contains('ai') && lastPending.dataset.streaming === 'true') {
              let contentDiv = lastPending.querySelector('.msg-content');
              if (contentDiv) {
                contentDiv.innerHTML = renderMarkdown(pendingText);
              } else {
                // 没有 contentDiv,退而用 innerHTML(只保留 time)
                let html = lastPending.innerHTML;
                const timeIdx = html.indexOf('<div class="time"');
                html = renderMarkdown(pendingText) + (timeIdx > -1 ? html.substring(timeIdx) : '');
                lastPending.innerHTML = html;
              }
              addTypingCursor(lastPending);
              lastPending.querySelectorAll('pre code[class*="language-"]').forEach(el => {
                if (!el.dataset.highlighted) { hljs.highlightElement(el); el.dataset.highlighted = 'true'; }
              });
            }
          }
          // 用最终回复覆盖
          const finalReply = data.reply || '';
          if (finalReply) {
            const lastMsg = messages.lastElementChild;
            if (lastMsg && lastMsg.classList.contains('ai')) {
              let contentDiv = lastMsg.querySelector('.msg-content');
              if (contentDiv) {
                contentDiv.innerHTML = renderMarkdown(finalReply);
              } else {
                const newContent = document.createElement('div');
                newContent.className = 'msg-content';
                newContent.innerHTML = renderMarkdown(finalReply);
                const timeEl = lastMsg.querySelector('.time');
                if (timeEl) lastMsg.insertBefore(newContent, timeEl);
                else lastMsg.appendChild(newContent);
              }
              contentDiv = lastMsg.querySelector('.msg-content');
              if (contentDiv) {
                contentDiv.querySelectorAll('pre code[class*="language-"]').forEach(el => {
                  if (!el.dataset.highlighted) { hljs.highlightElement(el); el.dataset.highlighted = 'true'; }
                });
              }
              let timeEl = lastMsg.querySelector('.time');
              if (!timeEl) {
                timeEl = document.createElement('div');
                timeEl.className = 'time';
                lastMsg.appendChild(timeEl);
              }
              timeEl.textContent = new Date().toLocaleTimeString();
              lastMsg.dataset.streaming = 'false';
              removeTypingCursor(lastMsg);
            } else {
              const div = document.createElement('div');
              div.className = 'msg ai';
              div.dataset.streaming = 'false';
              const contentDiv = document.createElement('div');
              contentDiv.className = 'msg-content';
              contentDiv.innerHTML = renderMarkdown(finalReply);
              div.appendChild(contentDiv);
              const time = document.createElement('div');
              time.className = 'time';
              time.textContent = new Date().toLocaleTimeString();
              div.appendChild(time);
              messages.appendChild(div);
            }
            messages.scrollTop = messages.scrollHeight;
            // ✨ 缓存到浏览器(只存核心字段,丢弃思考过程等大字段)
            if (currentTaskId && finalReply) {
              cacheAppend(currentTaskId, {
                id: data.message_id || 'ws_' + Date.now(),
                role: 'assistant',
                content: finalReply,
                timestamp: Date.now() / 1000,
              });
            }
          }
          typing.textContent = '';
          input.disabled = false;
          sendBtn.disabled = false;
          input.focus();
          break;
        case 'error':
          streamingTaskId = null;
          addSystemMsg('⚠️ ' + (data.message || '未知错误'));
          typing.textContent = '';
          input.disabled = false;
          sendBtn.disabled = false;
          break;
      }
    } catch(e) {
      appendStream(e.data);
    }
  };

  ws.onclose = () => {
    status.textContent = '● 离线';
    status.className = 'offline';
    input.disabled = true;
    sendBtn.disabled = true;
    setTimeout(connect, 3000);
  };

  ws.onerror = () => {
    status.textContent = '● 连接失败';
    status.className = 'offline';
  };
}

function addSystemMsg(text) {
  const div = document.createElement('div');
  div.className = 'msg system';
  div.textContent = text;
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight;
}

async function sendMessage() {
  const text = input.value.trim();
  if (!text || !ws || ws.readyState !== WebSocket.OPEN || !currentTaskId) return;

  // P1: 先保留到 state.db,确保切 task 不丢消息
  try {
    await fetch(`${API_BASE}/chat/message/retain`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ task_id: currentTaskId, role: 'user', content: text }),
    });
    // ✨ 缓存用户消息
    cacheAppend(currentTaskId, {
      id: 'retain_' + Date.now(),
      role: 'user',
      content: text,
      timestamp: Date.now() / 1000,
    });
  } catch(e) {
    console.warn('retain failed (non-fatal):', e);
  }

  // 乐观渲染
  const div = document.createElement('div');
  div.className = 'msg user';
  div.innerHTML = escapeHtml(text);
  const time = document.createElement('div');
  time.className = 'time';
  time.textContent = new Date().toLocaleTimeString();
  div.appendChild(time);
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight;

  msgId++;
  ws.send(JSON.stringify({
    type: 'message',
    msg_id: msgId,
    content: text,
    task_id: currentTaskId,
  }));

  input.value = '';
  typing.textContent = '助手正在输入';
  typing.className = 'typing-dots';
}

input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); sendMessage(); }
});
input.addEventListener('input', () => {
  input.style.height = 'auto';
  input.style.height = Math.min(input.scrollHeight, 120) + 'px';
});
sendBtn.addEventListener('click', sendMessage);

// ═══════════════════════════════════════════
//  附件上传
// ═══════════════════════════════════════════

async function handleFileSelect(input) {
  const file = input.files && input.files[0];
  if (!file) return;

  const maxSize = 10 * 1024 * 1024; // 10MB
  if (file.size > maxSize) {
    addSystemMsg(`⚠️ 文件过大(最大 ${maxSize/1024/1024}MB)`);
    input.value = '';
    return;
  }

  const nameEl = document.getElementById('upload-name');
  const btn = document.getElementById('upload-btn');
  const bar = document.getElementById('upload-progress-bar');

  nameEl.textContent = `📎 ${file.name}`;
  btn.classList.add('uploading');
  bar.style.width = '30%';

  try {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('task_id', currentTaskId || '');

    const resp = await fetch(`${API_BASE}/chat/attachment/upload`, {
      method: 'POST',
      body: formData,
    });
    bar.style.width = '90%';

    const data = await resp.json();
    if (data.ok) {
      // 在输入框插入文件链接
      const link = `[${data.filename}](${data.url})`;
      const textarea = document.getElementById('input');
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      const before = textarea.value.substring(0, start);
      const after = textarea.value.substring(end);
      textarea.value = before + (before && !before.endsWith(' ') ? ' ' : '') + link + (after && !after.startsWith(' ') ? ' ' : '') + after;
      textarea.focus();
      textarea.selectionStart = textarea.selectionEnd = start + link.length + 1;
      textarea.dispatchEvent(new Event('input'));
      nameEl.textContent = '✅ 已添加';
    } else {
      addSystemMsg(`⚠️ 上传失败: ${data.error || '未知错误'}`);
      nameEl.textContent = '';
    }
  } catch(e) {
    addSystemMsg(`⚠️ 上传失败: ${e.message}`);
    nameEl.textContent = '';
  } finally {
    bar.style.width = '100%';
    setTimeout(() => {
      btn.classList.remove('uploading');
      bar.style.width = '0%';
    }, 500);
    input.value = '';
  }
}

// ═══════════════════════════════════════════
//  移动消息到另一个 task
// ═══════════════════════════════════════════

let moveMessageId = null;
let moveTaskId = null;

function showMoveMessage(msgId) {
  moveMessageId = msgId;
  moveTaskId = currentTaskId;
  // 填充任务列表(排除当前 task)
  const sel = document.getElementById('move-task-select');
  sel.innerHTML = '';
  const available = tasks.filter(t => t.id !== currentTaskId);
  if (available.length === 0) {
    sel.innerHTML = '<option value="">— 没有其他任务 —</option>';
    sel.disabled = true;
  } else {
    available.forEach(t => {
      const opt = document.createElement('option');
      opt.value = t.id;
      opt.textContent = t.name;
      sel.appendChild(opt);
    });
    sel.disabled = false;
  }
  document.getElementById('move-overlay').style.display = 'flex';
}

function confirmMoveMessage() {
  const sel = document.getElementById('move-task-select');
  const targetId = sel.value;
  if (!targetId) return;
  const overlay = document.getElementById('move-overlay');
  overlay.style.display = 'none';

  fetch('/api/chat/messages/move', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ message_id: parseInt(moveMessageId), target_task_id: targetId }),
  })
  .then(r => r.json())
  .then(data => {
    if (data.ok) {
      if (data.moved) {
        const el = document.querySelector(`[data-msg-id="${moveMessageId}"]`);
        if (el) el.remove();
      }
      loadWiki(currentTaskId);
    } else {
      alert('移动失败: ' + (data.error || 'unknown'));
    }
  })
  .catch(e => alert('网络错误: ' + e.message));
}

function cancelMoveMessage() {
  document.getElementById('move-overlay').style.display = 'none';
  moveMessageId = null;
}

loadTasks();
connect();
setInterval(loadTasks, 30000);

// ── 无限滚动监听 ──
messages.addEventListener('scroll', function() {
  if (this.scrollTop < 80 && hasMoreHistory && !isLoadingMore && !loadingTaskId) {
    loadMoreHistory();
  }
});
</script>

<!-- 移动消息选择器 -->
<div id="move-overlay" class="move-overlay" style="display:none;">
  <div class="move-modal">
    <div class="move-modal-title">📨 移动到其他任务</div>
    <select id="move-task-select" class="move-task-select"></select>
    <div class="move-modal-actions">
      <button class="btn-cancel" onclick="cancelMoveMessage()">取消</button>
      <button class="btn-confirm" onclick="confirmMoveMessage()">移动</button>
    </div>
  </div>
</div>
</body>
</html>