@rexvane/piweb 0.3.22 → 0.3.24
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 +8 -7
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +11 -4
- package/.next/routes-manifest.json +8 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- 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.js.nft.json +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]/side-chat/route.js +5 -0
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
- 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/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 +2 -2
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +2 -2
- 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/health/route.js.nft.json +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 +3 -3
- package/.next/server/app/api/remote/route.js.nft.json +1 -1
- 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/web-auth/route.js.nft.json +1 -1
- 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/icon.svg/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +7 -6
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -6
- package/.next/server/app/index.segments/_full.segment.rsc +7 -6
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +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 +8 -7
- package/.next/server/chunks/{813.js → 1813.js} +1 -1
- package/.next/server/chunks/{430.js → 2430.js} +1 -1
- package/.next/server/chunks/{970.js → 2970.js} +1 -1
- package/.next/server/chunks/3421.js +1 -0
- package/.next/server/chunks/3924.js +6 -0
- package/.next/server/chunks/4807.js +1 -0
- package/.next/server/chunks/5393.js +1 -0
- package/.next/server/chunks/5802.js +2 -0
- package/.next/server/chunks/6229.js +1 -0
- package/.next/server/chunks/7496.js +1 -0
- package/.next/server/chunks/7662.js +2 -0
- package/.next/server/chunks/8686.js +1 -0
- package/.next/server/chunks/{593.js → 974.js} +1 -1
- package/.next/server/chunks/{920.js → 9920.js} +1 -1
- package/.next/server/functions-config-manifest.json +2 -1
- package/.next/server/instrumentation.js +1 -1
- 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/middleware.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/server/webpack-runtime.js +1 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
- package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
- package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
- package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
- package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
- package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +10 -0
- package/next.config.ts +2 -0
- package/package.json +9 -3
- package/scripts/release.mjs +3 -7
- package/scripts/runtime/check-installed.mjs +66 -0
- package/scripts/runtime/pi-packages.mjs +52 -0
- package/scripts/verify-package.mjs +29 -7
- package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
- package/src/app/globals.css +1576 -14
- package/src/components/AppShell.tsx +63 -13
- package/src/components/ChatInput.tsx +33 -29
- package/src/components/ChatWindow.tsx +6 -7
- package/src/components/ContextMeter.tsx +35 -16
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/FileViewer.tsx +89 -8
- package/src/components/GrowthTree.tsx +30 -5
- package/src/components/ModelSelector.tsx +21 -5
- package/src/components/SettingsPanel.tsx +25 -16
- package/src/components/chat/reading-anchor.ts +81 -0
- package/src/components/composer/ComposerImages.tsx +31 -11
- package/src/components/side-chat/SideChatPanel.module.css +131 -0
- package/src/components/side-chat/SideChatPanel.tsx +189 -0
- package/src/components/side-chat/icons.tsx +8 -0
- package/src/components/side-chat/request-history.ts +29 -0
- package/src/components/side-chat/store.ts +248 -0
- package/src/hooks/useFileViewer.ts +6 -4
- package/src/i18n.tsx +50 -8
- package/src/lib/files-service.ts +66 -2
- package/src/lib/path-security.ts +24 -32
- package/src/lib/session-reader.ts +1 -1
- package/src/lib/side-chat/context.ts +123 -0
- package/src/lib/side-chat/service.ts +191 -0
- package/src/lib/side-chat/types.ts +61 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/update-service.ts +4 -6
- package/.next/server/chunks/229.js +0 -1
- package/.next/server/chunks/393.js +0 -1
- package/.next/server/chunks/421.js +0 -1
- package/.next/server/chunks/496.js +0 -1
- package/.next/server/chunks/662.js +0 -2
- package/.next/server/chunks/686.js +0 -1
- package/.next/server/chunks/802.js +0 -2
- package/.next/server/chunks/807.js +0 -1
- package/.next/server/chunks/924.js +0 -6
- package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
- package/.next/static/chunks/4673.4cf66cbdd8364c57.js +0 -1
- package/.next/static/chunks/4686-09fcd0028079006c.js +0 -1
- package/.next/static/chunks/7411.67366dc5032a141d.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +0 -1
- package/.next/static/chunks/webpack-22ebffc20adb44ae.js +0 -1
- package/.next/static/css/6671d0d1589fdc0e.css +0 -1
- /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → lvKCJmjFj8LuqSojRVLRF}/_buildManifest.js +0 -0
- /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → lvKCJmjFj8LuqSojRVLRF}/_ssgManifest.js +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 生成中呼吸态、新步入场动画、窗口化渲染、键盘导航(↑↓ 移动,←→ 收展,Enter 打开)。
|
|
7
7
|
*/
|
|
8
8
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
-
import { IconAtOutline16, IconFileOutline16, IconTriangleRightFill14 } from "@/components/icons";
|
|
9
|
+
import { IconAtOutline16, IconFileOutline16, IconFolderClose16, IconTriangleRightFill14 } from "@/components/icons";
|
|
10
10
|
import { useI18n } from "@/i18n";
|
|
11
11
|
import { fileIconSrc, folderIconSrc } from "@/lib/file-icons";
|
|
12
12
|
import { flattenTree, treeNodeKey, type FlatRow, type TreeNode } from "@/lib/growth-tree";
|
|
@@ -182,14 +182,16 @@ export const GrowthTree = memo(function GrowthTree({
|
|
|
182
182
|
selected={row.node.kind === "file" && selectedPath === row.node.path}
|
|
183
183
|
focused={focused === treeNodeKey(row.node)}
|
|
184
184
|
onActivate={activate}
|
|
185
|
+
onOpenFolder={onOpenFile}
|
|
185
186
|
onFocusRow={setFocused}
|
|
186
187
|
onReference={onReference}
|
|
187
188
|
onOpenEditor={onOpenEditor}
|
|
188
189
|
pendingLabel={t.growthPending}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
lazyLabel={t.growthLazyDir}
|
|
191
|
+
deletedLabel={t.growthStatD}
|
|
192
|
+
referenceLabel={t.fileReference}
|
|
192
193
|
editorLabel={t.fileOpenEditor}
|
|
194
|
+
openFolderLabel={t.viewerOpenFolder}
|
|
193
195
|
/>
|
|
194
196
|
))}
|
|
195
197
|
</div>
|
|
@@ -205,6 +207,7 @@ const TreeRow = memo(function TreeRow({
|
|
|
205
207
|
selected,
|
|
206
208
|
focused,
|
|
207
209
|
onActivate,
|
|
210
|
+
onOpenFolder,
|
|
208
211
|
onFocusRow,
|
|
209
212
|
onReference,
|
|
210
213
|
onOpenEditor,
|
|
@@ -213,6 +216,7 @@ const TreeRow = memo(function TreeRow({
|
|
|
213
216
|
deletedLabel,
|
|
214
217
|
referenceLabel,
|
|
215
218
|
editorLabel,
|
|
219
|
+
openFolderLabel,
|
|
216
220
|
}: {
|
|
217
221
|
row: FlatRow;
|
|
218
222
|
top: number;
|
|
@@ -220,6 +224,7 @@ const TreeRow = memo(function TreeRow({
|
|
|
220
224
|
selected: boolean;
|
|
221
225
|
focused: boolean;
|
|
222
226
|
onActivate: (row: FlatRow) => void;
|
|
227
|
+
onOpenFolder: (node: TreeNode) => void;
|
|
223
228
|
onFocusRow: (path: string) => void;
|
|
224
229
|
onReference?: (path: string) => void;
|
|
225
230
|
onOpenEditor?: (path: string) => void;
|
|
@@ -228,6 +233,7 @@ const TreeRow = memo(function TreeRow({
|
|
|
228
233
|
deletedLabel: string;
|
|
229
234
|
referenceLabel: string;
|
|
230
235
|
editorLabel: string;
|
|
236
|
+
openFolderLabel: string;
|
|
231
237
|
}) {
|
|
232
238
|
const { node, depth, expanded } = row;
|
|
233
239
|
const isDir = node.kind === "dir";
|
|
@@ -253,6 +259,10 @@ const TreeRow = memo(function TreeRow({
|
|
|
253
259
|
onFocusRow(treeNodeKey(node));
|
|
254
260
|
onActivate(row);
|
|
255
261
|
}}
|
|
262
|
+
onDoubleClick={() => {
|
|
263
|
+
// 目录双击 = 打开文件夹预览(单击仍是展开/收起,不动既有习惯)
|
|
264
|
+
if (isDir) onOpenFolder(node);
|
|
265
|
+
}}
|
|
256
266
|
>
|
|
257
267
|
<span className="pw-tree-chevron" aria-hidden>
|
|
258
268
|
{isDir && <IconTriangleRightFill14 size={10} style={{ transform: expanded ? "rotate(90deg)" : undefined, transition: "transform var(--ds-duration-fast)" }} />}
|
|
@@ -264,8 +274,23 @@ const TreeRow = memo(function TreeRow({
|
|
|
264
274
|
{node.status === "R" && node.from && <span className="pw-tree-from">← {node.from.split("/").pop()}</span>}
|
|
265
275
|
</span>
|
|
266
276
|
{node.pending && <span className="pw-tree-pulse" aria-label={pendingLabel} />}
|
|
267
|
-
{!node.lazy && (onReference || (!isDir && onOpenEditor)) && (
|
|
277
|
+
{!node.lazy && (onReference || (!isDir && onOpenEditor) || isDir) && (
|
|
268
278
|
<span className="pw-tree-actions">
|
|
279
|
+
{isDir && (
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
className="icon-btn"
|
|
283
|
+
style={{ width: 18, height: 18, borderRadius: 5 }}
|
|
284
|
+
title={openFolderLabel}
|
|
285
|
+
aria-label={`${openFolderLabel}: ${node.path}`}
|
|
286
|
+
onClick={(e) => {
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
onOpenFolder(node);
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<IconFolderClose16 size={11} />
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
269
294
|
{onReference && (
|
|
270
295
|
<button
|
|
271
296
|
type="button"
|
|
@@ -27,6 +27,9 @@ export function ModelSelector({
|
|
|
27
27
|
onSelectLevel,
|
|
28
28
|
open: openProp,
|
|
29
29
|
onOpenChange,
|
|
30
|
+
modelOnly = false,
|
|
31
|
+
disabled = false,
|
|
32
|
+
defaultOption,
|
|
30
33
|
}: {
|
|
31
34
|
model?: { provider: string; id: string; name: string };
|
|
32
35
|
thinkingLevel?: string;
|
|
@@ -38,6 +41,10 @@ export function ModelSelector({
|
|
|
38
41
|
onSelectLevel: (level: string) => void;
|
|
39
42
|
open?: boolean;
|
|
40
43
|
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
/** Compact consumers can show just the model list. */
|
|
45
|
+
modelOnly?: boolean;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
defaultOption?: { label: string; selected: boolean; onSelect: () => void };
|
|
41
48
|
}) {
|
|
42
49
|
const [openState, setOpenState] = useState(false);
|
|
43
50
|
const open = openProp ?? openState;
|
|
@@ -104,10 +111,10 @@ export function ModelSelector({
|
|
|
104
111
|
|
|
105
112
|
const hasModel = Boolean(model?.id);
|
|
106
113
|
const modelName = hasModel ? (model?.name || model?.id || "") : "";
|
|
107
|
-
const effortLabel = hasModel && thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : undefined;
|
|
108
|
-
const triggerTitle = hasModel
|
|
114
|
+
const effortLabel = !modelOnly && hasModel && thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : undefined;
|
|
115
|
+
const triggerTitle = (defaultOption?.selected ? `${defaultOption.label} · ` : "") + (hasModel
|
|
109
116
|
? `${providerNames[model?.provider ?? ""] ?? model?.provider}/${modelName}${effortLabel ? ` · ${effortLabel}` : ""}`
|
|
110
|
-
: t.selectModel;
|
|
117
|
+
: t.selectModel);
|
|
111
118
|
|
|
112
119
|
return (
|
|
113
120
|
<div ref={ref} className="relative">
|
|
@@ -119,6 +126,7 @@ export function ModelSelector({
|
|
|
119
126
|
aria-label={triggerTitle}
|
|
120
127
|
aria-haspopup="menu"
|
|
121
128
|
aria-expanded={open}
|
|
129
|
+
disabled={disabled}
|
|
122
130
|
onClick={() => {
|
|
123
131
|
setOpen(!open);
|
|
124
132
|
setPane("model");
|
|
@@ -198,6 +206,14 @@ export function ModelSelector({
|
|
|
198
206
|
{pane === "model" && (
|
|
199
207
|
<div className="flex min-h-0 flex-col">
|
|
200
208
|
<div ref={listRef} className="max-h-[340px] overflow-y-auto py-1">
|
|
209
|
+
{defaultOption && (
|
|
210
|
+
<button type="button" role="menuitemradio" aria-checked={defaultOption.selected} data-selected={defaultOption.selected}
|
|
211
|
+
className="flex min-h-10 w-full items-center justify-between gap-3 rounded-[10px] px-3 py-2 text-left"
|
|
212
|
+
style={{ fontSize: 13, background: defaultOption.selected ? "var(--dsw-active)" : undefined }}
|
|
213
|
+
onClick={() => { defaultOption.onSelect(); setOpen(false); }}>
|
|
214
|
+
{defaultOption.label}{defaultOption.selected && <IconCheckOutline14 size={14} />}
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
201
217
|
{groups.length === 0 && (
|
|
202
218
|
<div className="px-3 py-3" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
|
|
203
219
|
{t.emptyModels}
|
|
@@ -213,7 +229,7 @@ export function ModelSelector({
|
|
|
213
229
|
{g.label}
|
|
214
230
|
</div>
|
|
215
231
|
{g.models.map((m) => {
|
|
216
|
-
const selected = model?.provider === m.provider && model?.id === m.id;
|
|
232
|
+
const selected = !defaultOption?.selected && model?.provider === m.provider && model?.id === m.id;
|
|
217
233
|
return (
|
|
218
234
|
<button
|
|
219
235
|
key={`${m.provider}/${m.id}`}
|
|
@@ -250,7 +266,7 @@ export function ModelSelector({
|
|
|
250
266
|
</div>
|
|
251
267
|
{/* 思考档位入口:模型只有一档(非推理模型 / models.json 没写 reasoning: true)时也显示,
|
|
252
268
|
但置灰并说明是模型不支持,而不是让入口凭空消失 */}
|
|
253
|
-
{hasModel && (thinkingLevels.length > 1 ? (
|
|
269
|
+
{hasModel && !modelOnly && (thinkingLevels.length > 1 ? (
|
|
254
270
|
<button
|
|
255
271
|
type="button"
|
|
256
272
|
className="mt-1 flex w-full items-center justify-between gap-3 border-t px-3 pb-1 pt-2 text-left transition-colors"
|
|
@@ -999,21 +999,25 @@ function ModelFieldAudit({ gaps, providerLabel, busy, onSave }: {
|
|
|
999
999
|
<div role="cell" className="min-w-0">{capacityCell(gap, "contextWindow", t.modelAuditColContext)}</div>
|
|
1000
1000
|
<div role="cell" className="min-w-0">{capacityCell(gap, "maxTokens", t.modelAuditColOutput)}</div>
|
|
1001
1001
|
<div role="cell" className="min-w-0">
|
|
1002
|
-
{
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1002
|
+
{/* 输入类型:文本 pi 总是接受(勾上不可点),图片这一勾就是 models.json 的 input 里加 image */}
|
|
1003
|
+
{gap.missing.includes("input") || true ? (
|
|
1004
|
+
<div className="flex items-center gap-3" role="group" aria-label={`${gap.id} ${t.modelAuditColVision}`}>
|
|
1005
|
+
<label className="flex cursor-default items-center gap-1" style={{ color: "var(--dsw-label-tertiary)" }} title={t.visionInputTextHint}>
|
|
1006
|
+
<input type="checkbox" checked disabled readOnly />
|
|
1007
|
+
{t.visionInputText}
|
|
1008
|
+
</label>
|
|
1009
|
+
<label className="flex items-center gap-1" style={{ color: "var(--dsw-label-secondary)" }} title={t.visionInputImageHint}>
|
|
1010
|
+
<input
|
|
1011
|
+
type="checkbox"
|
|
1012
|
+
checked={gap.missing.includes("input") ? rowOf(gap).vision === "image" : gap.vision === true}
|
|
1013
|
+
disabled={busy || !gap.missing.includes("input")}
|
|
1014
|
+
onChange={(event) => edit(gap, { vision: event.target.checked ? "image" : "text" })}
|
|
1015
|
+
aria-label={`${gap.id} ${t.visionInputImage}`}
|
|
1016
|
+
/>
|
|
1017
|
+
{t.visionInputImage}
|
|
1018
|
+
</label>
|
|
1019
|
+
</div>
|
|
1020
|
+
) : null}
|
|
1017
1021
|
</div>
|
|
1018
1022
|
</div>
|
|
1019
1023
|
))}
|
|
@@ -1360,7 +1364,12 @@ function ModelsSection() {
|
|
|
1360
1364
|
if (!filled) return true;
|
|
1361
1365
|
setAuditBusy(true);
|
|
1362
1366
|
try {
|
|
1363
|
-
|
|
1367
|
+
const result = await saveCustom(next);
|
|
1368
|
+
// 失败(典型是 revision 冲突:写入期间 pi 自己动过 models.json)也必须刷新本地副本与 revision。
|
|
1369
|
+
// 否则本地一直拿着旧 revision,之后每次保存都撞同一个 409——用户看到的就是「反复填、永远不成」。
|
|
1370
|
+
// call() 只在成功时 load(),这里把失败那一半补上。
|
|
1371
|
+
if (!result.success) await load();
|
|
1372
|
+
return Boolean(result.success);
|
|
1364
1373
|
} finally {
|
|
1365
1374
|
setAuditBusy(false);
|
|
1366
1375
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
const BEFORE_LAYOUT = "piweb:before-chat-layout";
|
|
6
|
+
|
|
7
|
+
/** Call before changing the columns, while the currently visible text still has its old geometry. */
|
|
8
|
+
export function preserveChatReadingPosition(): void {
|
|
9
|
+
window.dispatchEvent(new Event(BEFORE_LAYOUT));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ReadingAnchor {
|
|
13
|
+
element?: Element;
|
|
14
|
+
range?: Range;
|
|
15
|
+
top: number;
|
|
16
|
+
bottom: boolean;
|
|
17
|
+
width: number;
|
|
18
|
+
capturedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useChatReadingAnchor(rootRef: RefObject<HTMLElement | null>, scrollRef: RefObject<HTMLElement | null>, shouldPin: () => boolean): void {
|
|
22
|
+
const anchor = useRef<ReadingAnchor | null>(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const root = rootRef.current;
|
|
25
|
+
const scroller = scrollRef.current;
|
|
26
|
+
if (!root || !scroller) return;
|
|
27
|
+
let frame = 0;
|
|
28
|
+
const restore = () => {
|
|
29
|
+
const saved = anchor.current;
|
|
30
|
+
anchor.current = null;
|
|
31
|
+
if (!saved || Date.now() - saved.capturedAt > 500 || root.getBoundingClientRect().width === saved.width) return;
|
|
32
|
+
if (saved.bottom) {
|
|
33
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const rect = saved.range?.startContainer.isConnected ? saved.range.getBoundingClientRect() : saved.element?.isConnected ? saved.element.getBoundingClientRect() : null;
|
|
37
|
+
if (rect) scroller.scrollTop += rect.top - scroller.getBoundingClientRect().top - saved.top;
|
|
38
|
+
};
|
|
39
|
+
const capture = () => {
|
|
40
|
+
if (frame) cancelAnimationFrame(frame);
|
|
41
|
+
// A new drag frame may arrive before the previous restore frame. Finish it first.
|
|
42
|
+
restore();
|
|
43
|
+
const geometry = { width: root.getBoundingClientRect().width, capturedAt: Date.now() };
|
|
44
|
+
if (shouldPin()) {
|
|
45
|
+
anchor.current = { top: 0, bottom: true, ...geometry };
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const viewport = scroller.getBoundingClientRect();
|
|
49
|
+
const element = [...scroller.querySelectorAll("p, pre, li, h1, h2, h3, h4, table, [data-role='user']")].find((node) => {
|
|
50
|
+
const rect = node.getBoundingClientRect();
|
|
51
|
+
return rect.bottom > viewport.top + 8 && rect.top < viewport.bottom;
|
|
52
|
+
});
|
|
53
|
+
if (!element) return;
|
|
54
|
+
const rect = element.getBoundingClientRect();
|
|
55
|
+
const x = Math.max(viewport.left + 8, rect.left + 8);
|
|
56
|
+
const y = Math.max(viewport.top + 8, rect.top + 4);
|
|
57
|
+
const range = document.caretRangeFromPoint?.(x, y);
|
|
58
|
+
if (range && scroller.contains(range.startContainer) && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset < (range.startContainer.textContent?.length ?? 0)) {
|
|
59
|
+
range.setEnd(range.startContainer, range.startOffset + 1);
|
|
60
|
+
anchor.current = { element, range, top: range.getBoundingClientRect().top - viewport.top, bottom: false, ...geometry };
|
|
61
|
+
} else {
|
|
62
|
+
anchor.current = { element, top: rect.top - viewport.top, bottom: false, ...geometry };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const observer = new ResizeObserver(() => {
|
|
66
|
+
if (!anchor.current) return;
|
|
67
|
+
if (frame) cancelAnimationFrame(frame);
|
|
68
|
+
frame = requestAnimationFrame(() => {
|
|
69
|
+
frame = 0;
|
|
70
|
+
restore();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
observer.observe(root);
|
|
74
|
+
window.addEventListener(BEFORE_LAYOUT, capture);
|
|
75
|
+
return () => {
|
|
76
|
+
observer.disconnect();
|
|
77
|
+
window.removeEventListener(BEFORE_LAYOUT, capture);
|
|
78
|
+
if (frame) cancelAnimationFrame(frame);
|
|
79
|
+
};
|
|
80
|
+
}, [rootRef, scrollRef, shouldPin]);
|
|
81
|
+
}
|
|
@@ -99,7 +99,7 @@ function keyOf(image: ImageAttachment): string {
|
|
|
99
99
|
|
|
100
100
|
const srcOf = (image: ImageAttachment) => `data:${image.mimeType};base64,${image.data}`;
|
|
101
101
|
|
|
102
|
-
export function ComposerImages({ images, disabled, onRemove }: { images: ImageAttachment[]; disabled?: boolean; onRemove: (index: number) => void }) {
|
|
102
|
+
export function ComposerImages({ images, disabled, onRemove, onClear }: { images: ImageAttachment[]; disabled?: boolean; onRemove: (index: number) => void; onClear?: () => void }) {
|
|
103
103
|
const { t } = useI18n();
|
|
104
104
|
const collapsible = images.length > COLLAPSE_AT;
|
|
105
105
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -140,14 +140,19 @@ export function ComposerImages({ images, disabled, onRemove }: { images: ImageAt
|
|
|
140
140
|
|
|
141
141
|
return (
|
|
142
142
|
<>
|
|
143
|
-
<div ref={railRef} className="
|
|
143
|
+
<div ref={railRef} className="pointer-events-auto flex flex-wrap items-center gap-2 px-1" data-testid="composer-images">
|
|
144
144
|
{collapsible ? (
|
|
145
145
|
<div
|
|
146
146
|
className="pw-image-fan"
|
|
147
147
|
data-testid="image-fan"
|
|
148
148
|
data-expanded={fanned}
|
|
149
149
|
data-crowded={fanned && layout.gap < ROOMY_GAP ? true : undefined}
|
|
150
|
-
|
|
150
|
+
// 容器(composer-attachments)已经浮在输入卡上方、不占布局高度,这里不用再抬
|
|
151
|
+
style={{
|
|
152
|
+
position: "relative",
|
|
153
|
+
width: fanned ? "100%" : layout.width,
|
|
154
|
+
height: layout.height,
|
|
155
|
+
}}
|
|
151
156
|
>
|
|
152
157
|
{images.map((image, i) => {
|
|
153
158
|
const pose = layout.cards[i];
|
|
@@ -201,6 +206,21 @@ export function ComposerImages({ images, disabled, onRemove }: { images: ImageAt
|
|
|
201
206
|
</div>
|
|
202
207
|
))
|
|
203
208
|
)}
|
|
209
|
+
{images.length > 1 && onClear ? (
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
className="pw-chip"
|
|
213
|
+
data-testid="images-clear"
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
onClick={() => {
|
|
216
|
+
onClear();
|
|
217
|
+
setLifted(null);
|
|
218
|
+
setPreview(null);
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{t.imageClearAll}
|
|
222
|
+
</button>
|
|
223
|
+
) : null}
|
|
204
224
|
{collapsible ? (
|
|
205
225
|
<button
|
|
206
226
|
type="button"
|
|
@@ -280,23 +300,23 @@ function ImageViewer({
|
|
|
280
300
|
}, [count, index]);
|
|
281
301
|
|
|
282
302
|
return (
|
|
283
|
-
<div ref={dialogRef} className="pw-
|
|
284
|
-
<div className="pw-
|
|
285
|
-
<span className="pw-
|
|
286
|
-
<button type="button" className="pw-
|
|
303
|
+
<div ref={dialogRef} className="pw-lightbox" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
|
|
304
|
+
<div className="pw-lightbox-bar" onClick={(event) => event.stopPropagation()}>
|
|
305
|
+
<span className="pw-lightbox-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
|
|
306
|
+
<button type="button" className="pw-lightbox-btn" disabled={disabled} onClick={() => onRemove(index)}>
|
|
287
307
|
{t.delete}
|
|
288
308
|
</button>
|
|
289
|
-
<button ref={closeRef} type="button" className="pw-
|
|
309
|
+
<button ref={closeRef} type="button" className="pw-lightbox-btn" aria-label={t.close} onClick={onClose}>
|
|
290
310
|
✕
|
|
291
311
|
</button>
|
|
292
312
|
</div>
|
|
293
|
-
<img src={srcOf(images[index])} alt="" className="pw-
|
|
313
|
+
<img src={srcOf(images[index])} alt="" className="pw-lightbox-image" onClick={(event) => event.stopPropagation()} />
|
|
294
314
|
{count > 1 ? (
|
|
295
315
|
<>
|
|
296
|
-
<button type="button" className="pw-
|
|
316
|
+
<button type="button" className="pw-lightbox-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
|
|
297
317
|
‹
|
|
298
318
|
</button>
|
|
299
|
-
<button type="button" className="pw-
|
|
319
|
+
<button type="button" className="pw-lightbox-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
|
|
300
320
|
›
|
|
301
321
|
</button>
|
|
302
322
|
</>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
.panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
}
|
|
8
|
+
.header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 6px;
|
|
12
|
+
padding: 10px 14px;
|
|
13
|
+
border-bottom: 0.5px solid var(--dsw-border-l2);
|
|
14
|
+
}
|
|
15
|
+
.header button { width: 28px; height: 28px; flex-shrink: 0; }
|
|
16
|
+
.title { flex: 1; font-size: 13.5px; font-weight: 600; }
|
|
17
|
+
/* 历史记录:按钮 + 浮层菜单(原生 select 不跟深色主题) */
|
|
18
|
+
.history { position: relative; padding: 8px 14px 0; }
|
|
19
|
+
.historyButton {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 6px;
|
|
23
|
+
width: 100%;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
height: 30px;
|
|
26
|
+
padding: 0 8px;
|
|
27
|
+
border: 0.5px solid var(--dsw-border-l2);
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
background: var(--dsw-hover);
|
|
30
|
+
color: var(--dsw-label-primary);
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
text-align: left;
|
|
33
|
+
transition: background var(--ds-duration-fast) var(--ds-ease-in-out), border-color var(--ds-duration-fast) var(--ds-ease-in-out);
|
|
34
|
+
}
|
|
35
|
+
.historyButton:hover { background: var(--dsw-selector); }
|
|
36
|
+
.historyButton[aria-expanded="true"] { border-color: var(--dsw-border-l3); background: var(--dsw-selector); }
|
|
37
|
+
.historyMenu {
|
|
38
|
+
position: absolute;
|
|
39
|
+
left: 14px;
|
|
40
|
+
right: 14px;
|
|
41
|
+
top: calc(100% + 4px);
|
|
42
|
+
z-index: 30;
|
|
43
|
+
max-height: 260px;
|
|
44
|
+
overflow-y: auto;
|
|
45
|
+
padding: 4px;
|
|
46
|
+
border: 0.5px solid var(--dsw-border-l2);
|
|
47
|
+
border-radius: 10px;
|
|
48
|
+
background: var(--dsw-glass-popover);
|
|
49
|
+
box-shadow: var(--dsw-elevation-soft);
|
|
50
|
+
}
|
|
51
|
+
.historyItem {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 6px;
|
|
55
|
+
width: 100%;
|
|
56
|
+
padding: 6px 8px;
|
|
57
|
+
border-radius: 7px;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
color: var(--dsw-label-secondary);
|
|
60
|
+
text-align: left;
|
|
61
|
+
}
|
|
62
|
+
.historyItem:hover { background: var(--dsw-hover); color: var(--dsw-label-primary); }
|
|
63
|
+
.historyItem[aria-checked="true"] { background: var(--dsw-accent-soft); color: var(--dsw-accent); }
|
|
64
|
+
.historyMeta { flex: none; font-size: 11px; color: var(--dsw-label-caption); }
|
|
65
|
+
.messages { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
66
|
+
.messageContent { display: flex; flex-direction: column; gap: 20px; padding: 20px 16px; }
|
|
67
|
+
.empty { margin: 36px 8px; color: var(--dsw-label-secondary); text-align: center; line-height: 1.8; }
|
|
68
|
+
.empty p { margin: 0 0 8px; font-size: 14px; }
|
|
69
|
+
.empty span { font-size: 12px; color: var(--dsw-label-tertiary); }
|
|
70
|
+
.question {
|
|
71
|
+
align-self: flex-end;
|
|
72
|
+
max-width: 94%;
|
|
73
|
+
padding: 9px 12px;
|
|
74
|
+
border-radius: 14px 14px 4px 14px;
|
|
75
|
+
background: var(--dsw-accent-soft);
|
|
76
|
+
font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size));
|
|
77
|
+
line-height: 1.65;
|
|
78
|
+
white-space: pre-wrap;
|
|
79
|
+
overflow-wrap: anywhere;
|
|
80
|
+
}
|
|
81
|
+
.answer { min-width: 0; font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size)); line-height: 1.75; overflow-wrap: anywhere; }
|
|
82
|
+
.answer :global(.md) { font-size: inherit; }
|
|
83
|
+
.answer :global(.md pre), .answer :global(.md table) { max-width: 100%; }
|
|
84
|
+
.notice, .waiting { font-size: 12px; line-height: 1.7; color: var(--dsw-label-tertiary); }
|
|
85
|
+
.notice { margin: 4px 0 10px; }
|
|
86
|
+
.error, .storageError { font-size: 12px; line-height: 1.7; color: var(--dsw-danger); overflow-wrap: anywhere; }
|
|
87
|
+
.error button { margin-left: 10px; text-decoration: underline; }
|
|
88
|
+
.storageError { margin: 0; padding: 4px 14px; }
|
|
89
|
+
.composer {
|
|
90
|
+
margin: 10px 12px 12px;
|
|
91
|
+
padding: 11px 12px 9px;
|
|
92
|
+
border: 0.5px solid var(--dsw-border-l2);
|
|
93
|
+
border-radius: 14px;
|
|
94
|
+
background: var(--dsw-bg-base);
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
.composer:focus-within { border-color: var(--dsw-border-l3); }
|
|
98
|
+
.composer textarea {
|
|
99
|
+
display: block;
|
|
100
|
+
width: 100%;
|
|
101
|
+
min-height: 64px;
|
|
102
|
+
max-height: 180px;
|
|
103
|
+
resize: none;
|
|
104
|
+
border: 0;
|
|
105
|
+
outline: none;
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size));
|
|
110
|
+
line-height: 1.65;
|
|
111
|
+
}
|
|
112
|
+
.composer textarea::placeholder { color: var(--dsw-label-tertiary); }
|
|
113
|
+
.actions { display: flex; align-items: center; gap: 8px; justify-content: space-between; padding-top: 5px; }
|
|
114
|
+
.model { flex: 1; min-width: 0; }
|
|
115
|
+
/* 右下角的上下文环:夹在模型选择器与发送键之间 */
|
|
116
|
+
.meter { display: flex; align-items: center; flex: none; }
|
|
117
|
+
.model :global(.chip) { max-width: 100%; font-size: 12px; }
|
|
118
|
+
.model :global(.chip:disabled) { opacity: 0.6; }
|
|
119
|
+
.model :global([role="menu"]) { left: 0; right: auto; width: min(312px, calc(100% + 38px)); }
|
|
120
|
+
.send {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
width: 30px;
|
|
125
|
+
height: 30px;
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
background: var(--dsw-label-primary);
|
|
129
|
+
color: var(--dsw-bg-base);
|
|
130
|
+
}
|
|
131
|
+
.send:disabled { opacity: 0.3; cursor: default; }
|