@rexvane/piweb 0.3.24 → 0.3.26
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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +11 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/react-loadable-manifest.json +13 -20
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +6 -8
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/resources/route.js +1 -0
- package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +4 -4
- package/.next/server/app/api/github/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +4 -3
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +2 -2
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -7
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +11 -11
- package/.next/server/chunks/3924.js +3 -3
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/620.js +1 -0
- package/.next/server/chunks/6229.js +1 -1
- package/.next/server/chunks/7496.js +1 -1
- package/.next/server/chunks/7662.js +2 -2
- package/.next/server/chunks/8686.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -2
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
- package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
- package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
- package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
- package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
- package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
- package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
- package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
- package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
- package/.next/static/css/014eb1f21414e615.css +1 -0
- package/.next/static/css/56b129a13637fcfd.css +1 -0
- package/.next/static/css/7e3caf797e4cb19a.css +1 -0
- package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -2
- package/.next/types/validator.ts +9 -9
- package/README.md +1 -1
- package/next.config.ts +1 -1
- package/package.json +9 -9
- package/scripts/apply-global.mjs +87 -0
- package/scripts/launcher.mjs +21 -0
- package/scripts/testing/prepare-sdk-tools.mjs +19 -0
- package/src/app/api/agent/new/route.ts +2 -1
- package/src/app/api/agent/resources/route.ts +18 -0
- package/src/app/api/models/route.ts +22 -1
- package/src/app/api/plugins/route.ts +3 -2
- package/src/app/api/prompts/route.ts +3 -2
- package/src/app/api/remote/route.ts +22 -4
- package/src/app/api/sessions/[id]/export/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -2
- package/src/app/globals.css +1002 -1765
- package/src/components/AppShell.tsx +128 -93
- package/src/components/ChatInput.tsx +177 -98
- package/src/components/ChatWindow.tsx +189 -56
- package/src/components/ContextMeter.tsx +33 -9
- package/src/components/CustomSelect.module.css +142 -0
- package/src/components/CustomSelect.tsx +157 -0
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/ExtensionUI.tsx +9 -3
- package/src/components/FileViewer.tsx +24 -21
- package/src/components/GitPanel.tsx +35 -13
- package/src/components/GrowthTree.tsx +6 -2
- package/src/components/ImageViewer.tsx +72 -0
- package/src/components/MarkdownBlock.tsx +67 -96
- package/src/components/ModelSelector.tsx +36 -39
- package/src/components/PiMark.tsx +76 -41
- package/src/components/PreviewImage.tsx +33 -0
- package/src/components/ProjectPanel.tsx +8 -5
- package/src/components/PromptPanel.tsx +85 -20
- package/src/components/ProviderSetupModal.module.css +12 -11
- package/src/components/ProviderSetupModal.tsx +107 -36
- package/src/components/RemoteSection.tsx +53 -2
- package/src/components/SessionImportSection.tsx +30 -15
- package/src/components/SessionSidebar.tsx +72 -62
- package/src/components/SettingsPanel.tsx +49 -151
- package/src/components/TrajectoryAnalyzer.module.css +26 -11
- package/src/components/TrajectoryView.tsx +11 -3
- package/src/components/chat/markdown-actions.tsx +23 -0
- package/src/components/composer/ComposerFiles.tsx +224 -0
- package/src/components/composer/ComposerImages.tsx +59 -127
- package/src/hooks/usePiWeb.ts +47 -2
- package/src/i18n.tsx +62 -80
- package/src/lib/agent-manager.ts +365 -33
- package/src/lib/chat/file-path.ts +54 -0
- package/src/lib/chat/skill-expansion.ts +51 -0
- package/src/lib/extension-ui.ts +25 -1
- package/src/lib/font.ts +37 -39
- package/src/lib/git-service.ts +4 -2
- package/src/lib/growth-turns.ts +3 -1
- package/src/lib/model-draft.ts +69 -2
- package/src/lib/models-service.ts +53 -3
- package/src/lib/pi.ts +2 -2
- package/src/lib/remote/assets.json +1 -1
- package/src/lib/remote/supervisor.ts +48 -5
- package/src/lib/security-service.ts +2 -1
- package/src/lib/session-reader.ts +25 -13
- package/src/lib/session-retention.ts +11 -8
- package/src/lib/skills-service.ts +2 -1
- package/src/lib/slash-command-token.ts +14 -0
- package/src/lib/trajectory.ts +111 -15
- package/src/lib/types.ts +28 -3
- package/src/lib/update-service.ts +29 -9
- package/src/lib/workspace-store.ts +36 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/3421.js +0 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
- package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
- package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
- package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
- package/.next/static/css/08815507594e8b8d.css +0 -1
- package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
- package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
- package/.next/static/css/f15a853af8b5127f.css +0 -1
- package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
- package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
- package/src/components/side-chat/SideChatPanel.module.css +0 -131
- package/src/components/side-chat/SideChatPanel.tsx +0 -189
- package/src/components/side-chat/icons.tsx +0 -8
- package/src/components/side-chat/request-history.ts +0 -29
- package/src/components/side-chat/store.ts +0 -248
- package/src/lib/chat/stream-settings.ts +0 -192
- package/src/lib/side-chat/context.ts +0 -123
- package/src/lib/side-chat/service.ts +0 -191
- package/src/lib/side-chat/types.ts +0 -61
- /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
|
@@ -8,32 +8,25 @@
|
|
|
8
8
|
* 而 Hero 页(还没开会话)根本用不到它。ChatWindow 用 next/dynamic 引用本模块,
|
|
9
9
|
* 首屏不再下载这 472KB;AppShell 在空闲时预取,真正打开会话时通常已经就绪。
|
|
10
10
|
*
|
|
11
|
-
* 流式输出版(
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* - 逐帧推进:把「已到达的完整文本」和「已显示的文本」分开,每帧只把显示进度往前推一小步,
|
|
15
|
-
* 服务端 token 再来得快、再成串到达,界面也是匀速长出来的,不再一跳一跳。
|
|
11
|
+
* 流式输出版(v8):
|
|
12
|
+
* - 文本到达即整段显示:模型吐字速度由上游与网络决定,本地不再加一层追赶节奏(原先的
|
|
13
|
+
* 平滑 / 打字机 / 渐显 / 即时四样式与五档速度、打字光标已随设置项一并移除)。
|
|
16
14
|
* - 增量切分 + 分块记忆化:「已稳定的完整块」按 key 记忆化,扫描进度也复用上一帧,
|
|
17
|
-
*
|
|
15
|
+
* 每次更新代价只与「新增的那几个字」成正比(旧版每帧重扫全文才是长回答的卡顿来源)。
|
|
18
16
|
* - 代码围栏内的 tail 用纯文本渲染,等闭合后再交给 highlight.js 高亮一次,
|
|
19
17
|
* 避免流式代码块每帧重新高亮。
|
|
20
|
-
* - 贴底听对话区的(见 chat/chat-scroll
|
|
18
|
+
* - 贴底听对话区的(见 chat/chat-scroll);回答结束后换成整段渲染。
|
|
21
19
|
*/
|
|
22
|
-
import { memo, useContext, useEffect, useLayoutEffect, useRef, useState
|
|
20
|
+
import { memo, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
23
21
|
import ReactMarkdown from "react-markdown";
|
|
24
22
|
import rehypeHighlight from "rehype-highlight";
|
|
25
23
|
import remarkGfm from "remark-gfm";
|
|
26
24
|
import { ChatScrollContext } from "@/components/chat/chat-scroll";
|
|
25
|
+
import { PreviewImage } from "@/components/PreviewImage";
|
|
26
|
+
import { useMarkdownActions } from "@/components/chat/markdown-actions";
|
|
27
|
+
import { looksLikeFilePath } from "@/lib/chat/file-path";
|
|
27
28
|
import { IconCheckOutline14, IconCopyOutline16 } from "@/components/icons";
|
|
28
29
|
import { copyText } from "@/lib/clipboard";
|
|
29
|
-
import {
|
|
30
|
-
getServerStreamSettings,
|
|
31
|
-
getStreamSettings,
|
|
32
|
-
nextRevealEnd,
|
|
33
|
-
streamStepMs,
|
|
34
|
-
subscribeStreamSettings,
|
|
35
|
-
type StreamSettings,
|
|
36
|
-
} from "@/lib/chat/stream-settings";
|
|
37
30
|
import { useI18n } from "@/i18n";
|
|
38
31
|
|
|
39
32
|
type MarkdownTextNode = { value?: unknown; children?: MarkdownTextNode[] };
|
|
@@ -90,7 +83,7 @@ function SafeImage({ src, alt, title }: { src?: unknown; alt?: string; title?: s
|
|
|
90
83
|
const { t } = useI18n();
|
|
91
84
|
const source = typeof src === "string" ? src : "";
|
|
92
85
|
if (!source) return null;
|
|
93
|
-
if (allowed || isInlineImage(source)) return <
|
|
86
|
+
if (allowed || isInlineImage(source)) return <PreviewImage image={source} alt={alt ?? ""} title={title} />;
|
|
94
87
|
let host = source;
|
|
95
88
|
try {
|
|
96
89
|
host = new URL(source).host || source;
|
|
@@ -109,13 +102,39 @@ function SafeImage({ src, alt, title }: { src?: unknown; alt?: string; title?: s
|
|
|
109
102
|
* 流式分块时,多个块共用同一个 .md 容器(外边距规则因此照旧生效),
|
|
110
103
|
* 每块各自记忆化:只要 text 没变就不会重新解析。
|
|
111
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* 链接:外部链接加一个 ↗ 标记,让「点了会离开本站」一眼可见。
|
|
107
|
+
* 站内链接(同源)不加标记,避免到处都是符号。
|
|
108
|
+
*/
|
|
109
|
+
function MarkdownLink({ href, children, ...props }: { href?: string; children?: React.ReactNode } & Record<string, unknown>) {
|
|
110
|
+
let external = false;
|
|
111
|
+
if (typeof href === "string" && href) {
|
|
112
|
+
if (/^https?:\/\//i.test(href)) {
|
|
113
|
+
external = true;
|
|
114
|
+
try {
|
|
115
|
+
external = typeof window === "undefined" || new URL(href).origin !== window.location.origin;
|
|
116
|
+
} catch {
|
|
117
|
+
/* 解析不了就当外链 */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!external) return <a href={href} {...props}>{children}</a>;
|
|
122
|
+
return (
|
|
123
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="md-external" {...props}>
|
|
124
|
+
{children}
|
|
125
|
+
<span className="md-external-mark" aria-hidden="true">↗</span>
|
|
126
|
+
</a>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
const MarkdownContent = memo(function MarkdownContent({ text }: { text: string }) {
|
|
131
|
+
const actions = useMarkdownActions();
|
|
113
132
|
return (
|
|
114
133
|
<ReactMarkdown
|
|
115
134
|
remarkPlugins={[remarkGfm]}
|
|
116
135
|
rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
|
|
117
136
|
components={{
|
|
118
|
-
a: ({ node: _node, ...props }) => <
|
|
137
|
+
a: ({ node: _node, ...props }) => <MarkdownLink {...props} />,
|
|
119
138
|
img: ({ src, alt, title }) => <SafeImage src={src} alt={alt} title={title} />,
|
|
120
139
|
pre: ({ node, children, ...props }) => (
|
|
121
140
|
<div className="md-code-block">
|
|
@@ -123,6 +142,22 @@ const MarkdownContent = memo(function MarkdownContent({ text }: { text: string }
|
|
|
123
142
|
<pre {...props}>{children}</pre>
|
|
124
143
|
</div>
|
|
125
144
|
),
|
|
145
|
+
// 行内代码里常是文件路径:能认出来且能打开时,变成可点(只变色,不加下划线)
|
|
146
|
+
code: ({ node: _node, className, children }) => {
|
|
147
|
+
const raw = typeof children === "string" ? children : Array.isArray(children) ? children.join("") : "";
|
|
148
|
+
const path = raw.trim();
|
|
149
|
+
// 代码块里的 code 不参与路径识别。只看 className 不够:不写语言的围栏和缩进代码块也没有 className,
|
|
150
|
+
// 只有一行路径时整个代码块会变成按钮。代码块的文本总以换行结尾,行内代码不会
|
|
151
|
+
const block = Boolean(className) || raw.endsWith("\n");
|
|
152
|
+
if (actions.openFile && !block && looksLikeFilePath(path)) {
|
|
153
|
+
return (
|
|
154
|
+
<button type="button" className="md-file-path" title={path} onClick={() => actions.openFile!(path)}>
|
|
155
|
+
{children}
|
|
156
|
+
</button>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return <code className={className}>{children}</code>;
|
|
160
|
+
},
|
|
126
161
|
}}
|
|
127
162
|
>
|
|
128
163
|
{text}
|
|
@@ -298,103 +333,43 @@ export function splitStreamingMarkdown(text: string): StreamingSplit {
|
|
|
298
333
|
return createStreamingSplitter().update(text);
|
|
299
334
|
}
|
|
300
335
|
|
|
301
|
-
/** 流式偏好:设置面板一改,这里立刻换样式(正在流的那条回答不用重开) */
|
|
302
|
-
function useStreamSettings(): StreamSettings {
|
|
303
|
-
return useSyncExternalStore(subscribeStreamSettings, getStreamSettings, getServerStreamSettings);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
336
|
interface StreamView extends StreamingSplit {
|
|
307
337
|
/** 当前显示出来的部分(始终是目标文本的前缀) */
|
|
308
338
|
shown: string;
|
|
309
339
|
}
|
|
310
340
|
|
|
311
341
|
/**
|
|
312
|
-
* live:回答还在流。结束后(live 变 false
|
|
313
|
-
*
|
|
314
|
-
*
|
|
342
|
+
* live:回答还在流。结束后(live 变 false)换成整段 Markdown 渲染(和历史消息同一种渲染,
|
|
343
|
+
* 脚注、引用式链接这类跨块语法也就对了)。
|
|
344
|
+
*
|
|
345
|
+
* 文本到达即整段显示,不做逐帧平滑:模型吐字的速度由上游与网络决定,本地再加一层追赶节奏
|
|
346
|
+
* 只会让显示落后于实际到达的内容。切分器仍然保留——它让「已稳定的完整块」按 key 记忆化,
|
|
347
|
+
* 每次新增只重解析变化的那一块,而不是整篇重扫。
|
|
315
348
|
*/
|
|
316
349
|
export const StreamingMarkdownBlock = memo(function StreamingMarkdownBlock({ text, live = true }: { text: string; live?: boolean }) {
|
|
317
|
-
const settings = useStreamSettings();
|
|
318
350
|
const scroll = useContext(ChatScrollContext);
|
|
319
351
|
const splitterRef = useRef<{ update: (text: string) => StreamingSplit } | null>(null);
|
|
320
352
|
if (!splitterRef.current) splitterRef.current = createStreamingSplitter();
|
|
321
353
|
const [view, setView] = useState<StreamView>(() => ({ shown: text, ...splitterRef.current!.update(text) }));
|
|
322
|
-
const viewRef = useRef(view);
|
|
323
|
-
const targetRef = useRef(text);
|
|
324
|
-
const settingsRef = useRef(settings);
|
|
325
|
-
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
326
|
-
|
|
327
|
-
const commit = (shown: string) => {
|
|
328
|
-
const next: StreamView = { shown, ...splitterRef.current!.update(shown) };
|
|
329
|
-
viewRef.current = next;
|
|
330
|
-
setView(next);
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
const stopTimer = () => {
|
|
334
|
-
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
335
|
-
timerRef.current = null;
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
/** 一帧:只把显示进度往前推一点。用 setTimeout 而非 rAF:后台标签页 / 测试环境同样确定。 */
|
|
339
|
-
const step = () => {
|
|
340
|
-
timerRef.current = null;
|
|
341
|
-
const target = targetRef.current;
|
|
342
|
-
const shown = viewRef.current.shown;
|
|
343
|
-
if (shown === target) return; // 追平,收工(下次文本变化再启动)
|
|
344
|
-
if (!target.startsWith(shown)) {
|
|
345
|
-
commit(target); // 目标被整体替换(编辑重发):直接跟上,不再逐帧
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const current = settingsRef.current;
|
|
349
|
-
commit(target.slice(0, nextRevealEnd(target, shown.length, current)));
|
|
350
|
-
timerRef.current = setTimeout(step, streamStepMs(current.speed));
|
|
351
|
-
};
|
|
352
354
|
|
|
353
|
-
|
|
354
|
-
const start = () => {
|
|
355
|
-
if (timerRef.current !== null) return;
|
|
356
|
-
const current = settingsRef.current;
|
|
357
|
-
if (current.style === "instant") {
|
|
358
|
-
if (viewRef.current.shown !== targetRef.current) commit(targetRef.current);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
timerRef.current = setTimeout(step, streamStepMs(current.speed));
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
// 目标变化:更新目标,节拍没在跑就起一个。已有节拍时不重置计时器(防抖会让正文冻住)
|
|
355
|
+
// 目标变化:整段跟上,不再排队
|
|
365
356
|
useEffect(() => {
|
|
366
|
-
|
|
367
|
-
|
|
357
|
+
const next: StreamView = { shown: text, ...splitterRef.current!.update(text) };
|
|
358
|
+
setView(next);
|
|
368
359
|
}, [text]);
|
|
369
360
|
|
|
370
|
-
// 偏好变化即时生效:切成「即时」就停表追平,切回别的样式就继续起节拍
|
|
371
|
-
useEffect(() => {
|
|
372
|
-
settingsRef.current = settings;
|
|
373
|
-
if (viewRef.current.shown === targetRef.current) return;
|
|
374
|
-
if (settings.style === "instant") {
|
|
375
|
-
stopTimer();
|
|
376
|
-
commit(targetRef.current);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
start();
|
|
380
|
-
}, [settings]);
|
|
381
|
-
|
|
382
|
-
useEffect(() => () => stopTimer(), []);
|
|
383
|
-
|
|
384
361
|
const settled = !live && view.shown === text;
|
|
385
362
|
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// 追平那一帧和换成整段渲染那一帧同样会变高,也要贴。
|
|
363
|
+
// 内容变高会让正文往下长:提交后(绘制前)跟着贴底,界面不再跳动。
|
|
364
|
+
// 贴不贴听对话区的(往上翻过、大纲正在跳转都会松开),这里不另量距离、不另设阈值。
|
|
389
365
|
useLayoutEffect(() => {
|
|
390
366
|
if (scroll?.shouldPin()) scroll.pin();
|
|
391
367
|
}, [view, settled, scroll]);
|
|
392
368
|
|
|
393
369
|
if (settled) return <MarkdownBlock text={text} />;
|
|
394
370
|
|
|
395
|
-
const {
|
|
396
|
-
// 尾巴常驻同一个 key
|
|
397
|
-
// 重挂会让渐显动画在「已经显示过的内容」上重播一遍,正是输出时闪烁几下的来源。
|
|
371
|
+
const { blocks, tail, tailInFence } = view;
|
|
372
|
+
// 尾巴常驻同一个 key 的容器:块升级时尾巴原地更新而不是整棵重挂。
|
|
398
373
|
// 围栏未闭合时用与最终渲染相同的结构(md-code-block > pre > code,含复制按钮,同样不折行),
|
|
399
374
|
// 闭合升级只有语法着色的差别,没有布局跳变;开头的 ``` 行不进代码。
|
|
400
375
|
let tailNode: React.ReactNode;
|
|
@@ -414,11 +389,7 @@ export const StreamingMarkdownBlock = memo(function StreamingMarkdownBlock({ tex
|
|
|
414
389
|
}
|
|
415
390
|
|
|
416
391
|
return (
|
|
417
|
-
<div
|
|
418
|
-
className={settings.caret ? "md pw-stream pw-stream-caret" : "md pw-stream"}
|
|
419
|
-
data-stream-style={settings.style}
|
|
420
|
-
data-stream-pending={shown === text ? undefined : ""}
|
|
421
|
-
>
|
|
392
|
+
<div className="md pw-stream">
|
|
422
393
|
{blocks.map((block, i) => <MarkdownContent key={i} text={block} />)}
|
|
423
394
|
{tail
|
|
424
395
|
? <div className="pw-stream-tail" key="tail">{tailNode}</div>
|
|
@@ -158,13 +158,11 @@ export function ModelSelector({
|
|
|
158
158
|
<div className="py-1">
|
|
159
159
|
<button
|
|
160
160
|
type="button"
|
|
161
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-left
|
|
161
|
+
className="isx-menu-item flex w-full items-center gap-2 px-3 py-2 text-left"
|
|
162
162
|
style={{ color: "var(--dsw-label-secondary)" }}
|
|
163
|
-
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
|
|
164
|
-
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
|
|
165
163
|
onClick={() => setPane("model")}
|
|
166
164
|
>
|
|
167
|
-
<IconChevronLeft14 size={14} /> {t.thinking}
|
|
165
|
+
<IconChevronLeft14 size={14} /> <span className="isx-menu-shift">{t.thinking}</span>
|
|
168
166
|
</button>
|
|
169
167
|
{thinkingLevels.length === 0 && (
|
|
170
168
|
<div className="px-4 py-3" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
|
|
@@ -179,21 +177,16 @@ export function ModelSelector({
|
|
|
179
177
|
type="button"
|
|
180
178
|
role="menuitemradio"
|
|
181
179
|
aria-checked={selected}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!selected) e.currentTarget.style.background = "var(--dsw-hover)";
|
|
185
|
-
}}
|
|
186
|
-
onMouseLeave={(e) => {
|
|
187
|
-
if (!selected) e.currentTarget.style.background = "transparent";
|
|
188
|
-
}}
|
|
180
|
+
data-active={selected ? "true" : undefined}
|
|
181
|
+
className="isx-menu-item flex w-full items-center justify-between px-4 py-2 text-left"
|
|
189
182
|
onClick={() => {
|
|
190
183
|
onSelectLevel(lv);
|
|
191
184
|
setOpen(false);
|
|
192
185
|
}}
|
|
193
186
|
>
|
|
194
|
-
<span style={{ fontSize: 14
|
|
187
|
+
<span className="isx-menu-shift" style={{ fontSize: 14 }}>{lv}</span>
|
|
195
188
|
{selected && (
|
|
196
|
-
<span style={{ display: "inline-flex"
|
|
189
|
+
<span className="pr-2" style={{ display: "inline-flex" }}>
|
|
197
190
|
<IconCheckOutline14 size={14} />
|
|
198
191
|
</span>
|
|
199
192
|
)}
|
|
@@ -205,13 +198,23 @@ export function ModelSelector({
|
|
|
205
198
|
|
|
206
199
|
{pane === "model" && (
|
|
207
200
|
<div className="flex min-h-0 flex-col">
|
|
208
|
-
|
|
201
|
+
{/* overflow-y-auto 会把 overflow-x 一并提升成 auto:Iconsax 悬停位移(.isx-menu-shift
|
|
202
|
+
translateX(4px))加上全局 10px 高的横向滚动条,会在列表底部露出一条横杠。
|
|
203
|
+
显式锁掉横向,去掉那条横条。 */}
|
|
204
|
+
<div ref={listRef} className="max-h-[340px] overflow-x-hidden overflow-y-auto py-1">
|
|
209
205
|
{defaultOption && (
|
|
210
|
-
<button
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{defaultOption.
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
role="menuitemradio"
|
|
209
|
+
aria-checked={defaultOption.selected}
|
|
210
|
+
data-selected={defaultOption.selected}
|
|
211
|
+
data-active={defaultOption.selected ? "true" : undefined}
|
|
212
|
+
className="isx-menu-item flex min-h-10 w-full items-center justify-between gap-3 rounded-[12px] px-3 py-2 text-left"
|
|
213
|
+
style={{ fontSize: 13 }}
|
|
214
|
+
onClick={() => { defaultOption.onSelect(); setOpen(false); }}
|
|
215
|
+
>
|
|
216
|
+
<span className="isx-menu-shift">{defaultOption.label}</span>
|
|
217
|
+
{defaultOption.selected && <span className="pr-2" style={{ display: "inline-flex" }}><IconCheckOutline14 size={14} /></span>}
|
|
215
218
|
</button>
|
|
216
219
|
)}
|
|
217
220
|
{groups.length === 0 && (
|
|
@@ -223,8 +226,9 @@ export function ModelSelector({
|
|
|
223
226
|
<section key={g.provider} role="group" aria-label={g.label}>
|
|
224
227
|
{/* 提供商名不吸顶:吸顶会在滚动时钉住并被下一组顶出成半截文字("卡住"观感) */}
|
|
225
228
|
<div
|
|
226
|
-
className="px-3 pb-1 pt-2"
|
|
229
|
+
className="truncate px-3 pb-1 pt-2"
|
|
227
230
|
style={{ fontSize: 12, fontWeight: 500, color: "var(--dsw-label-caption)" }}
|
|
231
|
+
title={g.label}
|
|
228
232
|
>
|
|
229
233
|
{g.label}
|
|
230
234
|
</div>
|
|
@@ -235,26 +239,21 @@ export function ModelSelector({
|
|
|
235
239
|
key={`${m.provider}/${m.id}`}
|
|
236
240
|
type="button"
|
|
237
241
|
role="menuitemradio"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!selected) e.currentTarget.style.background = "var(--dsw-hover)";
|
|
244
|
-
}}
|
|
245
|
-
onMouseLeave={(e) => {
|
|
246
|
-
if (!selected) e.currentTarget.style.background = "transparent";
|
|
247
|
-
}}
|
|
242
|
+
aria-checked={selected}
|
|
243
|
+
data-selected={selected}
|
|
244
|
+
data-active={selected ? "true" : undefined}
|
|
245
|
+
className="isx-menu-item flex min-h-10 w-full items-center justify-between gap-3 rounded-[12px] px-3 py-2 text-left"
|
|
246
|
+
title={m.name || m.id}
|
|
248
247
|
onClick={() => {
|
|
249
248
|
onSelectModel(m.provider, m.id);
|
|
250
249
|
setOpen(false);
|
|
251
250
|
}}
|
|
252
251
|
>
|
|
253
|
-
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 14, fontWeight: 500
|
|
252
|
+
<span className="isx-menu-shift min-w-0 flex-1 truncate" style={{ fontSize: 14, fontWeight: 500 }}>
|
|
254
253
|
{m.name}
|
|
255
254
|
</span>
|
|
256
255
|
{selected && (
|
|
257
|
-
<span style={{ display: "inline-flex",
|
|
256
|
+
<span className="pr-2" style={{ display: "inline-flex", flex: "none" }}>
|
|
258
257
|
<IconCheckOutline14 size={16} />
|
|
259
258
|
</span>
|
|
260
259
|
)}
|
|
@@ -269,18 +268,16 @@ export function ModelSelector({
|
|
|
269
268
|
{hasModel && !modelOnly && (thinkingLevels.length > 1 ? (
|
|
270
269
|
<button
|
|
271
270
|
type="button"
|
|
272
|
-
className="mt-1 flex w-full items-center justify-between gap-3 border-t px-3 pb-1 pt-2 text-left
|
|
273
|
-
style={{ borderColor: "var(--dsw-border-
|
|
274
|
-
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
|
|
275
|
-
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
|
|
271
|
+
className="isx-menu-item mt-1 flex w-full items-center justify-between gap-3 border-t px-3 pb-1.5 pt-2 text-left"
|
|
272
|
+
style={{ borderColor: "var(--dsw-border-l2)" }}
|
|
276
273
|
onClick={() => setPane("effort")}
|
|
277
274
|
>
|
|
278
|
-
<span style={{ fontSize: 13.5
|
|
279
|
-
<span className="flex min-w-0 items-center gap-1">
|
|
275
|
+
<span className="isx-menu-shift" style={{ fontSize: 13.5 }}>{t.thinking}</span>
|
|
276
|
+
<span className="isx-menu-sub flex min-w-0 items-center gap-1 pr-2">
|
|
280
277
|
<span className="truncate" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
|
|
281
278
|
{thinkingLevel ?? t.thinkingOff}
|
|
282
279
|
</span>
|
|
283
|
-
<IconChevronRight14 size={14} style={{ flex: "none"
|
|
280
|
+
<IconChevronRight14 size={14} style={{ flex: "none" }} />
|
|
284
281
|
</span>
|
|
285
282
|
</button>
|
|
286
283
|
) : (
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Iconsax 风格 PiMark(微曲圆角矢量 π 徽标)+ 现代品牌簇(PiWeb)
|
|
3
3
|
*/
|
|
4
4
|
"use client";
|
|
5
5
|
|
|
6
6
|
export function PiMark({
|
|
7
|
-
size =
|
|
7
|
+
size = 20,
|
|
8
8
|
className,
|
|
9
9
|
style,
|
|
10
10
|
}: {
|
|
@@ -16,64 +16,99 @@ export function PiMark({
|
|
|
16
16
|
<svg
|
|
17
17
|
width={size}
|
|
18
18
|
height={size}
|
|
19
|
-
viewBox="
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
20
21
|
aria-hidden
|
|
21
22
|
className={className}
|
|
22
|
-
style={{
|
|
23
|
+
style={{ display: "block", flex: "none", ...style }}
|
|
23
24
|
>
|
|
25
|
+
<defs>
|
|
26
|
+
<linearGradient id="isx-pi-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
27
|
+
<stop offset="0%" stopColor="#02defc" />
|
|
28
|
+
<stop offset="50%" stopColor="#7ffe00" />
|
|
29
|
+
<stop offset="100%" stopColor="#fc0197" />
|
|
30
|
+
</linearGradient>
|
|
31
|
+
</defs>
|
|
32
|
+
{/* 现代圆角 π 字符造型:水平顶梁与双流线支腿 */}
|
|
24
33
|
<path
|
|
34
|
+
d="M3.75 6.75C3.75 5.92 4.42 5.25 5.25 5.25H18.75C19.58 5.25 20.25 5.92 20.25 6.75C20.25 7.58 19.58 8.25 18.75 8.25H5.25C4.42 8.25 3.75 7.58 3.75 6.75Z"
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
/>
|
|
37
|
+
<path
|
|
38
|
+
d="M8.25 7.5V15.5C8.25 17.16 6.91 18.5 5.25 18.5C4.42 18.5 3.75 17.83 3.75 17C3.75 16.17 4.42 15.5 5.25 15.5C5.66 15.5 6 15.16 6 14.75V7.5H8.25Z"
|
|
39
|
+
fill="currentColor"
|
|
40
|
+
/>
|
|
41
|
+
<path
|
|
42
|
+
d="M15 7.5V14.5C15 15.88 16.12 17 17.5 17C18.33 17 19 17.67 19 18.5C19 19.33 18.33 20 17.5 20C14.46 20 12.75 17.54 12.75 14.5V7.5H15Z"
|
|
25
43
|
fill="currentColor"
|
|
26
|
-
fillRule="evenodd"
|
|
27
|
-
d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"
|
|
28
44
|
/>
|
|
29
|
-
<path fill="currentColor" d="M517.36 400 H634.72 V634.72 H517.36 Z" />
|
|
30
45
|
</svg>
|
|
31
46
|
);
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
/**
|
|
49
|
+
/** 品牌簇:Iconsax 风格 Squircle 徽标 + 渐变品牌字 */
|
|
35
50
|
export function BrandPi({ compact = false }: { compact?: boolean }) {
|
|
36
51
|
if (compact) {
|
|
37
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className="flex items-center justify-center rounded-[10px] border border-white/10 transition-transform duration-200 hover:scale-105"
|
|
55
|
+
style={{
|
|
56
|
+
width: 32,
|
|
57
|
+
height: 32,
|
|
58
|
+
background: "var(--dsw-bg-elevated)",
|
|
59
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.18)",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<PiMark size={18} style={{ color: "var(--dsw-label-primary)" }} />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
38
65
|
}
|
|
39
66
|
return (
|
|
40
67
|
<div
|
|
41
|
-
className="inline-flex items-center
|
|
42
|
-
style={{
|
|
43
|
-
color: "var(--dsw-label-primary)",
|
|
44
|
-
gap: 6,
|
|
45
|
-
}}
|
|
68
|
+
className="group inline-flex cursor-pointer select-none items-center gap-2.5 transition-transform duration-200"
|
|
69
|
+
style={{ color: "var(--dsw-label-primary)" }}
|
|
46
70
|
>
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
className="
|
|
50
|
-
style={{
|
|
51
|
-
fontSize: 16.5,
|
|
52
|
-
fontWeight: 600,
|
|
53
|
-
letterSpacing: "-0.02em",
|
|
54
|
-
color: "var(--dsw-label-primary)",
|
|
55
|
-
lineHeight: 1,
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
pi
|
|
59
|
-
</span>
|
|
60
|
-
<span
|
|
61
|
-
className="inline-flex items-center justify-center font-bold"
|
|
71
|
+
{/* Iconsax 风格圆角微光徽标盒 */}
|
|
72
|
+
<div
|
|
73
|
+
className="relative flex items-center justify-center rounded-[9px] border transition-all duration-300 group-hover:scale-105"
|
|
62
74
|
style={{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
borderRadius: 4,
|
|
69
|
-
background: "var(--dsw-label-primary)",
|
|
70
|
-
color: "var(--dsw-bg-base)",
|
|
71
|
-
lineHeight: 1,
|
|
72
|
-
textTransform: "uppercase",
|
|
75
|
+
width: 28,
|
|
76
|
+
height: 28,
|
|
77
|
+
background: "var(--dsw-bg-elevated)",
|
|
78
|
+
borderColor: "var(--dsw-border-l3)",
|
|
79
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
|
73
80
|
}}
|
|
74
81
|
>
|
|
75
|
-
|
|
76
|
-
</
|
|
82
|
+
<PiMark size={16} style={{ color: "var(--dsw-label-primary)" }} />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* 品牌名称:Pi (高反差主色) + Web (Iconsax 彩虹渐变字) */}
|
|
86
|
+
<div className="flex items-baseline gap-0.5">
|
|
87
|
+
<span
|
|
88
|
+
style={{
|
|
89
|
+
fontSize: 16.5,
|
|
90
|
+
fontWeight: 700,
|
|
91
|
+
letterSpacing: "-0.03em",
|
|
92
|
+
color: "var(--dsw-label-primary)",
|
|
93
|
+
lineHeight: 1,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
Pi
|
|
97
|
+
</span>
|
|
98
|
+
<span
|
|
99
|
+
style={{
|
|
100
|
+
fontSize: 16.5,
|
|
101
|
+
fontWeight: 600,
|
|
102
|
+
letterSpacing: "-0.02em",
|
|
103
|
+
background: "var(--isx-line-gradient)",
|
|
104
|
+
WebkitBackgroundClip: "text",
|
|
105
|
+
WebkitTextFillColor: "transparent",
|
|
106
|
+
lineHeight: 1,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
Web
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
77
112
|
</div>
|
|
78
113
|
);
|
|
79
114
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type CSSProperties } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { ImageViewer, imageSource, type PreviewImageSource } from "@/components/ImageViewer";
|
|
6
|
+
import { useI18n } from "@/i18n";
|
|
7
|
+
|
|
8
|
+
/** 聊天/正文里的图片点开只读图集:不改动、不删除已经落盘的消息内容 */
|
|
9
|
+
export function PreviewImage({ image, images = [image], index = 0, alt = "", title, className, style }: {
|
|
10
|
+
image: PreviewImageSource;
|
|
11
|
+
images?: PreviewImageSource[];
|
|
12
|
+
index?: number;
|
|
13
|
+
alt?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
}) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
const [preview, setPreview] = useState<number | null>(null);
|
|
20
|
+
return <>
|
|
21
|
+
<button type="button" className="pw-chat-image-preview" title={title ?? t.imagePreview} aria-label={alt ? `${t.imagePreview}: ${alt}` : t.imagePreview} onClick={(event) => {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
event.stopPropagation();
|
|
24
|
+
setPreview(index);
|
|
25
|
+
}}>
|
|
26
|
+
<img src={imageSource(image)} alt={alt} title={title} className={className} style={style} draggable={false} />
|
|
27
|
+
</button>
|
|
28
|
+
{preview !== null && images[preview] && createPortal(
|
|
29
|
+
<ImageViewer images={images} index={preview} onIndex={setPreview} onClose={() => setPreview(null)} />,
|
|
30
|
+
document.body,
|
|
31
|
+
)}
|
|
32
|
+
</>;
|
|
33
|
+
}
|
|
@@ -71,7 +71,9 @@ export function GrowthConsentDialog({ request, onAnswer }: { request: GrowthCons
|
|
|
71
71
|
export function roundTitle(round: GrowthEntry, t: Record<string, string>): string {
|
|
72
72
|
if (round.kind !== "round") return t.growthKindUser;
|
|
73
73
|
const label = t.growthRoundLabel.replace("{n}", String(round.n ?? ""));
|
|
74
|
-
|
|
74
|
+
// 修好之前记下的轮次,标题是技能展开的首行 `<skill name="x" location="…">`:显示成 /skill:x
|
|
75
|
+
const title = round.title?.replace(/^<skill\s[^>]*?\bname="([^"]+)"[^>]*>$/, "/skill:$1");
|
|
76
|
+
return title ? `${label} · ${title}` : label;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** 柱的颜色:你的修改为灰;pi 的一轮按主要改动类型(删除为主红、新增为主绿、否则黄) */
|
|
@@ -413,6 +415,7 @@ export function ProjectPanel({
|
|
|
413
415
|
data-active={selected?.commit === round.commit || undefined}
|
|
414
416
|
style={{ height: h, background: roundColor(round) }}
|
|
415
417
|
title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)} · ${t.growthRoundFiles.replace("{n}", String(round.stats.added + round.stats.modified + round.stats.deleted + round.stats.renamed))} +${round.stats.add} −${round.stats.del}`}
|
|
418
|
+
aria-label={`${roundTitle(round, tt)} · ${t.growthRoundFiles.replace("{n}", String(round.stats.added + round.stats.modified + round.stats.deleted + round.stats.renamed))} +${round.stats.add} −${round.stats.del}`}
|
|
416
419
|
onClick={() => growth.select(round.commit)}
|
|
417
420
|
/>
|
|
418
421
|
);
|
|
@@ -421,15 +424,16 @@ export function ProjectPanel({
|
|
|
421
424
|
</div>
|
|
422
425
|
)}
|
|
423
426
|
<div className="flex items-center gap-1 px-2 pb-2 pt-1" style={{ fontSize: 11.5 }}>
|
|
424
|
-
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthPrev} onClick={growth.prev} disabled={selectedIdx <= 0}>
|
|
427
|
+
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthPrev} aria-label={t.growthPrev} onClick={growth.prev} disabled={selectedIdx <= 0}>
|
|
425
428
|
<IconChevronLeft14 size={12} />
|
|
426
429
|
</button>
|
|
427
|
-
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthNext} onClick={growth.next} disabled={selectedIdx < 0 || selectedIdx >= rounds.length - 1}>
|
|
430
|
+
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthNext} aria-label={t.growthNext} onClick={growth.next} disabled={selectedIdx < 0 || selectedIdx >= rounds.length - 1}>
|
|
428
431
|
<IconChevronRight14 size={12} />
|
|
429
432
|
</button>
|
|
430
433
|
{/* 计数可点开轮数列表:单击直接弹列表(不做双击变输入框,避免 250ms 单双击
|
|
431
434
|
判定拖慢弹出);「输入轮号」是列表里一个固定入口,选中后当场变数字输入框 */}
|
|
432
|
-
|
|
435
|
+
{/* ref 包住按钮和列表:按在计数按钮上不算「点在外面」,否则 mousedown 先关、click 又打开,再点一下关不掉 */}
|
|
436
|
+
<div className="relative min-w-0 flex-1" ref={roundPickerRef}>
|
|
433
437
|
<button
|
|
434
438
|
type="button"
|
|
435
439
|
className="w-full truncate rounded px-1 text-left"
|
|
@@ -448,7 +452,6 @@ export function ProjectPanel({
|
|
|
448
452
|
</button>
|
|
449
453
|
{roundPickerOpen && (
|
|
450
454
|
<div
|
|
451
|
-
ref={roundPickerRef}
|
|
452
455
|
className="popover absolute bottom-7 left-0 z-50 flex w-[268px] max-w-[calc(100vw-24px)] flex-col"
|
|
453
456
|
role="listbox"
|
|
454
457
|
aria-label={t.growthPickRound}
|