@lincyaw/dsh 0.1.0 → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +8 -4
  2. package/build/client.mjs +7 -1
  3. package/build/dist/client.js +1364 -64
  4. package/build/dist/client.json +225 -21
  5. package/build/ensure.mjs +11 -2
  6. package/cordis.patch.yml +9 -0
  7. package/entries/events.mjs +7 -0
  8. package/entries/load.mjs +11 -0
  9. package/entries/telemetry.mjs +7 -0
  10. package/entries/turns.mjs +7 -0
  11. package/package.json +32 -19
  12. package/src/client/RootLayout.tsx +332 -0
  13. package/src/client/agent-requests.ts +64 -0
  14. package/src/client/assistant-setup.tsx +185 -0
  15. package/src/client/author-settings.ts +57 -0
  16. package/src/client/brand.tsx +21 -14
  17. package/src/client/contract.ts +48 -1
  18. package/src/client/document-context.ts +39 -0
  19. package/src/client/dsh.d.ts +27 -1
  20. package/src/client/fallback.tsx +23 -19
  21. package/src/client/frame.css +280 -120
  22. package/src/client/index.tsx +72 -69
  23. package/src/client/launch-target.ts +89 -0
  24. package/src/client/layout.ts +90 -77
  25. package/src/client/mascot.tsx +37 -0
  26. package/src/client/messages.ts +142 -49
  27. package/src/client/{chrome.tsx → navigation-shell.tsx} +20 -25
  28. package/src/client/observable.ts +16 -0
  29. package/src/client/paths.ts +17 -0
  30. package/src/client/root-reload.ts +163 -0
  31. package/src/client/runtime-diagnostics.tsx +228 -0
  32. package/src/client/settings-opener.ts +26 -0
  33. package/src/client/{source.tsx → source-links.tsx} +16 -26
  34. package/src/client/speech-settings.tsx +7 -0
  35. package/src/client/styles.css +42 -0
  36. package/src/client/theme.ts +4 -2
  37. package/src/client/workbench-keys.ts +36 -0
  38. package/src/client/workspace-events.ts +114 -0
  39. package/src/node/bindings.ts +345 -0
  40. package/src/node/context.ts +131 -20
  41. package/src/node/{dsh.ts → dsh-types.ts} +33 -2
  42. package/src/node/fake-collector.ts +46 -0
  43. package/src/node/fake-model.ts +80 -0
  44. package/src/node/launch.ts +127 -10
  45. package/src/node/policy.ts +20 -23
  46. package/src/node/routes.ts +21 -11
  47. package/src/node/server-routes.ts +3 -0
  48. package/src/node/service.ts +58 -207
  49. package/src/node/skills.ts +9 -31
  50. package/src/node/telemetry.ts +126 -0
  51. package/src/node/tools.ts +34 -11
  52. package/src/node/{turns/index.ts → turns.ts} +48 -37
  53. package/src/node/workbench.ts +51 -6
  54. package/src/node/workspace-events.ts +167 -0
  55. package/src/node/workspaces.ts +2 -2
  56. package/src/workspace-events.ts +31 -0
  57. package/web.patch.yml +27 -56
  58. package/rows/load.mjs +0 -8
  59. package/rows/turns.mjs +0 -7
  60. package/skills/presmium-author/SKILL.md +0 -95
  61. package/skills/presmium-author/references/assistance.md +0 -27
  62. package/skills/presmium-author/references/code.md +0 -52
  63. package/skills/presmium-author/references/directives.md +0 -592
  64. package/skills/presmium-author/references/publishing.md +0 -60
  65. package/skills/presmium-author/references/writing.md +0 -72
  66. package/skills/presmium-present/SKILL.md +0 -69
  67. package/skills/presmium-present/references/classroom.md +0 -46
  68. package/skills/presmium-present/references/delivery.md +0 -63
  69. package/skills/presmium-present/references/lecture.md +0 -58
  70. package/src/client/Frame.tsx +0 -314
  71. /package/{rows → entries}/context.mjs +0 -0
  72. /package/{rows → entries}/policy.mjs +0 -0
  73. /package/{rows → entries}/routes.mjs +0 -0
  74. /package/{rows → entries}/service.mjs +0 -0
  75. /package/{rows → entries}/skills.mjs +0 -0
  76. /package/{rows → entries}/tools.mjs +0 -0
  77. /package/{rows → entries}/workbench.mjs +0 -0
  78. /package/{rows → entries}/workspaces.mjs +0 -0
