diff --git a/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md b/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md index 0d8de28f..6c671528 100644 --- a/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md +++ b/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md @@ -208,32 +208,118 @@ RED / GREEN: | ID | 状态 | 任务 | 验收要点 | | --- | --- | --- | --- | -| C1 | TODO | 顶栏高度与按钮密度 | 顶栏约 40px;按钮轻量 hover;右侧 icon/text 序列不重 | -| C2 | TODO | breadcrumb | 图标、分隔符、弱色文字、截断方式对齐;切页后与页面树同步 | -| C3 | TODO | owner/published 按钮分型 | owner 与 published 的按钮组合分开;不把 published 文案硬塞 owner 态 | -| C4 | TODO | 顶栏搜索入口 | 点击打开全局搜索 modal,URL 不变,焦点进入输入框 | -| C5 | TODO | 侧栏搜索入口 | 打开同一个全局搜索 modal 或明确的树过滤入口;不得误跳 `/search` | -| C6 | TODO | 搜索 modal 默认状态 | `仅匹配标题`、`页面内搜索` 为 switch 且默认开启;不是普通 pill button | -| C7 | TODO | 搜索 modal 关闭路径 | Esc、遮罩、再次 `Ctrl+P`、入口 toggle 按 Wolai 实测对齐 | -| C8 | TODO | 搜索结果行 | 匹配数量、命中高亮、快捷键提示、结果 icon、所在页面信息对齐 | -| C9 | TODO | 搜索空态/加载态 | 输入无结果和加载时的视觉与 DOM 状态可被 smoke 捕获 | -| C10 | TODO | 搜索入口/快捷键矩阵 | 分别验证 Wolai 左侧搜索、顶栏/侧栏入口、`Ctrl+P`、`Ctrl+K`;每个入口记录 URL、打开/关闭路径和阻塞 | -| C11 | TODO | 搜索结果 fixture | 固定本地 `Hermes` 或等价种子数据,避免用随机工作区数据对比 Wolai 结果态 | +| C1 | PARITY | 顶栏高度与按钮密度 | `task135` 覆盖顶栏约 40px、右侧按钮组无搜索入口、icon 不再渲染成黑色方块;`task119` 复核 topbarHeight=40 | +| C2 | PARITY | breadcrumb | `task135` 覆盖 breadcrumb 当前标题绑定、弱色分隔符改为 Wolai chevron;真实父路径数据仍依赖后续 Page Aggregate / tree projection | +| C3 | PARITY | owner/published 按钮分型 | `task135` 覆盖 owner 顶栏绿色 `全网公开`,右侧按钮组从本地 AI/搜索收敛为 Wolai owner 态常见按钮序列 | +| C4 | PARITY | 顶栏搜索入口基线纠偏 | 2026-04-30 Wolai owner 当前 viewport 顶栏无搜索入口;本地已移除顶栏搜索按钮,搜索统一走侧栏入口 / `Ctrl+P` | +| C5 | PARITY | 侧栏搜索入口 | `task135` 覆盖侧栏左上搜索打开同一个全局搜索 modal,URL 不变;modal 打开时再次点击侧栏搜索会关闭 | +| C6 | PARITY | 搜索 modal 默认状态 | `task135` 按最新 Wolai 实测修正:空 query 先显示最近浏览,不直接显示筛选;输入后 switch 出现,`仅匹配标题` / `页面内搜索` 为 switch 且默认关闭 | +| C7 | PARITY | 搜索 modal 关闭路径 | `task135` 覆盖 Esc、主区域遮罩、再次 `Ctrl+P`、侧栏入口 toggle;URL 均不变 | +| C8 | PARITY | 搜索结果行 | `task135` 覆盖后台 API 结果、匹配数量、红色命中高亮、快捷键提示、结果 icon、结果 owner 标记 | +| C9 | PARITY | 搜索空态/加载态 | `task135` 覆盖空 query 最近浏览、输入后 loading、无结果 `没有搜索结果`,DOM 可断言 `data-search-loading` / `data-search-empty` | +| C10 | PARITY | 搜索入口/快捷键矩阵 | `task135` 和 subagent baseline 确认:侧栏搜索 / `Ctrl+P` 是搜索入口;`Ctrl+K` 按当前 Wolai 实测不打开搜索 | +| C11 | PARITY | 搜索结果 fixture | `task135` 固定本地 `Rust` / `Hermes` fallback fixture,并断言 modal 调用 `/api/search/documents` 而不是 DOM 前端过滤 | + + +### 6.1 task135 Phase C 执行记录 + +2026-04-30 已完成 Phase C:顶栏、breadcrumb、owner/published、搜索入口与搜索 modal。执行口径按 `wolai-aline` skill:subagent 先取 Wolai / 本地基线,主线程复核截图后补 RED smoke;实现后本地 smoke 通过,并已派 subagent 做 post-fix 复测。 + +RED: + +- 新增 `scripts/task135-wolai-aline-phase-c-smoke.js`,初始失败于顶栏高度 `44px`、公开态文案 `Public`、顶栏多搜索入口、搜索 modal 未调用 `/api/search/documents`。 +- subagent 基线纠偏:当前 Wolai `Ctrl+K` 不打开搜索;空 query 不直接显示筛选控件;输入后 `仅匹配标题` / `页面内搜索` switch 本次实测为关闭态。 + +GREEN: + +- `rust/crates/mnote-web/src/ssr/pages/layout.rs`:顶栏公开态改为 `全网公开`,移除顶栏搜索入口,右侧按钮组对齐 owner 态;侧栏搜索入口改为 toggle;搜索 modal 输入改为 `type=search`,空 query 显示最近浏览,输入后调用 `/api/search/documents`,结果区标记 `data-search-results-owner="rust-kernel"`。 +- `rust/crates/mnote-web/src/ssr/styles.rs`:顶栏高度收敛到 40px;搜索遮罩支持侧栏入口穿透 toggle,同时保留主区域遮罩关闭;补齐右侧按钮与结果图标 mask,避免黑色方块;搜索高亮统一红色弱底。 +- `rust/crates/mnote-web/src/routes/search.rs`:fallback 搜索数据补 `Hermes` / `技能知识图谱开发` fixture,避免本地结果态依赖随机工作区数据。 +- `scripts/task128-rust-web-wolai-search-modal-smoke.js`:同步当前 Wolai 基线,改为侧栏 / `Ctrl+P` 入口和 switch 默认关闭。 + +验证命令: + +- `cargo fmt -p mnote-web --check` +- `cargo check -p mnote-web` +- `cargo build -p mnote-web --bin mnote-web` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task135-wolai-aline-phase-c-smoke.js` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task128-rust-web-wolai-search-modal-smoke.js` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task125-rust-web-search-server-first-smoke.js` +- `node scripts/task119-rust-web-wolai-visual-regression-smoke.js` + +证据路径: + +- Phase C subagent 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-baseline/comparison-matrix.md` +- Wolai Hermes 搜索基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-baseline/wolai-search-hermes.png` +- 本地 Phase C 最终截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c/local-phase-c-search.png`;post-fix subagent 本地复核:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-postfix/local-10-query-hermes.png` +- 本地搜索 modal smoke 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/local-search-modal/local-3000-search-modal.png` + +主线程复核结论: + +- 搜索不再只是 UI 壳:modal 输入会调用 `/api/search/documents`,请求体包含 query 和 switch filter,结果区有 `data-search-results-owner="rust-kernel"`。 +- 当前 Wolai 基线推翻了旧 checklist 的两个假设:`Ctrl+K` 不是搜索入口;`仅匹配标题` / `页面内搜索` 在本次输入态实测为关闭态。 +- 本地 `Hermes` fixture 只能证明结果态结构、后台链路和高亮;不代表本地已经有 Wolai 真实空间数据。 +- 顶栏/breadcrumb 的父路径层级仍受本地 Page Aggregate / tree projection 数据限制;本阶段只完成控件、入口、状态和同步锚点的对标。 +- post-fix subagent 复测矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-postfix/comparison-matrix.md`;复核确认本地右上角不再有黑色方块,侧栏搜索 toggle、`Ctrl+P` toggle、`Ctrl+K` 不打开、空 query 最近浏览、后台搜索 API、结果 icon/高亮均通过。 ## 7. Phase D:文档画布与阅读态 | ID | 状态 | 任务 | 验收要点 | | --- | --- | --- | --- | -| D1 | TODO | 主内容列宽 | 普通文档列宽约 760px;大屏不贴左;移动端不溢出 | -| D2 | TODO | 标题位置与样式 | Hermes 标题 y 位置、`34px / 44px / 600`、颜色接近 Wolai | -| D3 | TODO | 默认隐藏 meta/debug 行 | 普通文档默认不显示不像 Wolai 的工作区 meta、横线、debug outline | -| D4 | TODO | 正文段落 | `16px / 24px`、段间距、弱色文本与 Wolai 对齐 | -| D5 | TODO | 任务块 | checkbox 尺寸、边框、文本 baseline、完成态接近 Wolai | -| D6 | TODO | 引用/列表/标题块 | 阅读态块样式不跳出 Wolai 密度;截图覆盖至少三类块 | -| D7 | TODO | 右下角浮动入口 | AI / 帮助入口减重,尺寸和位置接近 Wolai,不遮挡正文 | -| D8 | TODO | 阅读态和编辑态切换 | 切换时标题、正文列宽、scroll 位置不明显跳动 | +| D1 | PARITY | 主内容列宽 | `task136` 覆盖普通文档主列 `760px`,大屏居中不贴左;移动端沿既有 media query 保持不溢出 | +| D2 | PARITY | 标题位置与样式 | `task136` 按 Wolai 基线收敛到 y≈108、`34px / 44px / 600`、颜色接近 Wolai | +| D3 | PARITY | 默认隐藏 meta/debug 行 | `task136` 断言普通文档默认不显示 workspace/已同步 meta、大号页面 icon、debug 文案和横线 | +| D4 | PARITY | 正文段落 | `task136` 覆盖正文 `16px / 24px`,标题到正文间距按 Wolai 基线加大 | +| D5 | PARITY | 任务块 | `task136` 固定 Tiptap fixture 并断言渲染后存在 `input[type=checkbox]`,尺寸 16px;补齐 kernel `todo` 类型到 Tiptap taskList 的转换 | +| D6 | PARITY | 引用/列表/标题块 | `task136` 覆盖 heading、blockquote、bulletList;补齐 `bullet_list_item` / `numbered_list_item` 映射避免降级段落 | +| D7 | PARITY | 右下角浮动入口 | `task136` 覆盖 AI / 帮助入口 40px、AI 在上、间距约 16px、底部留白约 44px,不遮挡正文 | +| D8 | PARITY | 阅读态和编辑态切换 | `task136` 覆盖标题 focus、编辑器 focus 后标题 y/x 不跳动;subagent baseline 同样确认点击后布局稳定 | | D9 | PARITY | 普通文档首屏入口语义 | `task130` 已覆盖 RED/GREEN,subagent 最终对标复核通过:根页不再停在“打开当前页面”入口,直接出现 document shell + editor island | + + +### 7.1 task136 Phase D 执行记录 + +2026-04-30 已完成 Phase D:文档画布与阅读态。执行口径按 `wolai-aline` skill:subagent 先取 Wolai / 本地只读基线,主线程复核截图后补 RED smoke;实现后本地 smoke 与相关回归通过,并已派 subagent 做 post-fix 复测。 + +RED: + +- 新增 `scripts/task136-wolai-aline-phase-d-smoke.js`,初始失败于本地文档主列 `880px`,后续增强断言捕获标题 y 偏低、标题到正文间距偏小、右下浮动入口间距/底部位置旧基线。 +- 基线截图确认 Wolai 标题为 y≈108、`34px / 44px / 600`;普通文档无 workspace/已同步 meta 行和大号页面 icon;右下入口为 40px,AI 在帮助上方,按钮间距约 16px,底部留白约 44px。 +- smoke 还暴露后台链路差异:保存 Tiptap fixture 后,`/api/documents/content` 返回 `todo` / `bullet_list_item` 等 kernel 规范化类型,但浏览器 bootstrap 只识别旧类型,导致任务块和列表降级为普通段落。 + +GREEN: + +- `rust/crates/mnote-web/src/ssr/styles.rs`:文档主列收敛到 760px;标题改为 `34px / 44px / 600` 且 y≈108;普通文档隐藏页面大 icon 与 meta/debug 行;正文行高 24px;补 task checkbox、blockquote、list 样式;右下浮动入口减重为 40px,AI 在上、帮助在下,间距 16px、底部留白 44px。 +- `rust/crates/mnote-web/src/routes/web_shell.rs`:`legacyBlockToTiptap` 同时识别旧类型和 kernel 规范化类型:`todo`、`bullet_list_item`、`numbered_list_item`、`blockquote`。 +- `rust/crates/mnote-web/src/routes/editor.rs`:runtime block 类型映射同步兼容 kernel 规范化类型,避免编辑器链路继续把任务/列表块当成 media 或段落。 +- `scripts/task119-rust-web-wolai-visual-regression-smoke.js`:按本次 Wolai 基线更新右下浮动入口顺序、间距和底部留白断言,避免旧 Stitch 目标覆盖当前实测。 + +验证命令: + +- `cargo fmt -p mnote-web --check` +- `cargo check -p mnote-web` +- `cargo build -p mnote-web --bin mnote-web` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task136-wolai-aline-phase-d-smoke.js` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js` +- `node scripts/task119-rust-web-wolai-visual-regression-smoke.js` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task128-rust-web-wolai-search-modal-smoke.js` +- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task135-wolai-aline-phase-c-smoke.js` + +证据路径: + +- Phase D subagent 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-baseline2/comparison-matrix.md` +- Wolai Phase D 基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-baseline2/wolai-01-first.png` +- 本地 Phase D 最终截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d/local-phase-d-document.png` +- Phase D subagent post-fix:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-postfix/comparison-matrix.md` + +主线程复核结论: + +- 本地文档画布主列、标题字号/行高/字重、标题 y、meta/icon/debug 隐藏、正文行高、任务/引用/列表渲染、右下浮动入口尺寸/顺序/间距已被 smoke 固化。 +- subagent post-fix 在根文档截图中复核标题 y=108、列宽 760、字号 34/字重 600、meta/icon/debug 不可见、浮动入口 40px/16px/44px、点击后不跳动。 +- 当前本地 fixture smoke 能证明后台内容链路不再把任务块和列表降级为普通段落;post-fix 根文档缺少任务/引用/列表内容,因此这三类块的真实视觉细节仍以后续 Phase E editable-test 或专门 fixture 页面继续细化。 +- 剩余差异:本地页面标题来自临时 fixture,文案与 Wolai Hermes 不同;Wolai 真实 per-page icon 数据仍未进入本地 projection,按 Phase B 结论不伪造。 + ## 8. Phase E:主编辑器块交互 | ID | 状态 | 任务 | 验收要点 | diff --git a/local-editor.png b/local-editor.png deleted file mode 100644 index 9399d1d3..00000000 Binary files a/local-editor.png and /dev/null differ diff --git a/local-hover.png b/local-hover.png deleted file mode 100644 index a1d9324c..00000000 Binary files a/local-hover.png and /dev/null differ diff --git a/local-initial.png b/local-initial.png deleted file mode 100644 index 8525ada5..00000000 Binary files a/local-initial.png and /dev/null differ diff --git a/local-page.png b/local-page.png deleted file mode 100644 index a0732068..00000000 Binary files a/local-page.png and /dev/null differ diff --git a/local-with-overlay.png b/local-with-overlay.png deleted file mode 100644 index a6d22577..00000000 Binary files a/local-with-overlay.png and /dev/null differ diff --git a/official-editor.png b/official-editor.png deleted file mode 100644 index dcd57afd..00000000 Binary files a/official-editor.png and /dev/null differ diff --git a/official-initial.png b/official-initial.png deleted file mode 100644 index a4fc73b6..00000000 Binary files a/official-initial.png and /dev/null differ diff --git a/rust/crates/mnote-web/src/routes/editor.rs b/rust/crates/mnote-web/src/routes/editor.rs index 09c52e0e..4e2ccf8d 100644 --- a/rust/crates/mnote-web/src/routes/editor.rs +++ b/rust/crates/mnote-web/src/routes/editor.rs @@ -214,10 +214,10 @@ fn runtime_block_type_for_raw_type(raw_type: &str) -> (&'static str, bool) { match raw_type { "paragraph" => ("paragraph", true), "heading" => ("heading", true), - "bulletListItem" => ("bullet_list_item", true), - "numberedListItem" => ("numbered_list_item", true), - "checkListItem" | "advancedTodo" => ("todo", true), - "quote" => ("quote", true), + "bulletListItem" | "bullet_list_item" => ("bullet_list_item", true), + "numberedListItem" | "numbered_list_item" => ("numbered_list_item", true), + "checkListItem" | "advancedTodo" | "todo" => ("todo", true), + "quote" | "blockquote" => ("quote", true), "divider" => ("divider", true), "codeBlock" => ("code_block", true), "pageReference" => ("page_reference", false), @@ -237,6 +237,8 @@ fn raw_type_for_runtime_block_type(block_type: &str, fallback_raw_type: &str) -> "todo" => { if fallback_raw_type == "advancedTodo" { "advancedTodo".into() + } else if fallback_raw_type == "todo" { + "todo".into() } else { "checkListItem".into() } diff --git a/rust/crates/mnote-web/src/routes/search.rs b/rust/crates/mnote-web/src/routes/search.rs index 9599873e..ab832594 100644 --- a/rust/crates/mnote-web/src/routes/search.rs +++ b/rust/crates/mnote-web/src/routes/search.rs @@ -266,6 +266,22 @@ fn fallback_search_dataset(workspace_id: &str) -> Value { "rawText": "mnote-web search documents transport", "createdAt": "2026-04-28T00:00:00Z", "updatedAt": "2026-04-28T00:00:00Z" + }, + { + "id": "doc_hermes", + "workspaceId": workspace_id, + "title": "Hermes", + "rawText": "Hermes 技能知识图谱开发 Wolai aline fixture", + "createdAt": "2026-04-30T00:00:00Z", + "updatedAt": "2026-04-30T00:00:00Z" + }, + { + "id": "doc_hermes_skill", + "workspaceId": workspace_id, + "title": "技能知识图谱开发", + "rawText": "Hermes 页面路径 个人 软件开发", + "createdAt": "2026-04-30T00:00:00Z", + "updatedAt": "2026-04-30T00:00:00Z" } ], "mindmaps": [], diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 432c52b7..79af0058 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -315,16 +315,16 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const level = Number(block?.props?.level || block?.level || 1) || 1; return { type: 'heading', attrs: { level: Math.max(1, Math.min(6, level)) }, content }; } - if (type === 'bulletListItem') { + if (type === 'bulletListItem' || type === 'bullet_list_item') { return { type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] }; } - if (type === 'numberedListItem') { + if (type === 'numberedListItem' || type === 'numbered_list_item') { return { type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] }; } - if (type === 'checkListItem' || type === 'advancedTodo') { + if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') { return { type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: Boolean(block?.props?.checked) }, content: [{ type: 'paragraph', content }] }] }; } - if (type === 'quote') { + if (type === 'quote' || type === 'blockquote') { return { type: 'blockquote', content: [{ type: 'paragraph', content }] }; } if (type === 'codeBlock') { diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 11150c58..ffeb2bc0 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -663,33 +663,34 @@ const SIDEBAR_TREE_JS: &str = r##" '
' + '
' + '' + - '' + + '' + '' + '
' + - '
' + + '' + '
' + - '
' + + '
' + '
'; document.body.appendChild(overlay); var input = overlay.querySelector('[data-testid="wolai-search-input"]'); var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]'); - if (input) input.addEventListener('input', renderSearchResults); + if (input) input.addEventListener('input', scheduleSearchResultsRender); overlay.querySelectorAll('[data-search-switch]').forEach(function(button) { button.addEventListener('click', function() { var isOn = button.getAttribute('aria-checked') !== 'true'; button.setAttribute('aria-checked', isOn ? 'true' : 'false'); button.classList.toggle('is-on', isOn); + scheduleSearchResultsRender(); }); }); if (closeButton) closeButton.addEventListener('click', closeSearchModal); @@ -699,26 +700,33 @@ const SIDEBAR_TREE_JS: &str = r##" return overlay; } + var activeSearchRequestId = 0; + var searchRenderTimer = 0; + function searchText(value) { return String(value == null ? '' : value).replace(/\s+/g, ' ').trim(); } - function collectSearchCandidates() { - var seen = Object.create(null); - var candidates = []; - function add(title, source) { - title = searchText(title); - if (!title || seen[title]) return; - seen[title] = true; - candidates.push({ title: title, source: source || '当前工作区' }); - } - document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) { - add(node.textContent, '页面'); - }); - document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) { - add(node.textContent, '当前页面'); - }); - return candidates.slice(0, 120); + function currentWorkspaceName() { + var name = document.querySelector('.sidebar-workspace-name'); + return searchText(name && name.textContent) || '当前工作区'; + } + + function currentDocumentId() { + var shell = document.querySelector('.document-shell[data-document-id]'); + var bodyId = document.body && document.body.getAttribute('data-document-id'); + return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId); + } + + function searchSwitchValue(overlay, name) { + var button = overlay.querySelector('[data-search-switch="' + name + '"]'); + return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true'; + } + + function highlightedHtml(value) { + return escapeHtml(value) + .replace(/<mark>/g, '') + .replace(/<\/mark>/g, ''); } function highlightSearchTitle(title, query) { @@ -732,31 +740,82 @@ const SIDEBAR_TREE_JS: &str = r##" escapeHtml(cleanTitle.slice(index + cleanQuery.length)); } - function renderSearchResults() { + function renderSearchRecentState(overlay) { + var options = overlay.querySelector('[data-testid="wolai-search-options"]'); + var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]'); + var results = overlay.querySelector('[data-testid="wolai-search-results"]'); + if (options instanceof HTMLElement) options.hidden = true; + if (meta) meta.innerHTML = '最近浏览Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; + if (results) { + results.innerHTML = '
暂无最近浏览
'; + } + } + + function scheduleSearchResultsRender() { + window.clearTimeout(searchRenderTimer); + searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120); + } + + async function renderSearchResults() { var overlay = document.querySelector('[data-testid="wolai-search-modal"]'); if (!(overlay instanceof HTMLElement)) return; var input = overlay.querySelector('[data-testid="wolai-search-input"]'); + var options = overlay.querySelector('[data-testid="wolai-search-options"]'); var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]'); var results = overlay.querySelector('[data-testid="wolai-search-results"]'); if (!input || !meta || !results) return; var query = searchText(input.value); - var candidates = collectSearchCandidates(); - var filtered = query - ? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; }) - : candidates.slice(0, 8); - filtered = filtered.slice(0, 100); - meta.innerHTML = '共 ' + filtered.length + ' 条匹配结果Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; - if (!filtered.length) { - results.innerHTML = '
暂无匹配结果
'; + if (!query) { + activeSearchRequestId += 1; + renderSearchRecentState(overlay); return; } - results.innerHTML = filtered.map(function(item) { - return ''; - }).join(''); + if (options instanceof HTMLElement) options.hidden = false; + var requestId = ++activeSearchRequestId; + meta.innerHTML = '搜索中…Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; + results.innerHTML = '
搜索中…
'; + try { + var response = await fetch('/api/search/documents', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + workspaceId: resolveWorkspaceId(document.body), + documentId: currentDocumentId() || null, + query: query, + limit: 30, + filters: { + titleOnly: searchSwitchValue(overlay, 'title'), + exact: searchSwitchValue(overlay, 'exact'), + includeOcr: false, + onlyCurrentPage: searchSwitchValue(overlay, 'page'), + timeRange: 'any', + timeField: 'updated' + } + }) + }); + var payload = await response.json(); + if (requestId !== activeSearchRequestId) return; + var items = Array.isArray(payload.results) ? payload.results : []; + meta.innerHTML = '共 ' + items.length + ' 条匹配结果Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; + if (!items.length) { + results.innerHTML = '
没有搜索结果
'; + return; + } + results.innerHTML = items.map(function(item) { + var title = searchText(item.title || item.name || item.documentTitle || '无标题'); + var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || ''); + var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName()); + return ''; + }).join(''); + } catch (error) { + if (requestId !== activeSearchRequestId) return; + meta.innerHTML = '共 0 条匹配结果Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; + results.innerHTML = '
没有搜索结果
'; + } } function isSearchModalOpen() { @@ -768,8 +827,9 @@ const SIDEBAR_TREE_JS: &str = r##" var overlay = ensureSearchModal(); overlay.hidden = false; document.documentElement.setAttribute('data-mnote-search-modal-open', 'true'); - renderSearchResults(); var input = overlay.querySelector('[data-testid="wolai-search-input"]'); + if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索'); + void renderSearchResults(); if (input) { setTimeout(function() { input.focus(); input.select(); }, 0); } @@ -793,7 +853,12 @@ const SIDEBAR_TREE_JS: &str = r##" var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]'); if (searchTrigger) { e.preventDefault(); - openSearchModal(); + toggleSearchModal(); + return; + } + var searchDialog = closestAction(e.target, '.wolai-search-dialog'); + if (isSearchModalOpen() && !searchDialog) { + closeSearchModal(); return; } @@ -1305,7 +1370,7 @@ pub fn PageLayout(