Refine workspace reading and mobile layout
This commit is contained in:
parent
07eba5c402
commit
369c74483c
|
|
@ -5,6 +5,10 @@
|
||||||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## 0.60.9 — 2026-07-29
|
||||||
|
|
||||||
|
- 网页工作台改用更温和的暖白配色,任务与文件列表的分隔更加清楚;助手回复按完整轮次形成统一阅读块,手机端输入区也改为更紧凑的图标操作布局。
|
||||||
|
|
||||||
## 0.60.8 — 2026-07-29
|
## 0.60.8 — 2026-07-29
|
||||||
|
|
||||||
- 网页工作台完成一轮整体视觉优化:任务、对话和文件三栏层级更清晰,长回复阅读更舒适,按钮、输入框、弹窗、状态反馈及手机端触控尺寸也更加统一。
|
- 网页工作台完成一轮整体视觉优化:任务、对话和文件三栏层级更清晰,长回复阅读更舒适,按钮、输入框、弹窗、状态反馈及手机端触控尺寸也更加统一。
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-07-29(Web 工作台 Visual Polish v1,bump 0.60.8)
|
最后更新:2026-07-29(Web 工作台视觉复核修正,bump 0.60.9)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-07-29
|
### 2026-07-29
|
||||||
|
|
||||||
|
- **07-29 / 0.60.9 / Web 工作台视觉复核修正(桌面 + 手机实图)**:据上线截图把冷蓝灰改为暖白/浅米灰;任务与文件行恢复明确分隔,助手消息收进居中 52rem 文档型回答块,工具与进度对齐同一阅读轨道;同时修掉 `task_progress`-only assistant 被过滤工具后仍留下空“助手”壳。手机端 composer 改为独立状态行 + 3 个 44px SVG 图标键 + 发送键,缩短 placeholder、加入安全区并压缩操作/模型区;录音与润色状态改只更新 label/ARIA,不再覆盖 SVG DOM。相关 Python 21 项 + Node 5 项及 JS 语法、HTML/SVG 结构、CSS 括号检查通过;无 API、schema、migration 或依赖变化。
|
||||||
- **07-29 / 0.60.8 / Web 工作台 Visual Polish v1**:新增零构建 `web/static/ui.css` 作为内部轻量视觉基础层,集中颜色、间距、圆角、阴影与 focus/disabled 等控件状态;收敛三栏层级、任务/资源导航、对话正文与工具过程块、输入区、文件栏、modal/toast 及 ≤640px 触控尺寸,不改 DOM 契约、API 或业务交互。新增 CSS 文件接入、MIME 与 `no-cache` 回归测试;相关 Python 20 项 + Node 5 项通过,浏览器连接不可用故截图级视觉验收留真实页面复核。
|
- **07-29 / 0.60.8 / Web 工作台 Visual Polish v1**:新增零构建 `web/static/ui.css` 作为内部轻量视觉基础层,集中颜色、间距、圆角、阴影与 focus/disabled 等控件状态;收敛三栏层级、任务/资源导航、对话正文与工具过程块、输入区、文件栏、modal/toast 及 ≤640px 触控尺寸,不改 DOM 契约、API 或业务交互。新增 CSS 文件接入、MIME 与 `no-cache` 回归测试;相关 Python 20 项 + Node 5 项通过,浏览器连接不可用故截图级视觉验收留真实页面复核。
|
||||||
- **07-29 / 0.60.7 / 组合框内直接新建工作目录**:新对话目录 combobox 增加 allow-create 语义:输入时继续过滤已有目录,无完全同名项时在同一下拉内提供“创建工作目录「名称」”,明确选择后才进入新建状态,失焦搜索词不会误提交;删除外部新建按钮与二级输入框。纯前端改动,无 API、schema、migration 或目录绑定语义变化;全量 387 测试通过(17 项按环境跳过)。
|
- **07-29 / 0.60.7 / 组合框内直接新建工作目录**:新对话目录 combobox 增加 allow-create 语义:输入时继续过滤已有目录,无完全同名项时在同一下拉内提供“创建工作目录「名称」”,明确选择后才进入新建状态,失焦搜索词不会误提交;删除外部新建按钮与二级输入框。纯前端改动,无 API、schema、migration 或目录绑定语义变化;全量 387 测试通过(17 项按环境跳过)。
|
||||||
- **07-29 / 0.60.6 / 新对话目录选择改可搜索组合框**:将上一版独立搜索框 + 原生 select 合并为单一 combobox,同一输入区完成已选目录展示、即时过滤和下拉选择,并补方向键、Enter、Esc、失焦恢复与 ARIA 状态;最近目录置顶、明确选择 working_dir 和新建目录入口均保持不变。纯前端改动,无依赖、API、schema 或 migration;全量 387 测试通过(17 项按环境跳过)。
|
- **07-29 / 0.60.6 / 新对话目录选择改可搜索组合框**:将上一版独立搜索框 + 原生 select 合并为单一 combobox,同一输入区完成已选目录展示、即时过滤和下拉选择,并补方向键、Enter、Esc、失焦恢复与 ARIA 状态;最近目录置顶、明确选择 working_dir 和新建目录入口均保持不变。纯前端改动,无依赖、API、schema 或 migration;全量 387 测试通过(17 项按环境跳过)。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.60.8"
|
__version__ = "0.60.9"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,20 @@ class StaticVendorTests(unittest.TestCase):
|
||||||
self.assertIn(":focus-visible", css)
|
self.assertIn(":focus-visible", css)
|
||||||
self.assertIn("@media (max-width: 640px)", css)
|
self.assertIn("@media (max-width: 640px)", css)
|
||||||
|
|
||||||
|
def test_ui_polish_keeps_mobile_composer_and_reading_rail_contracts(self) -> None:
|
||||||
|
html = DEV_HTML.read_text(encoding="utf-8")
|
||||||
|
css = UI_CSS.read_text(encoding="utf-8")
|
||||||
|
frontend = _frontend_source()
|
||||||
|
|
||||||
|
self.assertIn("--list-divider:", css)
|
||||||
|
self.assertIn(".msg.assistant", css)
|
||||||
|
self.assertIn(".composer-tool .composer-label", css)
|
||||||
|
self.assertIn('aria-label="添加附件"', html)
|
||||||
|
self.assertIn('aria-label="语音输入"', html)
|
||||||
|
self.assertIn('aria-label="润色提示词"', html)
|
||||||
|
self.assertIn("hasVisibleAssistantContent", frontend)
|
||||||
|
self.assertIn('input.placeholder = mqPhone.matches', frontend)
|
||||||
|
|
||||||
def test_frontend_surfaces_context_and_cache_stats(self) -> None:
|
def test_frontend_surfaces_context_and_cache_stats(self) -> None:
|
||||||
src = _frontend_source()
|
src = _frontend_source()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1841,10 +1841,31 @@
|
||||||
<span id="chat-attach"></span>
|
<span id="chat-attach"></span>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="hint" id="chat-hint">就绪</span>
|
<span class="hint" id="chat-hint">就绪</span>
|
||||||
<span style="flex:1;"></span>
|
<span class="composer-spacer"></span>
|
||||||
<button type="button" class="small" id="chat-attach-btn" title="添加附件:手机可拍照 / 选照片,电脑直接开文件选择器(也可粘贴 / 拖拽)">📎 附件</button>
|
<button type="button" class="small composer-tool" id="chat-attach-btn"
|
||||||
<button type="button" class="small" id="chat-mic" title="语音输入:点击开始录音,再点结束并转写填入输入框(最长 60 秒)">🎙 语音</button>
|
aria-label="添加附件" title="添加附件:手机可拍照 / 选照片,电脑直接开文件选择器(也可粘贴 / 拖拽)">
|
||||||
<button type="button" class="small" id="chat-optimize" disabled title="用当前对话模型润色草稿(参考已选生图模型偏好)— 替换为更清晰可执行的 prompt,Ctrl+Z 可撤销">✨ 润色</button>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m21.4 11.6-8.9 8.9a6 6 0 0 1-8.5-8.5l9.6-9.6a4 4 0 0 1 5.7 5.7l-9.6 9.6a2 2 0 1 1-2.8-2.8l8.9-8.9"/>
|
||||||
|
</svg>
|
||||||
|
<span class="composer-label">附件</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="small composer-tool" id="chat-mic"
|
||||||
|
aria-label="语音输入" title="语音输入:点击开始录音,再点结束并转写填入输入框(最长 60 秒)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5M8 22h8"/>
|
||||||
|
</svg>
|
||||||
|
<span class="composer-label">语音</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="small composer-tool" id="chat-optimize" disabled
|
||||||
|
aria-label="润色提示词" title="用当前对话模型润色草稿(参考已选生图模型偏好)— 替换为更清晰可执行的 prompt,Ctrl+Z 可撤销">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m15 4 5 5L8 21H3v-5L15 4Z"/><path d="m13 6 5 5M5 3v4M3 5h4M19 16v5M16.5 18.5h5"/>
|
||||||
|
</svg>
|
||||||
|
<span class="composer-label">润色</span>
|
||||||
|
</button>
|
||||||
<button type="submit" class="primary" id="chat-action">发送</button>
|
<button type="submit" class="primary" id="chat-action">发送</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -620,7 +620,7 @@ export async function showNewConversationDraft() {
|
||||||
$("chat-form").style.display = "flex";
|
$("chat-form").style.display = "flex";
|
||||||
$("chat-input").readOnly = false;
|
$("chat-input").readOnly = false;
|
||||||
$("chat-input").classList.remove("readonly-locked");
|
$("chat-input").classList.remove("readonly-locked");
|
||||||
$("chat-input").placeholder = "输入消息…(Enter 发送,Shift+Enter 换行)";
|
setComposerPlaceholder(null);
|
||||||
$("chat-attach-btn").disabled = true; // task_dir 尚不存在,首发建 task 后开放
|
$("chat-attach-btn").disabled = true; // task_dir 尚不存在,首发建 task 后开放
|
||||||
$("chat-mic").disabled = false;
|
$("chat-mic").disabled = false;
|
||||||
$("btn-done").disabled = true;
|
$("btn-done").disabled = true;
|
||||||
|
|
@ -1275,7 +1275,7 @@ function finishRunningTool(run) {
|
||||||
function createLiveAssistantCard(run) {
|
function createLiveAssistantCard(run) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "msg assistant live-run";
|
card.className = "msg assistant live-run";
|
||||||
card.innerHTML = `<div class="role">助手</div>`;
|
card.innerHTML = `<div class="role">科研助手</div>`;
|
||||||
run.card = card;
|
run.card = card;
|
||||||
run.curSeg = null;
|
run.curSeg = null;
|
||||||
if (run.acc) {
|
if (run.acc) {
|
||||||
|
|
@ -1509,18 +1509,21 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "msg " + role;
|
card.className = "msg " + role;
|
||||||
card.dataset.idx = m.idx;
|
card.dataset.idx = m.idx;
|
||||||
const roleLabel = { user: "我", assistant: "助手", error: "错误" }[role] || role;
|
const roleLabel = { user: "我", assistant: "科研助手", error: "错误" }[role] || role;
|
||||||
let html = `<div class="role">${roleLabel}</div>`;
|
let html = `<div class="role">${roleLabel}</div>`;
|
||||||
|
let hasVisibleAssistantContent = false;
|
||||||
// 思考过程(thinking 模型持久化的 reasoning_content):默认折叠,与直播态同构
|
// 思考过程(thinking 模型持久化的 reasoning_content):默认折叠,与直播态同构
|
||||||
if (role === "assistant") {
|
if (role === "assistant") {
|
||||||
const rc = (typeof p.reasoning_content === "string" && p.reasoning_content)
|
const rc = (typeof p.reasoning_content === "string" && p.reasoning_content)
|
||||||
|| ((p.provider_specific_fields || {}).reasoning_content || "");
|
|| ((p.provider_specific_fields || {}).reasoning_content || "");
|
||||||
if (rc) {
|
if (rc) {
|
||||||
html += `<details class="reasoning"><summary>思考过程</summary><div class="reason-body">${escapeHtml(rc)}</div></details>`;
|
html += `<details class="reasoning"><summary>思考过程</summary><div class="reason-body">${escapeHtml(rc)}</div></details>`;
|
||||||
|
hasVisibleAssistantContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof p.content === "string" && p.content) {
|
if (typeof p.content === "string" && p.content) {
|
||||||
html += `<div class="body">${renderMd(p.content)}</div>`;
|
html += `<div class="body">${renderMd(p.content)}</div>`;
|
||||||
|
if (role === "assistant") hasVisibleAssistantContent = true;
|
||||||
// assistant 正文里 echo 的 <wd>/... 路径**永远**挂 chip(绕开 seenRels —— 上面
|
// assistant 正文里 echo 的 <wd>/... 路径**永远**挂 chip(绕开 seenRels —— 上面
|
||||||
// tool 结果可能 inline 过同图,但 chip 是小按钮无视觉污染,助手回复里有可
|
// tool 结果可能 inline 过同图,但 chip 是小按钮无视觉污染,助手回复里有可
|
||||||
// 点的"产物锚点"比没有好);强制 allowInlineMedia=false 防止大图被重复 inline。
|
// 点的"产物锚点"比没有好);强制 allowInlineMedia=false 防止大图被重复 inline。
|
||||||
|
|
@ -1555,6 +1558,7 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
html += buildAskUserCard(argsObj, { interactive: !answered, chosenLabel: chosen }).outerHTML;
|
html += buildAskUserCard(argsObj, { interactive: !answered, chosenLabel: chosen }).outerHTML;
|
||||||
|
hasVisibleAssistantContent = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const label = toolActivityLabel(fn, argsObj);
|
const label = toolActivityLabel(fn, argsObj);
|
||||||
|
|
@ -1564,6 +1568,7 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
|
||||||
<details class="tool-call"><summary>${escapeHtml(label)}</summary><pre>${escapeHtml(args)}</pre></details>
|
<details class="tool-call"><summary>${escapeHtml(label)}</summary><pre>${escapeHtml(args)}</pre></details>
|
||||||
${renderArtifactBarHtml(rels, isProducer)}
|
${renderArtifactBarHtml(rels, isProducer)}
|
||||||
`;
|
`;
|
||||||
|
hasVisibleAssistantContent = true;
|
||||||
}
|
}
|
||||||
// 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress)
|
// 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress)
|
||||||
}
|
}
|
||||||
|
|
@ -1571,6 +1576,9 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
|
||||||
const hasCopy = role === "user"
|
const hasCopy = role === "user"
|
||||||
|| (role === "assistant" && typeof p.content === "string" && p.content.trim());
|
|| (role === "assistant" && typeof p.content === "string" && p.content.trim());
|
||||||
if (hasCopy) html += msgActionsHtml();
|
if (hasCopy) html += msgActionsHtml();
|
||||||
|
// task_progress-only assistant rows feed the persistent progress dock.
|
||||||
|
// Once that tool call is filtered, do not leave an empty assistant shell.
|
||||||
|
if (role === "assistant" && !hasVisibleAssistantContent) continue;
|
||||||
card.innerHTML = html;
|
card.innerHTML = html;
|
||||||
highlightIn(card);
|
highlightIn(card);
|
||||||
wrap.appendChild(card);
|
wrap.appendChild(card);
|
||||||
|
|
@ -1828,9 +1836,7 @@ function applyChannelComposerLock(meta) {
|
||||||
const cfg = meta && channelCfg(meta.channel); // 微信 / 企业微信镜像 → 只读
|
const cfg = meta && channelCfg(meta.channel); // 微信 / 企业微信镜像 → 只读
|
||||||
input.readOnly = !!cfg;
|
input.readOnly = !!cfg;
|
||||||
input.classList.toggle("readonly-locked", !!cfg);
|
input.classList.toggle("readonly-locked", !!cfg);
|
||||||
input.placeholder = cfg
|
setComposerPlaceholder(cfg);
|
||||||
? `${cfg.label}对话请在${cfg.label}里进行 — web 端为只读镜像,可查看历史`
|
|
||||||
: "输入消息…(Enter 发送,Shift+Enter 换行,可粘贴 / 拖拽文件)";
|
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
const opt = $("chat-optimize");
|
const opt = $("chat-optimize");
|
||||||
if (opt) opt.disabled = true;
|
if (opt) opt.disabled = true;
|
||||||
|
|
@ -1841,6 +1847,24 @@ function applyChannelComposerLock(meta) {
|
||||||
if (attach) attach.disabled = !!cfg; // 附件按钮同口径(与拖拽/粘贴的 _composerLocked 一致)
|
if (attach) attach.disabled = !!cfg; // 附件按钮同口径(与拖拽/粘贴的 _composerLocked 一致)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setComposerPlaceholder(cfg) {
|
||||||
|
const input = $("chat-input");
|
||||||
|
if (!input) return;
|
||||||
|
if (cfg) {
|
||||||
|
input.placeholder = mqPhone.matches
|
||||||
|
? `${cfg.label}对话请在${cfg.label}中进行`
|
||||||
|
: `${cfg.label}对话请在${cfg.label}里进行 — web 端为只读镜像,可查看历史`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.placeholder = mqPhone.matches
|
||||||
|
? "输入消息…"
|
||||||
|
: "输入消息…(Enter 发送,Shift+Enter 换行,可粘贴 / 拖拽文件)";
|
||||||
|
}
|
||||||
|
|
||||||
|
mqPhone.addEventListener("change", () => {
|
||||||
|
setComposerPlaceholder(channelCfg(state.taskMeta && state.taskMeta.channel));
|
||||||
|
});
|
||||||
|
|
||||||
// 润色:同步调后端,把 textarea 内容替成优化后文本。用 execCommand('insertText')
|
// 润色:同步调后端,把 textarea 内容替成优化后文本。用 execCommand('insertText')
|
||||||
// 接 textarea 原生 undo 栈 — Ctrl+Z 一次回到原文。streaming 期间允许并行(后端
|
// 接 textarea 原生 undo 栈 — Ctrl+Z 一次回到原文。streaming 期间允许并行(后端
|
||||||
// 不与主对话 run 互斥,各跑各的 LLM)。
|
// 不与主对话 run 互斥,各跑各的 LLM)。
|
||||||
|
|
@ -1861,8 +1885,11 @@ async function optimizePrompt() {
|
||||||
const btn = $("chat-optimize");
|
const btn = $("chat-optimize");
|
||||||
state.optimizing = true;
|
state.optimizing = true;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const oldLabel = btn.textContent;
|
const label = btn.querySelector(".composer-label");
|
||||||
btn.textContent = "润色中…";
|
const oldLabel = label ? label.textContent : "润色";
|
||||||
|
const oldAriaLabel = btn.getAttribute("aria-label") || "润色提示词";
|
||||||
|
if (label) label.textContent = "润色中";
|
||||||
|
btn.setAttribute("aria-label", "正在润色");
|
||||||
const oldHint = $("chat-hint").textContent;
|
const oldHint = $("chat-hint").textContent;
|
||||||
$("chat-hint").textContent = "润色中…";
|
$("chat-hint").textContent = "润色中…";
|
||||||
try {
|
try {
|
||||||
|
|
@ -1891,7 +1918,8 @@ async function optimizePrompt() {
|
||||||
$("chat-hint").textContent = `润色失败:${e.message}`;
|
$("chat-hint").textContent = `润色失败:${e.message}`;
|
||||||
} finally {
|
} finally {
|
||||||
state.optimizing = false;
|
state.optimizing = false;
|
||||||
btn.textContent = oldLabel;
|
if (label) label.textContent = oldLabel;
|
||||||
|
btn.setAttribute("aria-label", oldAriaLabel);
|
||||||
syncOptimizeBtn();
|
syncOptimizeBtn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2064,7 +2092,11 @@ async function startVoiceInput(mode, startY) {
|
||||||
cv.height = Math.max(1, cv.clientHeight * dpr);
|
cv.height = Math.max(1, cv.clientHeight * dpr);
|
||||||
_vrHint(mode === "hold" ? "松开转写 · 上滑取消" : "说完按 Enter 结束 · Esc 取消");
|
_vrHint(mode === "hold" ? "松开转写 · 上滑取消" : "说完按 Enter 结束 · Esc 取消");
|
||||||
btn.classList.add("recording");
|
btn.classList.add("recording");
|
||||||
if (mode === "click") btn.textContent = "■ 停止";
|
if (mode === "click") {
|
||||||
|
const label = btn.querySelector(".composer-label");
|
||||||
|
if (label) label.textContent = "停止";
|
||||||
|
btn.setAttribute("aria-label", "停止录音");
|
||||||
|
}
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (!_voice) return;
|
if (!_voice) return;
|
||||||
const sec = Math.floor((Date.now() - _voice.t0) / 1000);
|
const sec = Math.floor((Date.now() - _voice.t0) / 1000);
|
||||||
|
|
@ -2080,7 +2112,9 @@ async function startVoiceInput(mode, startY) {
|
||||||
if (mode === "hold" && _holdReleased) {
|
if (mode === "hold" && _holdReleased) {
|
||||||
if (Date.now() - _pressT0 < HOLD_TOGGLE_MS && !_voice.cancelArmed) {
|
if (Date.now() - _pressT0 < HOLD_TOGGLE_MS && !_voice.cancelArmed) {
|
||||||
_voice.mode = "click";
|
_voice.mode = "click";
|
||||||
btn.textContent = "■ 停止";
|
const label = btn.querySelector(".composer-label");
|
||||||
|
if (label) label.textContent = "停止";
|
||||||
|
btn.setAttribute("aria-label", "停止录音");
|
||||||
_vrHint("说完按 Enter 结束 · Esc 取消");
|
_vrHint("说完按 Enter 结束 · Esc 取消");
|
||||||
} else {
|
} else {
|
||||||
stopVoiceInput(_voice.cancelArmed);
|
stopVoiceInput(_voice.cancelArmed);
|
||||||
|
|
@ -2096,7 +2130,9 @@ async function stopVoiceInput(discard) {
|
||||||
cancelAnimationFrame(v.raf);
|
cancelAnimationFrame(v.raf);
|
||||||
const btn = $("chat-mic");
|
const btn = $("chat-mic");
|
||||||
btn.classList.remove("recording");
|
btn.classList.remove("recording");
|
||||||
btn.textContent = "🎙 语音";
|
const label = btn.querySelector(".composer-label");
|
||||||
|
if (label) label.textContent = "语音";
|
||||||
|
btn.setAttribute("aria-label", "语音输入");
|
||||||
$("chat-form").classList.remove("recording");
|
$("chat-form").classList.remove("recording");
|
||||||
$("voice-rec-panel").classList.remove("cancel-armed");
|
$("voice-rec-panel").classList.remove("cancel-armed");
|
||||||
$("vr-time").textContent = "0:00";
|
$("vr-time").textContent = "0:00";
|
||||||
|
|
@ -2382,7 +2418,7 @@ async function sendMessage(overrideText) {
|
||||||
// assistant 流式占位卡
|
// assistant 流式占位卡
|
||||||
const asstCard = document.createElement("div");
|
const asstCard = document.createElement("div");
|
||||||
asstCard.className = "msg assistant live-run";
|
asstCard.className = "msg assistant live-run";
|
||||||
asstCard.innerHTML = `<div class="role">助手</div><div class="body streaming"></div>`;
|
asstCard.innerHTML = `<div class="role">科研助手</div><div class="body streaming"></div>`;
|
||||||
wrap.appendChild(asstCard);
|
wrap.appendChild(asstCard);
|
||||||
wrap.scrollTop = wrap.scrollHeight;
|
wrap.scrollTop = wrap.scrollHeight;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,16 @@
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
||||||
/* Neutral workspace palette */
|
/* Neutral workspace palette */
|
||||||
--bg: #f3f5f7;
|
--bg: #f6f4f1;
|
||||||
--panel: #ffffff;
|
--panel: #ffffff;
|
||||||
--panel-subtle: #f8f9fb;
|
--panel-subtle: #fbfaf8;
|
||||||
--panel-muted: #f4f6f8;
|
--panel-muted: #f7f4f0;
|
||||||
--border: #dde2e7;
|
--border: #e2ddd7;
|
||||||
--border-soft: #e9edf1;
|
--border-soft: #ede8e2;
|
||||||
--text: #20242a;
|
--list-divider: #ddd7d0;
|
||||||
--text-strong: #15181d;
|
--text: #292623;
|
||||||
--muted: #6f7782;
|
--text-strong: #1c1a18;
|
||||||
|
--muted: #756f69;
|
||||||
|
|
||||||
/* Institute red is deliberately reserved for primary emphasis. */
|
/* Institute red is deliberately reserved for primary emphasis. */
|
||||||
--accent: #b83a31;
|
--accent: #b83a31;
|
||||||
|
|
@ -27,9 +28,9 @@
|
||||||
--accent-soft: #fbecea;
|
--accent-soft: #fbecea;
|
||||||
--accent-ring: rgba(184, 58, 49, 0.16);
|
--accent-ring: rgba(184, 58, 49, 0.16);
|
||||||
|
|
||||||
--hover: #f0f3f5;
|
--hover: #f5f1ed;
|
||||||
--code-bg: #f3f5f7;
|
--code-bg: #f6f3ef;
|
||||||
--user-bg: #f1f5fa;
|
--user-bg: #f5f1eb;
|
||||||
--asst-bg: #ffffff;
|
--asst-bg: #ffffff;
|
||||||
|
|
||||||
--c-green: #238653;
|
--c-green: #238653;
|
||||||
|
|
@ -97,7 +98,7 @@ button.small {
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
button:hover:not(:disabled) {
|
||||||
border-color: #cbd2d9;
|
border-color: #cdc5bd;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary {
|
button.primary {
|
||||||
|
|
@ -136,7 +137,7 @@ select {
|
||||||
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover,
|
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover,
|
||||||
textarea:hover,
|
textarea:hover,
|
||||||
select:hover {
|
select:hover {
|
||||||
border-color: #c7ced5;
|
border-color: #c9c1b9;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus,
|
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus,
|
||||||
|
|
@ -148,7 +149,7 @@ select:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
color: #9aa1aa;
|
color: #9b958f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Application shell */
|
/* Application shell */
|
||||||
|
|
@ -202,7 +203,7 @@ header .who {
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitter::before {
|
.splitter::before {
|
||||||
background: #d7dde3;
|
background: #d9d3cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-head {
|
.pane-head {
|
||||||
|
|
@ -224,7 +225,7 @@ header .who {
|
||||||
|
|
||||||
#task-scroll,
|
#task-scroll,
|
||||||
#file-list {
|
#file-list {
|
||||||
scrollbar-color: #cbd1d7 transparent;
|
scrollbar-color: #cec7bf transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,27 +241,28 @@ header .who {
|
||||||
}
|
}
|
||||||
|
|
||||||
#task-list {
|
#task-list {
|
||||||
padding: 4px 8px 8px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row {
|
.task-row {
|
||||||
margin: 3px 0;
|
margin: 0;
|
||||||
padding: 9px 10px;
|
padding: 10px 12px 10px 9px;
|
||||||
border: 1px solid transparent;
|
border: 0;
|
||||||
border-radius: var(--r-md);
|
border-left: 3px solid transparent;
|
||||||
|
border-bottom: 1px solid var(--list-divider);
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row:hover {
|
.task-row:hover {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-color: var(--border-soft);
|
box-shadow: none;
|
||||||
box-shadow: 0 1px 3px rgba(24, 32, 41, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row.active {
|
.task-row.active {
|
||||||
padding-left: 8px;
|
padding-left: 9px;
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
border-color: #f0d2cf;
|
|
||||||
border-left: 3px solid var(--accent);
|
border-left: 3px solid var(--accent);
|
||||||
|
border-bottom-color: #e8c6c3;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +337,8 @@ header .who {
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Conversation: assistant content reads like a document, not a bubble stack. */
|
/* Conversation: one centred reading rail, with each assistant turn grouped as
|
||||||
|
a document block rather than a floating chat label. */
|
||||||
|
|
||||||
#chat-meta {
|
#chat-meta {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|
@ -345,7 +348,7 @@ header .who {
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-stream {
|
#chat-stream {
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
padding: 20px clamp(16px, 3vw, 36px) 24px;
|
padding: 20px clamp(16px, 3vw, 36px) 24px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
scrollbar-color: #cbd1d7 transparent;
|
scrollbar-color: #cbd1d7 transparent;
|
||||||
|
|
@ -364,14 +367,21 @@ header .who {
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg.assistant {
|
.msg.assistant {
|
||||||
padding: 8px 0;
|
width: min(100%, 52rem);
|
||||||
border-color: transparent;
|
max-width: min(100%, 52rem);
|
||||||
border-radius: 0;
|
align-self: center;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-left: 3px solid #cbc3bb;
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: #fffdfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg.assistant.live-run {
|
.msg.assistant.live-run {
|
||||||
padding: 12px 14px;
|
padding: 16px 18px;
|
||||||
border-color: rgba(184, 58, 49, 0.2);
|
border-color: rgba(184, 58, 49, 0.2);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
border-left-width: 3px;
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-lg);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px rgba(184, 58, 49, 0.05),
|
0 0 0 1px rgba(184, 58, 49, 0.05),
|
||||||
|
|
@ -387,6 +397,42 @@ header .who {
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg.assistant > .role {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
color: #6d665f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg.assistant > .role::before {
|
||||||
|
content: "";
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
flex: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg.tool {
|
||||||
|
width: min(100%, 52rem);
|
||||||
|
max-width: min(100%, 52rem);
|
||||||
|
align-self: center;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg.tool > .role {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-progress-dock .task-progress {
|
||||||
|
width: min(100%, 52rem);
|
||||||
|
max-width: 52rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.msg .body {
|
.msg .body {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
@ -410,7 +456,7 @@ header .who {
|
||||||
|
|
||||||
.tool-call,
|
.tool-call,
|
||||||
.reasoning {
|
.reasoning {
|
||||||
color: #4f5863;
|
color: #5f5851;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call summary,
|
.tool-call summary,
|
||||||
|
|
@ -444,13 +490,30 @@ header .who {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-lg);
|
||||||
background: #fcfcfd;
|
background: #fffdfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-form .row {
|
#chat-form .row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-tool {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-tool svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
#chat-action {
|
#chat-action {
|
||||||
min-width: 64px;
|
min-width: 64px;
|
||||||
}
|
}
|
||||||
|
|
@ -467,7 +530,7 @@ header .who {
|
||||||
.file-row {
|
.file-row {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-bottom-color: var(--border-soft);
|
border-bottom: 1px solid var(--list-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-row:hover {
|
.file-row:hover {
|
||||||
|
|
@ -475,7 +538,7 @@ header .who {
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-row .name {
|
.file-row .name {
|
||||||
color: #30363d;
|
color: #332f2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.storage-foot {
|
.storage-foot {
|
||||||
|
|
@ -486,7 +549,7 @@ header .who {
|
||||||
|
|
||||||
.storage-foot .bar {
|
.storage-foot .bar {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
background: #e5e9ed;
|
background: #e8e3de;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialogs and notifications */
|
/* Dialogs and notifications */
|
||||||
|
|
@ -557,13 +620,69 @@ header .who {
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-tabs {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pane-mid > .pane-head {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pane-mid > .pane-head > button {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-meta {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 7px 10px;
|
||||||
|
column-gap: 7px;
|
||||||
|
row-gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-meta > .spacer {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-meta .mdl-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-meta select {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-progress-dock {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#chat-stream {
|
#chat-stream {
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
padding: 14px 12px 18px;
|
padding: 14px 12px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
max-width: 97%;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg.assistant {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 13px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg.tool {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg .body {
|
.msg .body {
|
||||||
|
|
@ -573,14 +692,60 @@ header .who {
|
||||||
|
|
||||||
#chat-form {
|
#chat-form {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-input {
|
#chat-input {
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-form .row button {
|
#chat-form .row {
|
||||||
min-height: 36px;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-hint {
|
||||||
|
order: -1;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
min-height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-form .row .composer-tool {
|
||||||
|
width: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-tool svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-tool .composer-label {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-action {
|
||||||
|
width: 72px;
|
||||||
|
min-width: 72px;
|
||||||
|
height: 44px;
|
||||||
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue