1643 lines
84 KiB
HTML
1643 lines
84 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" />
|
||
<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(只读 + 停用/删除,DESIGN §8.5)— 复用 .sk-item/.sk-badge/.sk-empty */
|
||
#wechat-modal { z-index: 112; }
|
||
#wechat-modal .card { width: 440px; max-width: 94vw; }
|
||
#wechat-modal h3 {
|
||
margin: 0; padding: 12px 16px; font-size: 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
#wechat-modal h3 .spacer { flex: 1; }
|
||
#wechat-modal h3 svg { opacity: .85; }
|
||
#wechat-modal .sk-x {
|
||
border: none; background: transparent; font-size: 16px;
|
||
cursor: pointer; color: var(--muted); padding: 2px 6px;
|
||
}
|
||
#wx-body { padding: 16px; overflow: auto; }
|
||
#wx-body .wx-status { padding: 10px 12px; border-radius: 8px; font-size: 14px; margin-bottom: 14px; background: var(--code-bg, #f6f8fa); }
|
||
#wx-body .wx-status.ok { background: #e6f4ea; color: #1a7f37; }
|
||
#wx-body .wx-status.err { background: #ffebe9; color: #cf222e; }
|
||
#wx-body .wx-status.wait { background: #fff8c5; color: #7d4e00; }
|
||
#wx-body .wx-acts { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
#wx-qrbox { text-align: center; margin-top: 16px; }
|
||
#wx-qrbox img { width: 220px; height: 220px; border: 1px solid var(--line, #d0d7de); border-radius: 8px; }
|
||
#crons-modal { z-index: 112; }
|
||
#crons-modal .card {
|
||
width: 880px; max-width: 94vw; height: 78vh; max-height: 78vh;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
#crons-modal h3 {
|
||
margin: 0; padding: 12px 16px; font-size: 16px;
|
||
border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px;
|
||
}
|
||
#crons-modal h3 .spacer { flex: 1; }
|
||
#crons-modal h3 svg { opacity: .85; }
|
||
#crons-modal .cr-hint { font-size: 11px; font-weight: 400; color: var(--muted); }
|
||
#crons-modal .sk-x {
|
||
border: none; background: transparent; font-size: 16px;
|
||
cursor: pointer; color: var(--muted); padding: 2px 6px;
|
||
}
|
||
#cr-cols { flex: 1; display: flex; min-height: 0; }
|
||
#cr-list { width: 300px; flex-shrink: 0; overflow: auto; padding: 12px; border-right: 1px solid var(--border); }
|
||
#cr-detail { flex: 1; min-width: 0; overflow: auto; padding: 16px 20px; }
|
||
.cr-sched { font-size: 12px; color: var(--accent); margin-top: 2px; }
|
||
.cr-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
|
||
.cr-st { font-size: 10px; font-weight: 500; border-radius: 8px; padding: 0 6px; white-space: nowrap; }
|
||
.cr-st.ok { color: #2a8a4a; border: 1px solid #2a8a4a; }
|
||
.cr-st.error { color: #c0392b; border: 1px solid #c0392b; }
|
||
.cr-st.paused { color: var(--muted); border: 1px solid var(--border); }
|
||
.cr-d-row { display: flex; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||
.cr-d-row .k { width: 84px; flex-shrink: 0; color: var(--muted); }
|
||
.cr-d-row .v { flex: 1; min-width: 0; word-break: break-word; white-space: pre-wrap; }
|
||
.cr-acts { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
|
||
.cr-tabs { display: flex; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid var(--border); }
|
||
.cr-tab { appearance: none; background: none; border: none; border-bottom: 2px solid transparent; padding: 6px 12px; margin-bottom: -1px; font-size: 13px; color: var(--muted); cursor: pointer; }
|
||
.cr-tab:hover { color: var(--fg, inherit); }
|
||
.cr-tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||
.cr-hist-item { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border); font-size: 12px; cursor: pointer; border-radius: 4px; }
|
||
.cr-hist-item:hover { background: var(--hover, rgba(127,127,127,0.08)); }
|
||
.cr-hist-ts { width: 92px; flex-shrink: 0; color: var(--muted); }
|
||
.cr-hist-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.cr-hist-meta { flex-shrink: 0; display: flex; align-items: center; gap: 6px; color: var(--muted); }
|
||
.cr-hist-pager { display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 12px; }
|
||
@media (max-width: 760px) {
|
||
#crons-modal .card { width: 96vw; height: 88vh; max-height: 88vh; }
|
||
#cr-cols { flex-direction: column; }
|
||
#cr-list { width: auto; max-height: 30vh; border-right: none; border-bottom: 1px solid var(--border); }
|
||
#crons-modal .cr-hint { display: none; }
|
||
}
|
||
|
||
/* ───── 记忆查看 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; position: relative; }
|
||
/* 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;
|
||
}
|
||
/* 运行态标识(run_status):纯脉冲圆点,running 绿、cancelling 橙、error 红,文案在 hover title
|
||
(meta 行拥挤,不放文字)。数据源 = /v1/tasks 行 run_status + 本地 liveRuns 叠加;
|
||
run 开始/停止就地 patch,结束随列表重拉清掉 */
|
||
.task-row .meta .run-ind { flex-shrink: 0; display: inline-flex; align-self: center; }
|
||
.run-ind.running { color: var(--c-green); }
|
||
.run-ind.cancelling { color: var(--c-orange); }
|
||
.run-ind.error { color: var(--c-red); }
|
||
.run-ind .run-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||
.run-ind.running .run-dot, .run-ind.cancelling .run-dot { animation: run-pulse 1.2s ease-in-out infinite; }
|
||
@keyframes run-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
||
.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; }
|
||
/* 微信渠道专属:品牌绿徽章(白字 + 内嵌微信图标),与蓝灰状态徽章拉开区分 */
|
||
.badge.wx { background: #07C160; color: #fff; display: inline-flex; align-items: center;
|
||
gap: 3px; vertical-align: 1px; }
|
||
.badge.wx svg { width: 11px; height: 11px; fill: currentColor; display: block; }
|
||
/* 渠道镜像卡片(微信 / 企业微信):固定在「新建任务」下,绿调入口,与普通任务列表分离。
|
||
并排放(flex row,各 flex:1)省纵向空间;窄栏内卡片左大右小:左侧占主空间(点开对话),
|
||
右侧「⚙」固定宽度(点开弹框)。三态:未绑定(cc-placeholder)、已绑定无对话(cc-placeholder)、
|
||
已绑定有对话(active 高亮)。 */
|
||
#channel-cards { padding: 6px 12px 2px; display: flex; gap: 6px; }
|
||
#channel-cards:empty { display: none; }
|
||
.channel-card { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; padding: 7px 9px;
|
||
border-radius: 4px; border: 1px solid rgba(7,193,96,.35); background: rgba(7,193,96,.06); cursor: pointer; }
|
||
.channel-card:hover { background: rgba(7,193,96,.12); }
|
||
.channel-card.active { border-color: #07C160; background: var(--accent-soft); }
|
||
.channel-card.cc-placeholder { border-style: dashed; border-color: rgba(7,193,96,.5); }
|
||
.channel-card .cc-icon { width: 18px; height: 18px; border-radius: 4px; background: #07C160; color: #fff;
|
||
display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
||
.channel-card .cc-icon svg { width: 12px; height: 12px; fill: currentColor; display: block; }
|
||
.channel-card .cc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
||
.channel-card .cc-name { font-weight: 600; font-size: 13px; min-width: 0;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.channel-card .cc-meta { color: var(--muted); font-size: 11px;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.channel-card .cc-action { width: 28px; height: 28px; flex: none; display: inline-flex; align-items: center;
|
||
justify-content: center; border-radius: 4px; color: var(--muted); font-size: 14px; cursor: pointer; }
|
||
.channel-card .cc-action:hover { background: rgba(7,193,96,.12); color: #07C160; }
|
||
.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-top-sentinel, .msg-bot-sentinel { align-self: center; font-size: 12px; padding: 4px 0; opacity: 0.6; }
|
||
/* 跳转定位后的高亮闪一下 */
|
||
.msg-jump-flash { animation: jump-flash 1.2s ease; }
|
||
@keyframes jump-flash {
|
||
0%, 100% { box-shadow: none; }
|
||
18% { box-shadow: 0 0 0 2px var(--accent), 0 0 0 6px var(--accent-soft); }
|
||
}
|
||
/* 消息目录:悬浮在对话右缘的圆点轨道。容器 pointer-events:none 让圆点间隙漏给
|
||
滚动条/正文(不挡滚动),只有圆点本身可点;hover 圆点整列展开标题(ChatGPT 式)。 */
|
||
#msg-outline-rail {
|
||
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
|
||
max-height: 72%; display: flex; flex-direction: column; align-items: flex-end;
|
||
gap: 7px; padding: 8px 4px; overflow-y: auto; overflow-x: hidden; z-index: 5;
|
||
pointer-events: none; scrollbar-width: none;
|
||
}
|
||
#msg-outline-rail::-webkit-scrollbar { display: none; }
|
||
.ol-dot {
|
||
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
|
||
background: transparent; border: none; padding: 0; cursor: pointer;
|
||
max-width: 22px; pointer-events: auto; transition: max-width .18s ease;
|
||
}
|
||
#msg-outline-rail:hover .ol-dot { max-width: 240px; }
|
||
.ol-dot .ol-num { display: none; }
|
||
.ol-dot .ol-label {
|
||
font-size: 12px; line-height: 1.3; color: var(--muted); white-space: nowrap;
|
||
overflow: hidden; text-overflow: ellipsis; max-width: 0; opacity: 0;
|
||
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
|
||
padding: 2px 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||
transition: max-width .18s ease, opacity .18s ease;
|
||
}
|
||
#msg-outline-rail:hover .ol-label { max-width: 210px; opacity: 1; }
|
||
.ol-dot::after {
|
||
content: ""; flex: none; width: 16px; height: 4px; border-radius: 3px;
|
||
background: var(--border); transition: background .15s ease, width .15s ease;
|
||
}
|
||
.ol-dot:hover::after { background: var(--muted); }
|
||
.ol-dot.active::after { background: var(--accent); width: 20px; }
|
||
/* 阅读宽度: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); scroll-margin-top: 16px; }
|
||
.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;
|
||
}
|
||
/* ask_user 选项卡:question + 一组可点击选项(点击=发该选项 label 作为回复) */
|
||
.ask-user {
|
||
margin-top: 10px; padding: 10px 12px; border: 1px solid var(--border);
|
||
border-left: 3px solid var(--accent); border-radius: var(--r-md);
|
||
background: var(--accent-soft);
|
||
}
|
||
.ask-user.answered { border-left-color: var(--border); background: var(--code-bg); opacity: 0.85; }
|
||
.ask-q { font-size: 14px; font-weight: 600; margin-bottom: 8px; line-height: 1.45; }
|
||
.ask-options { display: flex; flex-direction: column; gap: 6px; }
|
||
.ask-option {
|
||
display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
|
||
width: 100%; text-align: left; padding: 8px 10px;
|
||
background: #fff; border: 1px solid var(--border); border-radius: var(--r-md);
|
||
cursor: pointer; transition: var(--t);
|
||
}
|
||
.ask-option:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
|
||
.ask-option:disabled { cursor: default; }
|
||
.ask-option.selected {
|
||
border-color: var(--accent); background: var(--accent-soft);
|
||
box-shadow: inset 0 0 0 1px var(--accent);
|
||
}
|
||
.ask-option.selected .ask-option-label::after { content: " ✓ 已选"; color: var(--accent); font-weight: 600; }
|
||
.ask-option-label { font-size: 13px; font-weight: 600; color: var(--text); }
|
||
.ask-option-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
|
||
.ask-hint { margin-top: 8px; font-size: 11px; color: var(--muted); }
|
||
.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; }
|
||
/* 微信渠道 task 只读镜像:输入框置灰禁手输,引导去微信对话 */
|
||
#chat-input.readonly-locked { background: #f0f0f0; color: var(--muted); cursor: not-allowed; }
|
||
#chat-form .hint { font-size: 11px; color: var(--muted); }
|
||
/* 附件托盘:粘贴 / 拖拽的文件 chip 独占一行,累积展示,不与状态文字抢容器 */
|
||
#chat-attach { display: none; flex-wrap: wrap; gap: 4px 0; align-items: center; }
|
||
#chat-attach.show { display: flex; }
|
||
/* 拖拽文件悬停整个输入区:虚线高亮提示可落点 */
|
||
#chat-form.drag-over { outline: 2px dashed var(--accent); outline-offset: -4px; background: var(--accent-soft); }
|
||
#chat-form.drag-over * { pointer-events: none; }
|
||
|
||
/* ───── 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; position: relative;
|
||
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; overscroll-behavior: contain; }
|
||
/* 预览缩放比例徽标(挂在 .card 上,放大滚动时不跟随内容滚走) */
|
||
.zoom-badge {
|
||
position: absolute; right: 14px; bottom: 12px; z-index: 3;
|
||
background: rgba(0,0,0,0.62); color: #fff;
|
||
font-size: 12px; font-weight: 500; padding: 3px 9px;
|
||
border-radius: var(--r-md); pointer-events: none;
|
||
opacity: 0; transition: opacity .25s ease;
|
||
}
|
||
.zoom-badge.show { opacity: 1; }
|
||
#file-preview-modal .body.center { display: flex; align-items: safe center; justify-content: safe 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; position: relative;
|
||
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; overscroll-behavior: contain; }
|
||
#mini-preview-modal .body.center { display: flex; align-items: safe center; justify-content: safe 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%; }
|
||
/* 手机端窄屏:目录轨道会挡正文 / 滚动,直接藏(覆盖 JS 的 inline display) */
|
||
#msg-outline-rail { display: none !important; }
|
||
#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>
|
||
|
||
<div id="crons-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"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
|
||
<span>定时任务</span>
|
||
<span class="spacer"></span>
|
||
<span class="cr-hint">新建 / 修改请在对话里说,例如「每天早八点把水泥简报发我邮箱」</span>
|
||
<button id="cr-close" class="sk-x" title="关闭">✕</button>
|
||
</h3>
|
||
<div id="cr-cols">
|
||
<div id="cr-list"><div class="muted" style="padding:8px;">加载中…</div></div>
|
||
<div id="cr-detail"><div class="sk-empty">← 选一个定时任务查看详情</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ───── 微信绑定 modal(ClawBot 扫码,§8.7)───── -->
|
||
<div id="wechat-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="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||
<span>绑定微信</span>
|
||
<span class="spacer"></span>
|
||
<button id="wx-close" class="sk-x" title="关闭">✕</button>
|
||
</h3>
|
||
<div id="wx-body">
|
||
<div class="muted" style="font-size:12px;margin-bottom:12px;">绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。</div>
|
||
<div id="wx-state" class="wx-status wait">加载中…</div>
|
||
<div class="wx-acts">
|
||
<button id="wx-bind" class="small">绑定 / 重新绑定</button>
|
||
<button id="wx-test" class="small" disabled>发送测试</button>
|
||
<button id="wx-unbind" class="small danger" disabled>解绑</button>
|
||
</div>
|
||
<div id="wx-qrbox" hidden>
|
||
<img id="wx-qrimg" alt="微信扫码二维码">
|
||
<div class="muted" style="font-size:12px;margin-top:6px;">用手机微信「扫一扫」上面的二维码,在手机上确认授权。</div>
|
||
</div>
|
||
<ul class="muted" style="font-size:12px;margin-top:14px;padding-left:18px;line-height:1.7;">
|
||
<li>需个人微信 8.0.70+ 且已灰度到「ClawBot」插件(设置→插件)。</li>
|
||
<li>绑定后先在微信给「微信 ClawBot」发句话,主动推送才开启(24h 窗口)。</li>
|
||
</ul>
|
||
<hr style="border:none;border-top:1px solid var(--line,#d0d7de);margin:18px 0;">
|
||
<div style="font-weight:600;font-size:14px;margin-bottom:6px;">企业微信(推送 + 对话)</div>
|
||
<div class="muted" style="font-size:12px;margin-bottom:10px;">无条件主动推(不挑活跃度、无 24h 窗口),适合定时简报必达。扫码授权一次拿成员身份。管理员在应用配「接收消息」回调后,还可在企业微信里直接和 zcbot 对话。</div>
|
||
<div id="wc-state" class="wx-status wait">加载中…</div>
|
||
<div class="wx-acts">
|
||
<button id="wc-bind" class="small">扫码绑定</button>
|
||
<button id="wc-test" class="small" disabled>发送测试</button>
|
||
<button id="wc-unbind" class="small danger" disabled>解绑</button>
|
||
</div>
|
||
<div class="wx-acts" style="margin-top:8px;">
|
||
<input id="wc-userid" class="small" placeholder="或手填成员 userid(无域名时)"
|
||
style="flex:1;min-width:150px;padding:6px 8px;border:1px solid var(--line,#d0d7de);border-radius:6px;">
|
||
<button id="wc-bind-id" class="small">保存</button>
|
||
</div>
|
||
<div class="muted" style="font-size:11px;margin-top:6px;">userid 见管理后台→通讯录→点成员→「账号」。没有 HTTPS 域名用手填;有域名可用上方扫码。</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>
|
||
<!-- 渠道镜像对话(微信 / 企业微信)固定卡片:由 chat.js loadChannelCards 填充;无则 :empty 隐藏 -->
|
||
<div id="channel-cards"></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>
|
||
<button id="hd-crons" 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"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></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>
|
||
<!-- 消息目录:悬浮在对话右缘的圆点轨道,每点 = 一轮提问;hover 出标题,点击定位 -->
|
||
<div id="msg-outline-rail" style="display:none;"></div>
|
||
<form id="chat-form" style="display:none;">
|
||
<textarea id="chat-input" placeholder="输入消息…(Enter 发送,Shift+Enter 换行,可粘贴 / 拖拽文件)"></textarea>
|
||
<span id="chat-attach"></span>
|
||
<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>
|