diff --git a/apps/presentation/dashboard/smoke/delegation-search-browser-smoke.mjs b/apps/presentation/dashboard/smoke/delegation-search-browser-smoke.mjs index 278c19753a..972a4d76db 100644 --- a/apps/presentation/dashboard/smoke/delegation-search-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/delegation-search-browser-smoke.mjs @@ -69,8 +69,17 @@ try { await dialog.getByText('未找到匹配的委派。', {exact: true}).waitFor(); await dialog.getByRole('button', {name: '清空', exact: true}).click(); await settled(); await rows(12); - await dialog.getByRole('button', {name: '更多记录', exact: true}).click(); await settled(); + await page.setViewportSize({width: 390, height: 844}); + const more = dialog.getByRole('button', {name: '更多记录', exact: true}); + assert.ok(await more.evaluate(node => { + const box = node.getBoundingClientRect(); + return box.top >= 0 && box.bottom <= innerHeight; + }), 'The next page must be reachable without scrolling through the current page'); + await more.focus(); await page.keyboard.press('Enter'); await settled(); await rows(13); + assert.equal(await page.evaluate(() => document.activeElement?.textContent), '已显示 13 / 13', + 'Finishing keyboard pagination keeps focus on its result'); + await page.setViewportSize({width: 1440, height: 1000}); await fixture('fault'); await search('reviewer'); await settled(); await dialog.getByRole('alert').waitFor(); await rows(0); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.css b/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.css index 7e1b342e30..72126048fb 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.css @@ -3,6 +3,8 @@ .conversation-requests-count {font-variant-numeric:tabular-nums;color:var(--pw-text);} .conversation-requests-entry > span {color:var(--pw-muted);} .conversation-requests-dialog {margin:auto;width:min(880px,calc(100vw - 32px));max-width:none;max-height:calc(100dvh - 48px);padding:0;border:1px solid var(--pw-line);border-radius:12px;background:var(--pw-bg);color:var(--pw-text);} +.conversation-requests-dialog[open] {display:flex;flex-direction:column;overflow:hidden;} +.conversation-requests-dialog > header,.conversation-requests-dialog > nav,.conversation-requests-search {flex-shrink:0;} .conversation-requests-dialog::backdrop {background:color-mix(in srgb,var(--pw-text) 20%,transparent);} .conversation-requests-dialog > header {display:flex;justify-content:space-between;align-items:center;padding:20px 24px 12px;} .conversation-requests-dialog > header > div {display:flex;align-items:center;gap:12px;} @@ -13,7 +15,7 @@ .conversation-requests-dialog > nav {display:flex;gap:8px;padding:0 24px 16px;border-bottom:1px solid var(--pw-line);} .conversation-requests-dialog > nav > button[aria-pressed=true] {background:var(--pw-text);color:var(--pw-bg);} .conversation-requests-dialog > nav > button:last-child {margin-left:auto;} -.conversation-requests-body {max-height:calc(100dvh - 210px);overflow:auto;overscroll-behavior:contain;padding:0 24px 24px;font-size:var(--text-body);line-height:var(--leading-prose);} +.conversation-requests-body {min-height:0;flex:1 1 auto;overflow:auto;overscroll-behavior:contain;padding:0 24px 24px;font-size:var(--text-body);line-height:var(--leading-prose);} .conversation-requests-group h3 {display:flex;flex-wrap:wrap;align-items:baseline;gap:12px;margin:24px 0 8px;font-size:var(--text-body);font-weight:600;overflow-wrap:anywhere;} .conversation-requests-group h3 span {color:var(--pw-muted);font-weight:400;} .conversation-request-row {border-top:1px solid var(--pw-line);} @@ -36,7 +38,7 @@ .conversation-reply-context span {color:var(--pw-muted);} .conversation-requests-dialog :focus-visible,.conversation-requests-entry :focus-visible {outline:2px solid var(--pw-blue);outline-offset:2px;} .conversation-requests-dialog h2:focus {outline:none;} -@media(max-width:640px) {.conversation-requests-dialog {width:calc(100vw - 16px);max-height:calc(100dvh - 16px);}.conversation-requests-dialog > header,.conversation-requests-dialog > nav,.conversation-requests-body {padding-inline:16px;}.conversation-request-row > summary {grid-template-columns:16px minmax(0,1fr) auto;}.conversation-request-row > summary > time {grid-column:3;grid-row:2;}.conversation-request-row > summary strong {grid-column:2 / -1;}.conversation-request-row > summary > span {grid-column:2;grid-row:2;}.conversation-requests-body {max-height:calc(100dvh - 180px);}} +@media(max-width:640px) {.conversation-requests-dialog {width:calc(100vw - 16px);max-height:calc(100dvh - 16px);}.conversation-requests-dialog > header,.conversation-requests-dialog > nav,.conversation-requests-body {padding-inline:16px;}.conversation-request-row > summary {grid-template-columns:16px minmax(0,1fr) auto;}.conversation-request-row > summary > time {grid-column:3;grid-row:2;}.conversation-request-row > summary strong {grid-column:2 / -1;}.conversation-request-row > summary > span {grid-column:2;grid-row:2;}} .conversation-request-toggle {grid-column:1;grid-row:1 / span 2;align-self:center;color:var(--pw-muted);} .conversation-request-row[open] > summary .conversation-request-toggle {transform:rotate(90deg);} @@ -58,5 +60,8 @@ .project-peer-grants > ul > li {display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px 0;overflow-wrap:anywhere;} .project-peer-grant:not([open]) > summary {display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer;} @media(max-width:640px) {.project-peer-request > summary {align-items:flex-start;flex-direction:column;gap:4px;}.project-peer-request > summary > span {flex-shrink:1;}} -.conversation-requests-search + .conversation-requests-body {max-height:calc(100dvh - 280px);} -@media(max-width:640px) {.conversation-requests-search {padding-inline:16px;flex-wrap:wrap;}.conversation-requests-search input {flex-basis:100%;}.conversation-requests-search + .conversation-requests-body {max-height:calc(100dvh - 290px);}} +@media(max-width:640px) {.conversation-requests-search {padding-inline:16px;flex-wrap:wrap;}.conversation-requests-search input {flex-basis:100%;}} + +.conversation-requests-pagination {display:flex;flex-shrink:0;align-items:center;justify-content:space-between;gap:12px;padding:12px 24px;border-top:1px solid var(--pw-line);font-variant-numeric:tabular-nums;} +.conversation-requests-pagination > div {display:flex;align-items:center;gap:8px;color:var(--pw-muted);} +@media(max-width:640px) {.conversation-requests-pagination {padding-inline:16px;flex-wrap:wrap;}} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.tsx b/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.tsx index b0884edfc6..642fb0e13d 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/conversation-requests.tsx @@ -1,5 +1,5 @@ import {useEffect, useRef, useState} from "react"; -import {ChevronRight, ListChecks, RefreshCw, X} from "lucide-react"; +import {ChevronRight, CircleAlert, ListChecks, RefreshCw, X} from "lucide-react"; import {fetchChatSession, fetchChatSessions, requestJson, type ChatImageAttachment, type ChatVisibleMessage, type ChatReplyContext} from "../../data/chat"; import {z} from "zod"; import {fetchConversationRequests, type ConversationRequest, type ConversationRequestInventory} from "../../data/conversation-requests"; @@ -211,6 +211,9 @@ export function ConversationRequests({sessionId, ownerFallback, refreshKey}: {se const dialog = useRef(null); const trigger = useRef(null); const requestAbort = useRef(null); + const moreButton = useRef(null); + const pageCountStatus = useRef(null); + const restorePagingFocus = useRef(false); useEffect(() => { setOpen(false); setHistory(false); setPages([]); setError(false); setSearch(""); setSearchDraft(""); }, [sessionId]); @@ -264,6 +267,14 @@ export function ConversationRequests({sessionId, ownerFallback, refreshKey}: {se } finally {if (current === generation.current) setBusy(false);} } const page = pages[0], last = pages.at(-1); + useEffect(() => { + if (!busy && restorePagingFocus.current) { + restorePagingFocus.current = false; + if (document.activeElement === document.body || document.activeElement === moreButton.current) { + (moreButton.current ?? pageCountStatus.current)?.focus({preventScroll: true}); + } + } + }, [busy]); const unknown = page && (!page.coverage.scan_complete || page.coverage.unreadable > 0); const rows = [...new Map(pages.flatMap(value => value.rows).map(row => [row.request_id, row])).values()]; const recipients = [...new Set(rows.map(row => `${row.goal_id}\n${row.agent_id}`))]; @@ -302,14 +313,12 @@ export function ConversationRequests({sessionId, ownerFallback, refreshKey}: {se
{error ?

{zh ? "委派记录暂时无法核验。" : "Delegation records cannot be verified."}

: null} - {unknown ?

{zh ? "部分记录未能读取,数量与列表尚不完整。" : "Some records could not be read. Counts and list are incomplete."}

: null} {busy && !rows.length ?

{zh ? "正在读取委派…" : "Loading delegations…"}

: null} {!busy && noSession ?

{zh ? "尚无可读取的管家会话。" : "No readable steward conversation yet."}

: null} {!busy && !noSession && !error && page && !unknown && !rows.length ?

{search ? (zh ? "未找到匹配的委派。" : "No matching delegations.") : history ? (zh ? "暂无委派记录。" : "No delegation records.") : (zh ? "当前没有待跟进的委派。" : "No delegations awaiting follow-up.")}

: null} - {!busy && !error && page && search && rows.length ?

{zh ? `匹配 ${page.matched} 条${unknown ? "(部分记录)" : ""}` : `${page.matched} matches${unknown ? " (partial records)" : ""}`}

: null} {recipients.map(recipient => {const [goal, agent] = recipient.split("\n"); return

{agent}{goal}

{rows.filter(row => row.goal_id === goal && row.agent_id === agent).map(row =>
@@ -333,8 +342,17 @@ export function ConversationRequests({sessionId, ownerFallback, refreshKey}: {se
)} ;})} - {last?.next_offset !== null && last?.next_offset !== undefined ? : null} + {page && !error ? : null} ; } diff --git a/examples/personal-workspace-browser/assets/delegation-pagination/before-desktop.png b/examples/personal-workspace-browser/assets/delegation-pagination/before-desktop.png new file mode 100644 index 0000000000..0905dfc7ab Binary files /dev/null and b/examples/personal-workspace-browser/assets/delegation-pagination/before-desktop.png differ diff --git a/examples/personal-workspace-browser/assets/delegation-pagination/before-narrow.png b/examples/personal-workspace-browser/assets/delegation-pagination/before-narrow.png new file mode 100644 index 0000000000..396513aec4 Binary files /dev/null and b/examples/personal-workspace-browser/assets/delegation-pagination/before-narrow.png differ diff --git a/examples/personal-workspace-browser/assets/delegation-pagination/desktop.png b/examples/personal-workspace-browser/assets/delegation-pagination/desktop.png new file mode 100644 index 0000000000..b4cad3f329 Binary files /dev/null and b/examples/personal-workspace-browser/assets/delegation-pagination/desktop.png differ diff --git a/examples/personal-workspace-browser/assets/delegation-pagination/expanded-narrow.png b/examples/personal-workspace-browser/assets/delegation-pagination/expanded-narrow.png new file mode 100644 index 0000000000..fed9a92877 Binary files /dev/null and b/examples/personal-workspace-browser/assets/delegation-pagination/expanded-narrow.png differ diff --git a/examples/personal-workspace-browser/assets/delegation-pagination/narrow.png b/examples/personal-workspace-browser/assets/delegation-pagination/narrow.png new file mode 100644 index 0000000000..d2af81224e Binary files /dev/null and b/examples/personal-workspace-browser/assets/delegation-pagination/narrow.png differ