@@ -1,72 +0,0 @@
1
- # 组织内容与演示步骤
2
-
3
- 用于新写讲义、调整结构,或解决“网页能读但投影不好讲”的问题。具体指令选项查 [指令参考](directives.md)。
4
-
5
- ## 让读者跟得上
6
-
7
- 先用一句话说清这节要回答的问题,再给例子、证据和解释。讲解需要能独立阅读;投影内容需要在当前一步看清。不要为了填满幻灯片把整段讲解搬上屏。
8
-
9
- 沿用现有头部。新文稿最小示例:
10
-
11
- ````md
12
- ---
13
- title: 分词入门
14
- lang: zh-CN
15
- plugins:
16
- - '@lincyaw/slidev-addon-core'
17
- renderers:
18
- web: {}
19
- slides: {}
20
- ---
21
-
22
- # 分词入门
23
-
24
- 这份讲义解释怎样从文本得到模型使用的 token。
25
-
26
- (merge)=
27
- ## 为什么要合并
28
-
29
- 先观察重复出现的片段。
30
-
31
- :::{slide}
32
- - 统计相邻片段
33
- - 合并最常见的一对
34
- :::
35
-
36
- 先统计,再合并。每次合并都会改变下一次统计的对象。
37
-
38
- :::{slide}
39
- :reveal:
40
- 合并以后,重新统计。
41
- :::
42
-
43
- 这一段在揭示小结时讲。
44
- ````
45
-
46
- `lang` 决定产物界面和编译时文本,默认 `zh-CN`。官方插件和三个基础渲染端随 `presmium` 安装;要使用插件的指令,仍需在头部 `plugins` 中列出提供它的包。
47
-
48
- ## 决定什么上屏、什么时候出现
49
-
50
- `##` 默认开始一节,即一张幻灯片;`#` 分章。第一个 `##` 之前是封面。已有文稿可用 `sectionDepth` 改章节标题级别。
51
-
52
- 普通段落、列表、引用默认是讲解,进入幻灯片备注;代码、图片、表格、公式和插件组件默认上屏。用 `{slide}` 把需要投影的文字包起来。
53
-
54
- 步骤从 0 开始。`:reveal:` 在下一步显示;`:reveal: items` 逐项显示列表。占多步的块有 `span + 1` 个状态。它后面的讲解段落依次绑定到这些步骤,超出的段落留在最后一步;首个上屏块前的讲解属于第 0 步。
55
-
56
- 改完实际前进和后退一遍。如果讲解跳错步骤,用 `presmium ir` 找到对应章节,核对每个块的 `role`、`at`、`span`,再改源文稿。
57
-
58
- ## 保留稳定的位置
59
-
60
- 章节前用 `(merge)=` 写稳定标签;块也可加标签或 `:label:`。章节标签在文稿内唯一,块标签在章节内唯一。已发布的链接、笔记和讲授轨依赖这些标签,改标题时保留标签。
61
-
62
- 多文件用 `{include}`,路径相对于写指令的文件。引入文件的头部不覆盖入口配置。代码引用的路径规则不同,见 [代码引用与执行](code.md)。
63
-
64
- 交叉引用用 MyST 的 `{ref}`、`{numref}`、`{eq}` 角色,检查不存在的目标。需要长期引用的位置先命名,再写引用。
65
-
66
- ## 某种形态放不下或不适用
67
-
68
- 先精简一页要表达的内容,再决定是否拆节。`{hints}` 可给幻灯片设置局部缩放、布局,或给网页设置滚动叙事。不要选需要 `::right::` 插槽的 Slidev 布局,MyST 文稿不能提供它。
69
-
70
- 用 `{only} web, notebook` 等限制媒介。被限制的内容不会占用其他媒介的步骤,需在两边检查前后顺序。教师专用内容属于受众筛选,见 [发布与复现](publishing.md),不能用媒介限定代替。
71
-
72
- 需要测验、练习、预测后揭示或媒体时,先查 [指令参考](directives.md) 是否已有合适能力。只有需要新交互逻辑时才开发组件。
@@ -1,69 +0,0 @@
1
- ---
2
- name: presmium-present
3
- description: "帮助讲者用 Presmium 完成放映、学生访问与课堂互动,准备离线授课,导出 PDF、notebook 或讲授视频,并录制、回放和修订讲授。Use when presenting or teaching with a Presmium lecture, distributing its outputs, running a classroom, or recording and replaying a talk."
4
- ---
5
-
6
- # 放映、授课与交付
7
-
8
- 以讲者和学生实际使用的场景为起点,准备能打开、能操作的内容。先区分讲者自己的预览、投影画面与学生拿到的版本,再选择启动和发布方式。
9
-
10
- ## 按这次要完成的事行动
11
-
12
- | 用户的目标 | 默认做法 | 详细步骤 |
13
- |---|---|---|
14
- | 马上投影讲一遍 | 网页放映视图加演讲者窗口;已有幻灯片则沿用 | 下文“开始放映” |
15
- | 把讲义发给学生、自学或部署网站 | 构建约定版本,交付静态站点或 notebook | [发布、导出与离线](references/delivery.md) |
16
- | 断网也能上课 | 提前加载页面和要用的执行运行时,再断网试讲 | [发布、导出与离线](references/delivery.md) |
17
- | 让学生扫码跟随、答题、提问 | 准备学生站点与独立课堂服务,走一遍学生流程 | [课堂互动](references/classroom.md) |
18
- | 导出 PDF 或每步一页的讲义 | 用 `presmium-export`,检查实际页与链接 | [发布、导出与离线](references/delivery.md) |
19
- | 录课、发布回放、合成语音或导出视频 | 在网页放映视图录制,或合成讲授,再构建回放 | [录制与回放](references/lecture.md) |
20
-
21
- 优先查看现有文稿的头部、构建脚本与产物,沿用用户已有的形态和部署方式。只读取选中任务的参考。
22
-
23
- ## 开始放映
24
-
25
- 本机边改边讲,在文稿目录运行 `presmium dev --to web`,打开命令打印的网页地址。已经构建并准备分发的版本按 [发布、导出与离线](references/delivery.md) 启动静态服务器。
26
-
27
- 网页导航栏的“视图”可选文档、滚动叙事与放映:文档适合阅读,滚动叙事随讲解滚动推进,放映逐节逐步展示上屏内容。直接进入放映可在网页地址加 `?view=present`。
28
-
29
- | 网页放映按键 | 用途 |
30
- |---|---|
31
- | `→`、`↓`、`PageDown`、空格 | 下一步,章节结束后进入下一节 |
32
- | `←`、`↑`、`PageUp`、`Backspace` | 上一步 |
33
- | `o` | 总览,选择后按 `Enter` 跳转 |
34
- | `p` | 打开演讲者窗口 |
35
- | `Esc` | 退出放映 |
36
- | `b`、`f` | 黑屏、冻结听众画面,再按恢复;需要讲台插件 |
37
- | `s`、`h` | 给指针下的块加聚光、提示;需要引导插件 |
38
-
39
- 全屏使用底栏的“全屏”按钮。把放映窗口放到投影屏,演讲者窗口留在自己的屏幕;窗口中有当前步骤讲解、下一步预告、计时器和课堂面板。两个窗口在同一浏览器、同一源下同步,弹窗被拦截时允许它。
40
-
41
- 黑屏与冻结需要头部列出 `@lincyaw/slidev-addon-stage`;聚光与提示需要 `@lincyaw/slidev-addon-guide`。缺少功能时先核对插件,再重新构建。
42
-
43
- 定位与恢复:网页地址支持 `?view=present&section=<章节ID>&step=<步>`,演讲者窗口用 `view=presenter`。放映时地址随位置更新,刷新可以回到原处。
44
-
45
- 用户用幻灯片形态时,打开报告或开发命令给出的地址,演讲者地址为 `/presenter/<页号>`。空格与左右键逐步、上下键翻页、`o` 总览、`g` 跳转、`d` 明暗;讲台插件的 `f` 是冻结。圈画用 Slidev 绘图工具,在开发服务器中保存到文稿的 `.presmium/drawings/`,静态站点中的圈画只留在浏览器。
46
-
47
- ## 上课前走一遍实际路径
48
-
49
- 验证这堂课会用的内容:听众窗口前进与后退、演讲备注同步、长代码能否看清、运行按钮和录制输出、声音与视频。需要课堂时用另一个浏览器或学生设备加入,验证跟随、一次作答与结果揭示。
50
-
51
- 离线课要实际断网试用;学生版要打开学生产物核对内容;PDF 要打开文件看页数、缺页和溢出。只验证本次会用的能力,并说明未验证的外部条件。
52
-
53
- ## 给学生的是哪个地址
54
-
55
- `presmium dev`、`presmium serve` 与工作台是作者自己的开发环境,提供完整文稿。`presmium serve --host 0.0.0.0` 没有登录,能连接的人可作为作者使用工作区;它不提供按读者隔离的学生视图。
56
-
57
- 给学生分享时用 `presmium build --audience student` 生成静态版本,课堂互动另接独立的 `presmium-classroom-server`。公开完整讲义则按用户选择构建完整版本。具体命令见对应参考,避免把作者服务的地址或启动令牌当成学生链接。
58
-
59
- ## 现场出了问题
60
-
61
- | 现象 | 先检查 |
62
- |---|---|
63
- | 学生打不开链接 | 学生设备能否到达站点与课堂服务;地址是否误写成 `localhost` |
64
- | 演讲者窗口不跟随 | 是否同一浏览器、同一源,弹窗是否打开 |
65
- | 静态站点重建后白屏或 404 | 重启静态服务器,再查 `base` 与幻灯片路由回落 |
66
- | 没有课堂、录制或讲授入口 | 是否装了所需插件,当前是静态站点还是开发服务 |
67
- | 断网后能看不能运行 | 是否提前取得执行运行时;远端环境与课堂仍需网络 |
68
-
69
- 内容或步骤写错时转到 `presmium-author`。交付时告诉用户可打开的地址或文件、讲者和学生各走哪条路径,以及下一次课前需要重新准备什么。
@@ -1,46 +0,0 @@
1
- # 课堂互动
2
-
3
- 用于学生扫码加入、跟随讲者、答题和提问。先准备学生能访问的讲义站点,再配置课堂同步;只有讲者自己能打开的本机预览不能作为学生链接。
4
-
5
- ## 准备站点与服务
6
-
7
- 文稿列出 `@lincyaw/slidev-addon-session`、`@lincyaw/slidev-addon-classroom`;代码练习校验还需要执行插件。给学生构建静态版本,受众标记见 `presmium-author`。
8
-
9
- 需要独立课堂服务时安装 `pnpm add -g @lincyaw/classroom-server`,在文稿目录运行:
10
-
11
- ```sh
12
- presmium-classroom-server --port 1234 --deck .
13
- ```
14
-
15
- 服务打印可用的网络地址。把学生能到达的地址写进文稿头部,下面的 IP 换成服务所在机器的实际地址:
16
-
17
- ```yaml
18
- presmium:
19
- presmium.session:
20
- server: ws://192.168.1.10:1234
21
- ```
22
-
23
- 配置会写入产物,随后运行 `presmium build --audience student --out dist-student`,静态站点的服务方式见 [发布、导出与离线](delivery.md)。主讲也使用带同一课堂服务配置的讲义;完整版与学生版通过稳定标签换算位置。
24
-
25
- 课堂服务默认监听所有网卡,没有 TLS 和身份认证,拿到链接的人可加入;按授课网络部署。HTTPS 站点应使用可从浏览器访问的 WSS 服务地址。学生访问的是静态站点与课堂服务,不是作者工作区。
26
-
27
- `presmium dev` 与 `presmium serve` 内置同步可用于作者本机演练。不要为了给学生开课而暴露作者工作区:能访问该服务的人具有作者权限,且拿到完整文稿。
28
-
29
- ## 实际走完一堂课
30
-
31
- 1. 主讲打开讲义,在网页导航的“课堂”或幻灯片演讲者面板中创建课堂。把站点的网络地址用于加入链接,避免 `localhost`。用学生设备或另一浏览器扫描二维码加入。
32
- 2. 验证默认跟随:主讲翻到另一节、推进一步,学生到对应位置。学生点“自主浏览”后可以自己翻,点“回到主讲进度”恢复。学生版没有的教师步骤不要求学生看到。
33
- 3. 测验先“开放作答”,学生提交,主讲“关闭作答”后才揭示正确答案。练习提交后查看通过情况;提问、投票和理解度在“互动”中汇总。
34
- 4. 下课先结束,再归档。归档保存到课堂服务 `--deck` 目录的 `.presmium/classroom/<课堂号>.json`;此后不能再加入或写入。个人笔记不进入归档,归档默认被 git 忽略,需要保留时单独备份。
35
-
36
- 课堂在服务内存中,未归档前重启会丢失记录。主讲窗口意外关闭时,在同一浏览器、同一讲义地址重新打开,选择“以主讲身份回到课堂”。
37
-
38
- ## 预测与问答怎样揭示
39
-
40
- `{predict}` 在网页文档视图中让学生先保存预测再看后续内容;放映和幻灯片仍由主讲推进揭晓。需要使用时实际验证学生那一侧,不能只看投影画面。
41
-
42
- 启用助手插件并配置模型时,主讲可为提问生成 AI 回答,先在自己窗口查看,再点“发给课堂”。模型配置保存在主讲浏览器,学生不需要模型密钥。不要把生成完成当作已分享给课堂。
43
-
44
- ## 无法加入时
45
-
46
- 先分别验证学生设备能否打开静态网页、能否连接课堂服务。检查地址、端口、WS/WSS 与站点协议。修改配置后重建并重启静态服务器,再生成加入链接。
@@ -1,63 +0,0 @@
1
- # 发布、导出与离线
2
-
3
- 从最终交付物选择构建方式。以下命令在文稿目录运行;先查看已有 `renderers` 与 `base`,不要覆盖现有部署约定。
4
-
5
- ## 给读者一个网页地址
6
-
7
- `presmium build --to web` 得到静态网页,单独构建网页且没有显式 `base` 时位于根路径。需要本机静态服务器且项目没有现成工具时,安装 `pnpm add -D sirv-cli`,然后:
8
-
9
- ```sh
10
- pnpm exec sirv dist/web --port 8080
11
- ```
12
-
13
- 打开终端打印的地址。网页不加全站 SPA 回落,章节有自己的 HTML。重建后重启 sirv,避免它找不到新生成的哈希文件。
14
-
15
- 学生版构建到单独目录,例如 `presmium build --to web --audience student --out dist-student`,再把服务器目录换成 `dist-student/web`。受众标记与发布检查见 `presmium-author` 的发布参考。静态站点可部署到用户选择的托管服务,构建不等于已部署。
16
-
17
- ## 同时交付网页与幻灯片
18
-
19
- 同时构建、两边都没写 `base` 时,网页在 `/web/`,幻灯片在 `/slides/`。保留整个站点结构:
20
-
21
- ```sh
22
- presmium build --to web,slides
23
- pnpm exec sirv dist --single slides/index.html --port 8080
24
- ```
25
-
26
- 网页从 `/web/` 打开,幻灯片从 `/slides/` 打开。上面的回落适合本机演示;正式托管最好只对 `/slides/*` 回落到 `/slides/index.html`,或配置 `renderers.slides.routerMode: hash`,避免网页不存在的地址也落到幻灯片。
27
-
28
- 只构建幻灯片、未设 `base` 时,可用 `pnpm exec sirv dist/slides --single --port 8080`。已有子路径时按该路径服务产物,不要把 `/slides/` 的产物误当根站点。
29
-
30
- 网页章节标题旁可跳到幻灯片,幻灯片导航可回网页。分开部署时配置两边的实际地址,检查两个方向和当前步骤是否正确:
31
-
32
- ```yaml
33
- presmium:
34
- presmium.web:
35
- slidesUrl: https://example.com/slides/
36
- webUrl: https://example.com/web/
37
- ```
38
-
39
- ## 导出 PDF 或图片
40
-
41
- 文稿需列出 `@lincyaw/slidev-addon-publish`。项目中使用:
42
-
43
- ```sh
44
- pnpm exec presmium-export --format pdf --output lecture.pdf
45
- ```
46
-
47
- 用 `--with-clicks` 把每一步展开为一页,`--range 1,3-5` 选择页,`--dark` 导出暗色,`--format png` 改为图片。`presmium-export` 会按导出所需的根路径准备文稿,避免静态站点子路径导致媒体丢失。
48
-
49
- 导出后检查页数、缺页、长代码溢出,以及关键链接。运行面板在纸面上显示静态降级与录制输出。缺页或超时时先读错误,再重试或调整 `--wait`、`--timeout`,不能把不完整文件当成完成。
50
-
51
- ## 给学生可运行的 notebook
52
-
53
- `presmium build --to notebook` 生成 `dist/notebook/<入口名>.ipynb`。图片等文件在旁边的 `media/`,交付整个目录。内核默认 `python3`,由 `renderers.notebook.kernel` 选择。
54
-
55
- 无法转换的交互组件会降级,查看构建报告。需要链接到在线交互版时设 `renderers.notebook.webUrl`,并在 Jupyter 中确认图片与代码单元格可用。
56
-
57
- ## 准备离线课
58
-
59
- 联网时打开实际授课地址,等页面与离线缓存准备好。网页页面和搜索会缓存,较大的执行运行时在第一次使用时取得;提前运行要用的代码,或选“保存以离线使用”。幻灯片离线需发布插件,会预缓存运行时。
60
-
61
- 在同一浏览器、同一地址下断网试讲:翻页、搜索、媒体、运行代码。远端 Jupyter、课堂同步与外部模型仍需网络。需要完全离线时提前准备录制输出或本地可用的运行方式。
62
-
63
- 头部的 `presmium: { presmium.publish: { offline: false } }` 会关闭离线缓存,先检查现有配置。变更站点地址或重新构建后重新演练;“以前打开过”不能证明这次课的资源已齐。
@@ -1,58 +0,0 @@
1
- # 录制与回放
2
-
3
- 用于录课、发布讲授回放、修改逐字稿、从讲解合成语音和导出视频。先决定是保留现场声音,还是根据讲义讲解合成讲授。
4
-
5
- ## 录制一遍
6
-
7
- 文稿列出 `@lincyaw/slidev-addon-lecture`;指针、聚光等重放还需引导插件。在文稿目录运行 `presmium dev --to web`,进入放映视图,用右上角录制栏开始。
8
-
9
- 按需要选择麦克风、摄像头和屏幕,允许浏览器相应权限。翻页、推进、聚光和标注随声音记录。停止后保存;离开放映视图也会结束录制。先录一小节并回放,确认声音和操作同步,再录完整讲授。
10
-
11
- 重录某节时选已有讲授,定位后选“重录本节”,离开该节即停止并替换它。给长期回放引用的章节与块写显式标签,避免改稿后无法定位。
12
-
13
- 文件在 `lectures/<id>/track.json` 与旁边的音视频目录。提交轨迹文件并单独保存媒体;音视频默认被忽略,只有轨迹不能恢复声音。
14
-
15
- ## 发布和检查回放
16
-
17
- 在头部 `renderers` 加 `lecture: {}`,然后:
18
-
19
- ```sh
20
- presmium build --to web,lecture
21
- ```
22
-
23
- 保留 `dist/web/` 与 `dist/lecture/` 的相对位置,按 [发布、导出与离线](delivery.md) 服务整个站点。网页地址加 `?view=lecture&track=<ID>`,也可从“视图”选择讲授。分开部署时在头部写 `presmium: { presmium.lecture: { url: <回放资源地址> } }`。
24
-
25
- 验证声音、字幕、逐字稿跳转、章节定位和“我来试试”后返回讲授。改稿后运行 `presmium check`,处理 `presmium.lecture` 报告的失效引用;不能只因音频还能播放就认为回放有效。
26
-
27
- ## 用讲解合成讲授
28
-
29
- 文稿另列出 `@lincyaw/slidev-addon-speech`,在 `renderers.lecture.voice` 选择引擎与音色。语音引擎由运营者环境配置,凭据不写进文稿。例如:
30
-
31
- ```yaml
32
- renderers:
33
- web: {}
34
- lecture:
35
- voice: { engine: volcengine, voice: zh_female_xiaohe_uranus_bigtts }
36
- ```
37
-
38
- 使用已有语音服务时沿用其引擎和音色。自定义引擎通过环境变量 `PRESMIUM_SPEECH_<ID>_URL`、`_PROVIDER` 与可选的 `_KEY` 定义;火山引擎可用 `PRESMIUM_SPEECH_VOLCENGINE_KEY`。确认目标服务与费用安排后再发起需要上传或计费的操作。
39
-
40
- `presmium build --to web,lecture` 合成后用 `?view=lecture&track=script` 播放。合成片段保存在 `.presmium/speech/`,随 `speech.lock.json` 保存后,别人构建可沿用缓存。
41
-
42
- 讲解中 `{cue}` 安排聚光、推进与停顿,`{say}` 改读法;语法见作者技能的指令参考。写了音色却缺引擎或片段时,检查会指出缺少的句子;只想发布现场录制时不要额外配置合成音色。
43
-
44
- ## 改逐字稿、换声音或整理为讲义
45
-
46
- 在开发服务中回放录制,打开逐字稿。转写前核对录音将发给哪个服务;逐句修订后检查时间与操作绑定。选择音色重新合成会把文字交给语音服务,完成后回放确认。
47
-
48
- 把逐字稿整理为讲解或译文时,在工作台中让 agent 修改源码并审阅结果。录音是素材,最终讲义仍需由作者确认内容和结构。
49
-
50
- ## 导出视频
51
-
52
- 项目中需有 `playwright-chromium` 及浏览器;缺失时按错误提示安装。导出命令:
53
-
54
- ```sh
55
- pnpm exec presmium-lecture export --track <ID> --out lecture.webm
56
- ```
57
-
58
- 默认 1280×720、10 fps,可用 `--size`、`--fps` 调整,输出 WebM。导出后打开文件核对起止、声音与画面同步。合成声音在文件元数据中保留 AI 生成标识。
@@ -1,314 +0,0 @@
1
- import { Group, Panel, Separator, usePanelRef } from 'react-resizable-panels'
2
- import { ChangesPanel, DocumentView, FilesPanel, HomePanel, PmDialog, PmIcon, StatusItems, useWorkbench, WorkbenchProvider } from '@lincyaw/workbench'
3
- import type { Workbench } from '@lincyaw/workbench'
4
- import type { IconName } from '@lincyaw/design'
5
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
6
- import type { ReactNode } from 'react'
7
- import { Logo, useFrameMessages } from './brand.tsx'
8
- import { ACTIVITY, CENTER_MIN, clamp, DOCK_MAX, DOCK_MIN, NARROW, RIGHTBAR_MAX_RATIO, RIGHTBAR_MIN, SIDE_COLLAPSE, SIDE_MAX, SIDE_MIN } from './layout.ts'
9
- import type { Activity, LayoutActions, LayoutState, NarrowView } from './layout.ts'
10
-
11
- interface FrameProps {
12
- useStore: DshSelectorHook<LayoutState>
13
- useSessions: DshSelectorHook<{ byId: Record<string, { id: string, title?: string, cwd?: string, retainedBy: { mainView?: number } }> }>
14
- usePanelInfo: DshSelectorHook<LayoutState['panelInfo']>
15
- actions: LayoutActions
16
- renderSlot(name: string, props?: Record<string, unknown>, options?: Record<string, unknown>): ReactNode
17
- }
18
-
19
- interface WorkspaceNavigation {
20
- workspaceStore: DshObservable<{ items: readonly { workspaceId: string, path: string }[] }>
21
- panelStore: DshObservable<readonly DshSlotEntry[]>
22
- openWorkspace(id: string): Promise<void>
23
- }
24
-
25
- const workspacePath = (path: string) => path.replace(/\\/g, '/').replace(/\/$/, '')
26
-
27
- export function frameFor(workbench: Workbench, openSettings: () => void, openWorkspace: WorkspaceNavigation['openWorkspace'], workspaceStore: WorkspaceNavigation['workspaceStore'], panelStore: WorkspaceNavigation['panelStore']) {
28
- return function PresmiumFrame(props: FrameProps) {
29
- return (
30
- <WorkbenchProvider workbench={workbench}>
31
- <Frame {...props} workbench={workbench} openSettings={openSettings} openWorkspace={openWorkspace} workspaceStore={workspaceStore} panelStore={panelStore} />
32
- </WorkbenchProvider>
33
- )
34
- }
35
- }
36
-
37
- function solve(info: LayoutState['layoutInfo']) {
38
- const viewport = info.viewportWidth
39
- const narrow = viewport < NARROW
40
- const collapsed = viewport < SIDE_COLLAPSE
41
- const side = narrow ? 0 : (collapsed ? info.sideExpanded : info.sideOpen) ? info.side : 0
42
- const dock = narrow || !info.dockOpen ? 0 : collapsed ? Math.min(info.dock, Math.max(300, Math.round(viewport * 0.4))) : info.dock
43
- const room = viewport - ACTIVITY - side - dock - CENTER_MIN - 24
44
- const besideChat = room >= RIGHTBAR_MIN ? room : viewport - ACTIVITY - dock - CENTER_MIN - 16
45
- const available = besideChat >= RIGHTBAR_MIN || info.rightbarShown ? besideChat : viewport - ACTIVITY - CENTER_MIN - 16
46
- const preference = info.rightbar ?? Math.round(viewport * 0.3)
47
- const rightbar = narrow || available < RIGHTBAR_MIN ? 0 : Math.min(available, clamp(preference, RIGHTBAR_MIN, viewport * RIGHTBAR_MAX_RATIO))
48
- return { viewport, narrow, collapsed, side, dock, rightbar, track: info.rightbarTrack ? rightbar : 0 }
49
- }
50
-
51
- function Frame({ useStore, useSessions, usePanelInfo, actions, renderSlot, workbench, openSettings, openWorkspace, workspaceStore, panelStore }: FrameProps & WorkspaceNavigation & { workbench: Workbench, openSettings(): void }) {
52
- const info = useStore(state => state.layoutInfo)
53
- const activePanel = usePanelInfo(panel => panel.activePanelId)
54
- const t = useFrameMessages(workbench)
55
- const document_ = useWorkbench(state => state.document)
56
- const documentTitle = useWorkbench(state => state.documents?.documents.find(doc => doc.dir === state.document)?.title)
57
- const activeFile = useWorkbench(state => state.active)
58
- const sourceNavigation = useWorkbench(state => state.reveal?.seq)
59
- const workspaces = useSyncExternalStore(workspaceStore.subscribe, workspaceStore.getSnapshot).items
60
- const globalPanels = useSyncExternalStore(panelStore.subscribe, panelStore.getSnapshot)
61
- const session = useSessions(state => Object.values(state.byId).find(session => (session.retainedBy.mainView ?? 0) > 0))
62
- const [root, setRoot] = useState<string>()
63
- const workspaceId = root ? workspaces.find(workspace => workspacePath(workspace.path) === workspacePath(root))?.workspaceId : undefined
64
- const workspaceOpened = useRef(false)
65
- const pending = useWorkbench(state => state.review?.changes.length ?? 0)
66
- const frame = useRef<HTMLDivElement>(null)
67
- const rightbarTrack = useRef<HTMLDivElement>(null)
68
- const [rightbarBox, setRightbarBox] = useState<{ left: number, top: number, width: number, height: number }>()
69
- const [helpOpen, setHelpOpen] = useState(false)
70
- const geometry = solve(info)
71
- const { narrow, collapsed } = geometry
72
- const lastSource = useRef({ file: activeFile, navigation: sourceNavigation })
73
- useEffect(() => {
74
- const changed = lastSource.current.file !== activeFile || lastSource.current.navigation !== sourceNavigation
75
- lastSource.current = { file: activeFile, navigation: sourceNavigation }
76
- if (narrow && activeFile && (changed || (info.view !== 'conversation' && info.view !== 'sessions'))) actions.show('source')
77
- }, [narrow, activeFile, sourceNavigation, actions])
78
- const dockPanel = usePanelRef()
79
- const sidePanelRef = usePanelRef()
80
- useEffect(() => {
81
- if (narrow || info.dockOpen) dockPanel.current?.expand()
82
- else dockPanel.current?.collapse()
83
- }, [narrow, info.dockOpen, dockPanel])
84
- const sideShown = !narrow && (collapsed ? info.sideExpanded : info.sideOpen)
85
- useLayoutEffect(() => {
86
- if (sideShown || (narrow && info.view === 'sessions')) sidePanelRef.current?.expand()
87
- else sidePanelRef.current?.collapse()
88
- }, [sideShown, narrow, info.view, sidePanelRef])
89
-
90
- useLayoutEffect(() => {
91
- const track = rightbarTrack.current
92
- if (!track) { setRightbarBox(undefined); return }
93
- const measure = () => {
94
- const box = track.getBoundingClientRect()
95
- const origin = frame.current!.getBoundingClientRect()
96
- setRightbarBox(before => before?.left === box.left - origin.left && before.top === box.top - origin.top && before.width === box.width && before.height === box.height ? before : { left: box.left - origin.left, top: box.top - origin.top, width: box.width, height: box.height })
97
- }
98
- measure()
99
- const observer = new ResizeObserver(measure)
100
- observer.observe(track)
101
- observer.observe(frame.current!)
102
- return () => observer.disconnect()
103
- }, [info.rightbarShown, info.rightbarTrack, info.rightbarFullscreen, info.dockOpen, sideShown, narrow])
104
-
105
- useLayoutEffect(() => {
106
- const element = frame.current!
107
- let raf: number | undefined
108
- const measure = () => {
109
- const width = element.getBoundingClientRect().width
110
- if (width > 0) actions.setViewportWidth(width)
111
- }
112
- measure()
113
- const observer = new ResizeObserver(() => {
114
- raf ??= requestAnimationFrame(() => {
115
- raf = undefined
116
- measure()
117
- })
118
- })
119
- observer.observe(element)
120
- return () => {
121
- observer.disconnect()
122
- if (raf !== undefined) cancelAnimationFrame(raf)
123
- }
124
- }, [actions])
125
-
126
- const opened = useRef(document_)
127
- useEffect(() => {
128
- if (document_ !== undefined && document_ !== opened.current) {
129
- if (narrow) actions.show('files')
130
- else if (info.activity !== 'files') actions.choose('files')
131
- }
132
- if (document_ === undefined && narrow && (info.view === 'files' || info.view === 'source' || info.view === 'preview' || info.view === 'changes')) actions.show('documents')
133
- opened.current = document_
134
- }, [document_, narrow, actions, info.activity, info.view])
135
-
136
- useEffect(() => {
137
- let active = true
138
- void workbench.client.workbench().then(answer => { if (active) setRoot(answer.root) }).catch(() => {})
139
- return () => { active = false }
140
- }, [workbench])
141
- useEffect(() => {
142
- if (!root || workspaceOpened.current) return
143
- if (session?.cwd && workspacePath(session.cwd) === workspacePath(root)) {
144
- workspaceOpened.current = true
145
- return
146
- }
147
- if (!workspaceId) return
148
- workspaceOpened.current = true
149
- void openWorkspace(workspaceId).catch(() => { workspaceOpened.current = false })
150
- }, [root, workspaceId, openWorkspace, session?.id, session?.cwd])
151
- const sessionTitle = session?.title
152
- useEffect(() => {
153
- const title = sessionTitle ?? documentTitle
154
- document.title = title ? `${title} — Presmium` : 'Presmium'
155
- }, [sessionTitle, documentTitle])
156
-
157
- const sidebar = useMemo(() => renderSlot('sidebar', { collapsed: false, width: narrow ? geometry.viewport : Math.max(geometry.side, 264) }), [renderSlot, narrow, geometry.viewport, geometry.side])
158
- const conversation = useMemo(() => renderSlot('main', {}, { entryKey: 'conversation' }), [renderSlot])
159
- const overlays = useMemo(() => renderSlot('shell.overlay', {}), [renderSlot])
160
- const bottom = useMemo(() => renderSlot('shell.bottom', {}), [renderSlot])
161
- const panel = activePanel !== null ? renderSlot('main', {}, { entryKey: activePanel }) : null
162
- const rightbar = renderSlot('rightbar', { width: rightbarBox?.width ?? geometry.rightbar, viewportWidth: geometry.viewport, canShow: geometry.rightbar > 0 })
163
-
164
- const go = (target: Activity | NarrowView) => {
165
- if (target === 'sessions') { actions.toggleConversations(); return }
166
- if (narrow || target !== 'conversation') actions.selectPanel(null)
167
- if (target === 'documents') {
168
- workbench.closeDocument()
169
- if (narrow && workbench.state.document === undefined) actions.show('documents')
170
- return
171
- }
172
- if (narrow) actions.show(target as NarrowView)
173
- else if (target === 'files' || target === 'changes') actions.choose(target)
174
- }
175
- const sideContent = info.activity === 'sessions' ? 'sessions' : info.activity === 'changes' && document_ !== undefined ? 'changes' : 'files'
176
- const view: NarrowView = narrow ? info.view : 'documents'
177
-
178
- const documentShown = panel === null && (!narrow || view === 'source' || view === 'preview')
179
- const centre = <>
180
- {panel !== null && <div className="pmw-frame__panel" data-pmw-slot={`main:${activePanel}`}>{panel}</div>}
181
- {document_ !== undefined && <div className="pmw-frame__document" hidden={!documentShown}>
182
- <DocumentView only={narrow ? view === 'preview' ? 'preview' : 'source' : undefined} />
183
- </div>}
184
- {panel === null && (narrow
185
- ? view === 'changes' && document_ !== undefined ? <ChangesPanel /> : view === 'files' ? <FilesOrDocuments workbench={workbench} onOpen={() => actions.show('source')} />
186
- : view === 'documents' || (document_ === undefined && (view === 'source' || view === 'preview')) ? <HomePanel /> : null
187
- : document_ === undefined ? <HomePanel /> : null)}
188
- </>
189
-
190
- const changes = document_ === undefined ? [] : [{ id: 'changes' as const, icon: 'diff' as const, label: t.changes, pressed: narrow ? view === 'changes' : sideShown && sideContent === 'changes', count: pending }]
191
- const activities: { id: Activity | NarrowView, icon: IconName, label: string, pressed: boolean, count?: number }[] = narrow
192
- ? [
193
- { id: 'documents', icon: 'document', label: t.documents, pressed: activePanel === null && view === 'documents' },
194
- { id: 'files', icon: 'folder', label: t.files, pressed: view === 'files' },
195
- { id: 'source', icon: 'edit', label: t.source, pressed: view === 'source' },
196
- { id: 'preview', icon: 'slides', label: t.preview, pressed: view === 'preview' },
197
- ...changes,
198
- { id: 'sessions', icon: 'history', label: t.conversationHistory, pressed: view === 'sessions' || view === 'conversation' },
199
- ]
200
- : [
201
- { id: 'documents', icon: 'document', label: t.documents, pressed: document_ === undefined && activePanel === null && !sideShown },
202
- { id: 'files', icon: 'folder', label: t.files, pressed: sideShown && sideContent === 'files' },
203
- ...changes,
204
- { id: 'sessions', icon: 'history', label: t.conversationHistory, pressed: info.dockOpen || (sideShown && sideContent === 'sessions') },
205
- ]
206
-
207
- const sidePanel = (<>
208
- <aside className="pmw-frame__side" aria-label={t.side} data-shown={sideShown || undefined} data-content={sideContent}>
209
- <div className="pmw-frame__side-files" hidden={sideContent !== 'files'}><FilesOrDocuments workbench={workbench} /></div>
210
- {sideContent === 'changes' && <ChangesPanel />}
211
- </aside>
212
- <div className="pmw-frame__sidebar" data-pmw-slot="sidebar" data-shown={(narrow ? view === 'sessions' : sideShown && sideContent === 'sessions') || undefined}>{sidebar}</div>
213
- </>)
214
-
215
- return (
216
- <div
217
- ref={frame}
218
- className="pmw-frame pm-ui"
219
- data-narrow={narrow || undefined}
220
- data-collapsed={collapsed || undefined}
221
- data-view={narrow ? view : undefined}
222
- data-rightbar-fullscreen={info.rightbarFullscreen || undefined}
223
- >
224
- <a className="pmw-frame__skip" href="#pmw-centre">{t.skip}</a>
225
- <nav className="pmw-activity" aria-label={t.activity}>
226
- <span className="pmw-activity__logo"><Logo size={28} /></span>
227
- {activities.map(item => (
228
- <button key={item.id} type="button" className="pmw-activity__button" data-activity={item.id} aria-label={item.label} title={item.label} aria-pressed={item.pressed && (!narrow || activePanel === null)} onClick={() => go(item.id)}>
229
- <PmIcon name={item.icon} />
230
- {item.count ? <span className="pmw-activity__count">{item.count}</span> : null}
231
- </button>
232
- ))}
233
- <span className="pmw-activity__end">
234
- {globalPanels.map(({ options }) => {
235
- const id = options.id!
236
- const label = typeof options.label === 'function' ? options.label() : options.label ?? id
237
- return <button key={id} type="button" className="pmw-activity__button" data-activity={id} aria-label={label} title={label} aria-pressed={activePanel === id} onClick={() => {
238
- actions.selectPanel(activePanel === id ? null : id)
239
- if (narrow) actions.show(activePanel === id && document_ !== undefined ? 'preview' : 'documents')
240
- }}>
241
- {renderSlot('sidebar.panellist', { size: 20, active: activePanel === id }, { only: id })}
242
- </button>
243
- })}
244
- <button type="button" className="pmw-activity__button" data-activity="settings" aria-label={t.settings} title={t.settings} onClick={openSettings}><PmIcon name="sliders" /></button>
245
- <button type="button" className="pmw-activity__button" data-activity="help" aria-label={t.help} title={t.help} onClick={() => setHelpOpen(true)}><PmIcon name="help" /></button>
246
- </span>
247
- </nav>
248
-
249
- <Group className="pmw-frame__panels" disabled={narrow} onLayoutChanged={(layout, meta) => {
250
- if (!meta.isUserInteraction) return
251
- const width = info.viewportWidth - ACTIVITY
252
- if (layout.files !== undefined) actions.setSide(layout.files * width / 100)
253
- if (layout.files === 0 && sideShown) actions.toggleSidebar()
254
- if (layout.conversation !== undefined) {
255
- actions.setDockOpen(layout.conversation > 0)
256
- if (layout.conversation > 0) actions.setDock(layout.conversation * width / 100)
257
- }
258
- if (layout.rightbar !== undefined) actions.setRightbar(layout.rightbar * width / 100)
259
- }}>
260
- <Panel id="files" panelRef={sidePanelRef} groupResizeBehavior="preserve-pixel-size" collapsible collapsedSize={0} defaultSize={info.side} minSize={narrow ? 0 : SIDE_MIN} maxSize={SIDE_MAX} className="pmw-frame__side-panel" hidden={narrow ? view !== 'sessions' : !sideShown}>{sidePanel}</Panel>
261
- {sideShown && <Separator className="pmw-frame__handle" data-side="side" aria-label={t.resize} />}
262
- <Panel id="centre" minSize={narrow ? 0 : 200} className="pmw-frame__centre-panel" hidden={narrow && (view === 'sessions' || view === 'conversation') && panel === null}>
263
- <main id="pmw-centre" className="pmw-frame__centre" aria-label={documentTitle ?? t.centre}>{centre}</main>
264
- </Panel>
265
- {!narrow && info.rightbarShown && !info.rightbarFullscreen && geometry.track > 0 && (
266
- <>
267
- <Separator className="pmw-frame__handle" data-side="rightbar" aria-label={t.resize} />
268
- <Panel id="rightbar" groupResizeBehavior="preserve-pixel-size" defaultSize={geometry.track} minSize={RIGHTBAR_MIN} maxSize="70%"><div className="pmw-frame__rightbar-track" ref={rightbarTrack} /></Panel>
269
- </>
270
- )}
271
- {!narrow && info.dockOpen && <Separator className="pmw-frame__handle" data-side="dock" aria-label={t.resize} />}
272
- <Panel id="conversation" panelRef={dockPanel} groupResizeBehavior="preserve-pixel-size" collapsible collapsedSize={0} defaultSize={info.dock} minSize={narrow ? 0 : DOCK_MIN} maxSize={narrow ? '100%' : DOCK_MAX} hidden={narrow && view !== 'conversation'}>
273
- <aside className="pmw-frame__dock" aria-label={t.conversation} data-pmw-slot="main:conversation" data-shown={narrow ? view === 'conversation' || undefined : info.dockOpen || undefined} hidden={!narrow && !info.dockOpen}>
274
- {conversation}
275
- </aside>
276
- </Panel>
277
- </Group>
278
- <div className="pmw-frame__rightbar pmw-frame__rightbar-overlay" data-pmw-slot="rightbar" style={info.rightbarFullscreen ? { right: 0, width: geometry.viewport } : rightbarBox ?? { right: geometry.dock, width: geometry.track }}>{rightbar}</div>
279
-
280
- <footer className="pmw-frame__status" aria-label={t.status}>
281
- <StatusItems />
282
- </footer>
283
- <div className="pmw-frame__bottom" data-pmw-slot="shell.bottom">{bottom}</div>
284
- <div className="pmw-frame__overlay" data-pmw-slot="shell.overlay">{overlays}</div>
285
-
286
-
287
- <PmDialog open={helpOpen} label={t.help} closeLabel={t.close} onClose={() => setHelpOpen(false)}>
288
- <p>{t.helpText}</p>
289
- </PmDialog>
290
- </div>
291
- )
292
- }
293
-
294
- function FilesOrDocuments({ workbench, onOpen }: { workbench: Workbench, onOpen?: () => void }) {
295
- const document_ = useWorkbench(state => state.document)
296
- const documents = useWorkbench(state => state.documents)
297
- const t = useFrameMessages(workbench)
298
- if (document_ !== undefined) return <FilesPanel onOpen={onOpen} />
299
- return (
300
- <nav className="pmw-files" aria-label={t.documents}>
301
- <h2 className="pmw-files__title">{documents?.title ?? t.documents}</h2>
302
- <ul className="pmw-tree">
303
- {(documents?.documents ?? []).map(doc => (
304
- <li key={doc.dir}>
305
- <button type="button" className="pmw-tree__file" onClick={() => void workbench.openDocument(doc.dir)}>
306
- <PmIcon name="document" size={15} />
307
- {doc.title}
308
- </button>
309
- </li>
310
- ))}
311
- </ul>
312
- </nav>
313
- )
314
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes