1121 lines
53 KiB
HTML
1121 lines
53 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>zcbot 控制台</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
|
||
<!-- 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;
|
||
}
|
||
/* 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>
|
||
<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>
|