zcbot/web/static/dev.html

1160 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>zcbot 控制台</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%232563eb'/%3E%3Crect x='28' y='34' width='44' height='34' rx='8' fill='%23fff'/%3E%3Ccircle cx='40' cy='51' r='5' fill='%232563eb'/%3E%3Ccircle cx='60' cy='51' r='5' fill='%232563eb'/%3E%3Crect x='46' y='18' width='8' height='14' rx='4' fill='%23fff'/%3E%3Ccircle cx='50' cy='16' r='5' fill='%23fff'/%3E%3C/svg%3E" />
<!-- markdown + 防 XSS + 代码高亮(本地 vendor,失败优雅降级回 plain text) -->
<script src="vendor/markdown/marked.umd.js"></script>
<script src="vendor/markdown/purify.min.js"></script>
<script src="vendor/markdown/highlight.min.js"></script>
<link rel="stylesheet" href="vendor/markdown/github.min.css" />
<style>
:root {
--bg: #f7f7f7;
--panel: #ffffff;
--border: #e3e3e3;
--border-soft: #ececec;
--text: #222;
--muted: #888;
--accent: #c0392b;
--accent-soft: #fde9e7;
--hover: #f0f0f0;
--code-bg: #f4f4f4;
--user-bg: #eef4fb;
--asst-bg: #ffffff;
/* 语义色组:done/export/clear/abandon/delete 按钮 + dd-item + badge 共用 */
--c-green: #27ae60; --c-green-bg: #e9f7ef; --c-green-bd: #a9dfbf;
--c-blue: #2980b9; --c-blue-bg: #ebf5fb; --c-blue-bd: #aed6f1;
--c-purple: #8e44ad; --c-purple-bg: #f5eef8; --c-purple-bd: #d2b4de;
--c-orange: #e67e22; --c-orange-bg: #fef5e7; --c-orange-bd: #f5cba7;
--c-red: #c0392b; --c-red-bg: #fdedec; --c-red-bd: #f5b7b1;
/* 圆角:各档下调一档(没那么圆润) */
--r-sm: 3px; /* code / 小标签 */
--r-md: 4px; /* button / input / 消息气泡 / 卡片元素 */
--r-lg: 6px; /* modal card / 中型容器 */
--r-xl: 8px; /* 大 modal card(登录卡) */
--shadow-card: 0 12px 32px rgba(0,0,0,.18);
--shadow-card-lg: 0 20px 60px rgba(0,0,0,.12), 0 2px 6px rgba(0,0,0,.04);
--mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;
--t: all .15s;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font: 14px/1.5 -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text); background: var(--bg);
overflow: hidden; /* 视窗锁死,所有滚动在 pane 内 */
}
button, input, textarea, select { font: inherit; color: inherit; }
button {
background: #fff; border: 1px solid var(--border);
padding: 4px 10px; border-radius: var(--r-md); cursor: pointer;
transition: var(--t);
}
button:hover:not(:disabled) { background: var(--hover); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button.primary:hover { background: var(--accent); filter: brightness(1.08); }
button.danger:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
textarea, select {
background: #fff; border: 1px solid var(--border);
padding: 5px 8px; border-radius: var(--r-md); width: 100%;
}
input[type="checkbox"], input[type="radio"] { cursor: pointer; }
textarea { resize: vertical; min-height: 60px; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* 4 个 modal 共用骨架(admin / src-picker / new-task / file-preview) */
.modal {
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
display: none; align-items: center; justify-content: center;
}
.modal.show { display: flex; }
.modal > .card {
background: var(--panel); border-radius: var(--r-lg);
box-shadow: var(--shadow-card);
}
/* ───── login overlay ───── */
#login {
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background:
radial-gradient(1200px 600px at 15% 10%, rgba(192,57,43,0.10), transparent 60%),
radial-gradient(900px 500px at 85% 95%, rgba(52,73,94,0.10), transparent 60%),
linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
}
#login .card {
background: var(--panel);
padding: 32px 36px 28px;
border-radius: var(--r-xl);
width: 380px;
box-shadow: var(--shadow-card-lg);
border: 1px solid rgba(0,0,0,.04);
animation: login-in .35s cubic-bezier(.2,.7,.2,1);
}
@keyframes login-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
#login .brand { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
#login .brand .logo {
width: 32px; height: 32px; border-radius: var(--r-md);
background: linear-gradient(135deg, var(--accent), #8e2a20);
color: #fff; font-weight: 700; font-size: 16px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 10px rgba(192,57,43,.35);
}
#login .brand .name { font-size: 18px; font-weight: 600; letter-spacing: .2px; }
#login h2 { margin: 14px 0 18px; font-size: 15px; font-weight: 500; color: var(--muted); }
#login label {
display: block; margin-top: 12px; margin-bottom: 4px;
font-size: 12px; color: var(--muted); letter-spacing: .2px;
}
#login input {
padding: 9px 12px; border-radius: var(--r-md);
border: 1px solid var(--border); background: #fafafa;
transition: var(--t);
}
#login input:hover { background: #fff; }
#login input:focus, #admin-modal input:focus, #chpw-modal input:focus {
outline: none; background: #fff; border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(192,57,43,.12);
}
#login .err { color: var(--accent); font-size: 12px; margin-top: 12px; min-height: 1em; }
#login .actions { margin-top: 18px; display: flex; gap: 8px; }
#login .actions .primary {
flex: 1; padding: 9px 14px; font-size: 14px; font-weight: 500;
border-radius: var(--r-md); transition: var(--t);
box-shadow: 0 2px 6px rgba(192,57,43,.25);
}
#login .actions .primary:hover { box-shadow: 0 4px 12px rgba(192,57,43,.35); }
#login .actions .primary:active { transform: translateY(1px); }
#login .tabs {
display: flex; border-bottom: 1px solid var(--border);
margin: 0 0 14px; gap: 4px;
}
#login .tabs button {
background: none; border: none; border-bottom: 2px solid transparent;
padding: 8px 4px; margin-right: 16px; font-size: 13px;
color: var(--muted); cursor: pointer; transition: var(--t);
}
#login .tabs button:hover { color: var(--text); background: none; }
#login .tabs button.active { color: var(--accent); border-bottom-color: var(--accent); }
#login .tab-body { display: none; }
#login .tab-body.active { display: block; animation: tab-in .2s ease-out; }
@keyframes tab-in {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
#login code { background: var(--code-bg); padding: 1px 5px; border-radius: var(--r-sm); font-size: 11.5px; }
#login .card-footer { margin-top: 10px; display: flex; justify-content: flex-end; }
#login .ghost-link {
color: var(--muted); font-size: 12px; text-decoration: none;
padding: 2px 4px; border-radius: var(--r-md); transition: var(--t);
}
#login .ghost-link:hover { color: var(--accent); background: var(--accent-soft); }
/* ───── admin add-user modal ───── */
#admin-modal { z-index: 110; }
#admin-modal .card { padding: 20px 24px; width: 360px; }
#admin-modal h3 { margin: 0 0 12px; font-size: 15px; }
#admin-modal label {
display: block; margin-top: 10px; margin-bottom: 4px;
font-size: 12px; color: var(--muted);
}
#admin-modal input {
width: 100%; padding: 8px 10px; border-radius: var(--r-md);
border: 1px solid var(--border); background: #fafafa;
}
#admin-modal .err { color: var(--accent); font-size: 12px; margin-top: 10px; min-height: 1em; }
#admin-modal .actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; }
/* ───── change-password modal(复用选入文件的头/体/脚分隔布局)───── */
#chpw-modal { z-index: 110; }
#chpw-modal .card { width: 400px; display: flex; flex-direction: column; }
#chpw-modal h3 {
margin: 0; padding: 14px 18px; font-size: 16px;
border-bottom: 1px solid var(--border);
}
#chpw-modal .body { padding: 16px 18px; }
#chpw-modal label {
display: block; margin-top: 12px; margin-bottom: 4px;
font-size: 12px; color: var(--muted);
}
#chpw-modal .body > label:first-child { margin-top: 0; }
#chpw-modal input {
width: 100%; padding: 8px 10px; border-radius: var(--r-md);
border: 1px solid var(--border); background: #fafafa;
}
#chpw-modal .err { color: var(--accent); font-size: 12px; margin-top: 10px; min-height: 1em; }
#chpw-modal .actions {
padding: 12px 18px; border-top: 1px solid var(--border);
display: flex; gap: 8px; justify-content: flex-end;
}
/* ───── 3-pane layout ───── */
#app { display: none; height: 100vh; }
#app.ready {
--left-grid-width: var(--left-pane-width, 320px);
--right-grid-width: var(--right-pane-width, 320px);
display: grid;
grid-template-columns: var(--left-grid-width) 6px minmax(0, 1fr) 6px var(--right-grid-width);
grid-template-rows: auto 1fr;
grid-template-areas:
"head head head head head"
"left split-left mid split-right right";
}
/* 折叠左 pane:rail 模式,列收成 40px,pane 内只留一个展开按钮(类 VS Code 范式) */
body.left-collapsed #app.ready { --left-grid-width: 40px; }
body.left-collapsed #pane-left > * { display: none; }
body.left-collapsed #pane-left > .pane-head:first-child {
display: flex; justify-content: center; align-items: center;
padding: 6px 4px; border-bottom: none; background: transparent;
position: static; /* 取消 sticky,rail 太窄不需要滚 */
}
body.left-collapsed #pane-left > .pane-head:first-child > * { display: none; }
body.left-collapsed #pane-left > .pane-head:first-child > #pane-toggle-left { display: inline-block; }
/* 折叠右 pane:同左侧 rail,只留展开按钮 */
body.right-collapsed #app.ready { --right-grid-width: 40px; }
body.right-collapsed #pane-right > * { display: none; }
body.right-collapsed #pane-right > .pane-head:first-child {
display: flex; justify-content: center; align-items: center;
padding: 6px 4px; border-bottom: none; background: transparent;
position: static;
}
body.right-collapsed #pane-right > .pane-head:first-child > * { display: none; }
body.right-collapsed #pane-right > .pane-head:first-child > #pane-toggle-right { display: inline-block; }
header {
grid-area: head; background: #fff; border-bottom: 1px solid var(--border);
padding: 8px 14px; display: flex; align-items: center; gap: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
header .brand { display: flex; align-items: center; gap: 8px; }
header .brand .logo {
width: 24px; height: 24px; border-radius: var(--r-md);
background: linear-gradient(135deg, var(--accent), #8e2a20);
color: #fff; font-weight: 700; font-size: 13px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 6px rgba(192,57,43,.28);
}
header .title { font-weight: 600; font-size: 15px; letter-spacing: .2px; }
header .who { color: var(--muted); font-size: 12px; font-family: var(--mono); }
header .spacer { flex: 1; }
.pane { border-right: 1px solid var(--border); background: var(--panel); overflow: auto; min-height: 0; }
/* 左 pane:flex column,顶部多行 pane-head 固定,只让 #task-scroll 滚 — 滚动条不再覆盖顶栏 */
#pane-left { grid-area: left; display: flex; flex-direction: column; overflow: hidden; }
#pane-left > .pane-head { flex-shrink: 0; }
#task-scroll { flex: 1; min-height: 0; overflow: auto; }
/* min-height: 0 + overflow: hidden 让内部 flex 子项的 overflow: auto 真正生效(否则被默认 min-height: auto 顶出) */
#pane-mid { grid-area: mid; display: flex; flex-direction: column; border-right: 1px solid var(--border); background: var(--panel); min-height: 0; min-width: 0; overflow: hidden; }
/* flex 列:pane-head / crumbs / 上传状态固定,#file-list 独占滚动,存储条钉底 */
#pane-right { grid-area: right; border-right: none; display: flex; flex-direction: column; overflow: hidden; background: var(--panel); min-height: 0; }
#pane-right > .pane-head, #pane-right > #file-crumbs, #pane-right > .upload-status { flex-shrink: 0; }
#file-list { flex: 1 1 auto; overflow: auto; min-height: 0; }
.splitter {
min-width: 6px; background: var(--bg); cursor: col-resize;
position: relative; z-index: 5;
}
.splitter::before {
content: ""; position: absolute; top: 0; bottom: 0; left: 2px; width: 1px;
background: var(--border);
}
.splitter:hover, body.resizing-panes .splitter.active { background: var(--accent-soft); }
.splitter:hover::before, body.resizing-panes .splitter.active::before { background: var(--accent); }
#split-left { grid-area: split-left; }
#split-right { grid-area: split-right; }
body.resizing-panes { cursor: col-resize; user-select: none; }
.pane-head {
padding: 8px 12px; border-bottom: 1px solid var(--border);
display: flex; gap: 8px; align-items: center; background: #fafafa;
position: sticky; top: 0;
}
.pane-head .label { font-weight: 600; font-size: 13px; white-space: nowrap; flex-shrink: 0; }
.pane-head .spacer { flex: 1; }
/* 左 pane:title 行(#fafafa)下面的 filter / sort 子行换成白底 + 极淡分隔,弱化层级 */
#pane-left .pane-head + .pane-head {
background: #fff;
border-bottom: 1px solid var(--border-soft);
}
/* 对话顶栏按钮:常态中性 + hover 上语义色 — 完成 绿/导出 蓝/清空 紫/废弃 橙/删除 红
同色组合并 selector(export ≈ sp-copy 蓝, abandon ≈ sp-move 橙) */
#btn-done:hover:not(:disabled) { color: var(--c-green); border-color: var(--c-green-bd); background: var(--c-green-bg); }
#btn-export:hover:not(:disabled),
#sp-copy:hover:not(:disabled) { color: var(--c-blue); border-color: var(--c-blue-bd); background: var(--c-blue-bg); }
#btn-clear-msgs:hover:not(:disabled) { color: var(--c-purple); border-color: var(--c-purple-bd); background: var(--c-purple-bg); }
#btn-abandon:hover:not(:disabled),
#sp-move:hover:not(:disabled) { color: var(--c-orange); border-color: var(--c-orange-bd); background: var(--c-orange-bg); }
#btn-delete-task:hover:not(:disabled) { color: var(--c-red); border-color: var(--c-red-bd); background: var(--c-red-bg); }
/* ───── floating dropdown menu ───── */
/* 单例:position: fixed 逃出 pane overflow 裁剪;右上角触发,向下展开 */
.dd-toggle {
padding: 2px 6px; font-size: 14px; line-height: 1;
background: transparent; border: 1px solid transparent;
color: var(--muted); border-radius: var(--r-sm); cursor: pointer;
}
.dd-toggle:hover { background: var(--hover); color: var(--text); border-color: var(--border); }
#floating-menu {
display: none; position: fixed;
min-width: 132px; background: #fff;
border: 1px solid var(--border); border-radius: var(--r-md);
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
z-index: 60; padding: 4px 0;
}
#floating-menu.show { display: block; }
.dd-item {
display: block; width: 100%; text-align: left;
padding: 6px 14px; font-size: 13px; line-height: 1.4;
background: transparent; border: 0; border-radius: 0;
cursor: pointer; color: var(--text);
}
.dd-item:hover { background: var(--hover); }
.dd-item:disabled { color: var(--muted); cursor: not-allowed; opacity: 0.55; }
.dd-item:disabled:hover { background: transparent; }
.dd-item.act-complete, .dd-item.act-download { color: var(--c-green); }
.dd-item.act-abandon { color: var(--c-orange); }
.dd-item.act-export, .dd-item.act-rename { color: var(--c-blue); }
.dd-item.act-delete { color: var(--accent); }
/* ───── task list ───── */
.task-row {
padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: pointer;
}
.task-row:hover { background: var(--hover); }
.task-row.active { background: var(--accent-soft); border-left: 3px solid var(--accent); padding-left: 9px; }
.task-row .desc { font-weight: 500; color: var(--text); margin-bottom: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(320px)被 shrink 后断行 */
/* tabular-nums 让数字等宽(条 / tok 计数跨行对齐) */
.task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; min-width: 0;
align-items: baseline; font-variant-numeric: tabular-nums; }
.task-row .meta > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.task-row .meta .badge { flex-shrink: 0; }
/* 数字槽位:固定 min-width + 右对齐;time-ago 也锁宽 → 整个右侧组位置稳定,跨行"条/tok"才能对齐 */
.task-row .meta .num { flex-shrink: 0; text-align: right; min-width: 44px; }
.task-row .meta .num.right-group { margin-left: auto; } /* 把数字+时间整组挤到右侧 */
.task-row .meta .time-ago { flex-shrink: 0; text-align: right; min-width: 64px; }
.task-row .badge {
display: inline-block; padding: 0 6px; border-radius: var(--r-md); font-size: 11px;
background: #eef; color: #336;
}
.badge.completed { background: var(--c-green-bg); color: var(--c-green); }
.badge.abandoned { background: var(--accent-soft); color: var(--accent); }
.badge.active { background: #eef; color: #336; }
.empty { padding: 24px; color: var(--muted); text-align: center; font-size: 13px; }
/* ───── chat ───── */
#chat-meta { padding: 8px 12px; border-bottom: 1px solid var(--border); background: #fafafa;
font-size: 12px; color: var(--muted); display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
#chat-meta .tid { font-family: var(--mono); color: var(--text); }
#chat-meta .spacer { flex: 1; }
/* 模型下拉标签:桌面文字 / 手机 emoji 二选一(swap 在 @media 640px 内) */
.mdl-icon { display: none; }
/* 同 wd 并发软警告 banner — 非阻塞,只提示中间产物互覆风险 */
#wd-concurrent-warn { padding: 6px 12px; border-bottom: 1px solid #f0c36d;
background: #fff8e1; color: #6a4500; font-size: 12px; }
#wd-concurrent-warn .tname { font-weight: 600; }
#wd-concurrent-warn .rs { font-family: var(--mono); opacity: 0.7; }
#chat-stream {
flex: 1; overflow-y: auto; overflow-x: hidden; padding: 12px;
display: flex; flex-direction: column; gap: 8px;
min-height: 0; /* 允许在 flex 容器里收缩 + 触发自身滚动 */
}
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: 92%; }
.msg.user { background: var(--user-bg); align-self: flex-end; }
.msg.assistant, .msg.system, .msg.tool, .msg.error { background: var(--asst-bg); align-self: flex-start; }
.msg.error { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.cancelled-badge { margin-top: 8px; padding: 4px 10px; font-size: 12px; color: var(--accent); background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--r-md); display: inline-block; }
.msg .role { font-size: 11px; color: var(--muted); margin-bottom: 2px; font-family: var(--mono); }
.msg .body { word-wrap: break-word; font-size: 14px; line-height: 1.55; }
.msg.assistant.live-run { border-color: rgba(220, 38, 38, 0.28); box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.08), 0 8px 24px rgba(220, 38, 38, 0.08); }
.msg .body.streaming { min-width: 96px; min-height: 22px; }
.msg .body.streaming:empty::before { content: "思考中"; color: var(--muted); }
.msg .body.streaming::after {
content: "";
display: inline-block;
width: 1.15em;
height: 1.15em;
margin-left: 8px;
vertical-align: -0.18em;
border: 2px solid rgba(220, 38, 38, 0.18);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } }
/* markdown 输出:.msg .body 与 file-preview .md-render 共用一组规则 */
.msg .body > :first-child, .md-render > :first-child { margin-top: 0; }
.msg .body > :last-child, .md-render > :last-child { margin-bottom: 0; }
.msg .body p, .md-render p { margin: 0.4em 0; }
.msg .body h1, .msg .body h2, .msg .body h3, .msg .body h4,
.md-render h1, .md-render h2, .md-render h3, .md-render h4 {
margin: 0.8em 0 0.3em; line-height: 1.3;
}
.msg .body h1, .md-render h1 { font-size: 1.4em; }
.msg .body h2, .md-render h2 { font-size: 1.25em; }
.msg .body h3, .md-render h3 { font-size: 1.1em; }
.msg .body h4, .md-render h4 { font-size: 1em; font-weight: 600; }
.msg .body ul, .msg .body ol, .md-render ul, .md-render ol { margin: 0.4em 0; padding-left: 1.6em; }
.msg .body li, .md-render li { margin: 0.15em 0; }
.msg .body li > p, .md-render li > p { margin: 0.15em 0; }
.msg .body blockquote, .md-render blockquote {
margin: 0.4em 0; padding: 4px 12px; border-left: 3px solid var(--accent);
background: var(--accent-soft); color: #555;
}
.msg .body code:not(pre code), .md-render code:not(pre code) {
background: var(--code-bg); padding: 1px 5px; border-radius: var(--r-sm);
font-family: var(--mono); font-size: 0.92em;
}
.msg .body pre, .md-render pre {
margin: 0.5em 0; padding: 10px; background: #f6f8fa; border-radius: var(--r-md);
overflow-x: auto; font-size: 12.5px; line-height: 1.4;
}
.msg .body pre code, .md-render pre code { font-family: var(--mono); background: transparent; padding: 0; }
.msg .body table, .md-render table { border-collapse: collapse; margin: 0.5em 0; font-size: 13px; }
.msg .body th, .msg .body td, .md-render th, .md-render td {
border: 1px solid var(--border); padding: 4px 8px;
}
.msg .body th, .md-render th { background: #fafafa; font-weight: 600; }
.msg .body a, .md-render a { color: var(--accent); }
.msg .body img, .md-render img { max-width: 100%; }
.msg .body hr, .md-render hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; }
.tool-call { margin-top: 6px; font-family: var(--mono); font-size: 12px; }
.tool-call summary {
cursor: pointer; padding: 4px 6px; background: var(--code-bg); border-radius: var(--r-sm);
color: #555;
}
.tool-call summary:hover { background: #ebebeb; }
.tool-call pre {
margin: 4px 0 0; padding: 8px; background: var(--code-bg); border-radius: var(--r-sm);
overflow-x: auto; max-height: 300px; white-space: pre-wrap;
}
.task-progress {
margin-top: 8px; padding: 8px 10px;
border: 1px solid var(--border-soft); border-radius: var(--r-md);
background: #fafafa; font-size: 12px;
}
.task-progress .tp-title {
margin-bottom: 5px; color: var(--muted); font-family: var(--mono); font-size: 11px;
}
.task-progress .tp-list { display: grid; gap: 4px; }
.task-progress .tp-step {
display: grid; grid-template-columns: 18px minmax(0, 1fr);
align-items: start; gap: 6px; min-height: 18px;
}
.task-progress .tp-mark {
width: 18px; height: 18px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid var(--border); background: #fff; color: var(--muted);
font-size: 11px; line-height: 1;
}
.task-progress .tp-step.completed .tp-mark {
color: #fff; background: var(--c-green); border-color: var(--c-green);
}
.task-progress .tp-step.in_progress .tp-mark {
color: var(--accent); border-color: var(--accent); background: var(--accent-soft);
}
.task-progress .tp-text { overflow-wrap: anywhere; line-height: 1.45; }
.task-progress .tp-step.completed .tp-text { color: var(--muted); text-decoration: line-through; }
#task-progress-dock {
flex-shrink: 0; display: none;
padding: 8px 12px; border-top: 1px solid var(--border);
background: #fff;
}
#task-progress-dock.show { display: block; }
#task-progress-dock .task-progress {
margin-top: 0; border-color: rgba(192,57,43,0.22);
background: linear-gradient(180deg, #fff, #fffafa);
}
/* media tool 摘要 banner(model / size / cost / elapsed,折叠态也可见) */
.tool-banner {
display: inline-flex; flex-wrap: wrap; gap: 6px;
margin-left: 8px; font-size: 11px; vertical-align: middle;
}
.tool-banner .kv {
padding: 1px 6px; border-radius: var(--r-sm); background: #fff;
border: 1px solid var(--border); color: #555;
}
.tool-banner .kv.cost { color: #b34a4a; border-color: #e0c4c4; }
.tool-banner .kv.model { color: var(--accent); border-color: #e0c4c4; }
/* ───── artifact chips(对话内点产物预览/下载) ───── */
.artifact-bar { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px; font-family: var(--mono); }
.art-chip {
font: inherit; font-size: 11px; line-height: 1.4;
padding: 2px 8px 2px 6px; border: 1px solid var(--border);
background: #fff; color: #555; border-radius: 999px; cursor: pointer;
max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
display: inline-flex; align-items: center; gap: 4px;
transition: var(--t);
}
.art-chip::before { content: "📄"; font-size: 11px; }
.art-chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
#chat-hint .art-chip { margin: 0 2px; vertical-align: middle; font-family: var(--mono); }
.paste-chip-wrap {
display: inline-flex; align-items: center; max-width: 280px; margin: 0 2px;
vertical-align: middle;
}
.paste-chip-wrap .art-chip {
margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0;
max-width: 230px;
}
.paste-chip-del {
border: 1px solid var(--border); border-left: 0; background: #fff; color: var(--muted);
border-radius: 0 999px 999px 0; padding: 2px 7px; line-height: 1.4;
font-size: 11px; cursor: pointer; transition: var(--t);
}
.paste-chip-del:hover { background: var(--c-red-bg); border-color: var(--c-red-bd); color: var(--c-red); }
/* 内联图片/视频:产物 chip 替代,fetch 完直接展示 */
.art-media {
border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden;
background: #fff; display: inline-block; line-height: 0;
}
.art-media .art-media-loading, .art-media .art-media-error {
display: inline-block; padding: 6px 10px; font-size: 11px;
color: var(--muted); line-height: 1.4; font-family: var(--mono);
}
.art-media .art-media-error { color: #b34a4a; }
.art-media img {
display: block; max-width: 360px; max-height: 280px;
width: auto; height: auto; cursor: zoom-in;
}
.art-media video {
display: block; max-width: 480px; max-height: 320px;
width: auto; height: auto; background: #000;
}
#chat-form {
border-top: 1px solid var(--border); padding: 10px; background: #fafafa;
display: flex; flex-direction: column; gap: 6px;
flex-shrink: 0; /* 输入区固定在底,不被消息挤压 */
}
#chat-form .row { display: flex; gap: 8px; }
#chat-form textarea { flex: 1; }
#chat-form .hint { font-size: 11px; color: var(--muted); }
/* ───── files ───── */
.crumbs { padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 12px; background: #fafafa; }
.crumbs a { margin-right: 4px; }
.file-row {
display: flex; padding: 6px 12px; border-bottom: 1px solid var(--border);
align-items: center; gap: 8px;
}
.file-row:hover { background: var(--hover); }
.file-row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-row .size { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.ico-dir::before { content: "▸ "; color: var(--accent); }
.ico-file::before { content: "· "; color: var(--muted); }
/* 拖拽上传 overlay:hover 整个 pane-right 时铺一层提示 */
#pane-right { position: relative; }
#file-droparea {
position: absolute; inset: 0; pointer-events: none;
display: none; align-items: center; justify-content: center;
background: rgba(192,57,43,0.06); border: 2px dashed var(--accent);
color: var(--accent); font-size: 14px; font-weight: 500;
z-index: 10;
}
#file-droparea.show { display: flex; }
.upload-status {
display: none; padding: 6px 12px; border-bottom: 1px solid var(--border-soft);
background: #fff; color: var(--muted); font-size: 12px;
}
.upload-status.show { display: block; }
.upload-status .bar {
height: 4px; margin-top: 4px; background: var(--border-soft);
border-radius: 999px; overflow: hidden;
}
.upload-status .bar > span {
display: block; height: 100%; width: 0%;
background: var(--accent); transition: width .12s linear;
}
/* 存储用量条:钉在文件面板底部。用量来自后台扫描(默 15min),非实时;超额变红。
用 class 选择器(非 #id)压低特异性,让折叠/手机隐藏规则能盖住它 */
.storage-foot {
display: none; flex-shrink: 0; align-items: center; gap: 8px;
padding: 7px 12px; border-top: 1px solid var(--border); background: #fff;
font-size: 11px; color: var(--muted); font-family: var(--mono); cursor: default;
}
.storage-foot.show { display: flex; }
.storage-foot .lbl { flex-shrink: 0; }
.storage-foot .bar {
flex: 1 1 auto; min-width: 0; height: 6px; border-radius: 3px;
background: var(--border-soft); overflow: hidden;
}
.storage-foot .bar > i {
display: block; height: 100%; width: 0;
background: linear-gradient(90deg, var(--accent), #8e2a20);
transition: width .3s ease;
}
.storage-foot .txt { flex-shrink: 0; white-space: nowrap; }
.storage-foot.nolimit .bar { display: none; }
.storage-foot.over .bar > i { background: #c0392b; }
.storage-foot.over .txt { color: #c0392b; font-weight: 600; }
/* ───── source picker modal(选入文件:勾源 + 复制/移动到主区当前目录) ───── */
#src-picker-modal { z-index: 95; }
#src-picker-modal .card {
width: 560px; max-height: 82vh;
display: flex; flex-direction: column;
}
#src-picker-modal h3 {
margin: 0; padding: 14px 18px; font-size: 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
#src-picker-modal h3 .dest {
font-size: 12px; color: var(--muted); font-weight: 400;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#src-picker-modal .hint {
padding: 8px 18px; font-size: 12px; color: var(--muted);
border-bottom: 1px solid var(--border);
}
#sp-crumbs { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12px; background: #fafafa; }
#sp-crumbs a { margin-right: 4px; }
#sp-list { flex: 1; overflow: auto; min-height: 240px; max-height: 50vh; }
#sp-list .sp-row {
padding: 6px 14px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px; font-size: 13px;
}
#sp-list .sp-row:hover { background: var(--hover); }
#sp-list .sp-row .sp-cb { flex-shrink: 0; margin: 0; }
#sp-list .sp-row .sp-name {
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
cursor: pointer;
}
#sp-list .sp-row.disabled .sp-name { color: var(--muted); cursor: not-allowed; }
#sp-list .sp-row .sp-size { font-size: 11px; color: var(--muted); font-family: var(--mono); }
#src-picker-modal .actions {
padding: 12px 18px; border-top: 1px solid var(--border);
display: flex; gap: 8px; align-items: center;
}
#src-picker-modal .actions .count { flex: 1; font-size: 12px; color: var(--muted); }
/* ───── new task modal ───── */
#new-task-modal { z-index: 80; }
#new-task-modal .card { padding: 20px; width: 420px; }
#new-task-modal h3 { margin: 0 0 12px; font-size: 16px; }
#new-task-modal label { display: block; margin-top: 8px; font-size: 12px; color: var(--muted); }
#new-task-modal .err { color: var(--accent); font-size: 12px; margin-top: 8px; min-height: 1em; }
#new-task-modal .actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; }
/* ───── file preview modal(略深的遮罩 0.5 + 更重阴影) ─────
bottom 让出 chat-form 高度(--preview-bottom-inset 由 JS 按需写),输入区不被遮挡,可继续打字 */
#file-preview-modal {
background: rgba(0,0,0,0.5); z-index: 90;
bottom: var(--preview-bottom-inset, 0);
}
#file-preview-modal .card {
width: 90vw; height: 90vh; max-width: 1200px;
max-height: calc(100vh - var(--preview-bottom-inset, 0px) - 32px);
display: flex; flex-direction: column;
box-shadow: 0 12px 32px rgba(0,0,0,.22);
}
#file-preview-modal .hdr {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border-bottom: 1px solid var(--border);
}
#file-preview-modal .hdr .name {
flex: 1; font-weight: 500; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
#file-preview-modal .body { flex: 1; overflow: auto; padding: 12px; position: relative; }
#file-preview-modal .body.center { display: flex; align-items: center; justify-content: center; }
#file-preview-modal .body .ph { color: var(--muted); font-size: 13px; text-align: center; }
#file-preview-modal .body img.preview-img {
max-width: 100%; max-height: 100%; object-fit: contain;
display: block; margin: 0 auto;
}
#file-preview-modal .body video.preview-video {
max-width: 100%; max-height: 100%; display: block; margin: 0 auto; outline: none;
}
#file-preview-modal .body iframe.preview-frame { width: 100%; height: 100%; border: 0; }
#file-preview-modal .body pre.preview-text {
margin: 0; padding: 8px; background: var(--code-bg);
border-radius: var(--r-md); white-space: pre-wrap; word-break: break-word;
font-family: var(--mono); font-size: 12px; line-height: 1.5;
}
/* .md-render 通用样式已与 .msg .body 合并到上方 chat 段;这里只保留 file-preview 专属 */
#file-preview-modal .body .md-render { max-width: 860px; margin: 0 auto; line-height: 1.7; }
#file-preview-modal .body .docx-host { background: #fff; }
#file-preview-modal .body .xlsx-tabs {
display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;
border-bottom: 1px solid var(--border); padding-bottom: 6px;
}
#file-preview-modal .body .xlsx-tabs button.active {
background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
#file-preview-modal .body .xlsx-sheet { overflow: auto; }
#file-preview-modal .body .xlsx-sheet table { border-collapse: collapse; font-size: 12px; }
#file-preview-modal .body .xlsx-sheet td, #file-preview-modal .body .xlsx-sheet th {
border: 1px solid var(--border); padding: 4px 8px; white-space: nowrap;
}
#mini-preview-modal { background: rgba(0,0,0,0.18); z-index: 96; align-items: flex-start; justify-content: flex-end; padding: 56px 18px 0 0; }
#mini-preview-modal .card {
width: min(520px, 92vw); height: min(420px, 72vh);
display: flex; flex-direction: column;
box-shadow: var(--shadow-card);
}
#mini-preview-modal .hdr {
display: flex; align-items: center; gap: 8px;
padding: 7px 10px; border-bottom: 1px solid var(--border);
}
#mini-preview-modal .hdr .name {
flex: 1; font-weight: 500; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
#mini-preview-modal .body { flex: 1; overflow: auto; padding: 10px; }
#mini-preview-modal .body.center { display: flex; align-items: center; justify-content: center; }
#mini-preview-modal .body .ph { color: var(--muted); font-size: 12px; text-align: center; }
#mini-preview-modal .body img.preview-img,
#mini-preview-modal .body video.preview-video {
max-width: 100%; max-height: 100%; display: block; margin: 0 auto;
}
#mini-preview-modal .body iframe.preview-frame { width: 100%; height: 100%; border: 0; }
#mini-preview-modal .body pre.preview-text {
margin: 0; padding: 8px; background: var(--code-bg);
border-radius: var(--r-md); white-space: pre-wrap; word-break: break-word;
font-family: var(--mono); font-size: 12px; line-height: 1.5;
}
.small { font-size: 12px; }
.muted { color: var(--muted); }
/* ───── mobile tab nav (header) — 桌面隐藏,手机断点内显示 ───── */
.mobile-tabs { display: none; gap: 4px; }
.mobile-tabs button {
padding: 5px 10px; font-size: 12px; line-height: 1.2;
background: transparent; border: 1px solid var(--border);
border-radius: var(--r-md); color: var(--muted); cursor: pointer;
transition: var(--t);
}
.mobile-tabs button.active {
background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
/* ───── responsive: tablet (641-1024px) ─────
断点内强制 rail(纯 CSS,不写 localStorage;回桌面用户偏好仍生效) */
@media (min-width: 641px) and (max-width: 1024px) {
#app.ready {
--left-grid-width: 40px;
--right-grid-width: min(var(--right-pane-width, 260px), 260px);
grid-template-columns: 40px 0 minmax(0, 1fr) 6px var(--right-grid-width);
}
#split-left { display: none; }
#pane-left > * { display: none; }
#pane-left > .pane-head:first-child {
display: flex; justify-content: center; align-items: center;
padding: 6px 4px; border-bottom: none; background: transparent;
position: static;
}
#pane-left > .pane-head:first-child > * { display: none; }
#pane-left > .pane-head:first-child > #pane-toggle-left { display: inline-block; }
}
/* ───── responsive: phone (≤ 640px) — 单列 + tab 切换 ─────
JS 在进入手机视口时会清掉 body.left-collapsed,这里无需为它写覆盖
iOS 用 100dvh 避免地址栏/工具栏挤压视口高度 */
@media (max-width: 640px) {
html, body { height: 100dvh; }
#app.ready {
height: 100dvh;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
grid-template-areas: "head" "main";
}
/* 三 pane 默认隐藏,body.mv-* 决定显示哪个 */
#pane-left, #pane-mid, #pane-right {
grid-area: main; border-right: none; display: none;
}
body.mv-left #pane-left { display: block; }
body.mv-mid #pane-mid { display: flex; }
body.mv-right #pane-right { display: flex; }
/* 折叠按钮在手机不可见 */
#pane-toggle-left, #pane-toggle-right, .splitter { display: none !important; }
/* header 紧凑化 */
header { padding: 6px 10px; gap: 6px; flex-wrap: wrap; }
header .who { display: none; }
header .title { font-size: 14px; }
/* tab 按钮:整行铺底,order:99 让它换行到 header 第二行 */
.mobile-tabs { display: flex; order: 99; flex-basis: 100%; }
.mobile-tabs button { flex: 1; }
#hd-chpw, #hd-logout { padding: 4px 8px; font-size: 12px; }
/* iOS 防 focus 自动缩放:input/textarea 字号 ≥ 16 */
textarea,
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
font-size: 16px;
}
/* chat / 文件 微调 */
.msg { max-width: 96%; }
#chat-meta { padding: 6px 10px; gap: 6px; font-size: 11px; }
#chat-meta .tid { display: none; }
#chat-meta .desc {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60vw;
}
/* 模型下拉:手机端 label 文字 → emoji */
.mdl-text { display: none; }
.mdl-icon { display: inline; }
/* 对话面板顶栏:藏 "对话" label / spacer,5 个按钮自然换行,文字 nowrap 防内部断行 */
#pane-mid > .pane-head { flex-wrap: wrap; gap: 6px; padding: 6px 8px; }
#pane-mid > .pane-head > .label,
#pane-mid > .pane-head > .spacer { display: none; }
#pane-mid > .pane-head > button { white-space: nowrap; padding: 3px 8px; }
#chat-form { padding: 8px; }
.art-media img, .art-media video { max-width: 100%; }
.file-row { padding: 8px 12px; }
/* 4 个 modal 卡片自适应宽度 */
#login .card { width: min(92vw, 380px); padding: 24px 22px 22px; }
#admin-modal .card { width: min(92vw, 360px); }
#new-task-modal .card { width: min(92vw, 420px); }
#src-picker-modal .card { width: min(96vw, 560px); max-height: 88dvh; }
#file-preview-modal .card {
width: 100vw; height: 100dvh;
max-width: 100vw;
max-height: calc(100dvh - var(--preview-bottom-inset, 0px));
border-radius: 0;
}
}
/* ───── embed mode (?embed=1&parent_origin=...) —— 父页面 iframe 嵌入 ─────
藏左上 brand / 用户名 / 退出登录;桌面整层 header 去掉(没 mobile-tabs 切换需求);
"+ 新建任务" 由 JS 移到任务面板 pane-head。 */
body.embed-mode #login { display: none !important; }
body.embed-mode header .brand,
body.embed-mode header #hd-who,
body.embed-mode header #hd-chpw,
body.embed-mode header #hd-logout { display: none; }
@media (min-width: 641px) {
body.embed-mode header { display: none; }
}
#embed-waiting {
position: fixed; inset: 0; z-index: 90;
display: none; align-items: center; justify-content: center;
background: var(--bg); color: var(--muted); font-size: 13px;
flex-direction: column; gap: 12px; padding: 24px;
}
body.embed-mode.embed-waiting #embed-waiting { display: flex; }
body.embed-mode.embed-waiting #app { visibility: hidden; }
#embed-waiting .text { text-align: center; max-width: 80%; }
#embed-waiting .err { color: var(--accent); font-size: 12px; max-width: 80%; text-align: center; min-height: 1em; }
@keyframes embed-spin { to { transform: rotate(360deg); } }
#embed-waiting .spinner {
width: 24px; height: 24px; border-radius: 50%;
border: 2px solid var(--border); border-top-color: var(--accent);
animation: embed-spin .8s linear infinite;
}
</style>
</head>
<body>
<!-- ───── 预渲染闸门:embed 模式必须在 #login 解析前标记 body ─────
#login 默认 display:flex(且带 login-in 动画),而 embedInit() 在 body 末尾才跑;
单文件很长,浏览器常在跑到底部脚本前就先把登录卡画出来 → "登录页一闪而过"。
这里同步读 ?embed=1 并提前加 embed-mode(CSS 即把 #login 隐藏),底部逻辑不变。 -->
<script>
try {
if (new URLSearchParams(location.search).get("embed") === "1") {
document.body.classList.add("embed-mode");
}
} catch (e) {}
</script>
<!-- ───── login overlay ───── -->
<div id="login">
<div class="card">
<div class="brand">
<div class="logo">Z</div>
<div class="name">zcbot</div>
</div>
<h2>登录到控制台</h2>
<div class="tabs">
<button data-tab="pw" class="active" id="tab-pw">邮箱密码</button>
<button data-tab="key" id="tab-key">UUID + PLATFORM_KEY</button>
</div>
<!-- tab 1: 邮箱 + 密码(默认) -->
<div class="tab-body active" id="body-pw">
<label for="li-email">邮箱</label>
<input id="li-email" type="email" autocomplete="username" placeholder="you@example.com" />
<label for="li-password">密码</label>
<input id="li-password" type="password" autocomplete="current-password" placeholder="密码" />
<div class="small muted" style="margin-top: 10px;">
管理员发用户:<code>python main.py user add --email X --password Y</code>
</div>
</div>
<!-- tab 2: UUID + PLATFORM_KEY(platform 服务端 / 调试用) -->
<div class="tab-body" id="body-key">
<label for="li-uid">user_id (UUID)</label>
<input id="li-uid" type="text" autocomplete="off" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
<label for="li-pkey">PLATFORM_KEY</label>
<input id="li-pkey" type="password" autocomplete="off" placeholder="$PLATFORM_KEY env 值" />
<div class="small muted" style="margin-top: 10px;">
平台服务端机器对机器入口;手动登录用于本地调试 / 接管已有 user_id。
</div>
</div>
<div class="err" id="li-err"></div>
<div class="actions">
<button class="primary" id="li-go">登录</button>
</div>
<div class="card-footer">
<a href="#" id="open-admin-add" class="ghost-link">+ 管理员添加用户</a>
</div>
</div>
</div>
<!-- ───── admin add-user modal ───── -->
<div id="admin-modal" class="modal">
<div class="card">
<h3>添加用户</h3>
<label for="ad-email">邮箱</label>
<input id="ad-email" type="email" autocomplete="off" placeholder="new@example.com" />
<label for="ad-password">密码</label>
<input id="ad-password" type="password" autocomplete="new-password" placeholder="≥ 6 字符" />
<label for="ad-token">管理员口令</label>
<input id="ad-token" type="password" autocomplete="off" placeholder="$ZCBOT_ADMIN_TOKEN env 值" />
<div class="err" id="ad-err"></div>
<div class="actions">
<button id="ad-cancel">取消</button>
<button class="primary" id="ad-go">创建</button>
</div>
</div>
</div>
<!-- ───── change-password modal(顶栏「改密码」入口,需已登录)───── -->
<div id="chpw-modal" class="modal">
<div class="card">
<h3>修改密码</h3>
<div class="body">
<label for="cp-old">旧密码</label>
<input id="cp-old" type="password" autocomplete="current-password" placeholder="当前密码" />
<label for="cp-new">新密码</label>
<input id="cp-new" type="password" autocomplete="new-password" placeholder="≥ 6 字符" />
<label for="cp-new2">确认新密码</label>
<input id="cp-new2" type="password" autocomplete="new-password" placeholder="再输一次新密码" />
<div class="err" id="cp-err"></div>
</div>
<div class="actions">
<button id="cp-cancel">取消</button>
<button class="primary" id="cp-go">确认修改</button>
</div>
</div>
</div>
<!-- ───── embed-mode waiting overlay (token 握手中) ───── -->
<div id="embed-waiting">
<div class="spinner"></div>
<div class="text">等待登录…</div>
<div class="err"></div>
</div>
<!-- ───── main 3-pane ───── -->
<div id="app">
<header>
<div class="brand">
<div class="logo">Z</div>
<div class="title">zcbot</div>
</div>
<div class="who" id="hd-who"></div>
<div class="spacer"></div>
<button id="hd-chpw" title="修改登录密码">改密码</button>
<button id="hd-logout">退出登录</button>
<!-- 手机 tab(桌面 display:none):任务 / 对话 / 文件 -->
<div class="mobile-tabs" role="tablist">
<button id="mv-tab-left" data-mv="mv-left" class="active">任务</button>
<button id="mv-tab-mid" data-mv="mv-mid">对话</button>
<button id="mv-tab-right" data-mv="mv-right">文件</button>
</div>
</header>
<!-- left -->
<div class="pane" id="pane-left">
<div class="pane-head">
<span class="label">任务</span>
<span class="small muted" id="task-count" style="font-size:11px;"></span>
<span class="spacer"></span>
<select id="filter-status" class="small" style="width: auto;">
<option value="">(全部)</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
<option value="abandoned">已废弃</option>
</select>
<button id="btn-refresh-tasks" class="small" title="刷新"></button>
<button id="pane-toggle-left" class="small" title="折叠任务列表"></button>
</div>
<div class="pane-head">
<button id="hd-new" class="primary" style="flex:1;">+ 新建任务</button>
</div>
<div class="pane-head" style="gap: 6px;">
<input id="filter-q" class="small" placeholder="搜索名称/描述..." style="flex:1; padding: 3px 6px;" />
<select id="filter-wd" class="small" style="flex:1; padding: 3px 6px;">
<option value="">(全部目录)</option>
</select>
</div>
<div class="pane-head" style="gap: 6px;">
<span class="small muted" style="white-space:nowrap;">排序</span>
<select id="filter-order" class="small" style="flex:1; width:auto;">
<option value="-created_at">创建时间 ↓(新→旧)</option>
<option value="created_at">创建时间 ↑(旧→新)</option>
<option value="-updated_at">更新时间 ↓</option>
<option value="updated_at">更新时间 ↑</option>
<option value="name">名称 A→Z</option>
<option value="-name">名称 Z→A</option>
<option value="status,-created_at">状态分组(同状态按时间倒序)</option>
</select>
</div>
<div id="task-scroll">
<div id="task-list"><div class="empty">加载中…</div></div>
<div id="task-sentinel" style="padding:10px 12px;font-size:11px;color:var(--muted);text-align:center;min-height:1px;"></div>
</div>
</div>
<div id="split-left" class="splitter" role="separator" aria-orientation="vertical" title="拖拽调整任务栏宽度"></div>
<!-- middle -->
<div id="pane-mid">
<div class="pane-head">
<span class="label">对话</span>
<span class="spacer"></span>
<button id="btn-export" class="small" disabled>导出对话记录</button>
<button id="btn-clear-msgs" class="small" disabled title="清空当前任务的对话历史(messages + token 累计归零),工作目录文件保留">清空对话</button>
<button id="btn-done" class="small" disabled>完成</button>
<button id="btn-abandon" class="small danger" disabled>废弃</button>
<button id="btn-delete-task" class="small danger" disabled title="硬删除:清 DB 行 + messages,FS 文件不动">删除</button>
</div>
<div id="chat-meta"><span class="muted">(未选中任务)</span></div>
<div id="wd-concurrent-warn" style="display:none;"></div>
<div id="chat-stream"><div class="empty">请在左侧选一个任务</div></div>
<div id="task-progress-dock"></div>
<form id="chat-form" style="display:none;">
<textarea id="chat-input" placeholder="输入消息…(Enter 发送,Shift+Enter 换行,Ctrl+V 可粘贴文件)"></textarea>
<div class="row">
<span class="hint" id="chat-hint">就绪</span>
<span style="flex:1;"></span>
<button type="button" class="small" id="chat-optimize" disabled title="用当前对话模型润色草稿(参考已选生图模型偏好)— 替换为更清晰可执行的 prompt,Ctrl+Z 可撤销">✨ 润色</button>
<button type="submit" class="primary" id="chat-action">发送</button>
</div>
</form>
</div>
<div id="split-right" class="splitter" role="separator" aria-orientation="vertical" title="拖拽调整文件栏宽度"></div>
<!-- right -->
<div id="pane-right">
<div class="pane-head">
<span class="label">文件</span>
<span id="files-proj" class="muted small" style="margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:0 1 auto;" title=""></span>
<span class="spacer"></span>
<button id="btn-src-pick" class="small" title="选入:从其他目录勾选文件 / 目录,复制或移动到当前主目录"></button>
<button id="btn-upload" class="small" title="上传文件到当前目录(也可直接把文件拖到本面板)"></button>
<button id="btn-refresh-files" class="small"></button>
<button id="pane-toggle-right" class="small" title="折叠文件列表"></button>
</div>
<div id="file-upload-status" class="upload-status"></div>
<div id="file-crumbs" class="crumbs muted">加载中…</div>
<div id="file-list"></div>
<div id="storage-foot" class="storage-foot" title="">
<span class="lbl">存储</span>
<span class="bar"><i id="storage-foot-bar"></i></span>
<span class="txt" id="storage-foot-txt"></span>
</div>
<div id="file-droparea">松开以上传到当前目录</div>
<input type="file" id="upload-input" multiple style="display:none;" />
</div>
</div>
<!-- ───── source picker modal(选入文件:勾源 → 复制/移动到主区当前目录) ───── -->
<div id="src-picker-modal" class="modal">
<div class="card">
<h3>
<span>选入到</span>
<span class="dest" id="sp-dest" title=""></span>
</h3>
<div class="hint">勾选要带入的文件 / 目录(可跨目录,选择跨切换保留);底部按钮把它们复制或移动到此处。</div>
<div id="sp-crumbs"></div>
<div id="sp-list"></div>
<div class="actions">
<span class="count">已选 <span id="sp-count">0</span></span>
<button id="sp-cancel">取消</button>
<button id="sp-copy" disabled title="复制(新副本,源保留)">复制到此处</button>
<button id="sp-move" disabled title="移动(源消失;working_dir 顶层目录不可移)">移动到此处</button>
</div>
</div>
</div>
<!-- ───── floating dropdown menu (single instance) ───── -->
<div id="floating-menu"></div>
<!-- ───── new task modal ───── -->
<div id="new-task-modal" class="modal">
<div class="card">
<h3>新建任务</h3>
<label for="nt-name">任务名(必填)</label>
<input id="nt-name" placeholder="例如 初稿大纲" />
<label for="nt-wd-sel">工作目录</label>
<select id="nt-wd-sel">
<option value="__new__">+ 新建(跟随任务名)</option>
</select>
<input id="nt-wd-new" placeholder="新目录名" style="margin-top:6px;" />
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
<label for="nt-desc">描述(可选,任务长描述)</label>
<input id="nt-desc" />
<label for="nt-skill">智能体类型(可选)</label>
<select id="nt-skill">
<option value="">(默认 · 不限定)</option>
</select>
<label for="nt-model">模型</label>
<select id="nt-model"></select>
<div class="err" id="nt-err"></div>
<div class="actions">
<button id="nt-cancel">取消</button>
<button class="primary" id="nt-go">创建</button>
</div>
</div>
</div>
<!-- ───── file preview modal ───── -->
<div id="file-preview-modal" class="modal">
<div class="card">
<div class="hdr">
<span class="name" id="fp-name"></span>
<span class="small muted" id="fp-meta"></span>
<button class="small" id="fp-download" title="下载原文件">下载</button>
<button class="small" id="fp-close" title="关闭 (Esc)">×</button>
</div>
<div class="body" id="fp-body"></div>
</div>
</div>
<!-- ───── compact secondary preview (for pasted chips while main preview is open) ───── -->
<div id="mini-preview-modal" class="modal">
<div class="card">
<div class="hdr">
<span class="name" id="mp-name"></span>
<span class="small muted" id="mp-meta"></span>
<button class="small" id="mp-download" title="下载原文件">下载</button>
<button class="small" id="mp-close" title="关闭 (Esc)">×</button>
</div>
<div class="body" id="mp-body"></div>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>