From 0fd7c59e083337a8bf7f8ac73629edf1a3a0c2ee Mon Sep 17 00:00:00 2001 From: lofyer Date: Tue, 4 Aug 2026 16:19:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AC=94=E8=AE=B0=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=94=B9=E4=B8=BA=E5=A4=9A=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=EF=BC=8CAI=20=E5=A4=9A=E8=BD=AE=E4=BC=9A=E8=AF=9D=E4=B8=8E=20T?= =?UTF-8?q?XT/MD=20=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 笔记独立窗口从「一窗一条」改为单窗口多标签,与阅读器一致: 标签集在主进程侧为权威,notes:tabsChanged 只能收窄不能新增, 否则渲染层可以谎报持有某条笔记来越权读取。存活编辑器上限 3 并 LRU 回收,回收前序列化未保存内容。笔记没有自动保存,关标签与 关窗都做二次确认,取消关闭必须回报主进程复位 closePending, 否则窗口再也关不掉而看门狗仍会销毁未保存内容。 AI 助手支持多轮会话:会话独立落盘,先取历史再写提问, 历史只发文本不重发图像,失败与取消都保留已流出的残片。 新增 TXT/MD 内置阅读(转内存 EPUB 复用 epub 渲染管线), 补上渲染层遗漏的可阅读格式白名单:主进程本就放行 txt/md, 但渲染层另有两份白名单漏了,表现为卡片上没有「阅读」按钮。 书库卡片封面改用 contain 完整显示,留白由同图模糊层垫底, 修正不同比例封面被裁切程度不一导致的观感不一致;多选复选框 去掉衬底色块,恢复原生外观。 其余:PDF 画质档位与画布尺寸钳制、原子写入、笔记资源托管、 GitHub Pages 站点。 --- AGENTS.md | 67 +- README.md | 5 +- main.js | 363 ++++++- preload.js | 43 + site/.nojekyll | 0 site/404.html | 61 ++ site/CNAME | 1 + site/DEPLOY.md | 357 +++++++ site/assets/build-assets.ps1 | 102 ++ site/assets/icon-dark-256.png | Bin 0 -> 63700 bytes site/assets/icon-dark-32.png | Bin 0 -> 2206 bytes site/assets/icon-dark-512.png | Bin 0 -> 237703 bytes site/assets/icon-light-256.png | Bin 0 -> 56339 bytes site/assets/icon-light-32.png | Bin 0 -> 1692 bytes site/assets/icon-light-512.png | Bin 0 -> 201979 bytes site/assets/og-cover.jpg | Bin 0 -> 82936 bytes site/assets/shot-ai-confirm.jpg | Bin 0 -> 158542 bytes site/assets/shot-ai.jpg | Bin 0 -> 309570 bytes site/assets/shot-annotations.jpg | Bin 0 -> 83145 bytes site/assets/shot-library.jpg | Bin 0 -> 239643 bytes site/assets/shot-reader.jpg | Bin 0 -> 359759 bytes site/assets/shot-search.jpg | Bin 0 -> 151671 bytes site/assets/style.css | 722 +++++++++++++ site/download.html | 237 +++++ site/faq.html | 330 ++++++ site/features.html | 321 ++++++ site/index.html | 280 +++++ site/privacy.html | 292 +++++ site/robots.txt | 4 + site/sitemap.xml | 28 + src/_test/ai-sessions.test.js | 862 +++++++++++++++ src/_test/ai.test.js | 339 +++++- src/_test/annotations.test.js | 88 ++ src/_test/atomic-file.test.js | 91 ++ src/_test/electron/ai-scope.integration.js | 81 +- src/_test/electron/annotation.integration.js | 40 + .../electron/library-notes.integration.js | 319 +++++- .../electron/reader-features.integration.js | 324 +++++- src/_test/library.test.js | 92 ++ src/_test/local-import.test.js | 8 +- src/_test/main.test.js | 180 +++- src/_test/pdf-render.test.js | 269 +++++ src/_test/reader.test.js | 28 + src/_test/text-adapter.test.js | 710 +++++++++++++ src/_test/ui.test.js | 357 ++++++- src/atomic-file.js | 78 ++ src/library/local-import.js | 1 + src/library/store.js | 122 ++- src/reader/ai-client.js | 118 ++- src/reader/ai-images.js | 155 +++ src/reader/ai-sessions.js | 999 ++++++++++++++++++ src/reader/annotations.js | 115 +- src/reader/note-window.js | 250 +++++ src/reader/store.js | 62 +- src/settings.js | 25 +- src/ui/app.js | 71 ++ src/ui/index.html | 15 +- src/ui/note-window.css | 265 +++++ src/ui/note.html | 105 ++ src/ui/reader.css | 34 + src/ui/reader.html | 57 +- src/ui/reader/pdf-adapter.mjs | 80 +- src/ui/reader/shell.mjs | 875 +++++++++++++-- src/ui/reader/text-adapter.mjs | 754 +++++++++++++ src/ui/style.css | 119 ++- src/ui/views/library.js | 65 +- src/ui/views/note-shell.js | 610 +++++++++++ src/ui/views/notes.js | 46 +- 68 files changed, 11721 insertions(+), 301 deletions(-) create mode 100644 site/.nojekyll create mode 100644 site/404.html create mode 100644 site/CNAME create mode 100644 site/DEPLOY.md create mode 100644 site/assets/build-assets.ps1 create mode 100644 site/assets/icon-dark-256.png create mode 100644 site/assets/icon-dark-32.png create mode 100644 site/assets/icon-dark-512.png create mode 100644 site/assets/icon-light-256.png create mode 100644 site/assets/icon-light-32.png create mode 100644 site/assets/icon-light-512.png create mode 100644 site/assets/og-cover.jpg create mode 100644 site/assets/shot-ai-confirm.jpg create mode 100644 site/assets/shot-ai.jpg create mode 100644 site/assets/shot-annotations.jpg create mode 100644 site/assets/shot-library.jpg create mode 100644 site/assets/shot-reader.jpg create mode 100644 site/assets/shot-search.jpg create mode 100644 site/assets/style.css create mode 100644 site/download.html create mode 100644 site/faq.html create mode 100644 site/features.html create mode 100644 site/index.html create mode 100644 site/privacy.html create mode 100644 site/robots.txt create mode 100644 site/sitemap.xml create mode 100644 src/_test/ai-sessions.test.js create mode 100644 src/_test/atomic-file.test.js create mode 100644 src/_test/pdf-render.test.js create mode 100644 src/_test/text-adapter.test.js create mode 100644 src/atomic-file.js create mode 100644 src/reader/ai-images.js create mode 100644 src/reader/ai-sessions.js create mode 100644 src/reader/note-window.js create mode 100644 src/ui/note-window.css create mode 100644 src/ui/note.html create mode 100644 src/ui/reader/text-adapter.mjs create mode 100644 src/ui/views/note-shell.js diff --git a/AGENTS.md b/AGENTS.md index b3fdd29..82c724e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,15 +41,80 @@ worker 选择策略在 `pdf-adapter.mjs`: 改动这个 worker 后,用真实 PDF 对比三条路径的渲染结果(文本项数 + 非白像素数):官方 worker 全量数据 / 官方 worker range / 稀疏 worker range。三者应一致。 +## PDF 渲染画质 + +- 底栏「画质」档位(1/2/3)经 `renderOpts` 下发为 `opts.renderQuality`,持久化在 `reader.pdfRenderQuality`。 +- 生效倍率是 `max(devicePixelRatio, renderQuality)`,**HiDPI 屏上不叠乘**。所以断言时不能写「backing 翻倍」,只能断言 `backing / CSS 宽度 == 期望倍率`;开发机 dpr 常见 1.75,写死 2 会假失败。 +- 画布尺寸必须过 `clampCanvasSize()`:Chromium 单边上限 16384px、面积上限 268435456px。超限时浏览器**静默给出不可用画布**,整页空白且不报错。大幅面图纸(2000pt 以上)在 scale 5 下必然踩中。 +- 钳制后 backing 与 CSS 盒子的比例不再等于名义倍率,`pdfPage.render` 的 `transform` 必须用 `fit.scaleX` / `fit.scaleY`(两个方向分别算,`Math.floor` 的损失不同),否则画面错位或只画出一角。 +- 倍率变化必须走 `epoch++` 整篇重建,只改新渲染的页会让同屏出现清晰度不一致。 +- 集成测试里量画质前要先把视口滚回目标页:离屏页会被回收成空白占位,量到的是占位画布。画布尺寸在重建时立即变大,墨迹要等重绘完才落上去,断言墨迹必须轮询而不是只等比例。 + +## TXT 与 Markdown + +- `text-adapter.mjs` 把 txt/md 转成内存 EPUB 再交给 `epub-adapter` 渲染,定位器 `kind` 为 `txt` / `md`,结构与 epub 同源(`chapter` + `offset`)。新增重排格式时记得同步 `REFLOW_KINDS`。 +- 格式白名单有六处,改一处不够:`main.js` 的 `READABLE_EXT`、`library/store.js` 与 `library/local-import.js` 的 `BOOK_EXT`、`main.js` 里选文件对话框的 `extensions`,以及**渲染层的两份** `READABLE_RE`(`ui/views/library.js`、`ui/reader/shell.mjs`)。单测有正则锁死并逐项比对渲染层与 `READABLE_EXT` 是否一致。 +- 渲染层那两份漏掉格式**不会报错**:IPC 照样能打开,但卡片上的「阅读」按钮和封面点击入口静默消失,阅读器的「从书库打开」列表里也少掉这些书。用户看到的现象是"内置阅读器打不开 txt"。集成测试里直接调 `reader.open` 是查不出来的,必须从书库界面点「阅读」。 +- 书库卡片的「阅读」取的是**第一个可阅读文件**。同一条书目既有 txt 又有 pdf 时,加进白名单后打开的文件会从 pdf 变成 txt,断言里不要写死 PDF 画布。 +- Markdown 走 markdown-it(`html: false`)+ DOMPurify 双保险。裸 HTML 会被转义成文本,所以**净化断言必须按 DOM 查**(`querySelectorAll('script')`、`on*` 属性、`a[href]` 协议),用 `innerHTML` 匹配 `onerror` 会把转义后的字面量误判成漏网。 +- 编码探测支持 UTF-8 / UTF-16LE / UTF-16BE(含 BOM)与 GB18030。带 BOM 的 UTF-16LE 是记事本另存的默认之一,探测错会整本乱码且用户无从修正。 + ## AI 助手 - 上下文范围:`selection | page | document | page-image | region-image`。 - 只有 `selection` 需要选中文本;`page` 和 `document` 不依赖选区。 - `document` 无论多短都强制弹确认框,并提示可能超出模型上下文限制。 -- 正文在 `ai-client.js` 按 `MAX_CHARS` 截断,保留首尾(结论常在末尾,只留开头会让模型答非所问)。 +- 正文**完整发送,不做本地截断**。模型窗口够不够由接口自己判断,超限时把报错转成中文提示(`isContextOverflow` / `overflowHint`)。曾经按 `MAX_CHARS = 12000` 挖空中间,实测 40 页文档只发出 8 页,而界面仍显示全文字数,属于静默数据丢失,已移除。`clipContext` 保留给有明确预算上限的场合显式调用。 +- 界面展示的字数必须等于真正外发的字数。任何"先按上限裁剪再发送"的改动都要同步改 `aiCost` 与确认框文案,否则用户会把答非所问归因于模型。 - 图像只用 JPEG 单一编码路径,1600px / 目标 400 KB。**不要加格式回退**(PNG 等),维护成本高于收益;视觉模型按像素图块计费,格式不影响费用。 - 三种协议:Anthropic `/messages`、OpenAI Responses `/responses`、兼容 Chat `/chat/completions`。图像负载格式各不相同,改动时三条都要验。 +## AI 多轮会话 + +- 会话存在 `reader-ai-sessions/`,一个会话一个 JSON,外加可重建的 `index.json`。不进 `store.js`,避免整本阅读进度被聊天记录带着反复重写。 +- `ai:run` 里顺序是硬约束:**先 `historyFor()` 再 `appendUser()`**。顺序反了,当前这轮提问会出现在自己的历史里,模型收到两遍同样的问题。 +- 同一会话禁止并发生成(`ai:run` 里按 `sessionId` 查 `aiRuns`)。两轮同时写一个文件,后完成的那轮会把前一轮的消息覆盖掉。 +- 消息正文存的是**用户看见的那句提问**(`aiTurnTitle`),整篇正文只在 `contextRef` 里留 scope / 字数 / 哈希。把正文当消息存会让重开后的气泡变成十几万字原文,还会被 `LIMITS.question` 截成一段无意义的残句。 +- 失败和取消都要落盘,并且要把**已经流出来的残片**一起存(`streamed`)。只写空串的话,界面上明明显示着半截回答,一重开就消失。 +- 历史只发文本:`stream()` 只取 `role` 与 `text`,图像一律不重发。历史里内联 base64 会让每轮费用随轮数线性上涨。 +- 落盘失败不能把已经拿到的回答变成请求失败,`settleAiAssistant` 吞掉异常。 +- 书籍删除、孤立对账都要带上会话(`aiSessions.forgetMany` / `orphanReport`),删完调 `collectAiImages()`。内容寻址的图片没有引用者就永远不会被回收。 +- 集成测试里断言历史时**不能按固定下标取消息**:本轮提问固定在末尾,中间是历史。原来写死 `messages[1]` 的断言在多轮上线后会取到上一轮,表现为"图像尺寸无效"这种完全无关的报错。 + +## 笔记独立窗口 + +- `src/reader/note-window.js` 是**单窗口多标签**:一个笔记窗口,一条笔记一个标签,已开则切到该标签。同一条笔记两处编辑时 `reader:updateNote` 是整条覆盖、无版本校验,后保存者会把画布内容整块吃掉,所以「一条笔记只能有一个编辑器」是数据安全约束,不是体验优化。`openTab` 必须先 `tabOf()` 查重。 +- 阅读器内的笔记模态**保留**,因此「模态 + 独立窗口」仍可能撞车。靠列表按钮避开:已开窗时「编辑」变成「在窗口中编辑」并转为聚焦窗口,不再开模态。 +- 笔记消失时必须**关掉对应标签**(不是销毁整个窗口):`reader:removeNote` 调 `closeFor`,`library:removeMany` 与 `reader:purgeOrphans` 调 `closeForEntries`。留着标签,它下一次保存会把已删的笔记整条写回去。最后一个标签关掉后窗口才自行退场。 +- 三处窗口广播(`notifyNotesChanged`、`applyWindowIcons`、`notifyUiThemeChanged`)都要带 `noteWindow.all()`,漏一处笔记窗口就收不到笔记变更或主题切换。单测有正则锁死。 +- `notes:getOne` 要校验 `noteWindow.ownsNote(event.sender, noteId)`:笔记窗口只能读**自己已开标签**的那几条,否则这个通道就是遍历全部笔记的后门。开窗目标也必须过 `findNote()` 用 `listNotes()` 重新对账。 +- `notes:tabsChanged`(`setTabs`)只能**收窄**标签集,即只允许 `openNotes.delete`,绝不能 `set`。允许渲染层往里加 ID 等于让它自己扩权:谎报持有某条笔记后 `notes:getOne` 立刻放行,授权集合就形同虚设。新增标签只能走 `open()`,那条路径过 `findNote()` 对账。 +- 关窗拦截三件套必须配套:`close` 里 `preventDefault()` + `closePending` + 看门狗,渲染层处理完调 `notes:shutdownReady`,**用户取消时必须调 `notes:cancelClose`**。少了取消回报,`closePending` 一直为真会让之后每次点关闭都被当成「正在处理」静默忽略,而看门狗仍会在十秒后把带未保存内容的窗口直接销毁。 +- 笔记没有自动保存。切换标签只留在内存,LRU 回收前要把未保存内容序列化进 `pendingContent`,否则回收即丢改动。 +- 脏判定必须比对**序列化后的内容**(`contentKey` 与挂载后取的 `baselineKey`),不能用 `pointerdown`/`keydown` 之类的交互事件:只点选不改字也会被判脏,每个标签关闭时都弹一次无谓的确认。基线要在编辑器 `ready()` 之后取(画布有 version 1→2 归一化,拿磁盘原值当基线会让刚打开就显示已修改),保存成功后基线要跟着前移。 +- 笔记窗口**不**纳入 `isReaderSender`,不获得 AI 会话等阅读器权限。 +- PDF 底版草稿按 sender 隔离(`resolveDrafts` / `readDraft`),草稿不能跨窗口交接,笔记窗口必须自己 stage。 +- 编辑区吃满整窗要逐层 `min-height: 0`,缺一层 flex 子项就被内容顶高、画布溢出窗口。 +- 笔记本下拉框要 `max-width` + `min-width: 0`。只给 `max-width` 不够:`select` 的 min-content 以最长选项为准,长书名/长笔记本名照样把整行顶宽(实测取消限宽后从 260px 涨到 553px)。 +- 标题栏只放品牌名「笔记」,不要副标题。`note.html` 用的是 `style.css`,那里的 `.titlebar-left` **不是** flex(只有 `reader.css` 才是),把 `brand-sub` 放成 `.brand` 的同级会掉到下一行,把左侧块顶成 46px 而标题栏只有 44px。要加副标题只能像 `index.html` 那样塞进 `.brand` 内部。 +- `note.html` 不加载 `reader.css`,所以标签条样式必须在 `note-window.css` 里**自带一份**,也不能用只在 `reader.css` 里定义的变量(如 `--hover-bg-soft`),否则静默失效。 +- 集成测试四个坑:窗口刚建好时 `getURL()` 还是空串,找窗口必须轮询;断言窗口数量只能数**笔记窗口**,用总窗口数当基线会被阅读器窗口的开关搅乱;笔记页同时有多张卡片,找按钮必须限定在 `.note-card[data-note-id=...]` 内,全局找「编辑」会命中别的卡片;多标签后表单是每标签一份,查询要限定在当前激活的 `.note-tab-view` 内,或直接按 `[data-note-id]` 定位,否则量到的是别的标签。 +- 删除笔记要走真实 IPC(`reader:removeNote`),它内部已经调了 `closeFor`。测试里再手工补一次 `closeFor` 等于在验证自己造的假路径,还会因为重复处理而看到「标签没关掉」的假失败。 + +## 书库卡片封面 + +- 封面比例来源不一(内置生成 400x500,书源常见 0.65~0.75,还有方图和横图)。卡片盒子固定 `aspect-ratio: 3/4`,用 `background-size: cover` 会按各自比例裁掉不同的边,观感就是「预览大小不一致」,但**量盒子是量不出问题的**(每张都一样宽高),必须看截图或比对可见的封面内容。 +- 现在用 `contain` 完整显示封面,留白由 `::before` 里同一张图放大模糊后垫底。注意 **`::before` 会盖在父元素自己的背景之上**,所以清晰的那层必须单独画在 `::after` 里,只调 `z-index` 是压不住父元素背景的;照这个顺序:`::before`(模糊,z-index 0)→ `::after`(清晰,z-index 1)→ `.card-cover > *`(角标文字,z-index 2)。两个伪元素都要 `pointer-events: none`,否则挡掉封面的阅读点击。 +- 多选复选框不要加 `padding` + 背景色块。13px 的原生复选框套一圈衬底后看起来像加粗了边框,浅色封面上的可见性用 `filter: drop-shadow(...)` 解决。 + +## 笔记表单的选择器边界 + +`.note-edit-form` 里嵌着画布工具栏(`.canvas-note-root`)与富文本工具栏(`.ql-toolbar`),两者都有 ` 清理孤立批注

