zcbot/web/static/dev.html

1388 lines
67 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%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23c0392b'/%3E%3Cstop offset='1' stop-color='%238e2a20'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='22' fill='url(%23g)'/%3E%3Ctext x='50' y='54' font-family='Arial,Helvetica,sans-serif' font-size='62' font-weight='700' fill='%23fff' text-anchor='middle' dominant-baseline='central'%3EZ%3C/text%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:active:not(:disabled) { transform: translateY(1px); }
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; animation: modal-fade .18s ease-out; }
.modal.show > .card { animation: modal-pop .22s cubic-bezier(.2,.7,.2,1); }
.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, #admin-modal select {
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;
}
/* ───── 左侧 rail 底部「我的资源」入口(技能,后续可加记忆)───── */
#rail-resources {
flex-shrink: 0; border-top: 1px solid var(--border);
padding: 8px; display: flex; gap: 6px;
}
#rail-resources > button {
flex: 1; font-size: 13px;
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
#rail-resources > button svg { flex-shrink: 0; opacity: .85; }
/* 版本号:钉在右侧文件面板底部存储条最左,带细分隔线,纯展示
(垂直居中由 .storage-foot 的 align-items:center 提供;随存储条一起显隐) */
#app-version {
flex-shrink: 0; font-size: 11px; color: var(--muted);
font-family: var(--mono); white-space: nowrap; cursor: default;
padding-right: 8px; border-right: 1px solid var(--border-soft);
}
/* ───── 技能查看 modal(两栏 master-detail)───── */
#skills-modal { z-index: 112; }
#skills-modal .card {
width: 1000px; max-width: 94vw; height: 80vh; max-height: 80vh;
display: flex; flex-direction: column;
}
#skills-modal h3 {
margin: 0; padding: 12px 16px; font-size: 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
#skills-modal h3 .spacer { flex: 1; }
#skills-modal h3 svg { opacity: .85; }
#skills-modal .sk-x {
border: none; background: transparent; font-size: 16px;
cursor: pointer; color: var(--muted); padding: 2px 6px;
}
/* 三栏:平台列表 / 我的列表 / 正文 */
#sk-cols { flex: 1; display: flex; min-height: 0; }
.sk-pane {
width: 230px; flex-shrink: 0; overflow: auto;
padding: 12px; border-right: 1px solid var(--border);
}
#sk-detail { flex: 1; min-width: 0; overflow: auto; padding: 16px 20px; }
.sk-empty { color: var(--muted); font-size: 13px; padding: 24px 8px; text-align: center; }
.sk-group-title {
font-weight: 600; font-size: 12px; color: var(--muted); margin: 0 0 8px;
}
.sk-item {
padding: 7px 10px; border: 1px solid var(--border);
border-radius: var(--r-md); margin-bottom: 6px; cursor: pointer;
}
.sk-item:hover { border-color: var(--accent); background: #fafafa; }
.sk-item.active { border-color: var(--accent); background: rgba(120,120,200,0.07); }
.sk-item .sk-name { font-weight: 600; font-size: 13px; }
.sk-item .sk-desc {
font-size: 12px; color: var(--muted); margin-top: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sk-badge {
font-size: 10px; font-weight: 500; color: var(--accent);
border: 1px solid var(--accent); border-radius: 8px; padding: 0 5px;
white-space: nowrap;
}
.sk-name .sk-badge { margin-left: 4px; }
.sk-loaderr {
margin-top: 14px; padding: 8px 10px; font-size: 12px;
border: 1px solid var(--accent); border-radius: var(--r-md);
color: var(--accent); background: rgba(220,80,80,0.05);
}
/* 右栏正文头 + markdown */
.sk-d-head {
display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
padding-bottom: 10px; border-bottom: 1px solid var(--border);
}
.sk-d-head .sk-d-name { font-weight: 600; font-size: 15px; }
.sk-d-head .spacer { flex: 1; }
.sk-detail-md { font-size: 13px; line-height: 1.6; }
.sk-detail-md pre {
white-space: pre-wrap; word-break: break-word;
background: #f5f5f5; padding: 10px; border-radius: var(--r-md); overflow: auto;
}
.sk-detail-md code { word-break: break-word; }
.sk-detail-md h1, .sk-detail-md h2, .sk-detail-md h3 { margin: 14px 0 6px; }
.sk-detail-md table { border-collapse: collapse; }
.sk-detail-md th, .sk-detail-md td { border: 1px solid var(--border); padding: 4px 8px; }
/* 窄屏:三栏改上下堆叠 */
@media (max-width: 760px) {
#skills-modal .card { width: 96vw; height: 88vh; max-height: 88vh; }
#sk-cols { flex-direction: column; }
.sk-pane { width: auto; max-height: 26vh; border-right: none; border-bottom: 1px solid var(--border); }
}
/* ───── 记忆查看 modal(只读两栏;改走对话)───── */
#memory-modal { z-index: 112; }
#memory-modal .card {
width: 880px; max-width: 94vw; height: 80vh; max-height: 80vh;
display: flex; flex-direction: column;
}
#memory-modal h3 {
margin: 0; padding: 12px 16px; font-size: 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
#memory-modal h3 .spacer { flex: 1; }
#memory-modal h3 svg { opacity: .85; }
#memory-modal .sk-x {
border: none; background: transparent; font-size: 16px;
cursor: pointer; color: var(--muted); padding: 2px 6px;
}
#mem-hint {
padding: 8px 16px; font-size: 12px; color: var(--muted);
border-bottom: 1px solid var(--border); background: #fafafa;
}
#mem-cols { flex: 1; display: flex; min-height: 0; }
#mem-detail { flex: 1; min-width: 0; overflow: auto; padding: 16px 20px; }
@media (max-width: 760px) {
#memory-modal .card { width: 96vw; height: 88vh; max-height: 88vh; }
#mem-cols { flex-direction: column; }
}
/* ───── 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; }
#hd-admin {
text-decoration: none; color: var(--accent); font-size: 12px;
padding: 4px 10px; border: 1px solid var(--accent-soft); border-radius: var(--r-md);
}
#hd-admin:hover { background: var(--accent-soft); }
.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);
}
/* 筛选区折叠(默认展开):body.task-filters-collapsed 时藏起搜索/状态/目录/排序两行 */
body.task-filters-collapsed .task-filter-row { display: none; }
#filter-toggle { white-space: nowrap; flex-shrink: 0; }
body.task-filters-collapsed #filter-toggle { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
/* 对话顶栏只剩「完成」(绿)+「⋯」菜单;其余操作收进浮层菜单按语义色(见 .dd-item.act-*)。
file-picker 的 sp-copy/sp-move 仍复用蓝/橙。 */
#btn-done:hover:not(:disabled) { color: var(--c-green); border-color: var(--c-green-bd); background: var(--c-green-bg); }
#sp-copy:hover:not(:disabled) { color: var(--c-blue); border-color: var(--c-blue-bd); background: var(--c-blue-bg); }
#sp-move:hover:not(:disabled) { color: var(--c-orange); border-color: var(--c-orange-bd); background: var(--c-orange-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; transform-origin: top right; animation: menu-in .14s cubic-bezier(.2,.7,.2,1); }
/* 生图/生视频 模型弹层:同 floating-menu 的 fixed 定位骨架,内容是带标签的 select 行 */
#media-model-pop {
display: none; position: fixed;
min-width: 220px; 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: 8px;
}
#media-model-pop.show { display: block; transform-origin: top right; animation: menu-in .14s cubic-bezier(.2,.7,.2,1); }
#media-model-pop .mm-row { display: flex; align-items: center; gap: 8px; }
#media-model-pop .mm-row + .mm-row { margin-top: 6px; }
#media-model-pop .mm-label { font-size: 12px; color: var(--muted); white-space: nowrap; min-width: 48px; display: inline-flex; align-items: center; gap: 4px; }
#media-model-pop select { font-size: 12px; padding: 3px 6px; flex: 1; min-width: 0; }
/* meta 行的 ⚙ 触发按钮 */
#media-model-btn { line-height: 1; padding: 2px 7px; }
.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; }
/* 菜单项颜色 = 操作后果:正向(完成/下载)绿,破坏性(废弃)橙、(删除)红;
中性操作(导出/清空)不着色,降低色彩负载。rename(文件菜单)保留蓝。 */
.dd-item.act-complete, .dd-item.act-download { color: var(--c-green); }
.dd-item.act-abandon { color: var(--c-orange); }
.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 容器里收缩 + 触发自身滚动 */
}
/* 阅读宽度:assistant/system/tool 限到 ~48rem(约 60-80 字/行,长文不至于满屏铺开难回扫);
user 气泡更窄(36rem)。宽屏下提升可读性,窄屏 92% 仍生效(min 取小者) */
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: min(92%, 48rem); animation: msg-in .22s cubic-bezier(.2,.7,.2,1); }
.msg.user { background: var(--user-bg); align-self: flex-end; max-width: min(92%, 36rem); }
.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-summary {
cursor: pointer; list-style: none; color: var(--muted);
font-family: var(--mono); font-size: 11px; user-select: none;
}
.task-progress > .tp-summary::-webkit-details-marker { display: none; }
.task-progress > .tp-summary::before {
content: "▸"; display: inline-block; margin-right: 5px; transition: transform .12s;
}
.task-progress[open] > .tp-summary::before { transform: rotate(90deg); }
.task-progress > .tp-summary.tp-done { color: var(--c-green); }
.task-progress[open] > .tp-summary { margin-bottom: 5px; }
.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-bottom: 1px solid var(--border);
background: #fff;
}
#task-progress-dock.show { display: block; animation: dock-in .2s ease-out; }
#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; animation: modal-fade .14s ease-out; }
.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; animation: dock-in .18s ease-out; }
.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; }
.preview-spinner {
width: 22px; height: 22px; border-radius: 50%; margin: 0 auto 10px;
border: 2px solid var(--border); border-top-color: var(--accent);
animation: spin .8s linear infinite;
}
#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: flex; } /* flex(非 block):配合默认 flex-direction:column 让 #task-scroll flex:1 撑高可滚 */
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;
}
/* ───── 入场微动效(克制:仅淡入/轻位移,不影响交互速度)───── */
@keyframes msg-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes modal-fade{ from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-pop { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
@keyframes dock-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes menu-in { from { opacity: 0; transform: translateY(-4px) scale(.97); } to { opacity: 1; transform: none; } }
/* 拖拽放下:落点区域一次轻回弹脉冲(JS 加 .drop-pulse,动画结束自摘) */
@keyframes drop-pulse { 0% { box-shadow: inset 0 0 0 2px var(--accent); } 60% { box-shadow: inset 0 0 0 2px transparent; } 100% { box-shadow: inset 0 0 0 0 transparent; } }
.drop-pulse { animation: drop-pulse .4s ease-out; }
/* 系统开启"减弱动态效果"时,禁用入场动画(spinner/blink 等功能性动画保留) */
@media (prefers-reduced-motion: reduce) {
.msg, .modal.show, .modal.show > .card, #task-progress-dock.show,
#login .card, #login .tab-body.active,
#floating-menu.show, #file-droparea.show, .upload-status.show,
.drop-pulse { animation: none !important; }
button:active:not(:disabled) { transform: none; }
}
</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 值" />
<label for="ad-role">角色</label>
<select id="ad-role">
<option value="user" selected>user普通用户</option>
<option value="admin">admin可访问监控页</option>
</select>
<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>
<!-- ───── 技能查看 modal ───── -->
<div id="skills-modal" class="modal">
<div class="card">
<h3>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="3" width="7" height="7" rx="1.5"></rect><rect x="3" y="14" width="7" height="7" rx="1.5"></rect><rect x="14" y="14" width="7" height="7" rx="1.5"></rect></svg>
<span>技能</span>
<span class="spacer"></span>
<button id="sk-close" class="sk-x" title="关闭"></button>
</h3>
<div id="sk-cols">
<div id="sk-builtin" class="sk-pane"><div class="muted" style="padding:8px;">加载中…</div></div>
<div id="sk-user" class="sk-pane"></div>
<div id="sk-detail"><div class="sk-empty">← 选一个 skill 查看完整说明</div></div>
</div>
</div>
</div>
<!-- ───── 记忆查看 modal(只读;改记忆走对话)───── -->
<div id="memory-modal" class="modal">
<div class="card">
<h3>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
<span>记忆</span>
<span class="spacer"></span>
<button id="mem-close" class="sk-x" title="关闭"></button>
</h3>
<div id="mem-hint">跨任务共享的长期记忆,只读查看。<b>想改?直接在对话里跟我说</b>「记住…」「改成…」「忘掉…」,我会帮你维护。</div>
<div id="mem-cols">
<div id="mem-list" class="sk-pane"><div class="muted" style="padding:8px;">加载中…</div></div>
<div id="mem-detail"><div class="sk-empty">← 选 Core 或某条专题查看</div></div>
</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>
<a id="hd-admin" href="/static/admin.html" target="_blank" rel="noopener"
title="管理后台(仅管理员)" style="display:none;">管理</a>
<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>
<button id="filter-toggle" class="small" title="展开/收起筛选">筛选 ▾</button>
<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 task-filter-row" style="gap: 6px;">
<input id="filter-q" class="small" placeholder="搜索名称/描述..." style="flex:2; padding: 3px 6px;" />
<select id="filter-status" class="small" style="flex:1; width:auto;" title="按状态筛选">
<option value="">(全部)</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
<option value="abandoned">已废弃</option>
</select>
</div>
<div class="pane-head task-filter-row" style="gap: 6px;">
<select id="filter-wd" class="small" style="flex:1; padding: 3px 6px;" title="按工作目录筛选">
<option value="">(全部目录)</option>
</select>
<select id="filter-order" class="small" style="flex:1; width:auto;" title="排序方式">
<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 id="rail-resources" title="我的资源">
<button id="hd-skills" title="查看平台 / 我的 skill">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="3" width="7" height="7" rx="1.5"></rect><rect x="3" y="14" width="7" height="7" rx="1.5"></rect><rect x="14" y="14" width="7" height="7" rx="1.5"></rect></svg>
<span>技能</span>
</button>
<button id="hd-memory" title="查看跨任务长期记忆(改记忆请在对话里说)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
<span>记忆</span>
</button>
</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-done" class="small" disabled>完成</button>
<button id="btn-task-menu" class="small dd-toggle" disabled title="更多任务操作(导出 / 清空 / 废弃 / 删除)"></button>
</div>
<div id="chat-meta"><span class="muted">(未选中任务)</span></div>
<div id="wd-concurrent-warn" style="display:none;"></div>
<div id="task-progress-dock"></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 id="app-version" title="zcbot 版本"></span>
<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>
<!-- ───── 生图 / 生视频 模型弹层(meta 行 ⚙ 触发,fixed 逃出 pane overflow) ───── -->
<div id="media-model-pop"></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>