+ ${notes.length ? `

+

这些笔记目前仍可在「我的笔记」中查看,删除后无法恢复。

` : ''} + `, () => ({ + annotations: !!(document.getElementById('orphanDelAnnotations') || {}).checked, + notes: !!(document.getElementById('orphanDelNotes') || {}).checked + })); + if (!choice) return; + if (!choice.annotations && !choice.notes) return; + const r = await window.api.reader.purgeOrphans(choice); + if (!r || !r.ok) { + await confirmModal('清理失败', (r && r.error) || '未知错误'); + return; + } + const done = r.data || {}; + $('orphanStatus').textContent = + `已清理 ${done.annotationsRemoved || 0} 份批注、${done.notesRemoved || 0} 本阅读资料`; + $('orphanPurgeBtn').classList.add('hidden'); + orphanFound = null; +}; + const AI_PROTOCOL_INFO = { anthropic: { label: 'Anthropic', diff --git a/src/ui/index.html b/src/ui/index.html index 19ed4bd..65aff08 100644 --- a/src/ui/index.html +++ b/src/ui/index.html @@ -214,6 +214,17 @@ +
+
+
+
残留阅读资料
+
书籍移除时默认保留笔记、批注与进度。笔记仍可在「我的笔记」中查看,批注则没有查看入口。
+
未检查
+
+ + +
+
@@ -302,11 +313,11 @@
内置阅读 - PDF、EPUB、MOBI、AZW、AZW3 + PDF、EPUB、MOBI、AZW、AZW3、TXT、MD
书库导入与管理 - PDF、EPUB、MOBI、AZW、AZW3、TXT、DJVU、FB2、CBZ、CBR + PDF、EPUB、MOBI、AZW、AZW3、TXT、MD、DJVU、FB2、CBZ、CBR
MOBI、AZW 与 AZW3 由 Foliate 解析,支持无 DRM 的 MOBI/KF7/KF8 内容;DRM、KFX 与损坏文件可改用系统应用打开。 diff --git a/src/ui/note-window.css b/src/ui/note-window.css new file mode 100644 index 0000000..b563d52 --- /dev/null +++ b/src/ui/note-window.css @@ -0,0 +1,265 @@ +/* 笔记独立窗口。整窗给编辑区,画布不再受模态尺寸限制。 */ + +.note-window-body { + display: flex; + height: 100vh; + flex-direction: column; + overflow: hidden; +} + +/* 标签条与阅读器同构。note.html 不加载 reader.css,所以这里要自带一份, + class 名保持一致,改样式时两处要一起改。 */ +.doctabs { + height: 36px; + display: flex; + align-items: stretch; + flex-shrink: 0; + padding: 0 6px; + background: var(--bg-soft); + border-bottom: 1px solid var(--line); +} +.doctabs-list { + display: flex; + align-items: stretch; + flex: 1; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; +} +.doctabs-list::-webkit-scrollbar { height: 0; } +.doctab { + display: flex; + align-items: center; + max-width: 220px; + flex-shrink: 0; + margin: 4px 0; + padding: 0 8px 0 14px; + gap: 8px; + background: transparent; + border: 1px solid transparent; + border-radius: 8px; + color: var(--text-dim); + font-size: 13px; + cursor: pointer; +} +/* --hover-bg-soft 只在 reader.css 里定义,这里必须用 style.css 有的变量, + 否则 hover 背景静默失效 */ +.doctab:hover { background: var(--hover-bg); color: var(--text); } +.doctab.active { background: var(--bg-card); border-color: var(--line); color: var(--text); } +.doctab-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.doctab.active .doctab-name { color: var(--accent-bright); font-weight: 600; } +.doctab-fmt { + padding: 0 5px; + flex-shrink: 0; + border: 1px solid var(--line); + border-radius: 5px; + color: var(--text-dim); + font-size: 10px; +} +/* 未保存标记:标签上必须看得见,否则关窗时才发现有改动 */ +.doctab-dirty { + width: 7px; + height: 7px; + flex-shrink: 0; + border-radius: 50%; + background: var(--accent-bright); +} +.doctab-close { + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background: transparent; + border: none; + border-radius: 5px; + color: var(--text-dim); + font-size: 11px; + cursor: pointer; +} +.doctab-close:hover { background: var(--danger); color: #fff; } + +/* 每个标签一个视图,非活跃的整块隐藏。用 display:none 而不是移出 DOM: + 编辑器实例要留着,切回来不必重建,也不会丢未保存内容。 */ +.note-tab-views { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; +} +.note-tab-view { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; +} +.note-tab-view.inactive { display: none; } + +.note-dirty-box { width: 420px; max-width: 92vw; } + +.note-dirty-notice { + margin: 0 0 4px; + color: var(--text-dim); + font-size: 13px; + line-height: 1.6; +} + +.note-window { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + padding: 14px 18px 16px; + gap: 10px; +} + +.note-window-loading, +.note-window-error { + padding: 24px; + color: var(--text-dim); + text-align: center; +} + +.note-window-error { + color: var(--danger); +} + +.note-window-form { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + gap: 10px; +} + +.note-window-head { + display: flex; + align-items: center; + flex: 0 0 auto; + gap: 10px; +} + +.note-window-title { + height: 34px; + padding: 0 10px; + flex: 1; + min-width: 0; + background: var(--input-bg); + color: var(--text); + border: 1px solid var(--line); + border-radius: 9px; + font-size: 14px; +} + +.note-window-title:focus { + border-color: var(--accent); + outline: none; +} + +.note-window-badge { + padding: 4px 10px; + flex: 0 0 auto; + background: var(--hover-strong); + color: var(--text-dim); + border-radius: 999px; + font-size: 12px; +} + +/* 编辑区吃满剩余空间。min-height:0 缺一层,flex 子项就会被内容顶高、画布溢出窗口 */ +.note-window-editor { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; +} + +.note-window-editor > .mixed-note-editor, +.note-window-editor .mixed-note-canvas, +.note-window-editor .mixed-note-text, +.note-window-editor .rich-note-editor { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; +} + +.note-window-editor .ql-container { + min-height: 0; + flex: 1; +} + +.note-window-quote { + max-height: 88px; + margin: 0; + padding: 8px 10px; + flex: 0 0 auto; + overflow-y: auto; + background: var(--hover); + color: var(--text-dim); + border-left: 3px solid var(--accent); + border-radius: 0 8px 8px 0; + font-size: 12px; +} + +.note-window-meta { + display: flex; + align-items: center; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 12px; +} + +.note-window-meta label { + display: flex; + align-items: center; + min-width: 0; + color: var(--text-dim); + font-size: 12px; + gap: 6px; +} + +/* 笔记本名过长会把整行顶宽,须同时限宽并允许收缩到 0 */ +.note-window-meta select, +.note-window-meta input[type="text"] { + height: 28px; + max-width: 260px; + min-width: 0; + text-overflow: ellipsis; + padding: 0 8px; + background: var(--input-bg); + color: var(--text); + border: 1px solid var(--line); + border-radius: 8px; + font-size: 12px; +} + +.note-window-meta select:focus, +.note-window-meta input[type="text"]:focus { + border-color: var(--accent); + outline: none; +} + +.note-window-tags input { width: 220px; } + +.note-window-pin { cursor: pointer; } + +.note-window-actions { + display: flex; + align-items: center; + flex: 0 0 auto; + gap: 12px; +} + +.note-window-status { + flex: 1; + color: var(--text-dim); + font-size: 12px; +} + +.note-window-status.error { color: var(--danger); } + +@media (max-width: 720px) { + .note-window { padding: 10px 12px 12px; } + .note-window-tags input { width: 140px; } +} diff --git a/src/ui/note.html b/src/ui/note.html new file mode 100644 index 0000000..13034e8 --- /dev/null +++ b/src/ui/note.html @@ -0,0 +1,105 @@ + + + + + + PeopleLib + + + + + + +
+
+ + + + 笔记 + +
+
+
+ + + + +
+
+ +
+
+
+ +
+
正在加载笔记…
+ + + + + +
+
+ + + + + + + + + + + + + + diff --git a/src/ui/reader.css b/src/ui/reader.css index eb62b36..28ad4f9 100644 --- a/src/ui/reader.css +++ b/src/ui/reader.css @@ -468,6 +468,15 @@ input[type="color"], padding: 6px 8px; word-break: break-word; flex-shrink: 0; } .ai-status.warn { color: var(--warn-text); border-color: var(--warn-line); } +.ai-sessions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } +.ai-session-label { font-size: 11px; color: var(--text-dim); flex: none; } +.ai-session-select { + flex: 1; min-width: 0; + background: var(--bg-card); border: 1px solid var(--line); color: var(--text); + border-radius: 6px; padding: 3px 6px; font-size: 11px; +} +.ai-session-actions { display: flex; flex-wrap: wrap; gap: 5px; flex-shrink: 0; } +.ai-session-actions .tb-btn { flex: 1 1 auto; } .ai-scope { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } .ai-scope-label { font-size: 11px; color: var(--text-dim); flex: none; } .ai-scope-select { @@ -511,6 +520,31 @@ input[type="color"], .ai-output.streaming { border-color: var(--accent); } .ai-output > :first-child { margin-top: 0; } .ai-output > :last-child { margin-bottom: 0; } +.ai-thread { display: flex; flex-direction: column; gap: 10px; } +.ai-thread-notice { + padding: 5px 8px; border: 1px dashed var(--line); border-radius: 7px; + color: var(--text-dim); font-size: 11px; line-height: 1.6; +} +.ai-msg { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.ai-msg-meta { + display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; + color: var(--text-dim); font-size: 11px; line-height: 1.5; +} +.ai-msg-context { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ai-msg-body { + padding: 7px 10px; border: 1px solid var(--line); border-radius: 9px; + background: var(--input-bg); overflow-wrap: anywhere; +} +.ai-msg-body > :first-child { margin-top: 0; } +.ai-msg-body > :last-child { margin-bottom: 0; } +.ai-msg-body.ai-output-plain { white-space: pre-wrap; } +.ai-msg-user .ai-msg-body { + border-left: 2px solid var(--accent); background: var(--accent-faint); white-space: pre-wrap; +} +.ai-msg-assistant.ai-msg-streaming .ai-msg-body { border-color: var(--accent); } +.ai-msg-error .ai-msg-body { border-color: var(--danger); color: var(--error-text); } +.ai-msg-flags { color: var(--text-dim); font-size: 11px; line-height: 1.6; } +.ai-msg-actions { display: flex; flex-wrap: wrap; gap: 5px; } .ai-output p, .ai-output ul, .ai-output ol, diff --git a/src/ui/reader.html b/src/ui/reader.html index 59dbb7c..98277f0 100644 --- a/src/ui/reader.html +++ b/src/ui/reader.html @@ -142,6 +142,17 @@ +
+ + + + +